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