diff options
Diffstat (limited to 'httpclient.go')
-rw-r--r-- | httpclient.go | 411 |
1 files changed, 411 insertions, 0 deletions
diff --git a/httpclient.go b/httpclient.go new file mode 100644 index 0000000..f4880e6 --- /dev/null +++ b/httpclient.go @@ -0,0 +1,411 @@ +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...) +} |