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 content-type to route.
closes
#7
master
1 parent
5f957b6
commit
ed5cd6a65f104a44abd1a8864cf18c9729af4297
Mark George
authored
on 12 May 2022
Patch
Showing
4 changed files
src/client.go
src/main.go
src/parsing.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 { 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."` Args struct { URL string } `positional-args:"yes" required:"yes"` } func (opts *ClientCommand) Execute(args []string) error { sendRequest(opts) return nil } func sendRequest(opts *ClientCommand) { client := http.Client{} request, _ := http.NewRequest(opts.Method, opts.Args.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 `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."` URL string `positional-arg-name:"URL" description:"The URL to send the request to." required:"true"` Args struct { URL string } `positional-args:"yes" required:"yes"` } func (opts *ClientCommand) Execute(args []string) error { sendRequest(opts) return nil } func sendRequest(opts *ClientCommand) { client := http.Client{} request, _ := http.NewRequest(opts.Method, opts.Args.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) } }
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.2" 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.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) } } }
Ignore Space
Show notes
View
src/parsing.go
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "errors" "strconv" "strings" ) var ( headers map[string]string = make(map[string]string) bodies map[string]string = make(map[string]string) contentTypes map[string]string = make(map[string]string) statuses map[string]int = make(map[string]int) ) func parseHeader(header string) (string, string, error) { parts := strings.Split(header, ":") if len(parts) < 2 { return "", "", errors.New("header is not in the correct format") } else { name := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) // value had a ':' in it (probably a URL) so append the remaining part if len(parts) == 3 { value = value + ":" + strings.TrimSpace(parts[2]) } headers[name] = value return name, value, nil } } func parseRoute(route string) (string, error) { parts := strings.Split(route, "|") if len(parts) != 4 { return "", errors.New("route is not in the correct format") } else { path := parts[0] body := parts[1] contentType := parts[2] status := parts[3] code, err := strconv.Atoi(status) if err != nil { return "", errors.New("route is not in the correct format") } bodies[path] = body statuses[path] = code contentTypes[path] = contentType return path, nil } }
/** Author: Mark George <mark.george@otago.ac.nz> License: Zero-Clause BSD License */ package main import ( "errors" "strconv" "strings" ) var ( headers map[string]string = make(map[string]string) bodies map[string]string = make(map[string]string) statuses map[string]int = make(map[string]int) ) func parseHeader(header string) (string, string, error) { parts := strings.Split(header, ":") if len(parts) < 2 { return "", "", errors.New("header is not in the correct format") } else { name := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) // value had a ':' in it (probably a URL) so append the remaining part if len(parts) == 3 { value = value + ":" + strings.TrimSpace(parts[2]) } headers[name] = value return name, value, nil } } func parseRoute(route string) (string, string, int, error) { parts := strings.Split(route, "|") if len(parts) != 3 { return "", "", 0, errors.New("route is not in the correct format") } else { path := parts[0] body := parts[1] status := parts[2] code, err := strconv.Atoi(status) if err != nil { return "", "", 0, errors.New("route is not in the correct format") } bodies[path] = body statuses[path] = code return path, body, code, nil } }
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:"Add a route which is made up of a path, a response body, a content type, and a response status code, all separated by the pipe '|' character. Repeat for additional routes. Example '/|Testing 123|text/plain|200'."` Cors bool `short:"c" long:"cors" description:"Enable Cross Origin Resource Sharing (CORS). Allows all requested methods and headers."` Headers []string `short:"H" long:"header" description:"A header to add to response in the form name:value. Use multiple times for multiple 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|content type|status code\"") os.Exit(1) } } fmt.Println() } else { // wildcard mode 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] contentType := contentTypes[path] code := statuses[path] // are we in wildcard mode? if code == 0 && len(statuses) == 0 { if req.Method == http.MethodGet { code = 200 } else { code = 204 } } else if code == 0 { // not in wildcard mode so this is a bad path code = 404 } showRequest(req) // CORS preflight if serverOptions.Cors && req.Method == http.MethodOptions { // was the OPTIONS request actually a CORS request? if secFetchMode := req.Header.Get("Sec-Fetch-Mode"); secFetchMode == "cors" { if globalOptions.Verbose { fmt.Println("Responding to CORS preflight") } if origin := req.Header.Get("Origin"); origin != "" { if globalOptions.Verbose { fmt.Println("Adding CORS response header - Access-Control-Allow-Origin: " + origin) } // allow the origin rsp.Header().Set("Access-Control-Allow-Origin", origin) if headers := req.Header.Get("Access-Control-Request-Headers"); headers != "" { if globalOptions.Verbose { fmt.Println("Adding CORS response header - Access-Control-Allow-Headers: " + headers) } // allow any requested headers rsp.Header().Set("Access-Control-Allow-Headers", headers) } if method := req.Header.Get("Access-Control-Request-Method"); method != "" { if globalOptions.Verbose { fmt.Println("Adding CORS response header - Access-Control-Allow-Methods: " + method) } // allow any requested methods rsp.Header().Set("Access-Control-Allow-Methods", method) } if globalOptions.Verbose { fmt.Println("Adding CORS response header - Access-Control-Max-Age: " + "600") } // allow browser to reuse the permissions for 10 minutes rsp.Header().Set("Access-Control-Max-Age", "600") rsp.WriteHeader(200) return } else { if globalOptions.Verbose { fmt.Println("CORS preflight did not contain 'Origin' header!") } rsp.Header().Set("Content-Type", "text/plain") rsp.WriteHeader(400) fmt.Fprint(rsp, "Origin header was not provided") return } } } // 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 CORS allow-origin header if serverOptions.Cors && req.Method != http.MethodOptions { if origin := req.Header.Get("Origin"); origin != "" { if globalOptions.Verbose { fmt.Println("Adding CORS response header - Access-Control-Allow-Origin: " + origin) } rsp.Header().Set("Access-Control-Allow-Origin", origin) } } // add content type if specified if len(contentType) > 0 { rsp.Header().Set("Content-Type", contentType) } // add status line rsp.WriteHeader(code) // add body fmt.Fprint(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."` Cors bool `short:"c" long:"cors" description:"Enable Cross Origin Resource Sharing (CORS). Allows all requested methods and headers."` Headers []string `short:"H" long:"header" description:"A header to add to response in the form name:value. Use multiple times for multiple 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) } } fmt.Println() } else { // wildcard mode 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] // are we in wildcard mode? if code == 0 && len(statuses) == 0 { if req.Method == http.MethodGet { code = 200 } else { code = 204 } } else if code == 0 { // not in wildcard mode so this is a bad path code = 404 } showRequest(req) // CORS preflight if serverOptions.Cors && req.Method == http.MethodOptions { // was the OPTIONS request actually a CORS request? if secFetchMode := req.Header.Get("Sec-Fetch-Mode"); secFetchMode == "cors" { if globalOptions.Verbose { fmt.Println("Responding to CORS preflight") } if origin := req.Header.Get("Origin"); origin != "" { if globalOptions.Verbose { fmt.Println("Adding CORS response header - Access-Control-Allow-Origin: " + origin) } // allow the origin rsp.Header().Set("Access-Control-Allow-Origin", origin) if headers := req.Header.Get("Access-Control-Request-Headers"); headers != "" { if globalOptions.Verbose { fmt.Println("Adding CORS response header - Access-Control-Allow-Headers: " + headers) } // allow any requested headers rsp.Header().Set("Access-Control-Allow-Headers", headers) } if method := req.Header.Get("Access-Control-Request-Method"); method != "" { if globalOptions.Verbose { fmt.Println("Adding CORS response header - Access-Control-Allow-Methods: " + method) } // allow any requested methods rsp.Header().Set("Access-Control-Allow-Methods", method) } if globalOptions.Verbose { fmt.Println("Adding CORS response header - Access-Control-Max-Age: " + "600") } // allow browser to reuse the permissions for 10 minutes rsp.Header().Set("Access-Control-Max-Age", "600") rsp.WriteHeader(200) return } else { if globalOptions.Verbose { fmt.Println("CORS preflight did not contain 'Origin' header!") } rsp.Header().Set("Content-Type", "text/plain") rsp.WriteHeader(400) fmt.Fprint(rsp, "Origin header was not provided") return } } } // 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 CORS allow-origin header if serverOptions.Cors && req.Method != http.MethodOptions { if origin := req.Header.Get("Origin"); origin != "" { if globalOptions.Verbose { fmt.Println("Adding CORS response header - Access-Control-Allow-Origin: " + origin) } rsp.Header().Set("Access-Control-Allow-Origin", origin) } } // add status line rsp.WriteHeader(code) // add body fmt.Fprint(rsp, body) }
Show line notes below