github.com/twilio/twilio-go@v1.20.1/client/client.go (about)

     1  // Package client provides internal utilities for the twilio-go client library.
     2  package client
     3  
     4  import (
     5  	"bytes"
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/http"
     9  	"net/url"
    10  	"regexp"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/pkg/errors"
    17  	"github.com/twilio/twilio-go/client/form"
    18  )
    19  
    20  var alphanumericRegex *regexp.Regexp
    21  var delimitingRegex *regexp.Regexp
    22  
    23  func init() {
    24  	alphanumericRegex = regexp.MustCompile(`^[a-zA-Z0-9]*$`)
    25  	delimitingRegex = regexp.MustCompile(`\.\d+`)
    26  }
    27  
    28  // Credentials store user authentication credentials.
    29  type Credentials struct {
    30  	Username string
    31  	Password string
    32  }
    33  
    34  func NewCredentials(username string, password string) *Credentials {
    35  	return &Credentials{Username: username, Password: password}
    36  }
    37  
    38  // Client encapsulates a standard HTTP backend with authorization.
    39  type Client struct {
    40  	*Credentials
    41  	HTTPClient          *http.Client
    42  	accountSid          string
    43  	UserAgentExtensions []string
    44  }
    45  
    46  // default http Client should not follow redirects and return the most recent response.
    47  func defaultHTTPClient() *http.Client {
    48  	return &http.Client{
    49  		CheckRedirect: func(req *http.Request, via []*http.Request) error {
    50  			return http.ErrUseLastResponse
    51  		},
    52  		Timeout: time.Second * 10,
    53  	}
    54  }
    55  
    56  func (c *Client) basicAuth() (string, string) {
    57  	return c.Credentials.Username, c.Credentials.Password
    58  }
    59  
    60  // SetTimeout sets the Timeout for HTTP requests.
    61  func (c *Client) SetTimeout(timeout time.Duration) {
    62  	if c.HTTPClient == nil {
    63  		c.HTTPClient = defaultHTTPClient()
    64  	}
    65  	c.HTTPClient.Timeout = timeout
    66  }
    67  
    68  func extractContentTypeHeader(headers map[string]interface{}) (cType string) {
    69  	headerType, ok := headers["Content-Type"]
    70  	if !ok {
    71  		return urlEncodedContentType
    72  	}
    73  	return headerType.(string)
    74  }
    75  
    76  const (
    77  	urlEncodedContentType = "application/x-www-form-urlencoded"
    78  	jsonContentType       = "application/json"
    79  	keepZeros             = true
    80  	delimiter             = '.'
    81  	escapee               = '\\'
    82  )
    83  
    84  func (c *Client) doWithErr(req *http.Request) (*http.Response, error) {
    85  	client := c.HTTPClient
    86  
    87  	if client == nil {
    88  		client = defaultHTTPClient()
    89  	}
    90  
    91  	res, err := client.Do(req)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	// Note that 3XX response codes are allowed for fetches
    97  	if res.StatusCode < 200 || res.StatusCode >= 400 {
    98  		err = &TwilioRestError{}
    99  		if decodeErr := json.NewDecoder(res.Body).Decode(err); decodeErr != nil {
   100  			err = errors.Wrap(decodeErr, "error decoding the response for an HTTP error code: "+strconv.Itoa(res.StatusCode))
   101  			return nil, err
   102  		}
   103  
   104  		return nil, err
   105  	}
   106  	return res, nil
   107  }
   108  
   109  // throws error if username and password contains special characters
   110  func (c *Client) validateCredentials() error {
   111  	username, password := c.basicAuth()
   112  	if !alphanumericRegex.MatchString(username) {
   113  		return &TwilioRestError{
   114  			Status:   400,
   115  			Code:     21222,
   116  			Message:  "Invalid Username. Illegal chars",
   117  			MoreInfo: "https://www.twilio.com/docs/errors/21222"}
   118  	}
   119  	if !alphanumericRegex.MatchString(password) {
   120  		return &TwilioRestError{
   121  			Status:   400,
   122  			Code:     21224,
   123  			Message:  "Invalid Password. Illegal chars",
   124  			MoreInfo: "https://www.twilio.com/docs/errors/21224"}
   125  	}
   126  	return nil
   127  }
   128  
   129  // SendRequest verifies, constructs, and authorizes an HTTP request.
   130  func (c *Client) SendRequest(method string, rawURL string, data url.Values,
   131  	headers map[string]interface{}, body ...byte) (*http.Response, error) {
   132  
   133  	contentType := extractContentTypeHeader(headers)
   134  
   135  	u, err := url.Parse(rawURL)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	valueReader := &strings.Reader{}
   141  	goVersion := runtime.Version()
   142  	var req *http.Request
   143  
   144  	//For HTTP GET Method there are no body parameters. All other parameters like query, path etc
   145  	// are added as information in the url itself. Also while Content-Type is json, we are sending
   146  	// json body. In that case, data variable conatins all other parameters than body, which is the
   147  	//same case as GET method. In that case as well all parameters will be added to url
   148  	if method == http.MethodGet || contentType == jsonContentType {
   149  		if data != nil {
   150  			v, _ := form.EncodeToStringWith(data, delimiter, escapee, keepZeros)
   151  			s := delimitingRegex.ReplaceAllString(v, "")
   152  
   153  			u.RawQuery = s
   154  		}
   155  	}
   156  
   157  	//data is already processed and information will be added to u(the url) in the
   158  	//previous step. Now body will solely contain json payload
   159  	if contentType == jsonContentType {
   160  		req, err = http.NewRequest(method, u.String(), bytes.NewBuffer(body))
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  	} else {
   165  		//Here the HTTP POST methods which is not having json content type are processed
   166  		//All the values will be added in data and encoded (all body, query, path parameters)
   167  		if method == http.MethodPost {
   168  			valueReader = strings.NewReader(data.Encode())
   169  		}
   170  		credErr := c.validateCredentials()
   171  		if credErr != nil {
   172  			return nil, credErr
   173  		}
   174  		req, err = http.NewRequest(method, u.String(), valueReader)
   175  		if err != nil {
   176  			return nil, err
   177  		}
   178  
   179  	}
   180  
   181  	if contentType == urlEncodedContentType {
   182  		req.Header.Add("Content-Type", urlEncodedContentType)
   183  	}
   184  
   185  	req.SetBasicAuth(c.basicAuth())
   186  
   187  	// E.g. "User-Agent": "twilio-go/1.0.0 (darwin amd64) go/go1.17.8"
   188  	userAgent := fmt.Sprintf("twilio-go/%s (%s %s) go/%s", LibraryVersion, runtime.GOOS, runtime.GOARCH, goVersion)
   189  
   190  	if len(c.UserAgentExtensions) > 0 {
   191  		userAgent += " " + strings.Join(c.UserAgentExtensions, " ")
   192  	}
   193  
   194  	req.Header.Add("User-Agent", userAgent)
   195  
   196  	for k, v := range headers {
   197  		req.Header.Add(k, fmt.Sprint(v))
   198  	}
   199  	return c.doWithErr(req)
   200  }
   201  
   202  // SetAccountSid sets the Client's accountSid field
   203  func (c *Client) SetAccountSid(sid string) {
   204  	c.accountSid = sid
   205  }
   206  
   207  // Returns the Account SID.
   208  func (c *Client) AccountSid() string {
   209  	return c.accountSid
   210  }