diff --git a/.gitignore b/.gitignore index ae506d9..2f9becb 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ compose.yml +*.compose.yml config*/ certs*/ @@ -20,3 +21,4 @@ todo.md .*.swp .aider* mtrace.json +.env diff --git a/Dockerfile b/Dockerfile index 8b837f6..7234efa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Builder -FROM golang:1.23.1-alpine AS builder +FROM golang:1.23.2-alpine AS builder RUN apk add --no-cache tzdata make WORKDIR /src diff --git a/Makefile b/Makefile index 0581220..c1f85a5 100755 --- a/Makefile +++ b/Makefile @@ -30,6 +30,9 @@ get: debug: make build && sudo GOPROXY_DEBUG=1 bin/go-proxy +mtrace: + bin/go-proxy debug-ls-mtrace > mtrace.json + run-test: make build && sudo GOPROXY_TEST=1 bin/go-proxy diff --git a/README.md b/README.md index 7d38c58..8593ad7 100755 --- a/README.md +++ b/README.md @@ -72,13 +72,16 @@ _Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_ 4. Setup `docker-socket-proxy` other docker nodes _(if any)_ (see [Multi docker nodes setup](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)) and then them inside `config.yml` -5. Done. You may now do some extra configuration +5. Run go-proxy `docker compose up -d` + then list all routes to see if further configurations are needed: + `docker exec go-proxy /app/go-proxy ls-routes` + +6. You may now do some extra configuration - With text editor (e.g. Visual Studio Code) - With Web UI via `http://localhost:3000` or `https://gp.y.z` - For more info, [See Wiki]([wiki](https://github.com/yusing/go-proxy/wiki)) [🔼Back to top](#table-of-content) -| ### Use JSON Schema in VSCode diff --git a/compose.example.yml b/compose.example.yml index 7097eee..ebb865d 100755 --- a/compose.example.yml +++ b/compose.example.yml @@ -14,7 +14,7 @@ services: # Make sure the value is same as `GOPROXY_API_ADDR` below (if you have changed it) # # environment: - # NEXT_PUBLIC_GOPROXY_API_ADDR: 127.0.0.1:8888 + # GOPROXY_API_ADDR: 127.0.0.1:8888 app: image: ghcr.io/yusing/go-proxy:latest container_name: go-proxy diff --git a/examples/microbin.yml b/examples/microbin.yml index a59a34a..9ede3b5 100644 --- a/examples/microbin.yml +++ b/examples/microbin.yml @@ -7,7 +7,7 @@ services: limits: memory: 256M env_file: .env - image: docker.i.sh/danielszabo99/microbin:latest + image: danielszabo99/microbin:latest ports: - 8080 restart: unless-stopped diff --git a/go.mod b/go.mod index 1d9b072..e7a4495 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/yusing/go-proxy -go 1.23.1 +go 1.23.2 require ( github.com/coder/websocket v1.8.12 github.com/docker/cli v27.3.1+incompatible github.com/docker/docker v27.3.1+incompatible github.com/fsnotify/fsnotify v1.7.0 - github.com/go-acme/lego/v4 v4.19.0 + github.com/go-acme/lego/v4 v4.19.2 github.com/puzpuzpuz/xsync/v3 v3.4.0 github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index 2044e34..d4f7e02 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-acme/lego/v4 v4.19.0 h1:c7YabBOwoa2URsPiCNGQsdzQnbd8Y23B4/66gxh4H7c= -github.com/go-acme/lego/v4 v4.19.0/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ= +github.com/go-acme/lego/v4 v4.19.2 h1:Y8hrmMvWETdqzzkRly7m98xtPJJivWFsgWi8fcvZo+Y= +github.com/go-acme/lego/v4 v4.19.2/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/internal/docker/idlewatcher/html/loading_page.html b/internal/docker/idlewatcher/html/loading_page.html index d8eaf00..405d23b 100644 --- a/internal/docker/idlewatcher/html/loading_page.html +++ b/internal/docker/idlewatcher/html/loading_page.html @@ -66,22 +66,23 @@ -
-
{{.Message}}
+
+
{{.Message}}
diff --git a/internal/docker/idlewatcher/http.go b/internal/docker/idlewatcher/http.go index f2b4605..19fbabb 100644 --- a/internal/docker/idlewatcher/http.go +++ b/internal/docker/idlewatcher/http.go @@ -4,84 +4,35 @@ import ( "bytes" _ "embed" "fmt" - "io" - "net/http" "strings" "text/template" ) type templateData struct { - Title string - Message string - RequestHeaders http.Header - SpinnerClass string + CheckRedirectHeader string + Title string + Message string } //go:embed html/loading_page.html var loadingPage []byte var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage))) -const ( - htmlContentType = "text/html; charset=utf-8" +const headerCheckRedirect = "X-GoProxy-Check-Redirect" - errPrefix = "\u1000" - - headerGoProxyTargetURL = "X-GoProxy-Target" - headerContentType = "Content-Type" - - spinnerClassSpinner = "spinner" - spinnerClassErrorSign = "error" -) - -func (w *watcher) makeSuccResp(redirectURL string, resp *http.Response) (*http.Response, error) { - h := make(http.Header) - h.Set("Location", redirectURL) - h.Set("Content-Length", "0") - h.Set(headerContentType, htmlContentType) - return &http.Response{ - StatusCode: http.StatusTemporaryRedirect, - Header: h, - Body: http.NoBody, - TLS: resp.TLS, - }, nil -} - -func (w *watcher) makeErrResp(errFmt string, args ...any) (*http.Response, error) { - return w.makeResp(errPrefix+errFmt, args...) -} - -func (w *watcher) makeResp(format string, args ...any) (*http.Response, error) { +func (w *watcher) makeRespBody(format string, args ...any) []byte { msg := fmt.Sprintf(format, args...) data := new(templateData) + data.CheckRedirectHeader = headerCheckRedirect data.Title = w.ContainerName data.Message = strings.ReplaceAll(msg, "\n", "
") data.Message = strings.ReplaceAll(data.Message, " ", " ") - data.RequestHeaders = make(http.Header) - data.RequestHeaders.Add(headerGoProxyTargetURL, "window.location.href") - if strings.HasPrefix(data.Message, errPrefix) { - data.Message = strings.TrimLeft(data.Message, errPrefix) - data.SpinnerClass = spinnerClassErrorSign - } else { - data.SpinnerClass = spinnerClassSpinner - } buf := bytes.NewBuffer(make([]byte, 128)) // more than enough err := loadingPageTmpl.Execute(buf, data) - if err != nil { // should never happen + if err != nil { // should never happen in production panic(err) } - return &http.Response{ - StatusCode: http.StatusAccepted, - Header: http.Header{ - headerContentType: {htmlContentType}, - "Cache-Control": { - "no-cache", - "no-store", - "must-revalidate", - }, - }, - Body: io.NopCloser(buf), - ContentLength: int64(buf.Len()), - }, nil + return buf.Bytes() } diff --git a/internal/docker/idlewatcher/round_trip.go b/internal/docker/idlewatcher/round_trip.go deleted file mode 100644 index 72363cc..0000000 --- a/internal/docker/idlewatcher/round_trip.go +++ /dev/null @@ -1,82 +0,0 @@ -package idlewatcher - -import ( - "context" - "net/http" -) - -type ( - roundTripper struct { - patched roundTripFunc - } - roundTripFunc func(*http.Request) (*http.Response, error) -) - -func (rt roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - return rt.patched(req) -} - -func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) { - // target site is ready, passthrough - if w.ready.Load() { - return origRoundTrip(req) - } - - // initial request - targetUrl := req.Header.Get(headerGoProxyTargetURL) - if targetUrl == "" { - return w.makeResp( - "%s is starting... Please wait", - w.ContainerName, - ) - } - - w.l.Debug("serving event") - - // stream request - rtDone := make(chan *http.Response, 1) - ctx, cancel := context.WithTimeout(req.Context(), w.WakeTimeout) - defer cancel() - - // loop original round trip until success in a goroutine - go func() { - for { - select { - case <-ctx.Done(): - return - case <-w.ctx.Done(): - return - default: - // wake the container and reset idle timer - select { - case w.wakeCh <- struct{}{}: - default: - } - resp, err := origRoundTrip(req) - if err == nil { - w.ready.Store(true) - rtDone <- resp - return - } - } - } - }() - - for { - select { - case resp := <-rtDone: - return w.makeSuccResp(targetUrl, resp) - case err := <-w.wakeDone: - if err != nil { - return w.makeErrResp("error waking up %s\n%s", w.ContainerName, err.Error()) - } - case <-ctx.Done(): - if ctx.Err() == context.DeadlineExceeded { - return w.makeErrResp("Timed out waiting for %s to fully wake", w.ContainerName) - } - return w.makeErrResp("idlewatcher has stopped\n%s", w.ctx.Err()) - case <-w.ctx.Done(): - return w.makeErrResp("idlewatcher has stopped\n%s", w.ctx.Err()) - } - } -} diff --git a/internal/docker/idlewatcher/waker.go b/internal/docker/idlewatcher/waker.go new file mode 100644 index 0000000..0d4d853 --- /dev/null +++ b/internal/docker/idlewatcher/waker.go @@ -0,0 +1,101 @@ +package idlewatcher + +import ( + "context" + "crypto/tls" + "net/http" + "time" + + gphttp "github.com/yusing/go-proxy/internal/net/http" +) + +type Waker struct { + *watcher + + client *http.Client + rp *gphttp.ReverseProxy +} + +func NewWaker(w *watcher, rp *gphttp.ReverseProxy) *Waker { + tr := &http.Transport{} + if w.NoTLSVerify { + tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + return &Waker{ + watcher: w, + client: &http.Client{ + Timeout: 1 * time.Second, + Transport: tr, + }, + rp: rp, + } +} + +func (w *Waker) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + w.wake(w.rp.ServeHTTP, rw, r) +} + +func (w *Waker) wake(next http.HandlerFunc, rw http.ResponseWriter, r *http.Request) { + // pass through if container is ready + if w.ready.Load() { + next(rw, r) + return + } + + ctx, cancel := context.WithTimeout(r.Context(), w.WakeTimeout) + defer cancel() + + if r.Header.Get(headerCheckRedirect) == "" { + // Send a loading response to the client + rw.Header().Set("Content-Type", "text/html; charset=utf-8") + rw.Write(w.makeRespBody("%s waking up...", w.ContainerName)) + return + } + + // wake the container and reset idle timer + // also wait for another wake request + w.wakeCh <- struct{}{} + + if <-w.wakeDone != nil { + http.Error(rw, "Error sending wake request", http.StatusInternalServerError) + return + } + + // maybe another request came in while we were waiting for the wake + if w.ready.Load() { + next(rw, r) + return + } + + for { + select { + case <-ctx.Done(): + http.Error(rw, "Waking timed out", http.StatusGatewayTimeout) + return + default: + } + + wakeReq, err := http.NewRequestWithContext( + ctx, + http.MethodHead, + w.URL.String(), + nil, + ) + if err != nil { + w.l.Errorf("new request err to %s: %s", r.URL, err) + http.Error(rw, "Internal server error", http.StatusInternalServerError) + return + } + + // we don't care about the response + _, err = w.client.Do(wakeReq) + if err == nil { + w.ready.Store(true) + rw.WriteHeader(http.StatusOK) + return + } + + // retry until the container is ready or timeout + time.Sleep(100 * time.Millisecond) + } +} diff --git a/internal/docker/idlewatcher/watcher.go b/internal/docker/idlewatcher/watcher.go index 6b4c1d4..3ae21cf 100644 --- a/internal/docker/idlewatcher/watcher.go +++ b/internal/docker/idlewatcher/watcher.go @@ -2,7 +2,6 @@ package idlewatcher import ( "context" - "net/http" "sync" "sync/atomic" "time" @@ -96,10 +95,8 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) { return w, nil } -func Unregister(entry *P.ReverseProxyEntry) { - if w, ok := watcherMap[entry.ContainerID]; ok { - w.refCount.Add(-1) - } +func (w *watcher) Unregister() { + w.refCount.Add(-1) } func Start() { @@ -133,12 +130,6 @@ func Stop() { mainLoopWg.Wait() } -func (w *watcher) PatchRoundTripper(rtp http.RoundTripper) roundTripper { - return roundTripper{patched: func(r *http.Request) (*http.Response, error) { - return w.roundTrip(rtp.RoundTrip, r) - }} -} - func (w *watcher) containerStop() error { return w.client.ContainerStop(w.ctx, w.ContainerID, container.StopOptions{ Signal: string(w.StopSignal), @@ -253,11 +244,9 @@ func (w *watcher) watchUntilCancel() { switch { // create / start / unpause case e.Action.IsContainerWake(): - w.ContainerRunning = true ticker.Reset(w.IdleTimeout) w.l.Info(e) default: // stop / pause / kill - w.ContainerRunning = false ticker.Stop() w.ready.Store(false) w.l.Info(e) @@ -272,13 +261,10 @@ func (w *watcher) watchUntilCancel() { w.l.Debug("wake signal received") ticker.Reset(w.IdleTimeout) err := w.wakeIfStopped() - if err != nil && err.IsNot(context.Canceled) { + if err != nil { w.l.Error(E.FailWith("wake", err)) } - select { - case w.wakeDone <- err: // this is passed to roundtrip - default: - } + w.wakeDone <- err } } } diff --git a/internal/net/http/middleware/custom_error_page.go b/internal/net/http/middleware/custom_error_page.go index 632fddb..f875c76 100644 --- a/internal/net/http/middleware/custom_error_page.go +++ b/internal/net/http/middleware/custom_error_page.go @@ -10,7 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/yusing/go-proxy/internal/api/v1/error_page" - gpHTTP "github.com/yusing/go-proxy/internal/net/http" + gphttp "github.com/yusing/go-proxy/internal/net/http" ) var CustomErrorPage = &Middleware{ @@ -21,8 +21,8 @@ var CustomErrorPage = &Middleware{ }, modifyResponse: func(resp *Response) error { // only handles non-success status code and html/plain content type - contentType := gpHTTP.GetContentType(resp.Header) - if !gpHTTP.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) { + contentType := gphttp.GetContentType(resp.Header) + if !gphttp.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) { errorPage, ok := error_page.GetErrorPageByStatus(resp.StatusCode) if ok { errPageLogger.Debugf("error page for status %d loaded", resp.StatusCode) @@ -46,8 +46,8 @@ func ServeStaticErrorPageFile(w http.ResponseWriter, r *http.Request) bool { if path != "" && path[0] != '/' { path = "/" + path } - if strings.HasPrefix(path, gpHTTP.StaticFilePathPrefix) { - filename := path[len(gpHTTP.StaticFilePathPrefix):] + if strings.HasPrefix(path, gphttp.StaticFilePathPrefix) { + filename := path[len(gphttp.StaticFilePathPrefix):] file, ok := error_page.GetStaticFile(filename) if !ok { errPageLogger.Errorf("unable to load resource %s", filename) diff --git a/internal/net/http/middleware/forward_auth.go b/internal/net/http/middleware/forward_auth.go index 9153fee..63781a5 100644 --- a/internal/net/http/middleware/forward_auth.go +++ b/internal/net/http/middleware/forward_auth.go @@ -14,7 +14,7 @@ import ( "time" E "github.com/yusing/go-proxy/internal/error" - gpHTTP "github.com/yusing/go-proxy/internal/net/http" + gphttp "github.com/yusing/go-proxy/internal/net/http" ) type ( @@ -58,7 +58,7 @@ func NewForwardAuthfunc(optsRaw OptionsRaw) (*Middleware, E.NestedError) { if ok { tr = tr.Clone() } else { - tr = gpHTTP.DefaultTransport.Clone() + tr = gphttp.DefaultTransport.Clone() } fa.client = http.Client{ @@ -72,7 +72,7 @@ func NewForwardAuthfunc(optsRaw OptionsRaw) (*Middleware, E.NestedError) { } func (fa *forwardAuth) forward(next http.HandlerFunc, w ResponseWriter, req *Request) { - gpHTTP.RemoveHop(req.Header) + gphttp.RemoveHop(req.Header) faReq, err := http.NewRequestWithContext( req.Context(), @@ -86,10 +86,10 @@ func (fa *forwardAuth) forward(next http.HandlerFunc, w ResponseWriter, req *Req return } - gpHTTP.CopyHeader(faReq.Header, req.Header) - gpHTTP.RemoveHop(faReq.Header) + gphttp.CopyHeader(faReq.Header, req.Header) + gphttp.RemoveHop(faReq.Header) - faReq.Header = gpHTTP.FilterHeaders(faReq.Header, fa.AuthResponseHeaders) + faReq.Header = gphttp.FilterHeaders(faReq.Header, fa.AuthResponseHeaders) fa.setAuthHeaders(req, faReq) fa.m.AddTraceRequest("forward auth request", faReq) @@ -110,8 +110,8 @@ func (fa *forwardAuth) forward(next http.HandlerFunc, w ResponseWriter, req *Req if faResp.StatusCode < http.StatusOK || faResp.StatusCode >= http.StatusMultipleChoices { fa.m.AddTraceResponse("forward auth response", faResp) - gpHTTP.CopyHeader(w.Header(), faResp.Header) - gpHTTP.RemoveHop(w.Header()) + gphttp.CopyHeader(w.Header(), faResp.Header) + gphttp.RemoveHop(w.Header()) redirectURL, err := faResp.Location() if err != nil { @@ -148,7 +148,7 @@ func (fa *forwardAuth) forward(next http.HandlerFunc, w ResponseWriter, req *Req return } - next.ServeHTTP(gpHTTP.NewModifyResponseWriter(w, req, func(resp *Response) error { + next.ServeHTTP(gphttp.NewModifyResponseWriter(w, req, func(resp *Response) error { fa.setAuthCookies(resp, authCookies) return nil }), req) diff --git a/internal/net/http/middleware/middleware.go b/internal/net/http/middleware/middleware.go index 1058f1c..beaf1b1 100644 --- a/internal/net/http/middleware/middleware.go +++ b/internal/net/http/middleware/middleware.go @@ -6,15 +6,15 @@ import ( "net/http" E "github.com/yusing/go-proxy/internal/error" - gpHTTP "github.com/yusing/go-proxy/internal/net/http" + gphttp "github.com/yusing/go-proxy/internal/net/http" U "github.com/yusing/go-proxy/internal/utils" ) type ( Error = E.NestedError - ReverseProxy = gpHTTP.ReverseProxy - ProxyRequest = gpHTTP.ProxyRequest + ReverseProxy = gphttp.ReverseProxy + ProxyRequest = gphttp.ProxyRequest Request = http.Request Response = http.Response ResponseWriter = http.ResponseWriter diff --git a/internal/net/http/middleware/test_utils.go b/internal/net/http/middleware/test_utils.go index 6286268..18f90a1 100644 --- a/internal/net/http/middleware/test_utils.go +++ b/internal/net/http/middleware/test_utils.go @@ -11,7 +11,7 @@ import ( "github.com/yusing/go-proxy/internal/common" E "github.com/yusing/go-proxy/internal/error" - gpHTTP "github.com/yusing/go-proxy/internal/net/http" + gphttp "github.com/yusing/go-proxy/internal/net/http" ) //go:embed test_data/sample_headers.json @@ -110,7 +110,7 @@ func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.N } else { proxyURL, _ = url.Parse("https://" + testHost) // dummy url, no actual effect } - rp := gpHTTP.NewReverseProxy(proxyURL, rr) + rp := gphttp.NewReverseProxy(proxyURL, rr) mid, setOptErr := middleware.WithOptionsClone(args.middlewareOpt) if setOptErr != nil { return nil, setOptErr diff --git a/internal/net/http/middleware/trace.go b/internal/net/http/middleware/trace.go index 593dfcb..8c169e2 100644 --- a/internal/net/http/middleware/trace.go +++ b/internal/net/http/middleware/trace.go @@ -5,7 +5,7 @@ import ( "sync" "time" - gpHTTP "github.com/yusing/go-proxy/internal/net/http" + gphttp "github.com/yusing/go-proxy/internal/net/http" U "github.com/yusing/go-proxy/internal/utils" ) @@ -36,7 +36,7 @@ func (tr *Trace) WithRequest(req *Request) *Trace { return nil } tr.URL = req.RequestURI - tr.ReqHeaders = gpHTTP.HeaderToMap(req.Header) + tr.ReqHeaders = gphttp.HeaderToMap(req.Header) return tr } @@ -45,8 +45,8 @@ func (tr *Trace) WithResponse(resp *Response) *Trace { return nil } tr.URL = resp.Request.RequestURI - tr.ReqHeaders = gpHTTP.HeaderToMap(resp.Request.Header) - tr.RespHeaders = gpHTTP.HeaderToMap(resp.Header) + tr.ReqHeaders = gphttp.HeaderToMap(resp.Request.Header) + tr.RespHeaders = gphttp.HeaderToMap(resp.Header) tr.RespStatus = resp.StatusCode return tr } diff --git a/internal/route/http.go b/internal/route/http.go index 3be7336..13fa463 100755 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -26,11 +26,8 @@ type ( PathPatterns PT.PathPatterns `json:"path_patterns"` entry *P.ReverseProxyEntry - mux http.Handler - handler *ReverseProxy - - regIdleWatcher func() E.NestedError - unregIdleWatcher func() + handler http.Handler + rp *ReverseProxy } URL url.URL @@ -63,8 +60,6 @@ func SetFindMuxDomains(domains []string) { func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) { var trans *http.Transport - var regIdleWatcher func() E.NestedError - var unregIdleWatcher func() if entry.NoTLSVerify { trans = DefaultTransportNoTLS.Clone() @@ -81,37 +76,15 @@ func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) { } } - if entry.UseIdleWatcher() { - // allow time for response header up to `WakeTimeout` - if entry.WakeTimeout > trans.ResponseHeaderTimeout { - trans.ResponseHeaderTimeout = entry.WakeTimeout - } - regIdleWatcher = func() E.NestedError { - watcher, err := idlewatcher.Register(entry) - if err.HasError() { - return err - } - // patch round-tripper - rp.Transport = watcher.PatchRoundTripper(trans) - return nil - } - unregIdleWatcher = func() { - idlewatcher.Unregister(entry) - rp.Transport = trans - } - } - httpRoutesMu.Lock() defer httpRoutesMu.Unlock() r := &HTTPRoute{ - Alias: entry.Alias, - TargetURL: (*URL)(entry.URL), - PathPatterns: entry.PathPatterns, - entry: entry, - handler: rp, - regIdleWatcher: regIdleWatcher, - unregIdleWatcher: unregIdleWatcher, + Alias: entry.Alias, + TargetURL: (*URL)(entry.URL), + PathPatterns: entry.PathPatterns, + entry: entry, + rp: rp, } return r, nil } @@ -121,60 +94,55 @@ func (r *HTTPRoute) String() string { } func (r *HTTPRoute) Start() E.NestedError { - if r.mux != nil { + if r.handler != nil { return nil } httpRoutesMu.Lock() defer httpRoutesMu.Unlock() - if r.regIdleWatcher != nil { - if err := r.regIdleWatcher(); err.HasError() { - r.unregIdleWatcher = nil + if r.entry.UseIdleWatcher() { + watcher, err := idlewatcher.Register(r.entry) + if err != nil { return err } - } - - if !r.entry.UseIdleWatcher() && (r.entry.URL.Port() == "0" || - r.entry.IsDocker() && !r.entry.ContainerRunning) { - // TODO: if it use idlewatcher, set mux to dummy mux + r.handler = idlewatcher.NewWaker(watcher, r.rp) + } else if r.entry.URL.Port() == "0" || + r.entry.IsDocker() && !r.entry.ContainerRunning { return nil - } - - if len(r.PathPatterns) == 1 && r.PathPatterns[0] == "/" { - r.mux = ReverseProxyHandler{r.handler} + } else if len(r.PathPatterns) == 1 && r.PathPatterns[0] == "/" { + r.handler = ReverseProxyHandler{r.rp} } else { mux := http.NewServeMux() for _, p := range r.PathPatterns { - mux.HandleFunc(string(p), r.handler.ServeHTTP) + mux.HandleFunc(string(p), r.rp.ServeHTTP) } - r.mux = mux + r.handler = mux } httpRoutes.Store(string(r.Alias), r) return nil } -func (r *HTTPRoute) Stop() E.NestedError { - if r.mux == nil { - return nil +func (r *HTTPRoute) Stop() (_ E.NestedError) { + if r.handler == nil { + return } httpRoutesMu.Lock() defer httpRoutesMu.Unlock() - if r.unregIdleWatcher != nil { - r.unregIdleWatcher() - r.unregIdleWatcher = nil + if waker, ok := r.handler.(*idlewatcher.Waker); ok { + waker.Unregister() } - r.mux = nil + r.handler = nil httpRoutes.Delete(string(r.Alias)) - return nil + return } func (r *HTTPRoute) Started() bool { - return r.mux != nil + return r.handler != nil } func (u *URL) String() string { @@ -214,7 +182,7 @@ func findMuxAnyDomain(host string) (http.Handler, error) { } sd := strings.Join(hostSplit[:n-2], ".") if r, ok := httpRoutes.Load(sd); ok { - return r.mux, nil + return r.handler, nil } return nil, fmt.Errorf("no such route: %s", sd) } @@ -236,7 +204,7 @@ func findMuxByDomains(domains []string) func(host string) (http.Handler, error) return nil, fmt.Errorf("%s does not match any base domain", host) } if r, ok := httpRoutes.Load(subdomain); ok { - return r.mux, nil + return r.handler, nil } return nil, fmt.Errorf("no such route: %s", subdomain) }