-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
333 lines (311 loc) · 10.3 KB
/
main.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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"strings"
"time"
"golang.org/x/exp/slices"
"github.com/alecthomas/kong"
"github.com/hokaccha/go-prettyjson"
"github.com/imroc/req/v3"
)
// Program version
const gurlVersion = "1.7.0"
// Cli arguments
var cli struct {
// Args
Url string `arg:"" help:"Url to access."`
// Flags
Auth string `help:"Basic HTTP authentication in the format username:password." placeholder:"auth" short:"u"`
BearerToken string `help:"Set bearer auth token." placeholder:"token" short:"b"`
CACert string `help:"CA certificate file." placeholder:"file" type:"path"`
ClientCert []string `help:"Client cert and key files separated by comma: \"cert.pem,key.pem\"." placeholder:"cert-file,key-file" type:"path"`
ContentType string `help:"Content-Type http header, default is application/json for POST, PUT and PATCH methods." placeholder:"content" short:"c"`
Data string `help:"Data payload (request body)." xor:"data" placeholder:"payload" short:"d"`
DataFile string `help:"Read data payload from file." xor:"data" placeholder:"file" short:"f" type:"path"`
DisableRedirect bool `help:"Disable redirects." default:"false"`
ForceHttp1 bool `help:"Force HTTP/1.1 to be used." default:"false"`
Headers map[string]string `help:"HTTP headers in the format: \"header1=value1;header2=value2\"." placeholder:"h1=v1;h2=v2" short:"H"`
Impersonate string `help:"Fully impersonate chrome, firefox or safari browser (this will automatically set headers, headers order and tls fingerprint)." enum:"chrome, firefox, safari, none" default:"none"`
Insecure bool `help:"Allow insecure SSL connections." short:"k" default:"false"`
Method string `help:"Http method: GET, HEAD, POST, PUT, PATCH, DELETE or OPTIONS." enum:"GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" short:"X" default:"GET"`
OutputFile string `help:"Save response to file." short:"o" placeholder:"file" type:"path"`
Proxy string `help:"Proxy to use, e.g.: \"http://user:pass@myproxy:8080\"." placeholder:"proxy"`
RawResponse bool `help:"Print raw response string (disable json prettify)." default:"false"`
Retries int `help:"Number of retries in case of errors and http status code >= 500." short:"r" default:"0"`
Timeout int `help:"Timeout in milliseconds." short:"t" default:"10000"`
TlsFinger string `help:"TLS Fingerprint: chrome, firefox, edge, safari, ios, android, random or go." enum:"chrome, firefox, edge, safari, ios, android, random, go" default:"go"`
Trace bool `help:"Show tracing/performance information." default:"false"`
UserAgent string `help:"Set User-Agent http header." placeholder:"agent" short:"A"`
Verbose bool `help:"Enable verbose/debug mode." short:"v" default:"false"`
Version kong.VersionFlag `help:"Show version and exit." short:"V"`
}
// Set application/json content-type http header for POST, PUT and PATCH methods
func setContentHeader(httpMethod string, request *req.Request) {
methods := []string{"POST", "PUT", "PATCH"}
if slices.Contains(methods, httpMethod) {
request.SetContentType("application/json; charset=utf-8")
}
}
// Configure our http request
func configRequest(request *req.Request) {
// Set default http scheme if no scheme is provided
request.GetClient().SetScheme("http")
// Set client timeout
request.GetClient().SetTimeout(time.Duration(cli.Timeout) * time.Millisecond)
// Set application/json content-type for POST, PUT and PATCH methods
setContentHeader(cli.Method, request)
if cli.Auth != "" {
splitAuth := strings.Split(cli.Auth, ":")
user, pass := splitAuth[0], splitAuth[1]
request.SetBasicAuth(user, pass)
}
if cli.BearerToken != "" {
request.SetBearerAuthToken(cli.BearerToken)
}
if cli.CACert != "" {
request.GetClient().SetRootCertsFromFile(cli.CACert)
}
if len(cli.ClientCert) > 1 {
request.GetClient().SetCertFromFile(cli.ClientCert[0], cli.ClientCert[1])
}
if cli.DisableRedirect {
request.GetClient().SetRedirectPolicy(req.NoRedirectPolicy())
}
if cli.ForceHttp1 {
request.GetClient().EnableForceHTTP1()
}
if len(cli.Headers) > 0 {
request.SetHeaders(cli.Headers)
}
if cli.Insecure {
request.GetClient().EnableInsecureSkipVerify()
}
if cli.OutputFile != "" {
request.SetOutputFile(cli.OutputFile)
// Register callback to show download progress
ProgressCallback := func(info req.DownloadInfo) {
if info.Response.Response != nil {
log.Printf("Downloading %.2f%%", float64(info.DownloadedSize)/float64(info.Response.ContentLength)*100.0)
}
}
request.SetDownloadCallback(ProgressCallback)
}
if cli.Proxy != "" {
request.GetClient().SetProxyURL(cli.Proxy)
}
if cli.Retries > 0 {
request.SetRetryCount(cli.Retries)
// Exponential backoff
request.SetRetryBackoffInterval(1*time.Second, 5*time.Second)
// Retry in case of errors or http status >= 500
request.AddRetryCondition(func(resp *req.Response, err error) bool {
return err != nil || resp.StatusCode >= 500
})
// Log to stderr if a retry occurs
request.AddRetryHook(func(resp *req.Response, err error) {
req := resp.Request.RawRequest
log.Printf("Retrying %v request to %v", req.Method, req.URL)
})
}
// Sites to check finger hash:
// - https://tls.peet.ws/api/clean
// - https://tools.scrapfly.io/api/fp/ja3
if cli.TlsFinger != "go" {
switch cli.TlsFinger {
case "chrome":
request.GetClient().SetTLSFingerprintChrome()
case "firefox":
request.GetClient().SetTLSFingerprintFirefox()
case "edge":
request.GetClient().SetTLSFingerprintEdge()
case "safari":
request.GetClient().SetTLSFingerprintSafari()
case "ios":
request.GetClient().SetTLSFingerprintIOS()
case "android":
request.GetClient().SetTLSFingerprintAndroid()
case "random":
request.GetClient().SetTLSFingerprintRandomized()
}
}
if cli.Trace {
request.GetClient().EnableTraceAll()
}
if cli.UserAgent != "" {
request.GetClient().SetUserAgent(cli.UserAgent)
} else {
request.GetClient().SetUserAgent(fmt.Sprintf("gurl %v", gurlVersion))
}
if cli.Verbose {
request.GetClient().EnableDumpAllWithoutBody().EnableDebugLog()
}
// Set impersonate as the last step to override possible earlier configurations
if cli.Impersonate != "none" {
switch cli.Impersonate {
case "chrome":
request.GetClient().ImpersonateChrome()
case "firefox":
request.GetClient().ImpersonateFirefox()
case "safari":
request.GetClient().ImpersonateSafari()
}
}
}
// Print raw string response or a prettified json if possible
func printResponse(rawStr string) {
if cli.RawResponse {
fmt.Print(rawStr)
return
}
var jsonObj map[string]interface{}
err := json.Unmarshal([]byte(rawStr), &jsonObj)
if err != nil {
fmt.Print(rawStr)
} else {
prettyJson, _ := prettyjson.Marshal(jsonObj)
fmt.Print(string(prettyJson))
}
}
// Print trace information
func showTraceInfo(resp *req.Response) {
if cli.Trace {
trace := resp.TraceInfo()
fmt.Print("\n\n------- TRACE INFO -------\n")
fmt.Println(trace)
fmt.Printf("\n%v\n", trace.Blame())
}
}
// Log error and quit if an error occurred
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}
// Read file content
func readFile(filename string) string {
content, err := os.ReadFile(filename)
checkErr(err)
return string(content)
}
// Do HEAD request
func doHeadRequest(request *req.Request) *req.Response {
// Always dump headers for HEAD requests
request.GetClient().EnableDumpAllWithoutBody()
resp, err := request.Head(cli.Url)
checkErr(err)
return resp
}
// Do GET request
func doGetRequest(request *req.Request) *req.Response {
resp, err := request.Get(cli.Url)
checkErr(err)
return resp
}
// Configure payload
func configPayload(request *req.Request) string {
if cli.ContentType != "" {
request.SetContentType(cli.ContentType)
}
var payload string
if cli.Data != "" {
payload = cli.Data
} else if cli.DataFile != "" {
payload = readFile(cli.DataFile)
}
return payload
}
// Do POST request
func doPostRequest(request *req.Request) *req.Response {
payload := configPayload(request)
if payload != "" {
request.SetBody(payload)
}
resp, err := request.Post(cli.Url)
checkErr(err)
return resp
}
// Do PUT request
func doPutRequest(request *req.Request) *req.Response {
payload := configPayload(request)
if payload != "" {
request.SetBody(payload)
}
resp, err := request.Put(cli.Url)
checkErr(err)
return resp
}
// Do PATCH request
func doPatchRequest(request *req.Request) *req.Response {
payload := configPayload(request)
if payload != "" {
request.SetBody(payload)
}
resp, err := request.Patch(cli.Url)
checkErr(err)
return resp
}
// Do DELETE request
func doDeleteRequest(request *req.Request) *req.Response {
// According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE
// DELETE method may have a body
payload := configPayload(request)
if payload != "" {
request.SetBody(payload)
}
resp, err := request.Delete(cli.Url)
checkErr(err)
return resp
}
// Do OPTIONS request
func doOptionsRequest(request *req.Request) *req.Response {
resp, err := request.Options(cli.Url)
checkErr(err)
return resp
}
// Run our cli
func run() {
// Parse cli arguments
kong.Parse(&cli,
kong.Name("gurl"),
kong.Description("A simple http client cli written in Go."),
kong.UsageOnError(),
kong.Vars{"version": gurlVersion},
kong.ConfigureHelp(kong.HelpOptions{
Compact: true,
Summary: true,
}))
// Create a new request object from client
request := req.C().R()
// Configure http request based on cli arguments
configRequest(request)
// Store response pointer
var resp *req.Response
// Execute cli command accordingly
switch cli.Method {
case "HEAD":
resp = doHeadRequest(request)
case "GET":
resp = doGetRequest(request)
case "POST":
resp = doPostRequest(request)
case "PUT":
resp = doPutRequest(request)
case "PATCH":
resp = doPatchRequest(request)
case "DELETE":
resp = doDeleteRequest(request)
case "OPTIONS":
resp = doOptionsRequest(request)
}
// Print raw response or a prettified json
printResponse(resp.String())
// Show trace info if needed
showTraceInfo(resp)
}
// Main function
func main() {
run()
}