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 }