package common import ( "bytes" "crypto/tls" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "os" "strings" ) // HTTPClient . type HTTPClient struct { LoggerOut io.Writer LoggerLevel int LoggerPrefix string httpClient http.Client initDone bool endpoint string apikey string username string password string id string csrf string conf HTTPClientConfig } // HTTPClientConfig is used to config HTTPClient type HTTPClientConfig struct { URLPrefix string ContentType string HeaderAPIKeyName string Apikey string HeaderClientKeyName string CsrfDisable bool LogOut io.Writer LogLevel int LogPrefix string } // Logger levels constants const ( HTTPLogLevelPanic = 0 HTTPLogLevelError = 1 HTTPLogLevelWarning = 2 HTTPLogLevelInfo = 3 HTTPLogLevelDebug = 4 ) // Inspired by syncthing/cmd/cli const insecure = false // HTTPNewClient creates a new HTTP client to deal with Syncthing func HTTPNewClient(baseURL string, cfg HTTPClientConfig) (*HTTPClient, error) { // Create w new Http client httpClient := http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: insecure, }, }, } lOut := cfg.LogOut if cfg.LogOut == nil { lOut = os.Stdout } client := HTTPClient{ LoggerOut: lOut, LoggerLevel: cfg.LogLevel, LoggerPrefix: cfg.LogPrefix, httpClient: httpClient, initDone: false, endpoint: baseURL, apikey: cfg.Apikey, conf: cfg, /* TODO - add user + pwd support username: c.GlobalString("username"), password: c.GlobalString("password"), */ } // Default set Content-Type to json if client.conf.ContentType == "" { client.conf.ContentType = "application/json" } if err := client.getCidAndCsrf(); err != nil { client.log(HTTPLogLevelError, "Cannot retrieve Client ID and/or CSRF: %v", err) return &client, err } client.log(HTTPLogLevelDebug, "HTTP client url %s init Done", client.endpoint) client.initDone = true return &client, nil } // GetLogLevel Get a readable string representing the log level func (c *HTTPClient) GetLogLevel() string { return c.LogLevelToString(c.LoggerLevel) } // LogLevelToString Convert an integer log level to string func (c *HTTPClient) LogLevelToString(lvl int) string { switch lvl { case HTTPLogLevelPanic: return "panic" case HTTPLogLevelError: return "error" case HTTPLogLevelWarning: return "warning" case HTTPLogLevelInfo: return "info" case HTTPLogLevelDebug: return "debug" } return "Unknown" } // SetLogLevel set the log level from a readable string func (c *HTTPClient) SetLogLevel(lvl string) error { switch strings.ToLower(lvl) { case "panic": c.LoggerLevel = HTTPLogLevelPanic case "error": c.LoggerLevel = HTTPLogLevelError case "warn", "warning": c.LoggerLevel = HTTPLogLevelWarning case "info": c.LoggerLevel = HTTPLogLevelInfo case "debug": c.LoggerLevel = HTTPLogLevelDebug default: return fmt.Errorf("Unknown level") } return nil } // GetClientID returns the id func (c *HTTPClient) GetClientID() string { return c.id } /*** ** High level functions ***/ // Get Send a Get request to client and return directly data of body response func (c *HTTPClient) Get(url string, out interface{}) error { return c._Request("GET", url, nil, out) } // Post Send a Post request to client and return directly data of body response func (c *HTTPClient) Post(url string, in interface{}, out interface{}) error { return c._Request("POST", url, in, out) } // Put Send a Put request to client and return directly data of body response func (c *HTTPClient) Put(url string, in interface{}, out interface{}) error { return c._Request("PUT", url, in, out) } // Delete Send a Delete request to client and return directly data of body response func (c *HTTPClient) Delete(url string, out interface{}) error { return c._Request("DELETE", url, nil, out) } /*** ** Low level functions ***/ // HTTPGet Send a Get request to client and return an error object func (c *HTTPClient) HTTPGet(url string, data *[]byte) error { _, err := c._HTTPRequest("GET", url, nil, data) return err } // HTTPGetWithRes Send a Get request to client and return both response and error func (c *HTTPClient) HTTPGetWithRes(url string, data *[]byte) (*http.Response, error) { return c._HTTPRequest("GET", url, nil, data) } // HTTPPost Send a POST request to client and return an error object func (c *HTTPClient) HTTPPost(url string, body string) error { _, err := c._HTTPRequest("POST", url, &body, nil) return err } // HTTPPostWithRes Send a POST request to client and return both response and error func (c *HTTPClient) HTTPPostWithRes(url string, body string) (*http.Response, error) { return c._HTTPRequest("POST", url, &body, nil) } // HTTPPut Send a PUT request to client and return an error object func (c *HTTPClient) HTTPPut(url string, body string) error { _, err := c._HTTPRequest("PUT", url, &body, nil) return err } // HTTPPutWithRes Send a PUT request to client and return both response and error func (c *HTTPClient) HTTPPutWithRes(url string, body string) (*http.Response, error) { return c._HTTPRequest("PUT", url, &body, nil) } // HTTPDelete Send a DELETE request to client and return an error object func (c *HTTPClient) HTTPDelete(url string) error { _, err := c._HTTPRequest("DELETE", url, nil, nil) return err } // HTTPDeleteWithRes Send a DELETE request to client and return both response and error func (c *HTTPClient) HTTPDeleteWithRes(url string) (*http.Response, error) { return c._HTTPRequest("DELETE", url, nil, nil) } // ResponseToBArray converts an Http response to a byte array func (c *HTTPClient) ResponseToBArray(response *http.Response) []byte { defer response.Body.Close() bytes, err := ioutil.ReadAll(response.Body) if err != nil { c.log(HTTPLogLevelError, "ResponseToBArray failure: %v", err.Error()) } return bytes } /*** ** Private functions ***/ // _HTTPRequest Generic function used by high level function to send requests func (c *HTTPClient) _Request(method string, url string, in interface{}, out interface{}) error { var err error var res *http.Response var body []byte if in != nil { body, err = json.Marshal(in) if err != nil { return err } sb := string(body) res, err = c._HTTPRequest(method, url, &sb, nil) } else { res, err = c._HTTPRequest(method, url, nil, nil) } if err != nil { return err } if res.StatusCode != 200 { return fmt.Errorf("HTTP status %s", res.Status) } // Don't decode response if no out data pointer is nil if out == nil { return nil } return json.Unmarshal(c.ResponseToBArray(res), out) } // _HTTPRequest Generic function that returns a new Request given a method, URL, and optional body and data. func (c *HTTPClient) _HTTPRequest(method, url string, body *string, data *[]byte) (*http.Response, error) { if !c.initDone { if err := c.getCidAndCsrf(); err == nil { c.initDone = true } } var err error var request *http.Request if body != nil { request, err = http.NewRequest(method, c.formatURL(url), bytes.NewBufferString(*body)) } else { request, err = http.NewRequest(method, c.formatURL(url), nil) } if err != nil { return nil, err } res, err := c.handleRequest(request) if err != nil { return res, err } if res.StatusCode != 200 { return res, errors.New(res.Status) } if data != nil { *data = c.ResponseToBArray(res) } return res, nil } func (c *HTTPClient) handleRequest(request *http.Request) (*http.Response, error) { if c.conf.ContentType != "" { request.Header.Set("Content-Type", c.conf.ContentType) } if c.conf.HeaderAPIKeyName != "" && c.apikey != "" { request.Header.Set(c.conf.HeaderAPIKeyName, c.apikey) } if c.conf.HeaderClientKeyName != "" && c.id != "" { request.Header.Set(c.conf.HeaderClientKeyName, c.id) } if c.username != "" || c.password != "" { request.SetBasicAuth(c.username, c.password) } if c.csrf != "" { request.Header.Set("X-CSRF-Token-"+c.id[:5], c.csrf) } c.log(HTTPLogLevelDebug, "HTTP %s %v", request.Method, request.URL) response, err := c.httpClient.Do(request) c.log(HTTPLogLevelDebug, "HTTP RESPONSE: %v\n", response) if err != nil { c.log(HTTPLogLevelInfo, "%v", err) return nil, err } // Detect client ID change cid := response.Header.Get(c.conf.HeaderClientKeyName) if cid != "" && c.id != cid { c.id = cid } // Detect CSR token change for _, item := range response.Cookies() { if c.id != "" && item.Name == "CSRF-Token-"+c.id[:5] { c.csrf = item.Value goto csrffound } } // OK CSRF found csrffound: if response.StatusCode == 404 { return nil, errors.New("Invalid endpoint or API call") } else if response.StatusCode == 401 { return nil, errors.New("Invalid username or password") } else if response.StatusCode == 403 { if c.apikey == "" { // Request a new Csrf for next requests c.getCidAndCsrf() return nil, errors.New("Invalid CSRF token") } return nil, errors.New("Invalid API key") } else if response.StatusCode != 200 { data := make(map[string]interface{}) // Try to decode error field of APIError struct json.Unmarshal(c.ResponseToBArray(response), &data) if err, found := data["error"]; found { return nil, fmt.Errorf(err.(string)) } body := strings.TrimSpace(string(c.ResponseToBArray(response))) if body != "" { return nil, fmt.Errorf(body) } return nil, errors.New("Unknown HTTP status returned: " + response.Status) } return response, nil } // formatURL Build full url by concatenating all parts func (c *HTTPClient) formatURL(endURL string) string { url := c.endpoint if !strings.HasSuffix(url, "/") { url += "/" } url += strings.TrimLeft(c.conf.URLPrefix, "/") if !strings.HasSuffix(url, "/") { url += "/" } return url + strings.TrimLeft(endURL, "/") } // Send request to retrieve Client id and/or CSRF token func (c *HTTPClient) getCidAndCsrf() error { // Don't use cid + csrf when apikey is set if c.apikey != "" { return nil } request, err := http.NewRequest("GET", c.endpoint, nil) if err != nil { return err } if _, err := c.handleRequest(request); err != nil { return err } if c.id == "" { return errors.New("Failed to get device ID") } if !c.conf.CsrfDisable && c.csrf == "" { return errors.New("Failed to get CSRF token") } return nil } // log Internal logger function func (c *HTTPClient) log(level int, format string, args ...interface{}) { if level > c.LoggerLevel { return } sLvl := strings.ToUpper(c.LogLevelToString(level)) fmt.Fprintf(c.LoggerOut, sLvl+": "+c.LoggerPrefix+format+"\n", args...) }