capture/main.go

318 lines
8.2 KiB
Go
Raw Normal View History

2017-11-08 00:10:54 +01:00
package main
import (
"bytes"
"compress/gzip"
2021-02-24 12:49:00 +01:00
_ "embed"
2018-07-21 19:50:53 +02:00
"encoding/json"
2017-11-18 13:23:36 +01:00
"fmt"
2019-11-16 17:50:13 +01:00
"io"
2017-11-08 00:10:54 +01:00
"io/ioutil"
"net/http"
2018-11-22 22:45:20 +01:00
"net/http/httptest"
2017-11-08 00:10:54 +01:00
"net/http/httputil"
2018-08-04 01:53:54 +02:00
"net/url"
2019-03-10 17:05:05 +01:00
"os"
2019-07-27 15:57:16 +02:00
"path"
2019-03-12 23:39:18 +01:00
"path/filepath"
2018-11-24 11:03:16 +01:00
"plugin"
2019-11-16 16:47:02 +01:00
"sort"
2018-07-21 19:50:53 +02:00
"strings"
2019-03-23 23:13:16 +01:00
"time"
2017-11-21 22:36:40 +01:00
)
2021-04-06 12:19:16 +02:00
// StatusInternalProxyError is any unknown proxy error.
const StatusInternalProxyError = 999
2021-02-24 12:49:00 +01:00
//go:embed dashboard.html
2021-04-06 12:13:58 +02:00
var dashboardHTML []byte
2021-02-24 12:49:00 +01:00
2017-11-08 00:10:54 +01:00
func main() {
2019-12-20 11:55:45 +01:00
cfg := ReadConfig()
2017-11-08 00:10:54 +01:00
fmt.Printf("Target is %s", cfg.TargetURL)
2019-12-20 11:55:45 +01:00
fmt.Printf("\nListening on http://localhost:%s", cfg.ProxyPort)
fmt.Printf("\nDashboard on http://localhost:%s", cfg.DashboardPort)
2019-11-09 00:27:02 +01:00
fmt.Println()
2018-11-22 22:45:20 +01:00
2019-12-20 11:55:45 +01:00
srv := NewCaptureService(cfg.MaxCaptures)
hdr := NewRecorderHandler(srv, NewPluginHandler(NewProxyHandler(cfg.TargetURL)))
2018-11-22 22:45:20 +01:00
2019-11-09 00:27:02 +01:00
go func() {
2019-12-20 11:55:45 +01:00
fmt.Println(http.ListenAndServe(":"+cfg.DashboardPort, NewDashboardHandler(hdr, srv, cfg)))
2019-11-09 00:27:02 +01:00
os.Exit(1)
}()
2019-12-20 11:55:45 +01:00
fmt.Println(http.ListenAndServe(":"+cfg.ProxyPort, hdr))
2019-11-09 00:27:02 +01:00
}
2017-11-18 13:23:36 +01:00
2021-04-06 12:13:58 +02:00
func NewDashboardHandler(h http.HandlerFunc, srv *CaptureService, cfg Config) http.Handler {
2019-06-21 01:25:57 +02:00
router := http.NewServeMux()
2021-04-06 12:13:58 +02:00
router.HandleFunc("/", NewDashboardHTMLHandler())
router.HandleFunc("/conn/", NewDashboardConnHandler(srv, cfg))
2019-11-09 00:27:02 +01:00
router.HandleFunc("/info/", NewDashboardInfoHandler(srv))
router.HandleFunc("/clear/", NewDashboardClearHandler(srv))
router.HandleFunc("/retry/", NewDashboardRetryHandler(srv, h))
2019-06-21 01:25:57 +02:00
return router
2017-11-08 00:10:54 +01:00
}
2018-12-01 23:59:33 +01:00
// NewDashboardConnHandler opens an event stream connection with the dashboard
2021-04-06 12:19:16 +02:00
// so that it is notified everytime a new capture arrives.
2021-04-06 12:13:58 +02:00
func NewDashboardConnHandler(srv *CaptureService, cfg Config) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
if _, ok := w.(http.Flusher); !ok {
http.Error(w, "streaming not supported", http.StatusInternalServerError)
2018-11-25 18:10:10 +01:00
return
}
2021-04-06 12:13:58 +02:00
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
2021-04-06 12:33:05 +02:00
sendEvent := func(event string, data interface{}) {
jsn, _ := json.Marshal(data)
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, jsn)
w.(http.Flusher).Flush()
}
sendEvent("config", cfg)
2021-04-06 12:13:58 +02:00
2018-11-25 18:10:10 +01:00
for {
2021-04-06 12:33:05 +02:00
sendEvent("captures", srv.DashboardItems())
2018-11-25 18:10:10 +01:00
select {
2019-06-21 01:09:58 +02:00
case <-srv.Updated():
2018-11-25 18:10:10 +01:00
case <-req.Context().Done():
return
}
}
2018-12-01 22:45:41 +01:00
}
2017-11-21 22:36:40 +01:00
}
2021-04-06 12:19:16 +02:00
// NewDashboardClearHandler clears all the captures.
2019-06-21 01:09:58 +02:00
func NewDashboardClearHandler(srv *CaptureService) http.HandlerFunc {
2018-12-01 22:45:41 +01:00
return func(rw http.ResponseWriter, req *http.Request) {
2019-06-21 01:09:58 +02:00
srv.RemoveAll()
2018-11-16 22:39:53 +01:00
rw.WriteHeader(http.StatusOK)
2018-12-01 22:45:41 +01:00
}
2018-09-07 16:45:02 +02:00
}
2021-04-06 12:19:16 +02:00
// NewDashboardHTMLHandler returns the dashboard html page.
2021-04-06 12:13:58 +02:00
func NewDashboardHTMLHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
// This redirect prevents accessing the dashboard page from paths other
// than the root path. This is important because the dashboard uses
// relative paths, so "/retry/" would become "/something/retry/".
if req.URL.Path != "/" {
2021-04-06 12:13:58 +02:00
http.Redirect(w, req, "/", http.StatusTemporaryRedirect)
return
}
2021-04-06 12:13:58 +02:00
w.Header().Add("Content-Type", "text/html")
w.Write(dashboardHTML)
2018-12-01 22:45:41 +01:00
}
2018-08-04 01:53:54 +02:00
}
2021-04-06 12:19:16 +02:00
// NewDashboardRetryHandler retries a request.
2019-06-21 01:09:58 +02:00
func NewDashboardRetryHandler(srv *CaptureService, next http.HandlerFunc) http.HandlerFunc {
2018-12-01 22:45:41 +01:00
return func(rw http.ResponseWriter, req *http.Request) {
2019-07-27 15:57:16 +02:00
id := path.Base(req.URL.Path)
2019-06-21 01:09:58 +02:00
capture := srv.Find(id)
2019-06-21 02:06:50 +02:00
2021-04-06 12:19:16 +02:00
// Create a new request based on the current one.
2019-11-16 16:47:02 +01:00
r, _ := http.NewRequest(capture.Req.Method, capture.Req.Url, bytes.NewReader(capture.Req.Body))
2018-11-25 22:24:59 +01:00
r.Header = capture.Req.Header
2019-06-21 02:06:50 +02:00
2018-11-25 22:24:59 +01:00
next.ServeHTTP(rw, r)
2018-12-01 22:45:41 +01:00
}
2018-11-25 22:24:59 +01:00
}
2021-04-06 12:19:16 +02:00
// NewDashboardInfoHandler returns the full capture info.
2019-06-21 01:09:58 +02:00
func NewDashboardInfoHandler(srv *CaptureService) http.HandlerFunc {
2018-12-01 22:45:41 +01:00
return func(rw http.ResponseWriter, req *http.Request) {
2019-07-27 15:57:16 +02:00
id := path.Base(req.URL.Path)
2019-06-21 01:09:58 +02:00
capture := srv.Find(id)
2018-11-22 22:45:20 +01:00
rw.Header().Add("Content-Type", "application/json")
2018-11-24 15:24:16 +01:00
json.NewEncoder(rw).Encode(dump(capture))
2018-12-01 22:45:41 +01:00
}
2018-08-02 00:36:23 +02:00
}
2021-04-06 12:19:16 +02:00
// NewPluginHandler loads plugin files in the current directory.
// They are loaded sorted by filename.
2019-06-21 01:09:58 +02:00
func NewPluginHandler(next http.HandlerFunc) http.HandlerFunc {
2019-03-12 23:39:18 +01:00
ex, err := os.Executable()
if err != nil {
fmt.Println("error: could not get executable:", err)
return next
}
exPath := filepath.Dir(ex)
files, err := ioutil.ReadDir(exPath)
2018-11-24 11:03:16 +01:00
if err != nil {
2019-03-10 16:15:15 +01:00
fmt.Println("error: could not read directory:", err)
2018-11-24 11:03:16 +01:00
return next
}
2019-03-10 16:15:15 +01:00
for _, file := range files {
if file.IsDir() {
continue
}
if strings.HasSuffix(file.Name(), ".so") {
2019-06-21 01:44:42 +02:00
fmt.Printf("Loading plugin '%s'\n", file.Name())
2019-03-12 23:39:18 +01:00
p, err := plugin.Open(exPath + "/" + file.Name())
2019-03-10 16:15:15 +01:00
if err != nil {
fmt.Println("error: could not open plugin:", err)
2019-03-10 17:05:05 +01:00
os.Exit(1)
2019-03-10 16:15:15 +01:00
}
fn, err := p.Lookup("Handler")
if err != nil {
fmt.Println("error: could not find plugin Handler function:", err)
2019-03-10 17:05:05 +01:00
os.Exit(1)
2019-03-10 16:15:15 +01:00
}
pluginHandler, ok := fn.(func(http.HandlerFunc) http.HandlerFunc)
if !ok {
fmt.Println("error: plugin Handler function should be 'func(http.HandlerFunc) http.HandlerFunc'")
2019-03-10 17:05:05 +01:00
os.Exit(1)
2019-03-10 16:15:15 +01:00
}
next = pluginHandler(next)
}
2018-11-24 11:03:16 +01:00
}
2019-03-10 16:15:15 +01:00
return next
2018-11-24 11:03:16 +01:00
}
2021-04-06 12:19:16 +02:00
// NewRecorderHandler records all the traffic data.
2019-06-21 01:09:58 +02:00
func NewRecorderHandler(srv *CaptureService, next http.HandlerFunc) http.HandlerFunc {
2019-11-16 16:47:02 +01:00
return func(rw http.ResponseWriter, r *http.Request) {
2018-11-24 15:24:16 +01:00
2019-11-16 16:47:02 +01:00
// Save req body for later.
2019-11-16 17:50:13 +01:00
reqBody := &bytes.Buffer{}
r.Body = ioutil.NopCloser(io.TeeReader(r.Body, reqBody))
2017-11-08 00:10:54 +01:00
2018-11-22 22:45:20 +01:00
rec := httptest.NewRecorder()
2019-11-16 16:47:02 +01:00
// Record Roundtrip.
2019-03-23 23:13:16 +01:00
start := time.Now()
2019-11-16 16:47:02 +01:00
next.ServeHTTP(rec, r)
2018-11-22 22:45:20 +01:00
2019-03-23 23:13:16 +01:00
elapsed := time.Since(start).Truncate(time.Millisecond) / time.Millisecond
2019-11-16 16:47:02 +01:00
resBody := rec.Body.Bytes()
// Respond to client with recorded response.
2019-03-10 16:46:38 +01:00
for k, v := range rec.Header() {
2018-11-22 22:45:20 +01:00
rw.Header()[k] = v
}
rw.WriteHeader(rec.Code)
2019-11-16 16:47:02 +01:00
rw.Write(resBody)
2018-11-22 22:45:20 +01:00
2019-11-16 16:47:02 +01:00
// Save req and res data.
req := Req{
Proto: r.Proto,
Method: r.Method,
Url: r.URL.String(),
Path: r.URL.Path,
2023-05-27 18:11:40 +02:00
Query: extractQueryString(r.RequestURI),
2019-11-16 16:47:02 +01:00
Header: r.Header,
2019-11-16 17:50:13 +01:00
Body: reqBody.Bytes(),
2019-11-16 16:47:02 +01:00
}
2023-05-27 18:11:40 +02:00
2019-11-16 16:47:02 +01:00
res := Res{
Proto: rec.Result().Proto,
Status: rec.Result().Status,
Code: rec.Code,
Header: rec.Header(),
Body: resBody,
}
2019-06-21 01:09:58 +02:00
srv.Insert(Capture{Req: req, Res: res, Elapsed: elapsed})
2018-12-01 22:45:41 +01:00
}
2018-11-22 22:45:20 +01:00
}
2021-04-06 12:19:16 +02:00
// NewProxyHandler is the reverse proxy handler.
2018-12-01 22:45:41 +01:00
func NewProxyHandler(URL string) http.HandlerFunc {
2018-11-22 22:45:20 +01:00
url, _ := url.Parse(URL)
proxy := httputil.NewSingleHostReverseProxy(url)
proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
2019-06-21 01:44:42 +02:00
fmt.Printf("Uh oh | %v | %s %s\n", err, req.Method, req.URL)
rw.WriteHeader(StatusInternalProxyError)
fmt.Fprintf(rw, "%v", err)
2018-11-22 22:45:20 +01:00
}
2018-12-01 22:45:41 +01:00
return func(rw http.ResponseWriter, req *http.Request) {
2018-11-22 22:45:20 +01:00
req.Host = url.Host
req.URL.Host = url.Host
req.URL.Scheme = url.Scheme
2018-11-11 17:54:35 +01:00
proxy.ServeHTTP(rw, req)
2018-12-01 22:45:41 +01:00
}
2018-08-04 01:53:54 +02:00
}
2019-11-23 15:00:26 +01:00
func dump(c *Capture) CaptureInfo {
2019-11-16 16:47:02 +01:00
req := c.Req
res := c.Res
2023-05-27 18:11:40 +02:00
query := ""
if len(req.Query) > 1 {
query = "?" + req.Query
}
2019-11-23 15:00:26 +01:00
return CaptureInfo{
2023-05-27 18:11:40 +02:00
Request: dumpContent(req.Header, req.Body, "%s %s%s %s\n\n", req.Method, req.Path, query, req.Proto),
2019-11-16 16:47:02 +01:00
Response: dumpContent(res.Header, res.Body, "%s %s\n\n", res.Proto, res.Status),
Curl: dumpCurl(req),
2018-11-24 15:24:16 +01:00
}
}
2019-11-16 16:47:02 +01:00
func dumpContent(header http.Header, body []byte, format string, args ...interface{}) string {
b := strings.Builder{}
fmt.Fprintf(&b, format, args...)
dumpHeader(&b, header)
b.WriteString("\n")
dumpBody(&b, header, body)
return b.String()
2018-07-21 19:50:53 +02:00
}
2019-11-16 16:47:02 +01:00
func dumpHeader(dst *strings.Builder, header http.Header) {
var headers []string
for k, v := range header {
headers = append(headers, fmt.Sprintf("%s: %s\n", k, strings.Join(v, " ")))
}
2019-11-16 16:47:02 +01:00
sort.Strings(headers)
for _, v := range headers {
dst.WriteString(v)
}
}
2018-09-07 16:45:02 +02:00
2019-11-16 16:47:02 +01:00
func dumpBody(dst *strings.Builder, header http.Header, body []byte) {
reqBody := body
if header.Get("Content-Encoding") == "gzip" {
reader, _ := gzip.NewReader(bytes.NewReader(body))
reqBody, _ = ioutil.ReadAll(reader)
}
dst.Write(reqBody)
2018-12-01 23:59:33 +01:00
}
2019-11-16 16:47:02 +01:00
func dumpCurl(req Req) string {
var b strings.Builder
2021-04-06 12:19:16 +02:00
// Build cmd.
2019-11-16 16:47:02 +01:00
fmt.Fprintf(&b, "curl -X %s %s", req.Method, req.Url)
2021-04-06 12:19:16 +02:00
// Build headers.
2019-11-16 16:47:02 +01:00
for k, v := range req.Header {
fmt.Fprintf(&b, " \\\n -H '%s: %s'", k, strings.Join(v, " "))
}
2021-04-06 12:19:16 +02:00
// Build body.
2019-11-16 16:47:02 +01:00
if len(req.Body) > 0 {
fmt.Fprintf(&b, " \\\n -d '%s'", req.Body)
}
return b.String()
2018-12-01 23:59:33 +01:00
}
2023-05-27 18:11:40 +02:00
func extractQueryString(uri string) string {
parts := strings.SplitN(uri, "?", 2)
if len(parts) != 2 {
return ""
}
return parts[1]
}