diff --git a/cmd/main.go b/cmd/main.go index 15f253e..f93475e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,15 +1,23 @@ package main import ( + "bytes" "crypto/tls" "crypto/x509" "flag" + "fmt" + "io/ioutil" "log" + "net" "net/http" "net/http/httputil" "net/url" + "os" + "strings" "github.com/dstotijn/gurp/pkg/proxy" + "github.com/dstotijn/gurp/pkg/reqlog" + "github.com/gorilla/mux" ) @@ -33,6 +41,8 @@ func main() { log.Fatalf("[FATAL] Could not parse CA: %v", err) } + reqLog := reqlog.NewRequestLog() + p, err := proxy.NewProxy(caCert, tlsCA.PrivateKey) if err != nil { log.Fatalf("[FATAL] Could not create Proxy: %v", err) @@ -40,34 +50,73 @@ func main() { p.UseRequestModifier(func(next proxy.RequestModifyFunc) proxy.RequestModifyFunc { return func(req *http.Request) { - log.Printf("[DEBUG] Incoming request: %v", req.URL) next(req) + clone := req.Clone(req.Context()) + var body []byte + if req.Body != nil { + // TODO: Use io.LimitReader. + body, err := ioutil.ReadAll(req.Body) + if err != nil { + log.Printf("[ERROR] Could not read request body for logging: %v", err) + return + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + } + reqLog.AddRequest(*clone, body) } }) p.UseResponseModifier(func(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc { return func(res *http.Response) error { - log.Printf("[DEBUG] Downstream response: %v %v %v", res.Proto, res.StatusCode, http.StatusText(res.StatusCode)) - return next(res) + if err := next(res); err != nil { + return err + } + clone := *res + var body []byte + if res.Body != nil { + // TODO: Use io.LimitReader. + var err error + body, err = ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("could not read response body: %v", err) + } + res.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + } + reqLog.AddResponse(clone, body) + return nil } }) - router := mux.NewRouter().SkipClean(true) - adminRouter := router.Host("gurp.proxy") + var adminHandler http.Handler if *dev { adminURL, err := url.Parse("http://localhost:3000") if err != nil { log.Fatalf("[FATAL] Invalid admin URL: %v", err) } - adminRouter.Handler(httputil.NewSingleHostReverseProxy(adminURL)) + adminHandler = httputil.NewSingleHostReverseProxy(adminURL) } else { if *adminPath == "" { log.Fatal("[FATAL] `adminPath` must be set") } - adminRouter.Handler(http.FileServer(http.Dir(*adminPath))) + adminHandler = http.FileServer(http.Dir(*adminPath)) } + router := mux.NewRouter().SkipClean(true) + + adminRouter := router.MatcherFunc(func(req *http.Request, match *mux.RouteMatch) bool { + hostname, _ := os.Hostname() + host, _, _ := net.SplitHostPort(req.Host) + return strings.EqualFold(host, hostname) || req.Host == "gurp.proxy" + }) + + // GraphQL server. + // adminRouter.Path("/graphql").Handler(...) + + // Admin interface. + adminRouter.Handler(adminHandler) + + // Fallback (default) is the Proxy handler. router.PathPrefix("").Handler(p) s := &http.Server{ diff --git a/pkg/reqlog/reqlog.go b/pkg/reqlog/reqlog.go new file mode 100644 index 0000000..153a3ab --- /dev/null +++ b/pkg/reqlog/reqlog.go @@ -0,0 +1,44 @@ +package reqlog + +import ( + "net/http" + "sync" +) + +type request struct { + req http.Request + body []byte +} + +type response struct { + res http.Response + body []byte +} + +type RequestLog struct { + reqStore []request + resStore []response + reqMu sync.Mutex + resMu sync.Mutex +} + +func NewRequestLog() RequestLog { + return RequestLog{ + reqStore: make([]request, 0), + resStore: make([]response, 0), + } +} + +func (rl *RequestLog) AddRequest(req http.Request, body []byte) { + rl.reqMu.Lock() + defer rl.reqMu.Unlock() + + rl.reqStore = append(rl.reqStore, request{req, body}) +} + +func (rl *RequestLog) AddResponse(res http.Response, body []byte) { + rl.resMu.Lock() + defer rl.resMu.Unlock() + + rl.resStore = append(rl.resStore, response{res, body}) +}