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  }