Initial main commit.
[src/xds/xds-server.git] / lib / common / httpclient.go
1 package common
2
3 import (
4         "bytes"
5         "crypto/tls"
6         "encoding/json"
7         "errors"
8         "fmt"
9         "io/ioutil"
10         "net/http"
11         "strings"
12 )
13
14 type HTTPClient struct {
15         httpClient http.Client
16         endpoint   string
17         apikey     string
18         username   string
19         password   string
20         id         string
21         csrf       string
22         conf       HTTPClientConfig
23 }
24
25 type HTTPClientConfig struct {
26         URLPrefix           string
27         HeaderAPIKeyName    string
28         Apikey              string
29         HeaderClientKeyName string
30         CsrfDisable         bool
31 }
32
33 // Inspired by syncthing/cmd/cli
34
35 const insecure = false
36
37 // HTTPNewClient creates a new HTTP client to deal with Syncthing
38 func HTTPNewClient(baseURL string, cfg HTTPClientConfig) (*HTTPClient, error) {
39
40         // Create w new Http client
41         httpClient := http.Client{
42                 Transport: &http.Transport{
43                         TLSClientConfig: &tls.Config{
44                                 InsecureSkipVerify: insecure,
45                         },
46                 },
47         }
48         client := HTTPClient{
49                 httpClient: httpClient,
50                 endpoint:   baseURL,
51                 apikey:     cfg.Apikey,
52                 conf:       cfg,
53                 /* TODO - add user + pwd support
54                 username:   c.GlobalString("username"),
55                 password:   c.GlobalString("password"),
56                 */
57         }
58
59         if client.apikey == "" {
60                 if err := client.getCidAndCsrf(); err != nil {
61                         return nil, err
62                 }
63         }
64         return &client, nil
65 }
66
67 // Send request to retrieve Client id and/or CSRF token
68 func (c *HTTPClient) getCidAndCsrf() error {
69         request, err := http.NewRequest("GET", c.endpoint, nil)
70         if err != nil {
71                 return err
72         }
73         if _, err := c.handleRequest(request); err != nil {
74                 return err
75         }
76         if c.id == "" {
77                 return errors.New("Failed to get device ID")
78         }
79         if !c.conf.CsrfDisable && c.csrf == "" {
80                 return errors.New("Failed to get CSRF token")
81         }
82         return nil
83 }
84
85 // GetClientID returns the id
86 func (c *HTTPClient) GetClientID() string {
87         return c.id
88 }
89
90 // formatURL Build full url by concatenating all parts
91 func (c *HTTPClient) formatURL(endURL string) string {
92         url := c.endpoint
93         if !strings.HasSuffix(url, "/") {
94                 url += "/"
95         }
96         url += strings.TrimLeft(c.conf.URLPrefix, "/")
97         if !strings.HasSuffix(url, "/") {
98                 url += "/"
99         }
100         return url + strings.TrimLeft(endURL, "/")
101 }
102
103 // HTTPGet Send a Get request to client and return an error object
104 func (c *HTTPClient) HTTPGet(url string, data *[]byte) error {
105         _, err := c.HTTPGetWithRes(url, data)
106         return err
107 }
108
109 // HTTPGetWithRes Send a Get request to client and return both response and error
110 func (c *HTTPClient) HTTPGetWithRes(url string, data *[]byte) (*http.Response, error) {
111         request, err := http.NewRequest("GET", c.formatURL(url), nil)
112         if err != nil {
113                 return nil, err
114         }
115         res, err := c.handleRequest(request)
116         if err != nil {
117                 return res, err
118         }
119         if res.StatusCode != 200 {
120                 return res, errors.New(res.Status)
121         }
122
123         *data = c.responseToBArray(res)
124
125         return res, nil
126 }
127
128 // HTTPPost Send a POST request to client and return an error object
129 func (c *HTTPClient) HTTPPost(url string, body string) error {
130         _, err := c.HTTPPostWithRes(url, body)
131         return err
132 }
133
134 // HTTPPostWithRes Send a POST request to client and return both response and error
135 func (c *HTTPClient) HTTPPostWithRes(url string, body string) (*http.Response, error) {
136         request, err := http.NewRequest("POST", c.formatURL(url), bytes.NewBufferString(body))
137         if err != nil {
138                 return nil, err
139         }
140         res, err := c.handleRequest(request)
141         if err != nil {
142                 return res, err
143         }
144         if res.StatusCode != 200 {
145                 return res, errors.New(res.Status)
146         }
147         return res, nil
148 }
149
150 func (c *HTTPClient) responseToBArray(response *http.Response) []byte {
151         defer response.Body.Close()
152         bytes, err := ioutil.ReadAll(response.Body)
153         if err != nil {
154                 // TODO improved error reporting
155                 fmt.Println("ERROR: " + err.Error())
156         }
157         return bytes
158 }
159
160 func (c *HTTPClient) handleRequest(request *http.Request) (*http.Response, error) {
161         if c.conf.HeaderAPIKeyName != "" && c.apikey != "" {
162                 request.Header.Set(c.conf.HeaderAPIKeyName, c.apikey)
163         }
164         if c.conf.HeaderClientKeyName != "" && c.id != "" {
165                 request.Header.Set(c.conf.HeaderClientKeyName, c.id)
166         }
167         if c.username != "" || c.password != "" {
168                 request.SetBasicAuth(c.username, c.password)
169         }
170         if c.csrf != "" {
171                 request.Header.Set("X-CSRF-Token-"+c.id[:5], c.csrf)
172         }
173
174         response, err := c.httpClient.Do(request)
175         if err != nil {
176                 return nil, err
177         }
178
179         // Detect client ID change
180         cid := response.Header.Get(c.conf.HeaderClientKeyName)
181         if cid != "" && c.id != cid {
182                 c.id = cid
183         }
184
185         // Detect CSR token change
186         for _, item := range response.Cookies() {
187                 if item.Name == "CSRF-Token-"+c.id[:5] {
188                         c.csrf = item.Value
189                         goto csrffound
190                 }
191         }
192         // OK CSRF found
193 csrffound:
194
195         if response.StatusCode == 404 {
196                 return nil, errors.New("Invalid endpoint or API call")
197         } else if response.StatusCode == 401 {
198                 return nil, errors.New("Invalid username or password")
199         } else if response.StatusCode == 403 {
200                 if c.apikey == "" {
201                         // Request a new Csrf for next requests
202                         c.getCidAndCsrf()
203                         return nil, errors.New("Invalid CSRF token")
204                 }
205                 return nil, errors.New("Invalid API key")
206         } else if response.StatusCode != 200 {
207                 data := make(map[string]interface{})
208                 // Try to decode error field of APIError struct
209                 json.Unmarshal(c.responseToBArray(response), &data)
210                 if err, found := data["error"]; found {
211                         return nil, fmt.Errorf(err.(string))
212                 } else {
213                         body := strings.TrimSpace(string(c.responseToBArray(response)))
214                         if body != "" {
215                                 return nil, fmt.Errorf(body)
216                         }
217                 }
218                 return nil, errors.New("Unknown HTTP status returned: " + response.Status)
219         }
220         return response, nil
221 }