GitBucket
4.21.2
Toggle navigation
Snippets
Sign in
Files
Branches
1
Releases
Issues
1
Pull requests
Labels
Priorities
Milestones
Wiki
Forks
mark.george
/
httpcat
Browse code
Add indent for JSON. Refactor display code.
Closes
#3
1 parent
0757ebd
commit
adaac98938b276167b1c107e85367ad8af9effaf
Mark George
authored
on 16 Apr 2022
Patch
Showing
6 changed files
src/client.go
src/display.go
src/global_options.go
src/main.go
src/proxy.go
src/server.go
Ignore Space
Show notes
View
src/client.go
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "fmt" "net/http" "os" "strings" ) type ClientCommand struct { URL string `short:"u" long:"url" description:"The URL to send the request to" required:"true"` Headers []string `short:"H" long:"header" description:"A header to add to request in the form name:value. Use multiple times for multiple headers."` Method string `short:"m" long:"method" choice:"GET" choice:"POST" choice:"PUT" choice:"DELETE" choice:"PATCH" choice:"HEAD" choice:"OPTIONS" description:"HTTP method for request" default:"GET"` Body string `short:"b" long:"body" description:"Request body to send"` } func (opts *ClientCommand) Execute(args []string) error { sendRequest(opts) return nil } func sendRequest(opts *ClientCommand) { client := http.Client{} request, _ := http.NewRequest(opts.Method, opts.URL, strings.NewReader(opts.Body)) // add extra headers if specified if len(opts.Headers) > 0 { for _, header := range opts.Headers { if name, value, err := parseHeader(header); err == nil { if globalOptions.Verbose { fmt.Println("Adding request header - " + name + ": " + value) } request.Header.Add(name, value) } else { fmt.Println("The following header is not in the correct format: " + header + "\nCorrect format is: 'name:value'") os.Exit(1) } } } if response, err := client.Do(request); err != nil { fmt.Println("Could not connect to server") } else { defer response.Body.Close() showResponse(response) } }
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "fmt" "net/http" "os" "strings" ) type ClientCommand struct { Headers []string `long:"header" description:"A header to add to request in the form name:value. Use multiple times for multiple headers."` Method string `short:"m" long:"method" description:"HTTP method for request" default:"GET"` URL string `short:"u" long:"url" description:"The URL to send the request to" required:"true"` Body string `short:"b" long:"body" description:"Request body to send"` } func (opts *ClientCommand) Execute(args []string) error { sendRequest(opts) return nil } func sendRequest(opts *ClientCommand) { client := http.Client{} request, _ := http.NewRequest(opts.Method, opts.URL, strings.NewReader(opts.Body)) // add extra headers if specified if len(opts.Headers) > 0 { for _, header := range opts.Headers { if name, value, err := parseHeader(header); err == nil { if globalOptions.Verbose { fmt.Println("Adding request header - " + name + ": " + value) } request.Header.Add(name, value) } else { fmt.Println("The following header is not in the correct format: " + header + "\nCorrect format is: 'name:value'") os.Exit(1) } } } if response, err := client.Do(request); err != nil { fmt.Println("Could not connect to server") } else { defer response.Body.Close() // show response if globalOptions.BodiesOnly { showResponseBody(response) } else if globalOptions.HeadersOnly { showResponseHeaders(response) } else { showEntireResponse(response) } } }
Ignore Space
Show notes
View
src/display.go
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httputil" "strings" "time" "github.com/fatih/color" ) func showRequest(req *http.Request) { showTimestamp() if !globalOptions.NoColour { color.Set(color.FgBlue) } if globalOptions.HeadersOnly { showRequestHeaders(req) } else if globalOptions.BodiesOnly { showRequestBody(req) } else { showRequestHeaders(req) fmt.Println() showRequestBody(req) } color.Unset() } func showResponse(rsp *http.Response) { showTimestamp() if !globalOptions.NoColour { color.Set(color.FgRed) } if globalOptions.HeadersOnly { showResponseHeaders(rsp) } else if globalOptions.BodiesOnly { showResponseBody(rsp) } else { showResponseHeaders(rsp) fmt.Println() showResponseBody(rsp) } color.Unset() } func showRequestBody(req *http.Request) { // is there a body? if req.Body == nil || req.Body == http.NoBody { // nope, nothing to do return } originalBody, _ := ioutil.ReadAll(req.Body) // clone body since reading from request it will consume it copy1 := ioutil.NopCloser(bytes.NewBuffer(originalBody)) copy2 := ioutil.NopCloser(bytes.NewBuffer(originalBody)) // put one copy back into original request req.Body = copy1 // read body from other copy body, _ := ioutil.ReadAll(copy2) defer copy2.Close() if !globalOptions.NoIndent { if strings.Contains(req.Header.Get("Content-Type"), "application/json") { // if req.Header.Get("Content-Type") == "application/json" { var indentedJson bytes.Buffer err := json.Indent(&indentedJson, body, "", " ") if err == nil { // display indented version fmt.Println(&indentedJson) return } } } fmt.Println(string(body[:])) } func showResponseBody(rsp *http.Response) { // is there a body? if rsp.Body == nil || rsp.Body == http.NoBody { // nope, nothing to do return } originalBody, _ := ioutil.ReadAll(rsp.Body) // clone body since reading from request it will consume it copy1 := ioutil.NopCloser(bytes.NewBuffer(originalBody)) copy2 := ioutil.NopCloser(bytes.NewBuffer(originalBody)) // put one copy back into original request rsp.Body = copy1 // read body from other copy body, _ := ioutil.ReadAll(copy2) defer copy2.Close() if !globalOptions.NoIndent { if strings.Contains(rsp.Header.Get("Content-Type"), "application/json") { // if rsp.Header.Get("Content-Type") == "application/json" { var indentedJson bytes.Buffer err := json.Indent(&indentedJson, body, "", " ") if err == nil { // display indented version fmt.Println(&indentedJson) return } } } fmt.Println(string(body[:])) } func showRequestHeaders(req *http.Request) { dumpBody := false dump, _ := httputil.DumpRequest(req, dumpBody) fmt.Println(strings.TrimSpace(string(dump[:]))) } func showResponseHeaders(rsp *http.Response) { dumpBody := false dumpRsp, _ := httputil.DumpResponse(rsp, dumpBody) fmt.Println(strings.TrimSpace(string(dumpRsp[:]))) } func showTimestamp() { if !globalOptions.NoTimestamps { fmt.Println(time.Now().Format(time.StampMilli)) } }
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "fmt" "io/ioutil" "net/http" "net/http/httputil" "time" "github.com/fatih/color" ) func showRequestBody(req *http.Request) { showTimestamp() if !globalOptions.NoColour { color.Set(color.FgBlue) } body, _ := ioutil.ReadAll(req.Body) fmt.Println(string(body[:])) } func showResponseBody(rsp *http.Response) { showTimestamp() if !globalOptions.NoColour { color.Set(color.FgRed) } body, _ := ioutil.ReadAll(rsp.Body) fmt.Println(string(body[:])) color.Unset() } func showRequestHeaders(req *http.Request) { showTimestamp() if !globalOptions.NoColour { color.Set(color.FgBlue) } // show request line fmt.Println(req.Method + " " + req.URL.Path + " " + req.Proto) // show headers for k := range req.Header { fmt.Println(k + ": " + req.Header[k][0]) } color.Unset() } func showResponseHeaders(rsp *http.Response) { showTimestamp() if !globalOptions.NoColour { color.Set(color.FgRed) } // show status line fmt.Println(rsp.Proto + " " + rsp.Status) // show headers for k := range rsp.Header { fmt.Println(k + ": " + rsp.Header[k][0]) } color.Unset() } func showEntireRequest(req *http.Request) { showTimestamp() if !globalOptions.NoColour { color.Set(color.FgBlue) } dump, _ := httputil.DumpRequest(req, true) fmt.Println(string(dump[:])) color.Unset() } func showEntireResponse(rsp *http.Response) { showTimestamp() if !globalOptions.NoColour { color.Set(color.FgRed) } dumpRsp, _ := httputil.DumpResponse(rsp, true) fmt.Println(string(dumpRsp[:])) color.Unset() } func showTimestamp() { if !globalOptions.NoTimestamps { fmt.Println(time.Now().Format(time.StampMilli)) } }
Ignore Space
Show notes
View
src/global_options.go
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main type GlobalOptions struct { Verbose bool `short:"v" long:"verbose" description:"Show additional details"` HeadersOnly bool `long:"headers" description:"Show headers and request/status line only (default is everything)"` BodiesOnly bool `long:"bodies" description:"Show bodies only (default is everything)"` NoColour bool `long:"nocolour" description:"Don't use colours (default is requests in blue and responses in red)"` NoTimestamps bool `long:"notimestamps" description:"Don't show timestamps"` NoIndent bool `long:"noindent" description:"Don't indent bodies (currently, only application/json bodies are indented)"` } var ( globalOptions = new(GlobalOptions) )
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main type GlobalOptions struct { Verbose bool `short:"v" long:"verbose" description:"Show additional details"` HeadersOnly bool `long:"headers" description:"Show headers and request/status line only (default is everything)"` BodiesOnly bool `long:"bodies" description:"Show bodies only (default is everything)"` NoColour bool `long:"nocolour" description:"Don't use colours (default is requests are blue and responses are red)"` NoTimestamps bool `long:"notimestamps" description:"Don't show timestamps"` } var ( globalOptions = new(GlobalOptions) )
Ignore Space
Show notes
View
src/main.go
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "fmt" "os" "github.com/jessevdk/go-flags" ) const version string = "2.1" type VersionCommand struct{} func (c *VersionCommand) Execute(args []string) error { fmt.Println("httpcat v" + version) return nil } func main() { parser := flags.NewParser(globalOptions, flags.Default) parser.AddCommand("client", "Send an HTTP request", "", &ClientCommand{}) parser.AddCommand("server", "Start a mock HTTP server", "", &ServerCommand{}) parser.AddCommand("proxy", "Start a reverse HTTP logging proxy", "", &ProxyCommand{}) parser.AddCommand("version", "Display version", "", &VersionCommand{}) _, err := parser.Parse() if err != nil { parseErr := err.(*flags.Error) if parseErr.Type.String() == flags.ErrCommandRequired.String() { os.Stderr.WriteString("\n") parser.WriteHelp(os.Stderr) } } }
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "fmt" "os" "github.com/jessevdk/go-flags" ) const version string = "2.0" type VersionCommand struct{} func (c *VersionCommand) Execute(args []string) error { fmt.Println("httpcat v" + version) return nil } func main() { parser := flags.NewParser(globalOptions, flags.Default) parser.AddCommand("client", "Send an HTTP request", "", &ClientCommand{}) parser.AddCommand("server", "Start a mock HTTP server", "", &ServerCommand{}) parser.AddCommand("proxy", "Start a reverse HTTP logging proxy", "", &ProxyCommand{}) parser.AddCommand("version", "Display version", "", &VersionCommand{}) _, err := parser.Parse() if err != nil { parseErr := err.(*flags.Error) if parseErr.Type.String() == flags.ErrCommandRequired.String() { os.Stderr.WriteString("\n") parser.WriteHelp(os.Stderr) } } }
Ignore Space
Show notes
View
src/proxy.go
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "fmt" "net/http" "net/http/httputil" "net/url" "strconv" ) type ProxyCommand struct { Port int `short:"p" long:"port" description:"The port that the proxy listens on." required:"true"` Target string `short:"t" long:"target" descrtiption:"The URL for the target web server that the proxy forwards requests to." required:"true"` } func (opts *ProxyCommand) Execute(args []string) error { fmt.Println("Proxying port " + strconv.Itoa(opts.Port) + " to " + opts.Target) proxy, err := CreateProxy(opts.Target) if err != nil { panic(err) } http.HandleFunc("/", RequestHandler(proxy)) if err := http.ListenAndServe(":"+strconv.Itoa(opts.Port), nil); err != nil { panic(err) } return nil } func CreateProxy(target string) (*httputil.ReverseProxy, error) { url, err := url.Parse(target) // bad target URL if err != nil { return nil, err } proxy := httputil.NewSingleHostReverseProxy(url) OriginalDirector := proxy.Director proxy.Director = func(req *http.Request) { FixHeaders(url, req) showRequest(req) // send request to target OriginalDirector(req) } proxy.ModifyResponse = func(rsp *http.Response) error { showResponse(rsp) return nil } proxy.ErrorHandler = func(rsp http.ResponseWriter, req *http.Request, err error) { fmt.Println("Error sending request to target server:") fmt.Println(" " + err.Error()) } return proxy, nil } func FixHeaders(url *url.URL, req *http.Request) { // TODO this may be necessary for proxying HTTPS (need to test) req.URL.Host = url.Host req.URL.Scheme = url.Scheme req.Host = url.Host } func RequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { proxy.ServeHTTP(w, r) } }
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "fmt" "net/http" "net/http/httputil" "net/url" "strconv" ) type ProxyCommand struct { Port int `short:"p" long:"port" description:"The port that the proxy listens on." required:"true"` Target string `short:"t" long:"target" descrtiption:"The target web server that the proxy forwards requests to." required:"true"` } func (opts *ProxyCommand) Execute(args []string) error { fmt.Println("Proxying port " + strconv.Itoa(opts.Port) + " to " + opts.Target) proxy, err := CreateProxy(opts.Target) if err != nil { panic(err) } http.HandleFunc("/", RequestHandler(proxy)) if err := http.ListenAndServe(":"+strconv.Itoa(opts.Port), nil); err != nil { panic(err) } return nil } func CreateProxy(target string) (*httputil.ReverseProxy, error) { url, err := url.Parse(target) // bad target URL if err != nil { return nil, err } proxy := httputil.NewSingleHostReverseProxy(url) OriginalDirector := proxy.Director proxy.Director = func(req *http.Request) { FixHeaders(url, req) showEntireRequest(req) // send request to target OriginalDirector(req) } proxy.ModifyResponse = func(rsp *http.Response) error { showEntireResponse(rsp) return nil } proxy.ErrorHandler = func(http.ResponseWriter, *http.Request, error) { fmt.Println("Could not connect to target server") } return proxy, nil } func FixHeaders(url *url.URL, req *http.Request) { // TODO this may be necessary for proxying HTTPS (need to test) req.URL.Host = url.Host req.URL.Scheme = url.Scheme req.Host = url.Host } func RequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { proxy.ServeHTTP(w, r) } }
Ignore Space
Show notes
View
src/server.go
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "fmt" "net/http" "os" "strconv" ) type ServerCommand struct { Port int `short:"p" long:"port" description:"Port to listen on." default:"8080"` Routes []string `short:"r" long:"route" description:"Route which is made up of a path, a response body, and a response status, all separated by the pipe '|' character. Repeat for additional routes." default:"/|testing|200"` Cors bool `short:"c" long:"cors" description:"Enable Cross Origin Resource Sharing (CORS)."` Headers []string `short:"H" long:"header" description:"A header to add to response in the form name:value. Repeat for additional headers."` } var ( serverOptions *ServerCommand ) func (opts *ServerCommand) Execute(args []string) error { serverOptions = opts // parse custom headers if provided if len(opts.Headers) > 0 { for _, header := range opts.Headers { if _, _, err := parseHeader(header); err != nil { fmt.Println("The following header is not in the correct format: " + header + "\nCorrect format is: 'name:value'") os.Exit(1) } } } fmt.Println("HTTP server listening on port " + strconv.Itoa(opts.Port) + "\n") if len(opts.Routes) > 0 { fmt.Println("Routes:") for _, route := range opts.Routes { if path, _, _, err := parseRoute(route); err == nil { fmt.Println(" " + path) http.HandleFunc(path, serverRequestHandler) } else { fmt.Println("The following route is not in the correct format: " + route + "\nCorrect format is: \"/path|response body|status code\"") os.Exit(1) } } } if err := http.ListenAndServe(":"+strconv.Itoa(opts.Port), nil); err != nil { fmt.Fprintf(os.Stderr, "Could not start server. Is port %d available?\n", opts.Port) os.Exit(1) } return nil } func serverRequestHandler(rsp http.ResponseWriter, req *http.Request) { path := req.RequestURI body := bodies[path] code := statuses[path] showRequest(req) // CORS if serverOptions.Cors && req.Method == http.MethodOptions { if globalOptions.Verbose { fmt.Println("CORS preflight") } if origin := req.Header.Get("Origin"); origin != "" { rsp.Header().Set("Access-Control-Allow-Origin", origin) if headers := req.Header.Get("Access-Control-Request-Headers"); headers != "" { rsp.Header().Set("Access-Control-Allow-Headers", headers) } if method := req.Header.Get("Access-Control-Request-Method"); method != "" { rsp.Header().Set("Access-Control-Allow-Method", method) } rsp.WriteHeader(200) return } else { fmt.Println("WARNING - CORS preflight did not contain 'Origin' header!") } } // add custom headers if provided if len(serverOptions.Headers) > 0 { for name, value := range headers { if globalOptions.Verbose { fmt.Println("Adding response header - " + name + ": " + value) } rsp.Header().Set(name, value) } } // add status line rsp.WriteHeader(code) // add body fmt.Fprintf(rsp, body) }
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "fmt" "net/http" "os" "strconv" ) type ServerCommand struct { Port int `short:"p" long:"port" description:"Port to listen on." default:"8080"` Routes []string `short:"r" long:"route" description:"Route which is made up of a path, a response body, and a response status, all separated by the pipe character. Repeat for additional routes." default:"/|testing|200"` Cors bool `short:"c" long:"cors" description:"Enable Cross Origin Resource Sharing (CORS)."` Headers []string `short:"H" long:"header" description:"A header to add to response in the form name:value. Repeat for additional headers."` } var ( serverOptions *ServerCommand ) func (opts *ServerCommand) Execute(args []string) error { serverOptions = opts // parse custom headers if provided if len(opts.Headers) > 0 { for _, header := range opts.Headers { if _, _, err := parseHeader(header); err != nil { fmt.Println("The following header is not in the correct format: " + header + "\nCorrect format is: 'name:value'") os.Exit(1) } } } fmt.Println("HTTP server listening on port " + strconv.Itoa(opts.Port) + "\n") if len(opts.Routes) > 0 { fmt.Println("Routes:") for _, route := range opts.Routes { if path, _, _, err := parseRoute(route); err == nil { fmt.Println(" " + path) http.HandleFunc(path, serverRequestHandler) } else { fmt.Println("The following route is not in the correct format: " + route + "\nCorrect format is: \"/path|response body|status code\"") os.Exit(1) } } } /* http.HandleFunc("/", serverRequestHandler) */ if err := http.ListenAndServe(":"+strconv.Itoa(opts.Port), nil); err != nil { fmt.Fprintf(os.Stderr, "Could not start server. Is port %d available?\n", opts.Port) os.Exit(1) } return nil } func serverRequestHandler(rsp http.ResponseWriter, req *http.Request) { path := req.RequestURI body := bodies[path] code := statuses[path] if globalOptions.BodiesOnly { showRequestBody(req) } else if globalOptions.HeadersOnly { showRequestHeaders(req) } else { showEntireRequest(req) } // CORS if serverOptions.Cors && req.Method == http.MethodOptions { if globalOptions.Verbose { fmt.Println("CORS preflight") } if origin := req.Header.Get("Origin"); origin != "" { rsp.Header().Set("Access-Control-Allow-Origin", origin) if headers := req.Header.Get("Access-Control-Request-Headers"); headers != "" { rsp.Header().Set("Access-Control-Allow-Headers", headers) } if method := req.Header.Get("Access-Control-Request-Method"); method != "" { rsp.Header().Set("Access-Control-Allow-Method", method) } rsp.WriteHeader(200) return } else { fmt.Println("WARNING - CORS preflight did not contain 'Origin' header!") } } // add custom headers if provided if len(serverOptions.Headers) > 0 { for name, value := range headers { if globalOptions.Verbose { fmt.Println("Adding response header - " + name + ": " + value) } rsp.Header().Set(name, value) } } // add status line rsp.WriteHeader(code) // add body fmt.Fprintf(rsp, body) }
Show line notes below