diff --git a/.gitignore b/.gitignore index 7a124407..32bfcad2 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ compose.yml go-proxy.yml bin/go-proxy.bak -logs/ \ No newline at end of file +logs/ +log/ \ No newline at end of file diff --git a/Makefile b/Makefile index 93e9db51..d760dd1e 100755 --- a/Makefile +++ b/Makefile @@ -13,14 +13,14 @@ quick-restart: # quick restart without restarting the container docker cp bin/go-proxy go-proxy:/app/go-proxy docker cp templates/* go-proxy:/app/templates docker cp entrypoint.sh go-proxy:/app/entrypoint.sh - docker exec -d go-proxy bash -c "/app/entrypoint.sh restart" + docker exec -d go-proxy bash /app/entrypoint.sh restart restart: docker kill go-proxy docker compose up -d go-proxy logs: - docker logs -f go-proxy + tail -f log/go-proxy.log get: go get -d -u ./src/go-proxy diff --git a/README.md b/README.md index 750774c4..0e05fc21 100755 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ In the examples domain `x.y.z` is used, replace them with your domain - subdomain matching **(domain name doesn't matter)** - path matching - HTTP proxy -- TCP/UDP Proxy (experimental, unable to release port on hot-reload) +- TCP/UDP Proxy - HTTP round robin load balance support (same subdomain and path across containers replicas) - Auto hot-reload when container start / die / stop. - Simple panel to see all reverse proxies and health (visit port [panel port] of go-proxy `https://*.y.z:[panel port]`) @@ -97,7 +97,8 @@ However, there are some labels you can manipulate with: - forward: path remain unchanged 1. apps.y.z/webdav -> webdav:80/webdav 2. apps.y.z./webdav/path/to/file -> webdav:80/webdav/path/to/file - - sub: remove path prefix from both URL and HTML attributes (`src`, `href` and `action`) + - sub: (experimental) remove path prefix from URL and also append path to HTML link attributes (`src`, `href` and `action`) and Javascript `fetch(url)` by response body substitution + e.g. apps.y.z/app1 -> webdav:80, `href="/path/to/file"` -> `href="/app1/path/to/file"` - `proxy..load_balance`: enable load balance - allowed: `1`, `true` diff --git a/bin/go-proxy b/bin/go-proxy index 115cc521..d9da5107 100755 Binary files a/bin/go-proxy and b/bin/go-proxy differ diff --git a/compose.example.yml b/compose.example.yml index 200dcf3e..cc014b26 100755 --- a/compose.example.yml +++ b/compose.example.yml @@ -19,7 +19,7 @@ services: volumes: - /path/to/cert.pem:/certs/cert.crt:ro - /path/to/privkey.pem:/certs/priv.key:ro - - ./go-proxy/logs:/app/log # path to logs + - ./log:/app/log # path to logs - /var/run/docker.sock:/var/run/docker.sock:ro extra_hosts: - host.docker.internal:host-gateway # required if you have containers in `host` network_mode diff --git a/entrypoint.sh b/entrypoint.sh index ca77871c..9cbf3dbd 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,12 +1,15 @@ #!/bin/bash if [ "$1" == "restart" ]; then + echo "restarting" killall go-proxy fi +if [ -z "$VERBOSITY" ]; then + VERBOSITY=1 +fi +echo "starting with verbosity $VERBOSITY" > log/go-proxy.log if [ "$DEBUG" == "1" ]; then - /app/go-proxy -v=$VERBOSITY -log_dir=log --stderrthreshold=0 & - if [ "$1" != "restart" ]; then - tail -f /dev/null - fi + /app/go-proxy -v=$VERBOSITY -log_dir=log --stderrthreshold=0 2>> log/go-proxy.log & + tail -f /dev/null else - /app/go-proxy -v=$VERBOSITY -log_dir=log --stderrthreshold=0 & + /app/go-proxy -v=$VERBOSITY -log_dir=log --stderrthreshold=0 fi \ No newline at end of file diff --git a/go.sum b/go.sum index 215c67fa..0076af92 100755 --- a/go.sum +++ b/go.sum @@ -74,16 +74,12 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -94,8 +90,6 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -108,8 +102,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/src/go-proxy/constants.go b/src/go-proxy/constants.go index 744ce285..d5f1cb4c 100644 --- a/src/go-proxy/constants.go +++ b/src/go-proxy/constants.go @@ -46,7 +46,7 @@ const ( const ( ProxyPathMode_Forward = "forward" - ProxyPathMode_Sub = "sub" // TODO: implement + ProxyPathMode_Sub = "sub" ProxyPathMode_RemovedPath = "" ) diff --git a/src/go-proxy/http_route.go b/src/go-proxy/http_route.go index e2b9c431..ab092508 100755 --- a/src/go-proxy/http_route.go +++ b/src/go-proxy/http_route.go @@ -65,20 +65,31 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) { initRewrite(pr) // disable compression pr.Out.Header.Set("Accept-Encoding", "identity") + // remove path prefix pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path) } route.Proxy.ModifyResponse = func(r *http.Response) error { contentType, ok := r.Header["Content-Type"] if !ok || len(contentType) == 0 { - glog.Infof("unknown content type for %s", r.Request.URL.String()) + if glog.V(3) { + glog.Infof("[Path sub] unknown content type for %s", r.Request.URL.String()) + } return nil } - if !strings.HasPrefix(contentType[0], "text/html") { - return nil + // disable cache + r.Header.Set("Cache-Control", "no-store") + + var err error = nil + switch { + case strings.HasPrefix(contentType[0], "text/html"): + err = utils.respHTMLSubPath(r, config.Path) + case strings.HasPrefix(contentType[0], "application/javascript"): + err = utils.respJSSubPath(r, config.Path) + default: + glog.V(4).Infof("[Path sub] unknown content type(s): %s", contentType) } - err := utils.respRemovePath(r, config.Path) if err != nil { - err = fmt.Errorf("failed to remove path prefix %s: %v", config.Path, err) + err = fmt.Errorf("[Path sub] failed to remove path prefix %s: %v", config.Path, err) r.Status = err.Error() r.StatusCode = http.StatusInternalServerError } @@ -96,7 +107,7 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) { rewrite(pr) r := pr.In glog.Infof("[Request] %s %s%s", r.Method, r.Host, r.URL.Path) - glog.V(4).InfoDepthf(1, "Headers: %v", r.Header) + glog.V(5).InfoDepthf(1, "Headers: %v", r.Header) } } else { route.Proxy.Rewrite = rewrite @@ -141,7 +152,6 @@ func httpProxyHandler(w http.ResponseWriter, r *http.Request) { r.URL.Path, err, ) - glog.Error(err) http.Error(w, err.Error(), http.StatusNotFound) return } diff --git a/src/go-proxy/main.go b/src/go-proxy/main.go index c5a44d2e..c1ba3c4c 100755 --- a/src/go-proxy/main.go +++ b/src/go-proxy/main.go @@ -4,6 +4,7 @@ import ( "flag" "net/http" "runtime" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -51,6 +52,12 @@ func main() { } }() + go func() { + for range time.Tick(100 * time.Millisecond) { + glog.Flush() + } + }() + mux := http.NewServeMux() mux.HandleFunc("/", httpProxyHandler) diff --git a/src/go-proxy/utils.go b/src/go-proxy/utils.go index b2e3ba08..765ec6e2 100755 --- a/src/go-proxy/utils.go +++ b/src/go-proxy/utils.go @@ -6,10 +6,14 @@ import ( "io" "net" "net/http" + "path" + "path/filepath" + "regexp" "strings" "sync" "time" + "github.com/golang/glog" xhtml "golang.org/x/net/html" ) @@ -79,7 +83,7 @@ func (*Utils) healthCheckHttp(targetUrl string) error { return err } -func (*Utils) healthCheckStream(scheme string, host string) error { +func (*Utils) healthCheckStream(scheme, host string) error { conn, err := net.DialTimeout(scheme, host, 5*time.Second) if err != nil { return err @@ -93,25 +97,49 @@ func (*Utils) snakeToCamel(s string) string { return strings.ReplaceAll(toHyphenCamel, "-", "") } -func htmlNodesSubPath(node *xhtml.Node, path string) { - if node.Type == xhtml.ElementNode { - for _, attr := range node.Attr { - switch attr.Key { - case "src": // img, script, etc. - case "href": // link - case "action": // form - if strings.HasPrefix(attr.Val, path) { - attr.Val = strings.Replace(attr.Val, path, "", 1) - } - } - } - } - for c := node.FirstChild; c != nil; c = c.NextSibling { - htmlNodesSubPath(c, path) +func tryAppendPathPrefixImpl(pOrig, pAppend string) string { + switch { + case strings.Contains(pOrig, "://"): + return pOrig + case pOrig == "", pOrig == "#", pOrig == "/": + return pAppend + case filepath.IsLocal(pOrig) && !strings.HasPrefix(pOrig, pAppend): + return path.Join(pAppend, pOrig) + default: + return pOrig } } -func (*Utils) respRemovePath(r *http.Response, path string) error { +var tryAppendPathPrefix func(string, string) string +var _ = func() int { + if glog.V(4) { + tryAppendPathPrefix = func(s1, s2 string) string { + replaced := tryAppendPathPrefixImpl(s1, s2) + glog.Infof("[Path sub] %s -> %s", s1, replaced) + return replaced + } + } else { + tryAppendPathPrefix = tryAppendPathPrefixImpl + } + return 1 +}() + +func htmlNodesSubPath(n *xhtml.Node, p string) { + if n.Type == xhtml.ElementNode { + for i, attr := range n.Attr { + switch attr.Key { + case "src", "href", "action": // img, script, link, form etc. + n.Attr[i].Val = tryAppendPathPrefix(attr.Val, p) + } + } + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + htmlNodesSubPath(c, p) + } +} + +func (*Utils) respHTMLSubPath(r *http.Response, p string) error { // remove all path prefix from relative path in script, img, a, ... doc, err := xhtml.Parse(r.Body) @@ -119,11 +147,14 @@ func (*Utils) respRemovePath(r *http.Response, path string) error { return err } - htmlNodesSubPath(doc, path) + if p[0] == '/' { + p = p[1:] + } + htmlNodesSubPath(doc, p) var buf bytes.Buffer err = xhtml.Render(&buf, doc) - + if err != nil { return err } @@ -132,3 +163,29 @@ func (*Utils) respRemovePath(r *http.Response, path string) error { return nil } + +func (*Utils) respJSSubPath(r *http.Response, p string) error { + var buf bytes.Buffer + + _, err := buf.ReadFrom(r.Body) + if err != nil { + return err + } + + if p[0] == '/' { + p = p[1:] + } + + js := buf.String() + + re := regexp.MustCompile(`fetch\(["'].+["']\)`) + replace := func(match string) string { + match = match[7 : len(match)-2] + replaced := tryAppendPathPrefix(match, p) + return fmt.Sprintf(`fetch(%q)`, replaced) + } + js = re.ReplaceAllStringFunc(js, replace) + + r.Body = io.NopCloser(strings.NewReader(js)) + return nil +} diff --git a/templates/panel.html b/templates/panel.html index a331dfde..21f02046 100755 --- a/templates/panel.html +++ b/templates/panel.html @@ -72,8 +72,8 @@ function updateHealthStatus() { let rows = document.querySelectorAll('tbody tr'); rows.forEach(row => { - let url = row.cells[2].textContent; - let cell = row.cells[3]; // Health column cell + let url = row.cells[3].textContent; + let cell = row.cells[4]; // Health column cell checkHealth(url, cell); }); } @@ -100,6 +100,7 @@ Alias Path + Path Mode URL Health @@ -111,6 +112,7 @@ {{$alias}} {{$path}} + {{$route.PathMode}} {{$route.Url.String}}