/
rapic.go
249 lines (206 loc) · 5.66 KB
/
rapic.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
package rapic
import (
"errors"
"net/http"
"net/http/cookiejar"
"net/url"
"strconv"
"strings"
"time"
"unicode"
"golang.org/x/net/publicsuffix"
)
type RequestMethods string
const (
METHOD_CONNECT RequestMethods = "CONNECT"
METHOD_DELETE RequestMethods = "DELETE"
METHOD_GET RequestMethods = "GET"
METHOD_HEAD RequestMethods = "HEAD"
METHOD_OPTIONS RequestMethods = "OPTIONS"
METHOD_PATCH RequestMethods = "PATCH"
METHOD_POST RequestMethods = "POST"
METHOD_PUT RequestMethods = "PUT"
METHOD_TRACE RequestMethods = "TRACE"
)
type Client struct {
Settings Settings
URL *url.URL
Query url.Values
Header http.Header
Cookie CookieJar
Authorization Authorization
}
type Settings struct {
// Follow is for auto follow the HTTP 3xx as redirects (def: true)
Follow bool
// FollowAuth Keep authorization header when redirect to a different host (def: false)
FollowAuth bool
// FollowReferer keep the referer header when a redirect happens (def: true)
FollowReferer bool
// MaxRedirect to set the maximum number of redirects to follow (def: 2)
MaxRedirect uint8
// AutoCookie store automatically cookie's if a set-cookie is found in header (def: true)
AutoCookie bool
// Retry if request is not 2xx or 3xx (def: true)
Retry bool
// MaxRetry to set the maximum number of retry (def: 1)
MaxRetry uint8
// WaitRetry to set the time to wait before retry in second (def: 10)
WaitRetry time.Duration
}
// TODO
type Response struct {
}
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}
// reqIsSuccess checks whether a request is successful.
// a request is successful when the http code is >= 100 or < 400
// with an exception at 401, in accordance with Digest Auth RFC7616-3.3.
func reqIsSuccess(code int) bool {
if (code >= 100 && code < 400) || code == 401 {
return true
}
return false
}
func pathFormatting(p string) string {
if !strings.HasPrefix(p, "/") {
p = "/" + p
} else if strings.HasPrefix(p, "//") {
p = strings.TrimPrefix(p, "/")
}
return strings.TrimSuffix(p, "/")
}
// New return an initialized Client struct.
// This is the main client, you can make request from it
// or create children for new request with their inheritance
//
// This function require a Endpoint URL with scheme Path.
// You can also specify a Auth with the optinal Authorization arg
func New(endpoint string, auth ...Authorization) (main Client, err error) {
main.Settings = Settings{
Follow: true,
FollowAuth: false,
FollowReferer: true,
MaxRedirect: 2,
AutoCookie: true,
Retry: true,
MaxRetry: 1,
WaitRetry: 10,
}
if main.URL, err = url.Parse(endpoint); err != nil {
return
}
if main.Query, err = url.ParseQuery(main.URL.RawQuery); err != nil {
return
}
main.Header = make(http.Header)
main.Authorization.Digest.Algorithm = DIGEST_SHA256
if len(auth) > 0 {
main.Authorization = auth[0]
}
main.Cookie.CookieJar, err = cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
return
}
// NewChild create a children client with parent inheritance.
// This new client are isolated from the parent.
//
// Can take a relative path from parent
func (parent *Client) NewChild(path ...string) (child Client) {
child = *parent
if len(path) > 0 {
child.URL.Path = child.URL.Path + pathFormatting(path[0])
}
return
}
// FlushHeader remove all stored header
func (c *Client) FlushHeader() {
c.Header = http.Header{}
}
// FlushQuery remove all stored query
func (c *Client) FlushQuery() {
c.Query = url.Values{}
}
// TODO
func (c *Client) Request(method RequestMethods, body *string, res *http.Response) (err error) {
req := &http.Request{}
c.URL.RawQuery = c.Query.Encode()
if body == nil {
if req, err = http.NewRequest(string(method), c.URL.String(), nil); err != nil {
return
}
} else {
if req, err = http.NewRequest(string(method), c.URL.String(), strings.NewReader(*body)); err != nil {
return
}
}
// Auth
if c.Authorization.Scheme != "" {
switch c.Authorization.Scheme {
case AUTH_BASIC:
req.SetBasicAuth(c.Authorization.Username, c.Authorization.Password)
case AUTH_BEARER:
c.Header.Set("Authorization", "Bearer "+c.Authorization.Token)
case AUTH_DIGEST:
if c.Authorization.Digest.URI != "*" {
c.Authorization.Digest.URI = c.URL.Path
}
if c.Authorization.Digest.Opaque == "" {
c.Authorization.Digest.Opaque = c.URL.Opaque
}
c.Header.Set("Authorization", "Digest "+c.Authorization.Digest.Build(
c.Authorization.Username,
c.Authorization.Password,
body,
method,
))
case AUTH_CUSTOM:
c.Header.Set("Authorization", c.Authorization.Value)
default:
err = errors.New("unknow Authorization Scheme")
return
}
}
// Header
req.Header = c.Header
// Request
for i := uint8(0); ; i++ {
cli := &http.Client{
Jar: c.Cookie,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if !c.Settings.Follow {
return http.ErrUseLastResponse
}
if !c.Settings.FollowReferer {
req.Header.Del("Referer")
}
if req.URL.Host != c.URL.Host && !c.Settings.FollowAuth {
req.Header.Del("Authorization")
}
nb := len(via)
if nb <= int(c.Settings.MaxRedirect) {
return errors.New("stopped after " + strconv.Itoa(nb) + " redirects")
}
return nil
},
}
res, err = cli.Do(req)
if (err == nil && reqIsSuccess(res.StatusCode)) || c.Settings.MaxRetry <= i {
break
}
if c.Settings.WaitRetry > 0 {
time.Sleep(c.Settings.WaitRetry * time.Second)
}
}
if c.Settings.AutoCookie {
if err == nil {
c.Cookie.Add(c.URL.String(), cookieParse(res.Header.Values("Set-Cookie"))...)
}
}
return
}