1 | package goagi |
||
2 | |||
3 | import ( |
||
4 | "bufio" |
||
5 | "strings" |
||
6 | "time" |
||
7 | ) |
||
8 | |||
9 | // ErrAGI goagi error |
||
10 | var ErrAGI = newError("AGI session") |
||
11 | |||
12 | const rwDefaultTimeout = time.Second * 1 |
||
13 | |||
14 | // Reader interface for AGI object. Can be net.Conn, os.File or crafted |
||
15 | type Reader interface { |
||
16 | Read(b []byte) (int, error) |
||
17 | SetReadDeadline(t time.Time) error |
||
18 | } |
||
19 | |||
20 | // Writer interface for AGI object. Can be net.Conn, os.File or crafted |
||
21 | type Writer interface { |
||
22 | SetWriteDeadline(t time.Time) error |
||
23 | Write(b []byte) (int, error) |
||
24 | } |
||
25 | |||
26 | /* |
||
27 | Debugger for AGI instance. Any interface that provides Printf method. |
||
28 | Usage example: |
||
29 | ```go |
||
30 | dbg := logger.New(os.Stdout, "myagi:", log.Lmicroseconds) |
||
31 | r, w := net.Pipe() |
||
32 | agi, err := goagi.New(r, w, dbg) |
||
33 | ``` |
||
34 | It should be used only for debugging as it give lots of output. |
||
35 | */ |
||
36 | type Debugger interface { |
||
37 | Printf(format string, v ...interface{}) |
||
38 | } |
||
39 | |||
40 | // AGI object |
||
41 | type AGI struct { |
||
42 | env map[string]string |
||
43 | arg []string |
||
44 | reader Reader |
||
45 | writer Writer |
||
46 | isHUP bool |
||
47 | debugger Debugger |
||
48 | rwtout time.Duration |
||
49 | } |
||
50 | |||
51 | const ( |
||
52 | codeUnknown int = 0 |
||
53 | codeEarly = 100 |
||
54 | codeSucc = 200 |
||
55 | codeE503 = 503 |
||
56 | codeE510 = 510 |
||
57 | codeE511 = 511 |
||
58 | codeE520 = 520 |
||
59 | ) |
||
60 | |||
61 | var codeMap = map[string]int{ |
||
62 | "100 ": codeEarly, |
||
63 | "200 ": codeSucc, |
||
64 | "503 ": codeE503, |
||
65 | "510 ": codeE510, |
||
66 | "511 ": codeE511, |
||
67 | "520 ": codeE520, |
||
68 | } |
||
69 | |||
70 | /* |
||
71 | New creates and returns AGI object. |
||
72 | Can be used to create agi and fastagi sessions. |
||
73 | Example for agi: |
||
74 | ```go |
||
75 | import ( |
||
76 | "github.com/staskobzar/goagi" |
||
77 | "os" |
||
78 | ) |
||
79 | |||
80 | agi, err := goagi.New(os.Stdin, os.Stdout, nil) |
||
81 | if err != nil { |
||
82 | panic(err) |
||
83 | } |
||
84 | agi.Verbose("Hello World!") |
||
85 | ``` |
||
86 | |||
87 | Fast agi example: |
||
88 | ```go |
||
89 | ln, err := net.Listen("tcp", "127.0.0.1:4573") |
||
90 | if err != nil { |
||
91 | panic(err) |
||
92 | } |
||
93 | for { |
||
94 | conn, err := ln.Accept() |
||
95 | if err != nil { |
||
96 | panic(err) |
||
97 | } |
||
98 | go func(conn net.Conn) { |
||
99 | agi, err := goagi.New(conn, conn, nil) |
||
100 | if err != nil { |
||
101 | panic(err) |
||
102 | } |
||
103 | agi.Verbose("Hello World!") |
||
104 | }(conn) |
||
105 | } |
||
106 | ``` |
||
107 | */ |
||
108 | func New(r Reader, w Writer, dbg Debugger) (*AGI, error) { |
||
109 | agi := &AGI{ |
||
110 | reader: r, |
||
111 | writer: w, |
||
112 | debugger: dbg, |
||
113 | rwtout: rwDefaultTimeout, |
||
114 | } |
||
115 | agi.dbg("[>] New AGI") |
||
116 | sessData, err := agi.sessionInit() |
||
117 | if err != nil { |
||
118 | return nil, ErrAGI.Msg("Failed to read setup: %s", err) |
||
119 | } |
||
120 | agi.sessionSetup(sessData) |
||
121 | return agi, nil |
||
122 | } |
||
123 | |||
124 | // Env returns AGI environment variable by key |
||
125 | func (agi *AGI) Env(key string) string { |
||
126 | agi.dbg("[>] Env for %q", key) |
||
127 | val, ok := agi.env[key] |
||
128 | if ok { |
||
129 | return val |
||
130 | } |
||
131 | return "" |
||
132 | } |
||
133 | |||
134 | // EnvArgs returns list of environment arguments |
||
135 | func (agi *AGI) EnvArgs() []string { |
||
136 | agi.dbg("[>] EnvArgs") |
||
137 | return agi.arg |
||
138 | } |
||
139 | |||
140 | func (agi *AGI) sessionInit() ([]string, error) { |
||
141 | agi.dbg("[>] sessionInit") |
||
142 | buf := bufio.NewReader(agi.reader) |
||
143 | data := make([]string, 0) |
||
144 | |||
145 | for { |
||
146 | tout := time.Now().Add(agi.rwtout) |
||
147 | if err := agi.reader.SetReadDeadline(tout); err != nil { |
||
148 | return nil, err |
||
149 | } |
||
150 | line, err := buf.ReadString('\n') |
||
151 | if err != nil { |
||
152 | return nil, err |
||
153 | } |
||
154 | if line == "\n" { |
||
155 | break |
||
156 | } |
||
157 | agi.dbg(" [v] read line: %q", line) |
||
158 | data = append(data, line[:len(line)-1]) |
||
159 | } |
||
160 | return data, nil |
||
161 | } |
||
162 | |||
163 | func (agi *AGI) dbg(pattern string, vargs ...interface{}) { |
||
164 | if agi.debugger != nil { |
||
165 | pattern += "\n" |
||
166 | agi.debugger.Printf(pattern, vargs...) |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
167 | } |
||
168 | } |
||
169 | |||
170 | // low level response from device and response as string, matched response |
||
171 | // code, true if channel reported as hangup and error. |
||
172 | // if timeout > 0 then will read with timeout |
||
173 | func (agi *AGI) read(timeout time.Duration) (resp string, code int, err error) { |
||
174 | agi.dbg("[>] readResponse") |
||
175 | buf := bufio.NewReader(agi.reader) |
||
176 | var builder strings.Builder |
||
177 | moreInputExpected := false |
||
178 | |||
179 | for { |
||
180 | if timeout > 0 { |
||
181 | tout := time.Now().Add(timeout) |
||
182 | if fail := agi.reader.SetReadDeadline(tout); fail != nil { |
||
183 | err = fail |
||
184 | return |
||
185 | } |
||
186 | } |
||
187 | |||
188 | line, fail := buf.ReadString('\n') |
||
189 | if fail != nil { |
||
190 | err = fail |
||
191 | return |
||
192 | } |
||
193 | |||
194 | agi.dbg(" [v] got line: %q", line) |
||
195 | |||
196 | builder.WriteString(line) |
||
197 | resp = builder.String() |
||
198 | if codeMatch, ok := matchCode(line); ok { |
||
199 | code = codeMatch |
||
200 | return |
||
201 | } |
||
202 | |||
203 | if matchPrefix(line, "520-") { |
||
204 | moreInputExpected = true |
||
205 | } |
||
206 | |||
207 | if matchPrefix(line, "HANGUP") { |
||
208 | agi.isHUP = true |
||
209 | builder.Reset() |
||
210 | continue |
||
211 | } |
||
212 | |||
213 | if !moreInputExpected { |
||
214 | err = ErrAGI.Msg("Invalid input while reading response: %q", resp) |
||
215 | return |
||
216 | } |
||
217 | } |
||
218 | } |
||
219 | |||
220 | func matchPrefix(line, pattern string) bool { |
||
221 | if len(line) < len(pattern) { |
||
222 | return false |
||
223 | } |
||
224 | return line[:len(pattern)] == pattern |
||
225 | } |
||
226 | |||
227 | func matchCode(data string) (int, bool) { |
||
228 | if len(data) < 4 { |
||
229 | return 0, false |
||
230 | } |
||
231 | if codeMatch, ok := codeMap[data[:4]]; ok { |
||
232 | return codeMatch, true |
||
233 | } |
||
234 | return 0, false |
||
235 | } |
||
236 | |||
237 | func (agi *AGI) write(command []byte) error { |
||
238 | agi.dbg("[>] readResponse") |
||
239 | if agi.rwtout > 0 { |
||
240 | tout := time.Now().Add(agi.rwtout) |
||
241 | agi.dbg(" [v] set write timeout: %dns", tout) |
||
242 | |||
243 | if err := agi.writer.SetWriteDeadline(tout); err != nil { |
||
244 | return err |
||
245 | } |
||
246 | } |
||
247 | |||
248 | agi.dbg(" [v] write command: %q\n", string(command)) |
||
249 | |||
250 | _, err := agi.writer.Write(command) |
||
251 | if err != nil { |
||
252 | return err |
||
253 | } |
||
254 | return nil |
||
255 | } |
||
256 | |||
257 | // write command, read and parse response |
||
258 | func (agi *AGI) execute(cmd string, timeout bool) (Response, error) { |
||
259 | agi.dbg("[>] execute cmd: %q", cmd) |
||
260 | if err := agi.write([]byte(cmd)); err != nil { |
||
261 | return nil, err |
||
262 | } |
||
263 | |||
264 | var tout time.Duration |
||
265 | if timeout { |
||
266 | tout = agi.rwtout |
||
267 | } |
||
268 | agi.dbg(" [v] read timeout=%d", tout) |
||
269 | |||
270 | resp, code, err := agi.read(tout) |
||
271 | if err != nil { |
||
272 | return nil, err |
||
273 | } |
||
274 | |||
275 | return agi.parseResponse(resp, code) |
||
276 | } |
||
277 |