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
WIP refactor & add reverse proxy
master
1 parent
6418fc3
commit
ac587a763ae8828831bee4bddf611f9cb5c9411c
Mark George
authored
on 25 Jan 2022
Patch
Showing
5 changed files
README.md
src/go.mod
src/go.sum
src/httpcat.go
src/proxy.go
Ignore Space
Show notes
View
README.md
httpcat =============== A netcat like tool for analysing/mocking HTTP requests (from both server and client perspective). My first experiment with Go. ## Usage Usage: Client mode httpcat -client [options] http://uri-to-send-request-to.com Currently only supports GET requests. Server mode httpcat -server [options] Options (either mode) -entire or -e : Display entire request/response instead of just the body. -verbose or -v : Be verbose. Options (client mode only) -accept or -a [accept string] : Adds 'Accept' header to request. Options (server mode only) -body or -b [body message] : Body to respond with. Response code will default to 200. -port or -p [port] : Port to listen on. -response or -r [response code] : Status code to respond with. Defaults to 204. -cors or -c : Enable Cross Origin Resource Sharing support. -separator or -s [separator string] : Use the provided separator to separate messages. ## Building ``` cd src go build -o ../httpcat ``` ### Cross Compiling Linux ``` GOOS=linux GOARCH=amd64 go build -o ../httpcat-linux ``` Windows ``` GOOS=windows GOARCH=amd64 go build -o ../httpcat-win64 ``` Intel Mac ``` GOOS=darwin GOARCH=amd64 go build -o ../httpcat-mac-intel ``` M1/ARM64 Mac ``` GOOS=darwin GOARCH=arm64 go build -o ../httpcat-mac-arm64 ``` ## To Do Add support for DELETE, PUT, and POST requests in client mode.
httpcat =============== A netcat like tool for analysing/mocking HTTP requests (from both server and client perspective). My first experiment with Go. ## Usage Usage: Client mode httpcat -client [options] http://uri-to-send-request-to.com Currently only supports GET requests. Server mode httpcat -server [options] Options (either mode) -entire or -e : Display entire request/response instead of just the body. -verbose or -v : Be verbose. Options (client mode only) -accept or -a [accept string] : Adds 'Accept' header to request. Options (server mode only) -body or -b [body message] : Body to respond with. Response code will default to 200. -port or -p [port] : Port to listen on. -response or -r [response code] : Status code to respond with. Defaults to 204. -cors or -c : Enable Cross Origin Resource Sharing support. -separator or -s [separator string] : Use the provided separator to separate messages. ## Building ``` cd src go build ``` ### Cross Compiling Linux ``` GOOS=linux GOARCH=amd64 go build -o ../httpcat-linux ``` Windows ``` GOOS=windows GOARCH=amd64 go build -o ../httpcat-win64 ``` Intel Mac ``` GOOS=darwin GOARCH=amd64 go build -o ../httpcat-mac-intel ``` M1/ARM64 Mac ``` GOOS=darwin GOARCH=arm64 go build -o ../httpcat-mac-arm64 ``` ## To Do Add support for DELETE, PUT, and POST requests in client mode.
Ignore Space
Show notes
View
src/go.mod
module proxy go 1.16 require ( github.com/fatih/color v1.13.0 )
module httpcat go 1.16
Ignore Space
Show notes
View
src/go.sum
0 → 100644
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Ignore Space
Show notes
View
src/httpcat.go
/** Author: Mark George <mark.george@otago.ac.nz> Warranty: None. Works for me. If it doesn't work for you then you already have the source code... License: WTFPL v2 <http://www.wtfpl.net/txt/copying/> */ package main import ( "flag" "fmt" "io/ioutil" "net/http" "net/http/httputil" "os" "strconv" ) var ( port int status int body string entire bool verbose bool server bool uri string accept string separator string cors bool ) func requestHandler(resp http.ResponseWriter, req *http.Request) { if verbose { fmt.Printf("Received a %s request to %s\n", req.Method, req.RequestURI) } // print entire request if entire { dump, _ := httputil.DumpRequest(req, true) fmt.Println(string(dump[:])) // print only request body } else { body, _ := ioutil.ReadAll(req.Body) fmt.Println(string(body[:])) } // cors if cors && req.Method == http.MethodOptions { if verbose { fmt.Println("CORS preflight") } if origin := req.Header.Get("Origin"); origin != "" { resp.Header().Set("Access-Control-Allow-Origin", origin) if headers := req.Header.Get("Access-Control-Request-Headers"); headers != "" { resp.Header().Set("Access-Control-Allow-Headers", headers) } if method := req.Header.Get("Access-Control-Request-Method"); method != "" { resp.Header().Set("Access-Control-Allow-Method", method) } resp.WriteHeader(200) return } else { if verbose { fmt.Println("Preflight did not contain 'Origin' header!") } } } // send response if body != "" { if status == 204 { status = 200 } // if we have a body then we don't want to return the default 204 status resp.WriteHeader(status) fmt.Fprintf(resp, body) } else { resp.WriteHeader(status) } if separator != "" { fmt.Println(separator) } } func startServer() { http.HandleFunc("/", requestHandler) if err := http.ListenAndServe(":"+strconv.Itoa(port), nil); err != nil { fmt.Fprintf(os.Stderr, "Could not start server. Is port %d available?\n", port) } } func sendRequest(uri string) { client := http.Client{} request, _ := http.NewRequest("GET", uri, nil) // add Accept header if required by user if accept != "" { request.Header.Add("Accept", accept) } response, err := client.Do(request) if err != nil { fmt.Fprintln(os.Stderr, err) } else { // print entire response if entire { dump, _ := httputil.DumpResponse(response, true) fmt.Println(string(dump[:])) // print only the response body } else { body, _ := ioutil.ReadAll(response.Body) fmt.Println(string(body[:])) } } } func parseCommandLine() { flag.IntVar(&port, "port", 8080, "") flag.IntVar(&port, "p", 8080, "") flag.IntVar(&status, "response", 204, "") flag.IntVar(&status, "r", 204, "") flag.StringVar(&body, "body", "", "") flag.StringVar(&body, "b", "", "") flag.BoolVar(&entire, "entire", false, "") flag.BoolVar(&entire, "e", false, "") flag.StringVar(&separator, "separator", "", "") flag.StringVar(&separator, "s", "", "") flag.BoolVar(&verbose, "verbose", false, "") flag.BoolVar(&verbose, "v", false, "") flag.StringVar(&accept, "accept", "", "") flag.StringVar(&accept, "a", "", "") flag.BoolVar(&cors, "cors", false, "") flag.BoolVar(&cors, "c", false, "") serverMode := flag.Bool("server", false, "") clientMode := flag.Bool("client", false, "") flag.Usage = usage flag.Parse() if !(*serverMode || *clientMode) || (*serverMode && *clientMode) { usage() os.Exit(1) } else { server = *serverMode } if *clientMode { uri = flag.Arg(0) if uri == "" { usage() os.Exit(1) } } } var usage = func() { fmt.Fprintf(os.Stderr, "Usage:\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Client mode\n") fmt.Fprintf(os.Stderr, " httpcat -client [options] http://uri-to-send-request-to.com\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Currently only supports GET requests.\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Server mode\n") fmt.Fprintf(os.Stderr, " httpcat -server [options]\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Options (either mode)\n") fmt.Fprintf(os.Stderr, " -entire or -e : Display entire request/response instead of just the body.\n") fmt.Fprintf(os.Stderr, " -verbose or -v : Be verbose.\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Options (client mode only)\n") fmt.Fprintf(os.Stderr, " -accept or -a [accept string] : Adds 'Accept' header to request.\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Options (server mode only)\n") fmt.Fprintf(os.Stderr, " -body or -b [body message] : Body to respond with. Response code will default to 200.\n") fmt.Fprintf(os.Stderr, " -port or -p [port] : Port to listen on.\n") fmt.Fprintf(os.Stderr, " -response or -r [response code] : Status code to respond with. Defaults to 204.\n") fmt.Fprintf(os.Stderr, " -cors or -c : Enable Cross Origin Resource Sharing support.\n") fmt.Fprintf(os.Stderr, " -separator or -s [separator string] : Use the provided separator to separate messages.\n") } func xmain() { parseCommandLine() if server { // server mode if verbose { fmt.Printf("Listening on port %d.\n", port) if entire { fmt.Println("Displaying entire request details.\n") } else { fmt.Println("Displaying bodies only.\n") } } startServer() } else { // client mode if verbose { fmt.Println("Sending GET request to " + uri) } sendRequest(uri) } }
/** Author: Mark George <mark.george@otago.ac.nz> Warranty: None. Works for me. If it doesn't work for you then you already have the source code... License: WTFPL v2 <http://www.wtfpl.net/txt/copying/> */ package main import ( "fmt" "net/http" "net/http/httputil" "flag" "strconv" "io/ioutil" "os" ) var ( port int status int body string entire bool verbose bool server bool uri string accept string separator string cors bool ) func requestHandler(resp http.ResponseWriter, req *http.Request) { if verbose { fmt.Printf("Received a %s request to %s\n", req.Method, req.RequestURI) } // print entire request if entire { dump, _ := httputil.DumpRequest(req, true) fmt.Println(string(dump[:])) // print only request body } else { body, _ := ioutil.ReadAll(req.Body) fmt.Println(string(body[:])) } // cors if cors && req.Method == http.MethodOptions { if verbose { fmt.Println("CORS preflight") } if origin := req.Header.Get("Origin"); origin != "" { resp.Header().Set("Access-Control-Allow-Origin", origin); if headers := req.Header.Get("Access-Control-Request-Headers"); headers != "" { resp.Header().Set("Access-Control-Allow-Headers", headers); } if method := req.Header.Get("Access-Control-Request-Method"); method != "" { resp.Header().Set("Access-Control-Allow-Method", method); } resp.WriteHeader(200); return; } else { if verbose { fmt.Println("Preflight did not contain 'Origin' header!") } } } // send response if body != "" { if status == 204 {status = 200} // if we have a body then we don't want to return the default 204 status resp.WriteHeader(status) fmt.Fprintf(resp, body) } else { resp.WriteHeader(status) } if separator != "" { fmt.Println(separator) } } func startServer() { http.HandleFunc("/", requestHandler) if err := http.ListenAndServe(":"+strconv.Itoa(port), nil); err != nil { fmt.Fprintf(os.Stderr, "Could not start server. Is port %d available?\n", port) } } func sendRequest(uri string) { client := http.Client{} request, _ := http.NewRequest("GET", uri, nil) // add Accept header if required by user if accept != "" { request.Header.Add("Accept", accept) } response, err := client.Do(request) if(err != nil) { fmt.Fprintln(os.Stderr, err) } else { // print entire response if entire { dump, _ := httputil.DumpResponse(response, true) fmt.Println(string(dump[:])) // print only the response body } else { body, _ := ioutil.ReadAll(response.Body) fmt.Println(string(body[:])) } } } func parseCommandLine() { flag.IntVar(&port, "port", 8080, "") flag.IntVar(&port, "p", 8080, "") flag.IntVar(&status, "response", 204, "") flag.IntVar(&status, "r", 204, "") flag.StringVar(&body, "body", "", "") flag.StringVar(&body, "b", "", "") flag.BoolVar(&entire, "entire", false, "") flag.BoolVar(&entire, "e", false, "") flag.StringVar(&separator, "separator", "", "") flag.StringVar(&separator, "s", "", "") flag.BoolVar(&verbose, "verbose", false, "") flag.BoolVar(&verbose, "v", false, "") flag.StringVar(&accept, "accept", "", "") flag.StringVar(&accept, "a", "", "") flag.BoolVar(&cors, "cors", false, "") flag.BoolVar(&cors, "c", false, "") serverMode := flag.Bool("server", false, "") clientMode := flag.Bool("client", false, "") flag.Usage = usage flag.Parse() if !(*serverMode || *clientMode) || (*serverMode && *clientMode) { usage() os.Exit(1) } else { server = *serverMode } if *clientMode { uri = flag.Arg(0) if uri == "" { usage() os.Exit(1) } } } var usage = func() { fmt.Fprintf(os.Stderr, "Usage:\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Client mode\n") fmt.Fprintf(os.Stderr, " httpcat -client [options] http://uri-to-send-request-to.com\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Currently only supports GET requests.\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Server mode\n") fmt.Fprintf(os.Stderr, " httpcat -server [options]\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Options (either mode)\n") fmt.Fprintf(os.Stderr, " -entire or -e : Display entire request/response instead of just the body.\n") fmt.Fprintf(os.Stderr, " -verbose or -v : Be verbose.\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Options (client mode only)\n") fmt.Fprintf(os.Stderr, " -accept or -a [accept string] : Adds 'Accept' header to request.\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " Options (server mode only)\n") fmt.Fprintf(os.Stderr, " -body or -b [body message] : Body to respond with. Response code will default to 200.\n") fmt.Fprintf(os.Stderr, " -port or -p [port] : Port to listen on.\n") fmt.Fprintf(os.Stderr, " -response or -r [response code] : Status code to respond with. Defaults to 204.\n") fmt.Fprintf(os.Stderr, " -cors or -c : Enable Cross Origin Resource Sharing support.\n") fmt.Fprintf(os.Stderr, " -separator or -s [separator string] : Use the provided separator to separate messages.\n") } func main() { parseCommandLine() if server { // server mode if verbose { fmt.Printf("Listening on port %d.\n", port) if entire { fmt.Println("Displaying entire request details.\n") } else { fmt.Println("Displaying bodies only.\n") } } startServer() } else { // client mode if verbose { fmt.Println("Sending GET request to " + uri) } sendRequest(uri) } }
Ignore Space
Show notes
View
src/proxy.go
0 → 100644
/** Author: Mark George <mark.george@otago.ac.nz License: WTFPL v2 <http://www.wtfpl.net/txt/copying/> Original Source: https://gist.github.com/yowu/f7dc34bd4736a65ff28d */ package main import ( // "flag" "fmt" // "io" // "log" "net/http" "net/http/httputil" "net/url" // "strings" "time" "github.com/fatih/color" ) var ( listen string target string ) func copyHeader(dst, src http.Header) { for k, vv := range src { for _, v := range vv { dst.Add(k, v) } } } func ServeProxy(target string, rsp http.ResponseWriter, req *http.Request) { url, _ := url.Parse(target) proxy := httputil.NewSingleHostReverseProxy(url) req.URL.Host = url.Host req.URL.Scheme = url.Scheme req.Header.Set("X-Forwarded-Host", req.Header.Get("Host")) proxy.ServeHTTP(rsp, req) // fmt.Println() // log.Println("\n") // // log the original request dumpReq, _ := httputil.DumpRequest(req, true) color.Set(color.FgBlue) fmt.Println(string(dumpReq[:])) fmt.Println() // // log the response // dumpRsp, _ := httputil.DumpResponse(resp, true) // color.Set(color.FgRed) // fmt.Println(string(dumpRsp[:])) // copyHeader(wr.Header(), resp.Header) // wr.WriteHeader(resp.StatusCode) // io.Copy(wr, resp.Body) // color.Unset() // defer resp.Body.Close() } 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) { LogRequest(req) // send request to target OriginalDirector(req) } proxy.ModifyResponse = func (rsp *http.Response) error { LogResponse(rsp) return nil } proxy.ErrorHandler = func (http.ResponseWriter, *http.Request, error) { fmt.Println("Could not connect to target server\n") } return proxy, nil } func RequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { proxy.ServeHTTP(w, r) } } func LogRequest(req *http.Request) error { color.Set(color.FgWhite) fmt.Println(time.Now().Format(time.StampMilli)) dumpReq, _ := httputil.DumpRequest(req, true) color.Set(color.FgBlue) fmt.Println(string(dumpReq[:])) color.Unset() return nil } func LogResponse(rsp *http.Response) { color.Set(color.FgWhite) fmt.Println(time.Now().Format(time.StampMilli)) dumpRsp, _ := httputil.DumpResponse(rsp, true) color.Set(color.FgRed) fmt.Println(string(dumpRsp[:])) color.Unset() } func main() { // flag.StringVar(&listen, "listen", "9090", "The port that the proxy listens on.") // flag.StringVar(&listen, "l", "9090", "The port that the proxy listens on.") // flag.StringVar(&target, "target", "8080", "The port that the proxy forwards requests to.") // flag.StringVar(&target, "t", "8080", "The port that the proxy forwards requests to.") // flag.Parse() // handler := &proxy{} // log.Println("Forwarding port", listen, "to port", target) // if err := http.ListenAndServe("localhost:"+listen, handler); err != nil { // log.Fatal("ListenAndServe:", err) // } target := "http://localhost:8080" listenPort := "9090" proxy, err := CreateProxy(target) if err != nil { panic(err) } http.HandleFunc("/", RequestHandler(proxy)) if err := http.ListenAndServe(":"+listenPort, nil); err != nil { panic(err) } }
Show line notes below