1// Copyright 2015 bat authors
2// Copyright 2020-2021 gurl authors
3//
4// Licensed under the Apache License, Version 2.0 (the "License"): you may
5// not use this file except in compliance with the License. You may obtain
6// a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16// Gurl is a Go implemented CLI cURL-like tool for humans
17// Gurl [flags] [METHOD] URL [ITEM [ITEM]]
18package main
19
20import (
21	"crypto/tls"
22	"flag"
23	"fmt"
24	"io"
25	"log"
26	"net/http"
27	"net/url"
28	"os"
29	"path/filepath"
30	"runtime"
31	"strconv"
32	"strings"
33
34	"github.com/skunkwerks/gurl/hamac"
35)
36
37const (
38	version              = "0.2.3"
39	printReqHeader uint8 = 1 << (iota - 1)
40	printReqBody
41	printRespHeader
42	printRespBody
43)
44
45var (
46	ver              bool
47	form             bool
48	pretty           bool
49	download         bool
50	insecureSSL      bool
51	auth             string
52	proxy            string
53	printV           string
54	printOption      uint8
55	body             string
56	bench            bool
57	benchN           int
58	benchC           int
59	hmacEnv          string
60	isjson           = flag.Bool("json", true, "Send the data as a JSON object")
61	method           = flag.String("method", "GET", "HTTP method")
62	URL              = flag.String("url", "", "HTTP request URL")
63	jsonmap          map[string]interface{}
64	contentJsonRegex = `application/(.*)json`
65)
66
67func init() {
68	flag.BoolVar(&ver, "v", false, "Print Version Number")
69	flag.BoolVar(&ver, "version", false, "Print Version Number")
70	flag.BoolVar(&pretty, "pretty", true, "Print JSON Pretty Format")
71	flag.BoolVar(&pretty, "p", true, "Print JSON Pretty Format")
72	flag.StringVar(&printV, "print", "A", "Print request and response")
73	flag.BoolVar(&form, "form", false, "Submitting as a form")
74	flag.BoolVar(&form, "f", false, "Submitting as a form")
75	flag.BoolVar(&download, "download", false, "Download the url content as file")
76	flag.BoolVar(&download, "d", false, "Download the url content as file")
77	flag.BoolVar(&insecureSSL, "insecure", false, "Allow connections to SSL sites without certs")
78	flag.BoolVar(&insecureSSL, "i", false, "Allow connections to SSL sites without certs")
79	flag.StringVar(&auth, "auth", "", "HTTP authentication username:password, USER[:PASS]")
80	flag.StringVar(&auth, "a", "", "HTTP authentication username:password, USER[:PASS]")
81	flag.StringVar(&proxy, "proxy", "", "Proxy host and port, PROXY_URL")
82	flag.BoolVar(&bench, "bench", false, "Sends bench requests to URL")
83	flag.BoolVar(&bench, "b", false, "Sends bench requests to URL")
84	flag.IntVar(&benchN, "b.N", 1000, "Number of requests to run")
85	flag.IntVar(&benchC, "b.C", 100, "Number of requests to run concurrently.")
86	flag.StringVar(&body, "body", "", "Raw data send as body")
87	flag.StringVar(&hmacEnv, "hmac", "", "name of env var to retrieve HMAC details")
88	jsonmap = make(map[string]interface{})
89}
90
91func parsePrintOption(s string) {
92	if strings.ContainsRune(s, 'A') {
93		printOption = printReqHeader | printReqBody | printRespHeader | printRespBody
94		return
95	}
96
97	if strings.ContainsRune(s, 'H') {
98		printOption |= printReqHeader
99	}
100	if strings.ContainsRune(s, 'B') {
101		printOption |= printReqBody
102	}
103	if strings.ContainsRune(s, 'h') {
104		printOption |= printRespHeader
105	}
106	if strings.ContainsRune(s, 'b') {
107		printOption |= printRespBody
108	}
109	return
110}
111
112func main() {
113	log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds)
114	flag.Usage = usage
115	flag.Parse()
116	args := flag.Args()
117
118	if len(args) > 0 {
119		args = filter(args)
120	}
121
122	if ver {
123		fmt.Println("Version:", version)
124		os.Exit(2)
125	}
126
127	parsePrintOption(printV)
128	if printOption&printReqBody != printReqBody {
129		defaultSetting.DumpBody = false
130	}
131
132	// read stdin into memory in single pass
133	var stdin []byte
134
135	if runtime.GOOS != "windows" {
136		fi, err := os.Stdin.Stat()
137		if err != nil {
138			panic(err)
139		}
140		if fi.Size() != 0 {
141			stdin, err = io.ReadAll(os.Stdin)
142			if err != nil {
143				log.Fatal("Read from Stdin", err)
144			}
145		}
146	}
147
148	if *URL == "" {
149		usage()
150	}
151	if strings.HasPrefix(*URL, ":") {
152		urlb := []byte(*URL)
153		if *URL == ":" {
154			*URL = "http://localhost/"
155		} else if len(*URL) > 1 && urlb[1] != '/' {
156			*URL = "http://localhost" + *URL
157		} else {
158			*URL = "http://localhost" + string(urlb[1:])
159		}
160	}
161	if !strings.HasPrefix(*URL, "http://") && !strings.HasPrefix(*URL, "https://") {
162		*URL = "http://" + *URL
163	}
164	u, err := url.Parse(*URL)
165	if err != nil {
166		log.Fatal(err)
167	}
168	if auth != "" {
169		userpass := strings.Split(auth, ":")
170		if len(userpass) == 2 {
171			u.User = url.UserPassword(userpass[0], userpass[1])
172		} else {
173			u.User = url.User(auth)
174		}
175	}
176	*URL = u.String()
177	httpreq := getHTTP(*method, *URL, args)
178	if u.User != nil {
179		password, _ := u.User.Password()
180		httpreq.GetRequest().SetBasicAuth(u.User.Username(), password)
181	}
182	// Insecure SSL Support
183	if insecureSSL {
184		httpreq.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
185	}
186	// Proxy Support
187	if proxy != "" {
188		purl, err := url.Parse(proxy)
189		if err != nil {
190			log.Fatal("Proxy Url parse err", err)
191		}
192		httpreq.SetProxy(http.ProxyURL(purl))
193	} else {
194		eurl, err := http.ProxyFromEnvironment(httpreq.GetRequest())
195		if err != nil {
196			log.Fatal("Environment Proxy Url parse err", err)
197		}
198		httpreq.SetProxy(http.ProxyURL(eurl))
199	}
200
201	// set body if supplied, or via stdin
202	if body != "" {
203		httpreq.Body(body)
204	}
205	if len(stdin) > 0 {
206		httpreq.Body(stdin)
207	}
208
209	// request body has now been finalised
210	// If HMAC was requested, sign body, & wrap signature as envelope
211	mac := hamac.New(os.Getenv(hmacEnv))
212	if mac.Enabled {
213		httpreq.SignBody(mac)
214	}
215
216	// AB bench
217	if bench {
218		httpreq.Debug(false)
219		RunBench(httpreq)
220		return
221	}
222
223	res, err := httpreq.Response()
224	if err != nil {
225		log.Fatalln("can't get the url", err)
226	}
227
228	// download file
229	if download {
230		var fl string
231		if disposition := res.Header.Get("Content-Disposition"); disposition != "" {
232			fls := strings.Split(disposition, ";")
233			for _, f := range fls {
234				f = strings.TrimSpace(f)
235				if strings.HasPrefix(f, "filename=") {
236					// Remove 'filename='
237					f = strings.TrimLeft(f, "filename=")
238
239					// Remove quotes and spaces from either end
240					f = strings.TrimLeft(f, "\"' ")
241					fl = strings.TrimRight(f, "\"' ")
242				}
243			}
244		}
245		if fl == "" {
246			_, fl = filepath.Split(u.Path)
247		}
248		fd, err := os.OpenFile(fl, os.O_RDWR|os.O_CREATE, 0666)
249		if err != nil {
250			log.Fatal("can't create file", err)
251		}
252		if runtime.GOOS != "windows" {
253			fmt.Println(Color(res.Proto, Magenta), Color(res.Status, Green))
254			for k, v := range res.Header {
255				fmt.Println(Color(k, Gray), ":", Color(strings.Join(v, " "), Cyan))
256			}
257		} else {
258			fmt.Println(res.Proto, res.Status)
259			for k, v := range res.Header {
260				fmt.Println(k, ":", strings.Join(v, " "))
261			}
262		}
263		fmt.Println("")
264		contentLength := res.Header.Get("Content-Length")
265		var total int64
266		if contentLength != "" {
267			total, _ = strconv.ParseInt(contentLength, 10, 64)
268		}
269		fmt.Printf("Downloading to \"%s\"\n", fl)
270		pb := NewProgressBar(total)
271		pb.Start()
272		multiWriter := io.MultiWriter(fd, pb)
273		_, err = io.Copy(multiWriter, res.Body)
274		if err != nil {
275			log.Fatal("Can't Write the body into file", err)
276		}
277		pb.Finish()
278		defer fd.Close()
279		defer res.Body.Close()
280		return
281	}
282
283	if runtime.GOOS != "windows" {
284		fi, err := os.Stdout.Stat()
285		if err != nil {
286			panic(err)
287		}
288		if fi.Mode()&os.ModeDevice == os.ModeDevice {
289			var dumpHeader, dumpBody []byte
290			dump := httpreq.DumpRequest()
291			dps := strings.Split(string(dump), "\n")
292			for i, line := range dps {
293				if len(strings.Trim(line, "\r\n ")) == 0 {
294					dumpHeader = []byte(strings.Join(dps[:i], "\n"))
295					dumpBody = []byte(strings.Join(dps[i:], "\n"))
296					break
297				}
298			}
299			if printOption&printReqHeader == printReqHeader {
300				fmt.Println(ColorfulRequest(string(dumpHeader)))
301				fmt.Println("")
302			}
303			if printOption&printReqBody == printReqBody {
304				if string(dumpBody) != "\r\n" {
305					fmt.Println(string(dumpBody))
306					fmt.Println("")
307				}
308			}
309			if printOption&printRespHeader == printRespHeader {
310				fmt.Println(Color(res.Proto, Magenta), Color(res.Status, Green))
311				for k, v := range res.Header {
312					fmt.Println(Color(k, Gray), ":", Color(strings.Join(v, " "), Cyan))
313				}
314				fmt.Println("")
315			}
316			if printOption&printRespBody == printRespBody {
317				body := formatResponseBody(res, httpreq, pretty)
318				fmt.Println(ColorfulResponse(body, res.Header.Get("Content-Type")))
319			}
320		} else {
321			body := formatResponseBody(res, httpreq, pretty)
322			_, err = os.Stdout.WriteString(body)
323			if err != nil {
324				log.Fatal(err)
325			}
326		}
327	} else {
328		var dumpHeader, dumpBody []byte
329		dump := httpreq.DumpRequest()
330		dps := strings.Split(string(dump), "\n")
331		for i, line := range dps {
332			if len(strings.Trim(line, "\r\n ")) == 0 {
333				dumpHeader = []byte(strings.Join(dps[:i], "\n"))
334				dumpBody = []byte(strings.Join(dps[i:], "\n"))
335				break
336			}
337		}
338		if printOption&printReqHeader == printReqHeader {
339			fmt.Println(string(dumpHeader))
340			fmt.Println("")
341		}
342		if printOption&printReqBody == printReqBody {
343			fmt.Println(string(dumpBody))
344			fmt.Println("")
345		}
346		if printOption&printRespHeader == printRespHeader {
347			fmt.Println(res.Proto, res.Status)
348			for k, v := range res.Header {
349				fmt.Println(k, ":", strings.Join(v, " "))
350			}
351			fmt.Println("")
352		}
353		if printOption&printRespBody == printRespBody {
354			body := formatResponseBody(res, httpreq, pretty)
355			fmt.Println(body)
356		}
357	}
358}
359
360var usageinfo string = `gurl is a Go implemented CLI cURL-like tool for humans,
361originally developed by https://github.com/astaxie/bat but forked to
362pick up critical JSON and header related patches, and avoid conflicting
363with other common tools, also named bat.
364
365Usage:
366
367	gurl [flags] [METHOD] URL [ITEM [ITEM]]
368
369flags:
370  -a, -auth=USER[:PASS]       Pass a username:password pair as the argument
371  -b, -bench=false            Sends bench requests to URL
372  -b.N=1000                   Number of requests to run
373  -b.C=100                    Number of requests to run concurrently
374  -body=""                    Send RAW data as body
375  -f, -form=false             Submitting the data as a form
376  -j, -json=true              Send the data in a JSON object as application/json
377  -hmac=HMAC_ENV_VAR          Environment variable to fetch HMAC details from
378  -p, -pretty=true            Print JSON Pretty Format
379  -i, -insecure=false         Allow connections to SSL sites without certs
380  -proxy=PROXY_URL            Proxy with host and port
381  -print="..."                String specifying what the output should
382                              contain, default will print all information.
383         "A" all request & response headers and bodies
384         "H" request headers
385         "B" request body
386         "h" response headers
387         "b" response body
388  -v, -version=true           Show Version Number
389
390METHOD:
391  gurl defaults to either GET (if there is no request data) or POST
392  (with request data).
393
394URL:
395  The only information needed to perform a request is a URL. The default
396  scheme is http://, which can be omitted from the argument; example.org
397  works just fine.
398
399HMAC:
400  gurl supports adding an HTTP header containing the HMAC signature of
401  the body. The default algorithm is sha256, also supported are sha512, sha1.
402
403  To simplify using different secrets, the only flag is the name of an
404  environment variable, which contains the algorithm, required header, and
405  secret, concatenated together, and separated by :.
406
407  sha256:x-my-signature:very_secret
408  sha1:x-most-wanted-header:bonnie_and_clyde
409
410ITEM:
411  Can be any of:
412    Query string   key=value
413    Header         key:value
414    Post data      key=value
415    JSON data      key:=value
416    File upload    key@/path/file
417
418Example:
419
420	gurl beego.me
421
422For more help & information please refer to https://github.com/skunkwerks/gurl
423`
424
425func usage() {
426	fmt.Println(usageinfo)
427	os.Exit(2)
428}
429