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