/
http.go
267 lines (231 loc) · 8.07 KB
/
http.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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
package adapters
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"github.com/pkg/errors"
"github.com/smartcontractkit/chainlink/core/services/keystore"
"github.com/smartcontractkit/chainlink/core/store"
"github.com/smartcontractkit/chainlink/core/store/models"
"github.com/smartcontractkit/chainlink/core/store/orm"
"github.com/smartcontractkit/chainlink/core/utils"
)
// HTTPGet requires a URL which is used for a GET request when the adapter is called.
type HTTPGet struct {
URL models.WebURL `json:"url"`
GET models.WebURL `json:"get"`
Headers http.Header `json:"headers"`
QueryParams QueryParameters `json:"queryParams"`
ExtendedPath ExtendedPath `json:"extPath"`
AllowUnrestrictedNetworkAccess bool `json:"-"`
}
// TaskType returns the type of Adapter.
func (hga *HTTPGet) TaskType() models.TaskType {
return TaskTypeHTTPGet
}
// Perform ensures that the adapter's URL responds to a GET request without
// errors and returns the response body as the "value" field of the result.
func (hga *HTTPGet) Perform(input models.RunInput, store *store.Store, _ *keystore.Master) models.RunOutput {
request, err := hga.GetRequest()
if err != nil {
return models.NewRunOutputError(err)
}
httpConfig := defaultHTTPConfig(store.Config)
httpConfig.AllowUnrestrictedNetworkAccess = hga.AllowUnrestrictedNetworkAccess
return sendRequest(input, request, httpConfig)
}
// GetURL retrieves the GET field if set otherwise returns the URL field
func (hga *HTTPGet) GetURL() string {
if hga.GET.String() != "" {
return hga.GET.String()
}
return hga.URL.String()
}
// GetRequest returns the HTTP request including query parameters and headers
func (hga *HTTPGet) GetRequest() (*http.Request, error) {
request, err := http.NewRequest("GET", hga.GetURL(), nil)
if err != nil {
return nil, err
}
appendExtendedPath(request, hga.ExtendedPath)
appendQueryParams(request, hga.QueryParams)
setHeaders(request, hga.Headers, "")
return request, nil
}
// HTTPPost requires a URL which is used for a POST request when the adapter is called.
type HTTPPost struct {
URL models.WebURL `json:"url"`
POST models.WebURL `json:"post"`
Headers http.Header `json:"headers"`
QueryParams QueryParameters `json:"queryParams"`
Body *string `json:"body,omitempty"`
ExtendedPath ExtendedPath `json:"extPath"`
AllowUnrestrictedNetworkAccess bool `json:"-"`
}
// TaskType returns the type of Adapter.
func (hpa *HTTPPost) TaskType() models.TaskType {
return TaskTypeHTTPPost
}
// Perform ensures that the adapter's URL responds to a POST request without
// errors and returns the response body as the "value" field of the result.
func (hpa *HTTPPost) Perform(input models.RunInput, store *store.Store, _ *keystore.Master) models.RunOutput {
request, err := hpa.GetRequest(input.Data().String())
if err != nil {
return models.NewRunOutputError(err)
}
httpConfig := defaultHTTPConfig(store.Config)
httpConfig.AllowUnrestrictedNetworkAccess = hpa.AllowUnrestrictedNetworkAccess
return sendRequest(input, request, httpConfig)
}
// GetURL retrieves the POST field if set otherwise returns the URL field
func (hpa *HTTPPost) GetURL() string {
if hpa.POST.String() != "" {
return hpa.POST.String()
}
return hpa.URL.String()
}
// GetRequest takes the request body and returns the HTTP request including
// query parameters and headers.
//
// HTTPPost's Body parameter overrides the given argument if present.
func (hpa *HTTPPost) GetRequest(body string) (*http.Request, error) {
if hpa.Body != nil {
body = *hpa.Body
}
reqBody := bytes.NewBufferString(body)
request, err := http.NewRequest("POST", hpa.GetURL(), reqBody)
if err != nil {
return nil, err
}
appendExtendedPath(request, hpa.ExtendedPath)
appendQueryParams(request, hpa.QueryParams)
setHeaders(request, hpa.Headers, "application/json")
return request, nil
}
func appendExtendedPath(request *http.Request, extPath ExtendedPath) {
// Remove early empty extPath entries
extPaths := []string(extPath[:])
for _, path := range extPaths {
if len(path) != 0 {
break
}
extPaths = extPaths[1:]
}
if len(extPaths) == 0 {
return
}
if strings.HasPrefix(extPath[0], "/") || strings.HasSuffix(request.URL.Path, "/") {
request.URL.Path = request.URL.Path + path.Join(extPaths...)
return
}
request.URL.Path = request.URL.Path + "/" + path.Join(extPaths...)
}
func appendQueryParams(request *http.Request, queryParams QueryParameters) {
q := request.URL.Query()
for k, v := range queryParams {
if !keyExists(k, q) {
q.Add(k, v[0])
}
}
request.URL.RawQuery = q.Encode()
}
func keyExists(key string, query url.Values) bool {
_, ok := query[key]
return ok
}
func setHeaders(request *http.Request, headers http.Header, contentType string) {
if headers != nil {
request.Header = headers
}
if contentType != "" {
request.Header.Set("Content-Type", contentType)
}
}
func sendRequest(input models.RunInput, request *http.Request, config utils.HTTPRequestConfig) models.RunOutput {
httpRequest := utils.HTTPRequest{
Request: request,
Config: config,
}
bytes, statusCode, _, err := httpRequest.SendRequest(context.TODO())
if err != nil {
return models.NewRunOutputError(err)
}
responseBody := string(bytes)
// This is either a client error caused on our end or a server error that persists even after retrying.
// Either way, there is no way for us to complete the run with a result.
if statusCode >= 400 {
return models.NewRunOutputError(errors.New(responseBody))
}
return models.NewRunOutputCompleteWithResult(responseBody, input.ResultCollection())
}
// QueryParameters are the keys and values to append to the URL
type QueryParameters url.Values
// UnmarshalJSON implements the Unmarshaler interface
func (qp *QueryParameters) UnmarshalJSON(input []byte) error {
var strs []string
var err error
// input is a string like "someKey0=someVal0&someKey1=someVal1"
if utils.IsQuoted(input) {
var decoded string
unmErr := json.Unmarshal(input, &decoded)
if unmErr != nil {
return fmt.Errorf("unable to unmarshal query parameters: %s", input)
}
strs = strings.FieldsFunc(trimQuestion(decoded), splitQueryString)
// input is an array of strings like
// ["someKey0", "someVal0", "someKey1", "someVal1"]
} else if err = json.Unmarshal(input, &strs); err != nil {
return fmt.Errorf("unable to unmarshal query parameters: %s", input)
}
values, err := buildValues(strs)
if err != nil {
return fmt.Errorf("unable to build query parameters: %s", input)
}
*qp = QueryParameters(values)
return err
}
func splitQueryString(r rune) bool {
return r == '=' || r == '&'
}
func trimQuestion(input string) string {
return strings.Replace(input, "?", "", -1)
}
func buildValues(input []string) (url.Values, error) {
values := url.Values{}
if len(input)%2 != 0 {
return nil, fmt.Errorf("invalid number of parameters: %s", input)
}
for i := 0; i < len(input); i = i + 2 {
values.Add(input[i], input[i+1])
}
return values, nil
}
// ExtendedPath is the path to append to a base URL
type ExtendedPath []string
// UnmarshalJSON implements the Unmarshaler interface
func (ep *ExtendedPath) UnmarshalJSON(input []byte) error {
values := []string{}
var err error
// if input is a string like "a/b/c"
if utils.IsQuoted(input) {
values = strings.Split(string(utils.RemoveQuotes(input)), "/")
// if input is an array of strings like ["a", "b", "c"]
} else {
err = json.Unmarshal(input, &values)
}
*ep = ExtendedPath(values)
return err
}
func defaultHTTPConfig(config orm.ConfigReader) utils.HTTPRequestConfig {
return utils.HTTPRequestConfig{
Timeout: config.DefaultHTTPTimeout().Duration(),
MaxAttempts: config.DefaultMaxHTTPAttempts(),
SizeLimit: config.DefaultHTTPLimit(),
AllowUnrestrictedNetworkAccess: config.DefaultHTTPAllowUnrestrictedNetworkAccess(),
}
}