[
  {
    "path": ".github/workflows/push.yml",
    "content": "on:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\nname: Push\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v4\n    - name: Install Go\n      uses: actions/setup-go@v5\n      with:\n        go-version-file: 'go.mod'\n    - name: Test\n      run: go test -race ./...\n    - name: Vet\n      run: go vet ./...\n    - name: Mod verify\n      run: go mod verify\n"
  },
  {
    "path": ".gitignore",
    "content": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n\nhttpstat\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2016 Dave Cheney\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "TARGETS = linux-386 linux-amd64 linux-arm linux-arm64 darwin-amd64 windows-386 windows-amd64\nCOMMAND_NAME = httpstat\nPACKAGE_NAME = github.com/davecheney/$(COMMAND_NAME)\nLDFLAGS = -ldflags=-X=main.version=$(VERSION)\nOBJECTS = $(patsubst $(COMMAND_NAME)-windows-amd64%,$(COMMAND_NAME)-windows-amd64%.exe, $(patsubst $(COMMAND_NAME)-windows-386%,$(COMMAND_NAME)-windows-386%.exe, $(patsubst %,$(COMMAND_NAME)-%-v$(VERSION), $(TARGETS)))) \n\nrelease: check-env $(OBJECTS) ## Build release binaries (requires VERSION)\n\nclean: check-env ## Remove release binaries\n\trm $(OBJECTS)\n\n$(OBJECTS): $(wildcard *.go)\n\tenv GOOS=`echo $@ | cut -d'-' -f2` GOARCH=`echo $@ | cut -d'-' -f3 | cut -d'.' -f 1` go build -o $@ $(LDFLAGS) $(PACKAGE_NAME)\n\n.PHONY: help check-env\n\ncheck-env:\nifndef VERSION\n\t$(error VERSION is undefined)\nendif\n\nhelp:\n\t@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-30s\\033[0m %s\\n\", $$1, $$2}'\n\n.DEFAULT_GOAL := help\n"
  },
  {
    "path": "README.md",
    "content": "# httpstat [![Build Status](https://github.com/davecheney/httpstat/actions/workflows/push.yml/badge.svg)](https://github.com/davecheney/httpstat/actions/workflows/push.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/davecheney/httpstat)](https://goreportcard.com/report/github.com/davecheney/httpstat)\n\n![Shameless](./screenshot.png)\n\nImitation is the sincerest form of flattery.\n\nBut seriously, https://github.com/reorx/httpstat is the new hotness, and this is a shameless rip off.\n\n## Installation\n`httpstat` requires Go 1.20 or later.\n```\ngo install github.com/davecheney/httpstat@latest\n```\n\n## Usage\n```\nhttpstat https://example.com/\n```\n## Features\n\n- Windows/BSD/Linux supported.\n- HTTP and HTTPS are supported, for self signed certificates use `-k`.\n- Skip timing the body of a response with `-I`.\n- Follow 30x redirects with `-L`.\n- Change HTTP method with `-X METHOD`.\n- Provide a `PUT` or `POST` request body with `-d string`. To supply the `PUT` or `POST` body as a file, use `-d @filename`.\n- Add extra request headers with `-H 'Name: value'`.\n- The response body is usually discarded, you can use `-o filename` to save it to a file, or `-O` to save it to the file name suggested by the server.\n- HTTP/HTTPS proxies supported via the usual `HTTP_PROXY`/`HTTPS_PROXY` env vars (as well as lower case variants).\n- Supply your own client side certificate with `-E cert.pem`.\n\n## Contributing\n\nBug reports are most welcome, but with the exception of #5, this project is closed.\n\nPull requests must include a `fixes #NNN` or `updates #NNN` comment. \n\nPlease discuss your design on the accompanying issue before submitting a pull request. If there is no suitable issue, please open one to discuss the feature before slinging code. Thank you.\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/davecheney/httpstat\n\ngo 1.23\n\nrequire github.com/fatih/color v1.18.0\n\nrequire (\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/pem\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"mime\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptrace\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n)\n\nconst (\n\thttpsTemplate = `` +\n\t\t`  DNS Lookup   TCP Connection   TLS Handshake   Server Processing   Content Transfer` + \"\\n\" +\n\t\t`[%s  |     %s  |    %s  |        %s  |       %s  ]` + \"\\n\" +\n\t\t`            |                |               |                   |                  |` + \"\\n\" +\n\t\t`   namelookup:%s      |               |                   |                  |` + \"\\n\" +\n\t\t`                       connect:%s     |                   |                  |` + \"\\n\" +\n\t\t`                                   pretransfer:%s         |                  |` + \"\\n\" +\n\t\t`                                                     starttransfer:%s        |` + \"\\n\" +\n\t\t`                                                                                total:%s` + \"\\n\"\n\n\thttpTemplate = `` +\n\t\t`   DNS Lookup   TCP Connection   Server Processing   Content Transfer` + \"\\n\" +\n\t\t`[ %s  |     %s  |        %s  |       %s  ]` + \"\\n\" +\n\t\t`             |                |                   |                  |` + \"\\n\" +\n\t\t`    namelookup:%s      |                   |                  |` + \"\\n\" +\n\t\t`                        connect:%s         |                  |` + \"\\n\" +\n\t\t`                                      starttransfer:%s        |` + \"\\n\" +\n\t\t`                                                                 total:%s` + \"\\n\"\n)\n\nvar (\n\t// Command line flags.\n\thttpMethod      string\n\tpostBody        string\n\tfollowRedirects bool\n\tonlyHeader      bool\n\tinsecure        bool\n\thttpHeaders     headers\n\tsaveOutput      bool\n\toutputFile      string\n\tshowVersion     bool\n\tclientCertFile  string\n\tfourOnly        bool\n\tsixOnly         bool\n\n\t// number of redirects followed\n\tredirectsFollowed int\n\n\tversion = \"devel\" // for -v flag, updated during the release process with -ldflags=-X=main.version=...\n)\n\nconst maxRedirects = 10\n\nfunc init() {\n\tflag.StringVar(&httpMethod, \"X\", \"GET\", \"HTTP method to use\")\n\tflag.StringVar(&postBody, \"d\", \"\", \"the body of a POST or PUT request; from file use @filename\")\n\tflag.BoolVar(&followRedirects, \"L\", false, \"follow 30x redirects\")\n\tflag.BoolVar(&onlyHeader, \"I\", false, \"don't read body of request\")\n\tflag.BoolVar(&insecure, \"k\", false, \"allow insecure SSL connections\")\n\tflag.Var(&httpHeaders, \"H\", \"set HTTP header; repeatable: -H 'Accept: ...' -H 'Range: ...'\")\n\tflag.BoolVar(&saveOutput, \"O\", false, \"save body as remote filename\")\n\tflag.StringVar(&outputFile, \"o\", \"\", \"output file for body\")\n\tflag.BoolVar(&showVersion, \"v\", false, \"print version number\")\n\tflag.StringVar(&clientCertFile, \"E\", \"\", \"client cert file for tls config\")\n\tflag.BoolVar(&fourOnly, \"4\", false, \"resolve IPv4 addresses only\")\n\tflag.BoolVar(&sixOnly, \"6\", false, \"resolve IPv6 addresses only\")\n\n\tflag.Usage = usage\n}\n\nfunc usage() {\n\tfmt.Fprintf(os.Stderr, \"Usage: %s [OPTIONS] URL\\n\\n\", os.Args[0])\n\tfmt.Fprintln(os.Stderr, \"OPTIONS:\")\n\tflag.PrintDefaults()\n\tfmt.Fprintln(os.Stderr, \"\")\n\tfmt.Fprintln(os.Stderr, \"ENVIRONMENT:\")\n\tfmt.Fprintln(os.Stderr, \"  HTTP_PROXY    proxy for HTTP requests; complete URL or HOST[:PORT]\")\n\tfmt.Fprintln(os.Stderr, \"                used for HTTPS requests if HTTPS_PROXY undefined\")\n\tfmt.Fprintln(os.Stderr, \"  HTTPS_PROXY   proxy for HTTPS requests; complete URL or HOST[:PORT]\")\n\tfmt.Fprintln(os.Stderr, \"  NO_PROXY      comma-separated list of hosts to exclude from proxy\")\n}\n\nfunc printf(format string, a ...interface{}) (n int, err error) {\n\treturn fmt.Fprintf(color.Output, format, a...)\n}\n\nfunc grayscale(code color.Attribute) func(string, ...interface{}) string {\n\treturn color.New(code + 232).SprintfFunc()\n}\n\nfunc main() {\n\tflag.Parse()\n\n\tif showVersion {\n\t\tfmt.Printf(\"%s %s (runtime: %s)\\n\", os.Args[0], version, runtime.Version())\n\t\tos.Exit(0)\n\t}\n\n\tif fourOnly && sixOnly {\n\t\tfmt.Fprintf(os.Stderr, \"%s: Only one of -4 and -6 may be specified\\n\", os.Args[0])\n\t\tos.Exit(-1)\n\t}\n\n\targs := flag.Args()\n\tif len(args) != 1 {\n\t\tflag.Usage()\n\t\tos.Exit(2)\n\t}\n\n\tif (httpMethod == \"POST\" || httpMethod == \"PUT\") && postBody == \"\" {\n\t\tlog.Fatal(\"must supply post body using -d when POST or PUT is used\")\n\t}\n\n\tif onlyHeader {\n\t\thttpMethod = \"HEAD\"\n\t}\n\n\turl := parseURL(args[0])\n\n\tvisit(url)\n}\n\n// readClientCert - helper function to read client certificate\n// from pem formatted file\nfunc readClientCert(filename string) []tls.Certificate {\n\tif filename == \"\" {\n\t\treturn nil\n\t}\n\tvar (\n\t\tpkeyPem []byte\n\t\tcertPem []byte\n\t)\n\n\t// read client certificate file (must include client private key and certificate)\n\tcertFileBytes, err := os.ReadFile(filename)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to read client certificate file: %v\", err)\n\t}\n\n\tfor {\n\t\tblock, rest := pem.Decode(certFileBytes)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tcertFileBytes = rest\n\n\t\tif strings.HasSuffix(block.Type, \"PRIVATE KEY\") {\n\t\t\tpkeyPem = pem.EncodeToMemory(block)\n\t\t}\n\t\tif strings.HasSuffix(block.Type, \"CERTIFICATE\") {\n\t\t\tcertPem = pem.EncodeToMemory(block)\n\t\t}\n\t}\n\n\tcert, err := tls.X509KeyPair(certPem, pkeyPem)\n\tif err != nil {\n\t\tlog.Fatalf(\"unable to load client cert and key pair: %v\", err)\n\t}\n\treturn []tls.Certificate{cert}\n}\n\nfunc parseURL(uri string) *url.URL {\n\tif !strings.Contains(uri, \"://\") && !strings.HasPrefix(uri, \"//\") {\n\t\turi = \"//\" + uri\n\t}\n\n\turl, err := url.Parse(uri)\n\tif err != nil {\n\t\tlog.Fatalf(\"could not parse url %q: %v\", uri, err)\n\t}\n\n\tif url.Scheme == \"\" {\n\t\turl.Scheme = \"http\"\n\t\tif !strings.HasSuffix(url.Host, \":80\") {\n\t\t\turl.Scheme += \"s\"\n\t\t}\n\t}\n\treturn url\n}\n\nfunc headerKeyValue(h string) (string, string) {\n\ti := strings.Index(h, \":\")\n\tif i == -1 {\n\t\tlog.Fatalf(\"Header '%s' has invalid format, missing ':'\", h)\n\t}\n\treturn strings.TrimRight(h[:i], \" \"), strings.TrimLeft(h[i:], \" :\")\n}\n\nfunc dialContext(network string) func(ctx context.Context, network, addr string) (net.Conn, error) {\n\treturn func(ctx context.Context, _, addr string) (net.Conn, error) {\n\t\treturn (&net.Dialer{\n\t\t\tTimeout:   30 * time.Second,\n\t\t\tKeepAlive: 30 * time.Second,\n\t\t\tDualStack: false,\n\t\t}).DialContext(ctx, network, addr)\n\t}\n}\n\n// visit visits a url and times the interaction.\n// If the response is a 30x, visit follows the redirect.\nfunc visit(url *url.URL) {\n\treq := newRequest(httpMethod, url, postBody)\n\n\tvar t0, t1, t2, t3, t4, t5, t6 time.Time\n\n\ttrace := &httptrace.ClientTrace{\n\t\tDNSStart: func(_ httptrace.DNSStartInfo) { t0 = time.Now() },\n\t\tDNSDone:  func(_ httptrace.DNSDoneInfo) { t1 = time.Now() },\n\t\tConnectStart: func(_, _ string) {\n\t\t\tif t1.IsZero() {\n\t\t\t\t// connecting to IP\n\t\t\t\tt1 = time.Now()\n\t\t\t}\n\t\t},\n\t\tConnectDone: func(net, addr string, err error) {\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"unable to connect to host %v: %v\", addr, err)\n\t\t\t}\n\t\t\tt2 = time.Now()\n\n\t\t\tprintf(\"\\n%s%s\\n\", color.GreenString(\"Connected to \"), color.CyanString(addr))\n\t\t},\n\t\tGotConn:              func(_ httptrace.GotConnInfo) { t3 = time.Now() },\n\t\tGotFirstResponseByte: func() { t4 = time.Now() },\n\t\tTLSHandshakeStart:    func() { t5 = time.Now() },\n\t\tTLSHandshakeDone:     func(_ tls.ConnectionState, _ error) { t6 = time.Now() },\n\t}\n\treq = req.WithContext(httptrace.WithClientTrace(context.Background(), trace))\n\n\ttr := &http.Transport{\n\t\tProxy:                 http.ProxyFromEnvironment,\n\t\tMaxIdleConns:          100,\n\t\tIdleConnTimeout:       90 * time.Second,\n\t\tTLSHandshakeTimeout:   10 * time.Second,\n\t\tExpectContinueTimeout: 1 * time.Second,\n\t\tForceAttemptHTTP2:     true,\n\t}\n\n\tswitch {\n\tcase fourOnly:\n\t\ttr.DialContext = dialContext(\"tcp4\")\n\tcase sixOnly:\n\t\ttr.DialContext = dialContext(\"tcp6\")\n\t}\n\n\tswitch url.Scheme {\n\tcase \"https\":\n\t\thost, _, err := net.SplitHostPort(req.Host)\n\t\tif err != nil {\n\t\t\thost = req.Host\n\t\t}\n\n\t\ttr.TLSClientConfig = &tls.Config{\n\t\t\tServerName:         host,\n\t\t\tInsecureSkipVerify: insecure,\n\t\t\tCertificates:       readClientCert(clientCertFile),\n\t\t\tMinVersion:         tls.VersionTLS12,\n\t\t}\n\t}\n\n\tclient := &http.Client{\n\t\tTransport: tr,\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\t// always refuse to follow redirects, visit does that\n\t\t\t// manually if required.\n\t\t\treturn http.ErrUseLastResponse\n\t\t},\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to read response: %v\", err)\n\t}\n\n\t// Print SSL/TLS version which is used for connection\n\tconnectedVia := \"plaintext\"\n\tif resp.TLS != nil {\n\t\tswitch resp.TLS.Version {\n\t\tcase tls.VersionTLS12:\n\t\t\tconnectedVia = \"TLSv1.2\"\n\t\tcase tls.VersionTLS13:\n\t\t\tconnectedVia = \"TLSv1.3\"\n\t\t}\n\t}\n\tprintf(\"\\n%s %s\\n\", color.GreenString(\"Connected via\"), color.CyanString(\"%s\", connectedVia))\n\n\tbodyMsg := readResponseBody(req, resp)\n\tresp.Body.Close()\n\n\tt7 := time.Now() // after read body\n\tif t0.IsZero() {\n\t\t// we skipped DNS\n\t\tt0 = t1\n\t}\n\n\t// print status line and headers\n\tprintf(\"\\n%s%s%s\\n\", color.GreenString(\"HTTP\"), grayscale(14)(\"/\"), color.CyanString(\"%d.%d %s\", resp.ProtoMajor, resp.ProtoMinor, resp.Status))\n\n\tnames := make([]string, 0, len(resp.Header))\n\tfor k := range resp.Header {\n\t\tnames = append(names, k)\n\t}\n\tsort.Sort(headers(names))\n\tfor _, k := range names {\n\t\tprintf(\"%s %s\\n\", grayscale(14)(k+\":\"), color.CyanString(strings.Join(resp.Header[k], \",\")))\n\t}\n\n\tif bodyMsg != \"\" {\n\t\tprintf(\"\\n%s\\n\", bodyMsg)\n\t}\n\n\tfmta := func(d time.Duration) string {\n\t\treturn color.CyanString(\"%7dms\", int(d/time.Millisecond))\n\t}\n\n\tfmtb := func(d time.Duration) string {\n\t\treturn color.CyanString(\"%-9s\", strconv.Itoa(int(d/time.Millisecond))+\"ms\")\n\t}\n\n\tcolorize := func(s string) string {\n\t\tv := strings.Split(s, \"\\n\")\n\t\tv[0] = grayscale(16)(v[0])\n\t\treturn strings.Join(v, \"\\n\")\n\t}\n\n\tfmt.Println()\n\n\tswitch url.Scheme {\n\tcase \"https\":\n\t\tprintf(colorize(httpsTemplate),\n\t\t\tfmta(t1.Sub(t0)), // dns lookup\n\t\t\tfmta(t2.Sub(t1)), // tcp connection\n\t\t\tfmta(t6.Sub(t5)), // tls handshake\n\t\t\tfmta(t4.Sub(t3)), // server processing\n\t\t\tfmta(t7.Sub(t4)), // content transfer\n\t\t\tfmtb(t1.Sub(t0)), // namelookup\n\t\t\tfmtb(t2.Sub(t0)), // connect\n\t\t\tfmtb(t3.Sub(t0)), // pretransfer\n\t\t\tfmtb(t4.Sub(t0)), // starttransfer\n\t\t\tfmtb(t7.Sub(t0)), // total\n\t\t)\n\tcase \"http\":\n\t\tprintf(colorize(httpTemplate),\n\t\t\tfmta(t1.Sub(t0)), // dns lookup\n\t\t\tfmta(t3.Sub(t1)), // tcp connection\n\t\t\tfmta(t4.Sub(t3)), // server processing\n\t\t\tfmta(t7.Sub(t4)), // content transfer\n\t\t\tfmtb(t1.Sub(t0)), // namelookup\n\t\t\tfmtb(t3.Sub(t0)), // connect\n\t\t\tfmtb(t4.Sub(t0)), // starttransfer\n\t\t\tfmtb(t7.Sub(t0)), // total\n\t\t)\n\t}\n\n\tif followRedirects && isRedirect(resp) {\n\t\tloc, err := resp.Location()\n\t\tif err != nil {\n\t\t\tif err == http.ErrNoLocation {\n\t\t\t\t// 30x but no Location to follow, give up.\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Fatalf(\"unable to follow redirect: %v\", err)\n\t\t}\n\n\t\tredirectsFollowed++\n\t\tif redirectsFollowed > maxRedirects {\n\t\t\tlog.Fatalf(\"maximum number of redirects (%d) followed\", maxRedirects)\n\t\t}\n\n\t\tvisit(loc)\n\t}\n}\n\nfunc isRedirect(resp *http.Response) bool {\n\treturn resp.StatusCode > 299 && resp.StatusCode < 400\n}\n\nfunc newRequest(method string, url *url.URL, body string) *http.Request {\n\treq, err := http.NewRequest(method, url.String(), createBody(body))\n\tif err != nil {\n\t\tlog.Fatalf(\"unable to create request: %v\", err)\n\t}\n\tfor _, h := range httpHeaders {\n\t\tk, v := headerKeyValue(h)\n\t\tif strings.EqualFold(k, \"host\") {\n\t\t\treq.Host = v\n\t\t\tcontinue\n\t\t}\n\t\treq.Header.Add(k, v)\n\t}\n\treturn req\n}\n\nfunc createBody(body string) io.Reader {\n\tif strings.HasPrefix(body, \"@\") {\n\t\tfilename := body[1:]\n\t\tf, err := os.Open(filename)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to open data file %s: %v\", filename, err)\n\t\t}\n\t\treturn f\n\t}\n\treturn strings.NewReader(body)\n}\n\n// getFilenameFromHeaders tries to automatically determine the output filename,\n// when saving to disk, based on the Content-Disposition header.\n// If the header is not present, or it does not contain enough information to\n// determine which filename to use, this function returns \"\".\nfunc getFilenameFromHeaders(headers http.Header) string {\n\t// if the Content-Disposition header is set parse it\n\tif hdr := headers.Get(\"Content-Disposition\"); hdr != \"\" {\n\t\t// pull the media type, and subsequent params, from\n\t\t// the body of the header field\n\t\tmt, params, err := mime.ParseMediaType(hdr)\n\n\t\t// if there was no error and the media type is attachment\n\t\tif err == nil && mt == \"attachment\" {\n\t\t\tif filename := params[\"filename\"]; filename != \"\" {\n\t\t\t\treturn filename\n\t\t\t}\n\t\t}\n\t}\n\n\t// return an empty string if we were unable to determine the filename\n\treturn \"\"\n}\n\n// readResponseBody consumes the body of the response.\n// readResponseBody returns an informational message about the\n// disposition of the response body's contents.\nfunc readResponseBody(req *http.Request, resp *http.Response) string {\n\tif isRedirect(resp) || req.Method == http.MethodHead {\n\t\treturn \"\"\n\t}\n\n\tw := io.Discard\n\tmsg := color.CyanString(\"Body discarded\")\n\n\tif saveOutput || outputFile != \"\" {\n\t\tfilename := outputFile\n\n\t\tif saveOutput {\n\t\t\t// try to get the filename from the Content-Disposition header\n\t\t\t// otherwise fall back to the RequestURI\n\t\t\tif filename = getFilenameFromHeaders(resp.Header); filename == \"\" {\n\t\t\t\tfilename = path.Base(req.URL.RequestURI())\n\t\t\t}\n\n\t\t\tif filename == \"/\" {\n\t\t\t\tlog.Fatalf(\"No remote filename; specify output filename with -o to save response body\")\n\t\t\t}\n\t\t}\n\n\t\tf, err := os.Create(filename)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"unable to create file %s: %v\", filename, err)\n\t\t}\n\t\tdefer f.Close()\n\t\tw = f\n\t\tmsg = color.CyanString(\"Body read\")\n\t}\n\n\tif _, err := io.Copy(w, resp.Body); err != nil && w != io.Discard {\n\t\tlog.Fatalf(\"failed to read response body: %v\", err)\n\t}\n\n\treturn msg\n}\n\ntype headers []string\n\nfunc (h headers) String() string {\n\tvar o []string\n\tfor _, v := range h {\n\t\to = append(o, \"-H \"+v)\n\t}\n\treturn strings.Join(o, \" \")\n}\n\nfunc (h *headers) Set(v string) error {\n\t*h = append(*h, v)\n\treturn nil\n}\n\nfunc (h headers) Len() int      { return len(h) }\nfunc (h headers) Swap(i, j int) { h[i], h[j] = h[j], h[i] }\nfunc (h headers) Less(i, j int) bool {\n\ta, b := h[i], h[j]\n\n\t// server always sorts at the top\n\tif a == \"Server\" {\n\t\treturn true\n\t}\n\tif b == \"Server\" {\n\t\treturn false\n\t}\n\n\tendtoend := func(n string) bool {\n\t\t// https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1\n\t\tswitch n {\n\t\tcase \"Connection\",\n\t\t\t\"Keep-Alive\",\n\t\t\t\"Proxy-Authenticate\",\n\t\t\t\"Proxy-Authorization\",\n\t\t\t\"TE\",\n\t\t\t\"Trailers\",\n\t\t\t\"Transfer-Encoding\",\n\t\t\t\"Upgrade\":\n\t\t\treturn false\n\t\tdefault:\n\t\t\treturn true\n\t\t}\n\t}\n\n\tx, y := endtoend(a), endtoend(b)\n\tif x == y {\n\t\t// both are of the same class\n\t\treturn a < b\n\t}\n\treturn x\n}\n"
  },
  {
    "path": "main_test.go",
    "content": "package main\n\nimport \"testing\"\n\nfunc TestParseURL(t *testing.T) {\n\ttests := []struct {\n\t\tin   string\n\t\twant string\n\t}{\n\t\t{\"https://golang.org\", \"https://golang.org\"},\n\t\t{\"https://golang.org:443/test\", \"https://golang.org:443/test\"},\n\t\t{\"localhost:8080/test\", \"https://localhost:8080/test\"},\n\t\t{\"localhost:80/test\", \"http://localhost:80/test\"},\n\t\t{\"//localhost:8080/test\", \"https://localhost:8080/test\"},\n\t\t{\"//localhost:80/test\", \"http://localhost:80/test\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tu := parseURL(test.in)\n\t\tif u.String() != test.want {\n\t\t\tt.Errorf(\"Given: %s\\nwant: %s\\ngot: %s\", test.in, test.want, u.String())\n\t\t}\n\t}\n}\n"
  }
]