summaryrefslogtreecommitdiffstats
path: root/httpclient.go
diff options
context:
space:
mode:
authorSebastien Douheret <sebastien.douheret@iot.bzh>2019-04-04 23:45:56 +0200
committerSebastien Douheret <sebastien.douheret@iot.bzh>2019-04-04 23:45:56 +0200
commit89ea6ebd3671e6ebbf6101525a5416427806f318 (patch)
tree5db52146365a9c2c439b77485f938cc8c2e3a727 /httpclient.go
parentee147062c3bebed83e34bf5ce71019c95f62b96f (diff)
Fixed package tree and go mod filev0.5.0
Change-Id: I1047094d5b80d0622e2c2ce674979f18207b8c0f Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
Diffstat (limited to 'httpclient.go')
-rw-r--r--httpclient.go411
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...)
+}