github.com/CyCoreSystems/ari@v4.8.4+incompatible/client/native/client.go (about) 1 package native 2 3 import ( 4 "context" 5 "encoding/base64" 6 "net/http" 7 "net/url" 8 "os" 9 "sync" 10 "time" 11 12 "github.com/inconshreveable/log15" 13 14 "github.com/CyCoreSystems/ari" 15 "github.com/CyCoreSystems/ari/rid" 16 "github.com/CyCoreSystems/ari/stdbus" 17 "github.com/pkg/errors" 18 "golang.org/x/net/websocket" 19 ) 20 21 // Logger defaults to a discard handler (null output). 22 // If you wish to enable logging, you can set your own 23 // handler like so: 24 // ari.Logger.SetHandler(log15.StderrHandler) 25 // 26 var Logger = log15.New() 27 28 func init() { 29 // Null logger, by default 30 Logger.SetHandler(log15.DiscardHandler()) 31 } 32 33 // Options describes the options for connecting to 34 // a native Asterisk ARI server. 35 type Options struct { 36 // Application is the the name of this ARI application 37 Application string 38 39 // URL is the root URL of the ARI server (asterisk box). 40 // Default to http://localhost:8088/ari 41 URL string 42 43 // WebsocketURL is the URL for ARI Websocket events. 44 // Defaults to the events directory of URL, with a protocol of ws. 45 // Usually ws://localhost:8088/ari/events. 46 WebsocketURL string 47 48 // WebsocketOrigin is the origin to report for the websocket connection. 49 // Defaults to http://localhost/ 50 WebsocketOrigin string 51 52 // Username for ARI authentication 53 Username string 54 55 // Password for ARI authentication 56 Password string 57 } 58 59 // Connect creates and connects a new Client to Asterisk ARI. 60 func Connect(opts *Options) (ari.Client, error) { 61 c := New(opts) 62 63 err := c.Connect() 64 if err != nil { 65 return c, err 66 } 67 68 info, err := c.Asterisk().Info(nil) 69 if err != nil { 70 return c, err 71 } 72 c.node = info.SystemInfo.EntityID 73 74 return c, err 75 } 76 77 // New creates a new ari.Client. This function should not be used directly unless you need finer control. 78 // nolint: gocyclo 79 func New(opts *Options) *Client { 80 if opts == nil { 81 opts = &Options{} 82 } 83 84 // Make sure we have an Application defined 85 if opts.Application == "" { 86 if os.Getenv("ARI_APPLICATION") != "" { 87 opts.Application = os.Getenv("ARI_APPLICATION") 88 } else { 89 opts.Application = rid.New("") 90 } 91 } 92 93 if opts.URL == "" { 94 if os.Getenv("ARI_URL") != "" { 95 opts.URL = os.Getenv("ARI_URL") 96 } else { 97 opts.URL = "http://localhost:8088/ari" 98 } 99 } 100 101 if opts.WebsocketURL == "" { 102 if os.Getenv("ARI_WSURL") != "" { 103 opts.WebsocketURL = os.Getenv("ARI_WSURL") 104 } else { 105 opts.WebsocketURL = "ws://localhost:8088/ari/events" 106 } 107 } 108 if opts.WebsocketOrigin == "" { 109 if os.Getenv("ARI_WSORIGIN") != "" { 110 opts.WebsocketOrigin = os.Getenv("ARI_WSORIGIN") 111 } else { 112 opts.WebsocketOrigin = "http://localhost/" 113 } 114 } 115 116 if opts.Username == "" { 117 opts.Username = os.Getenv("ARI_USERNAME") 118 } 119 if opts.Password == "" { 120 opts.Password = os.Getenv("ARI_PASSWORD") 121 } 122 123 return &Client{ 124 appName: opts.Application, 125 Options: opts, 126 } 127 } 128 129 // Client describes a native ARI client, which connects directly to an Asterisk HTTP-based ARI service. 130 type Client struct { 131 appName string 132 133 node string 134 135 // opts are the configuration options for the client 136 Options *Options 137 138 // WSConfig describes the configuration for the websocket connection to Asterisk, from which events will be received. 139 WSConfig *websocket.Config 140 141 // Connected is a flag indicating whether the Client is connected to Asterisk 142 Connected bool 143 144 // Bus the event bus for the Client 145 bus ari.Bus 146 147 // httpClient is the reusable HTTP client on which commands to Asterisk are sent 148 httpClient http.Client 149 150 cancel context.CancelFunc 151 } 152 153 // ApplicationName returns the client's ARI Application name 154 func (c *Client) ApplicationName() string { 155 return c.appName 156 } 157 158 // Close shuts down the ARI client 159 func (c *Client) Close() { 160 c.Bus().Close() 161 162 if c.cancel != nil { 163 c.cancel() 164 } 165 166 c.Connected = false 167 } 168 169 // Application returns the ARI Application accessors for this client 170 func (c *Client) Application() ari.Application { 171 return &Application{c} 172 } 173 174 // Asterisk returns the ARI Asterisk accessors for this client 175 func (c *Client) Asterisk() ari.Asterisk { 176 return &Asterisk{c} 177 } 178 179 // Bridge returns the ARI Bridge accessors for this client 180 func (c *Client) Bridge() ari.Bridge { 181 return &Bridge{c} 182 } 183 184 // Bus returns the Bus accessors for this client 185 func (c *Client) Bus() ari.Bus { 186 return c.bus 187 } 188 189 // Channel returns the ARI Channel accessors for this client 190 func (c *Client) Channel() ari.Channel { 191 return &Channel{c} 192 } 193 194 // DeviceState returns the ARI DeviceState accessors for this client 195 func (c *Client) DeviceState() ari.DeviceState { 196 return &DeviceState{c} 197 } 198 199 // Endpoint returns the ARI Endpoint accessors for this client 200 func (c *Client) Endpoint() ari.Endpoint { 201 return &Endpoint{c} 202 } 203 204 // LiveRecording returns the ARI LiveRecording accessors for this client 205 func (c *Client) LiveRecording() ari.LiveRecording { 206 return &LiveRecording{c} 207 } 208 209 // Mailbox returns the ARI Mailbox accessors for this client 210 func (c *Client) Mailbox() ari.Mailbox { 211 return &Mailbox{c} 212 } 213 214 // Playback returns the ARI Playback accessors for this client 215 func (c *Client) Playback() ari.Playback { 216 return &Playback{c} 217 } 218 219 // Sound returns the ARI Sound accessors for this client 220 func (c *Client) Sound() ari.Sound { 221 return &Sound{c} 222 } 223 224 // StoredRecording returns the ARI StoredRecording accessors for this client 225 func (c *Client) StoredRecording() ari.StoredRecording { 226 return &StoredRecording{c} 227 } 228 229 // TextMessage returns the ARI TextMessage accessors for this client 230 func (c *Client) TextMessage() ari.TextMessage { 231 return &TextMessage{c} 232 } 233 234 func (c *Client) createWSConfig() (err error) { 235 // Construct the websocket connection url 236 v := url.Values{} 237 v.Set("app", c.Options.Application) 238 wsurl := c.Options.WebsocketURL + "?" + v.Encode() 239 240 // Construct a websocket config 241 c.WSConfig, err = websocket.NewConfig(wsurl, c.Options.WebsocketOrigin) 242 if err != nil { 243 return errors.Wrap(err, "Failed to construct websocket config") 244 } 245 246 // Add the authorization header 247 c.WSConfig.Header.Set("Authorization", "Basic "+basicAuth(c.Options.Username, c.Options.Password)) 248 return nil 249 } 250 251 // Connect sets up and maintains and a websocket connection to Asterisk, passing any received events to the Bus 252 func (c *Client) Connect() error { 253 ctx, cancel := context.WithCancel(context.Background()) 254 c.cancel = cancel 255 256 if c.Connected { 257 cancel() 258 return errors.New("already connected") 259 } 260 261 if c.Options.Username == "" { 262 cancel() 263 return errors.New("no username found") 264 } 265 if c.Options.Password == "" { 266 cancel() 267 return errors.New("no password found") 268 } 269 270 // Construct the websocket config, if we don't already have one 271 if c.WSConfig == nil { 272 if err := c.createWSConfig(); err != nil { 273 cancel() 274 return errors.Wrap(err, "failed to create websocket configuration") 275 } 276 } 277 278 // Make sure the bus is set up 279 c.bus = stdbus.New() 280 281 // Setup and listen on the websocket 282 wg := new(sync.WaitGroup) 283 wg.Add(1) 284 go c.listen(ctx, wg) 285 wg.Wait() 286 c.Connected = true 287 288 return nil 289 } 290 291 func (c *Client) listen(ctx context.Context, wg *sync.WaitGroup) { 292 var signalUp sync.Once 293 294 for { 295 // Exit if our context has been closed 296 if ctx.Err() != nil { 297 return 298 } 299 300 // Dial Asterisk 301 ws, err := websocket.DialConfig(c.WSConfig) 302 if err != nil { 303 Logger.Error("failed to connect to Asterisk", "error", err) 304 time.Sleep(time.Second) 305 continue 306 } 307 308 // Signal that we are connected (the first time only) 309 if wg != nil { 310 signalUp.Do(wg.Done) 311 } 312 313 // Wait for context closure or read error 314 select { 315 case <-ctx.Done(): 316 case err = <-c.wsRead(ws): 317 Logger.Error("read failure on websocket", "error", err) 318 time.Sleep(10 * time.Millisecond) 319 } 320 321 // Make sure our websocket connection is closed before looping 322 err = ws.Close() 323 if err != nil { 324 Logger.Debug("failed to close websocket", "error", err) 325 } 326 327 } 328 } 329 330 // wsRead loops for the duration of a websocket connection, 331 // reading messages, decoding them to events, and passing 332 // them to the event bus. 333 func (c *Client) wsRead(ws *websocket.Conn) chan error { 334 errChan := make(chan error, 1) 335 336 go func() { 337 for { 338 var data []byte 339 err := websocket.Message.Receive(ws, &data) 340 if err != nil { 341 errChan <- errors.Wrap(err, "failed to receive websocket message") 342 return 343 } 344 e, err := ari.DecodeEvent(data) 345 if err != nil { 346 errChan <- errors.Wrap(err, "failed to devoce websocket message to event") 347 } 348 c.bus.Send(e) 349 350 } 351 }() 352 353 return errChan 354 } 355 356 // stamp imprints the node metadata onto the given Key 357 func (c *Client) stamp(key *ari.Key) *ari.Key { 358 if key == nil { 359 key = &ari.Key{} 360 } 361 362 ret := *key 363 ret.App = c.appName 364 ret.Node = c.node 365 return &ret 366 } 367 368 // basicAuth (stolen from net/http/client.go) creates a basic authentication header 369 func basicAuth(username, password string) string { 370 auth := username + ":" + password 371 return base64.StdEncoding.EncodeToString([]byte(auth)) 372 }