-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathinterface.go
226 lines (191 loc) · 7.54 KB
/
interface.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package starr
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"golang.org/x/net/publicsuffix"
)
// APIer is used by the sub packages to allow mocking the http methods in tests.
// It changes once in a while, so avoid making hard dependencies on it.
type APIer interface {
GetInitializeJS(ctx context.Context) (*InitializeJS, error)
// Login is used for non-API paths, like downloading backups or the initialize.js file.
Login(ctx context.Context) error
// Normal data, returns response. Do not use these in starr app methods.
// These methods are generally for non-api paths and will not ensure an /api uri prefix.
Get(ctx context.Context, req Request) (*http.Response, error) // Get request; Params are optional.
Post(ctx context.Context, req Request) (*http.Response, error) // Post request; Params should contain io.Reader.
Put(ctx context.Context, req Request) (*http.Response, error) // Put request; Params should contain io.Reader.
Delete(ctx context.Context, req Request) (*http.Response, error) // Delete request; Params are optional.
// Normal data, unmarshals into provided interface. Use these because they close the response body.
GetInto(ctx context.Context, req Request, output interface{}) error // API GET Request.
PostInto(ctx context.Context, req Request, output interface{}) error // API POST Request.
PutInto(ctx context.Context, req Request, output interface{}) error // API PUT Request.
DeleteAny(ctx context.Context, req Request) error // API Delete request.
}
// Config must satisfy the APIer struct.
var _ APIer = (*Config)(nil)
// InitializeJS is the data contained in the initialize.js file.
type InitializeJS struct {
App string
APIRoot string
APIKey string
Release string
Version string
InstanceName string
Theme string
Branch string
Analytics string
UserHash string
URLBase string
IsProduction bool
}
// Login POSTs to the login form in a Starr app and saves the authentication cookie for future use.
func (c *Config) Login(ctx context.Context) error {
if c.Client.Jar == nil {
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
return fmt.Errorf("cookiejar.New(publicsuffix): %w", err)
}
c.Client.Jar = jar
}
params := make(url.Values)
params.Add("username", c.Username)
params.Add("password", c.Password)
req := Request{URI: "/login", Body: bytes.NewBufferString(params.Encode())}
codeErr := &ReqError{}
resp, err := c.req(ctx, http.MethodPost, req)
if err != nil {
if !errors.As(err, &codeErr) { // pointer to a pointer, yup.
return fmt.Errorf("invalid reply authenticating as user '%s': %w", c.Username, err)
}
} else {
// Protect a nil map in case we don't get an error (which should be impossible).
codeErr.Header = resp.Header
}
closeResp(resp)
if u, _ := url.Parse(c.URL); strings.Contains(codeErr.Get("Location"), "loginFailed") ||
len(c.Client.Jar.Cookies(u)) == 0 {
return fmt.Errorf("%w: authenticating as user '%s' failed", ErrRequestError, c.Username)
}
c.cookie = true
return nil
}
// Get makes a GET http request and returns the body.
func (c *Config) Get(ctx context.Context, req Request) (*http.Response, error) {
return c.Req(ctx, http.MethodGet, req)
}
// Post makes a POST http request and returns the body.
func (c *Config) Post(ctx context.Context, req Request) (*http.Response, error) {
return c.Req(ctx, http.MethodPost, req)
}
// Put makes a PUT http request and returns the body.
func (c *Config) Put(ctx context.Context, req Request) (*http.Response, error) {
return c.Req(ctx, http.MethodPut, req)
}
// Delete makes a DELETE http request and returns the body.
func (c *Config) Delete(ctx context.Context, req Request) (*http.Response, error) {
return c.Req(ctx, http.MethodDelete, req)
}
// GetInto performs an HTTP GET against an API path and
// unmarshals the payload into the provided pointer interface.
func (c *Config) GetInto(ctx context.Context, req Request, output interface{}) error {
resp, err := c.api(ctx, http.MethodGet, req)
return decode(output, resp, err)
}
// PostInto performs an HTTP POST against an API path and
// unmarshals the payload into the provided pointer interface.
func (c *Config) PostInto(ctx context.Context, req Request, output interface{}) error {
resp, err := c.api(ctx, http.MethodPost, req)
return decode(output, resp, err)
}
// PutInto performs an HTTP PUT against an API path and
// unmarshals the payload into the provided pointer interface.
func (c *Config) PutInto(ctx context.Context, req Request, output interface{}) error {
resp, err := c.api(ctx, http.MethodPut, req)
return decode(output, resp, err)
}
// DeleteAny performs an HTTP DELETE against an API path, output is ignored.
func (c *Config) DeleteAny(ctx context.Context, req Request) error {
resp, err := c.api(ctx, http.MethodDelete, req)
closeResp(resp)
return err
}
// decode is an extra procedure to check an error and decode the JSON resp.Body payload.
func decode(output interface{}, resp *http.Response, err error) error {
if err != nil {
return err
} else if output == nil {
closeResp(resp) // read the body and close it.
return fmt.Errorf("this is a Starr library bug: %w", ErrNilInterface)
}
defer resp.Body.Close()
if err = json.NewDecoder(resp.Body).Decode(output); err != nil {
return fmt.Errorf("decoding Starr JSON response body: %w", err)
}
return nil
}
// GetInitializeJS returns the data from the initialize.js file.
// If the instance requires authentication, you must call Login() before this method.
func (c *Config) GetInitializeJS(ctx context.Context) (*InitializeJS, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.URL+"initialize.js", nil)
if err != nil {
return nil, fmt.Errorf("http.NewRequestWithContext(initialize.js): %w", err)
}
resp, err := c.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("httpClient.Do(req): %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, resp.Body)
return nil, &ReqError{Code: resp.StatusCode}
}
return readInitializeJS(resp.Body)
}
func readInitializeJS(input io.Reader) (*InitializeJS, error) { //nolint:cyclop
output := &InitializeJS{}
scanner := bufio.NewScanner(input)
for scanner.Scan() {
switch split := strings.Fields(scanner.Text()); {
case len(split) < 2: //nolint:mnd
continue
case split[0] == "apiRoot:":
output.APIRoot = strings.Trim(split[1], `"',`)
case split[0] == "apiKey:":
output.APIKey = strings.Trim(split[1], `"',`)
case split[0] == "version:":
output.Version = strings.Trim(split[1], `"',`)
case split[0] == "release:":
output.Release = strings.Trim(split[1], `"',`)
case split[0] == "instanceName:":
output.InstanceName = strings.Trim(split[1], `"',`)
case split[0] == "theme:":
output.Theme = strings.Trim(split[1], `"',`)
case split[0] == "branch:":
output.Branch = strings.Trim(split[1], `"',`)
case split[0] == "analytics:":
output.Analytics = strings.Trim(split[1], `"',`)
case split[0] == "userHash:":
output.UserHash = strings.Trim(split[1], `"',`)
case split[0] == "urlBase:":
output.URLBase = strings.Trim(split[1], `"',`)
case split[0] == "isProduction:":
output.IsProduction = strings.Trim(split[1], `"',`) == "true"
case strings.HasPrefix(split[0], "window."):
output.App = strings.TrimPrefix(split[0], "window.")
}
}
if err := scanner.Err(); err != nil {
return output, fmt.Errorf("scanning HTTP response: %w", err)
}
return output, nil
}