staskobzar /
goagi
| 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
Loading history...
|
|||
| 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 |