package main import ( "bytes" "crypto/tls" "encoding/json" "flag" "fmt" "html/template" "io" "io/ioutil" "log" "net/http" "net/http/httputil" "net/url" "os" "os/exec" "path" "strings" "gopkg.in/yaml.v2" ) var flags struct { addr string proxyURL string proxyClientCert string proxyClientKey string proxyMinikube bool } func init() { flag.StringVar(&flags.addr, "addr", "localhost:8080", "Address to listen on") flag.StringVar(&flags.proxyURL, "proxy-url", "", "Proxy requests to this URL") flag.StringVar(&flags.proxyClientCert, "proxy-client-cert", "", "Client certificate to use when connecting to proxy") flag.StringVar(&flags.proxyClientKey, "proxy-client-key", "", "Client key to use when connecting to proxy") flag.BoolVar(&flags.proxyMinikube, "proxy-minikube", false, "Shortcut for -proxy-url https://$(minikube ip):8443 -proxy-client-cert ~/.minikube/client.crt -proxy-client-key ~/.minikube/client.key") } var responses = []Response{} func main() { flag.Parse() if flags.proxyMinikube { err := proxyMinikube() if err != nil { log.Fatalf("Error: Setting up Minikube proxy: %s", err) } } if flag.NArg() == 1 { rs, err := loadResponses(flag.Arg(0)) if err != nil { log.Fatalf("Error: Parsing %s: %s", flag.Arg(0), err) } responses = rs } requestLog := make([]LogEntry, 0) http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { var resp *http.Response if flags.proxyURL != "" { resp = respondWithProxy(flags.proxyURL, w, req) } else { resp = respondWithStub(responses, w, req) } userAgent := req.Header.Get("User-Agent") log.Printf("%s %s - %d (%s, %q)", req.Method, req.URL, resp.StatusCode, req.RemoteAddr, userAgent) out, err := httputil.DumpRequest(req, true) if err != nil { log.Printf("Error: Dumping request: %s", err) return } if resp.Header.Get("Content-Type") == "application/json" { pretty, err := prettyfyJSON(resp.Body) if err != nil { log.Printf("Error: Prettyfying JSON: %s", err) } else { resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(pretty))) resp.Body = ioutil.NopCloser(bytes.NewReader(pretty)) } } requestLog = append(requestLog, LogEntry{ Request: out, Response: asResponse(req, resp), }) }) http.HandleFunc("/_log", func(w http.ResponseWriter, req *http.Request) { if strings.Contains(req.Header.Get("Accept"), "application/yaml") { rs := make([]Response, len(requestLog)) for i, log := range requestLog { rs[i] = log.Response } err := renderYAML(w, rs) if err != nil { log.Printf("Error: Render log: %s", err) } return } for i := len(requestLog) - 1; i >= 0; i-- { w.Write([]byte("------------------------------------------------------\n")) w.Write(requestLog[i].Request) w.Write([]byte("\n\n")) requestLog[i].Response.AsHTTP().Write(w) w.Write([]byte("\n")) } }) http.HandleFunc("/_stub", func(w http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": err := stubTmpl.Execute(w, nil) if err != nil { log.Printf("Error: Rendering stub template: %s", err) return } case "POST": err := req.ParseForm() if err != nil { log.Printf("Error: Parsing form: %s", err) return } responses = append(responses, readResponse(req.Form)) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } }) http.HandleFunc("/_stubs", func(w http.ResponseWriter, req *http.Request) { var err error if strings.Contains(req.Header.Get("Accept"), "application/yaml") { err = renderYAML(w, responses) } else { err = renderHTML(w, responses) } if err != nil { log.Printf("Error: Rendering stubs: %s", err) } }) http.HandleFunc("/_help", func(w http.ResponseWriter, req *http.Request) { urls := []struct { URL string Summary string }{ {URL: "/_log", Summary: "View all received requests with responses"}, {URL: "/_stub", Summary: "Add a new response stub"}, {URL: "/_stubs", Summary: "View all defined stubs"}, {URL: "/_help", Summary: "This help"}, } fmt.Fprint(w, ` /_help ") return nil } // LogEntry is a request/respond pair for logging. type LogEntry struct { Request Request Response Response } // Request is a stored serialized HTTP request. type Request []byte // Response is a mocked HTTP response. type Response struct { Method string `yaml:"method"` Path string `yaml:"path"` Status int `yaml:"status"` Headers []Header `yaml:"headers"` Body string `yaml:"body"` } func (resp Response) String() string { buf := new(bytes.Buffer) fmt.Fprintf(buf, "%s %s\r\n", resp.Method, resp.Path) for _, header := range resp.Headers { fmt.Fprintf(buf, "%s: %s\r\n", header.Name, header.Value) } fmt.Fprintf(buf, "\r\n%s", resp.Body) return buf.String() } // AsHTTP returns a http.Response representation. func (resp Response) AsHTTP() *http.Response { headers := make(map[string][]string) for _, header := range resp.Headers { h, ok := headers[header.Name] if !ok { h = []string{} } h = append(h, header.Value) headers[header.Name] = h } return &http.Response{ ProtoMajor: 1, ProtoMinor: 1, StatusCode: resp.Status, Header: headers, Body: ioutil.NopCloser(strings.NewReader(resp.Body)), } } func asResponse(req *http.Request, resp *http.Response) Response { headers := make([]Header, 0) for name, vals := range resp.Header { for _, val := range vals { headers = append(headers, Header{Name: name, Value: val}) } } buf := new(bytes.Buffer) io.Copy(buf, resp.Body) return Response{ Method: req.Method, Path: req.URL.Path, Status: resp.StatusCode, Headers: headers, Body: buf.String(), } } // Header is a single-valued HTTP header name and value type Header struct { Name string `yaml:"name"` Value string `yaml:"value"` } func readResponse(form url.Values) Response { r := Response{} r.Method = form.Get("method") r.Path = form.Get("path") r.Status = 200 headers := make([]Header, 0) for i, name := range form["header"] { headers = append(headers, Header{Name: name, Value: form["value"][i]}) } r.Body = form.Get("body") return r } func loadResponses(path string) ([]Response, error) { out, err := ioutil.ReadFile(path) if err != nil { return nil, err } err = yaml.Unmarshal(out, &responses) if err != nil { return nil, err } return responses, nil } var stubTmpl = template.Must(template.New("").Parse(`
`))