refactored http import name, fixed and simplified idlewatcher/idlewaker implementation, dependencies update

This commit is contained in:
yusing 2024-10-07 12:45:07 +08:00
parent 929b7f7059
commit 921ce23dde
19 changed files with 194 additions and 261 deletions

2
.gitignore vendored
View file

@ -1,4 +1,5 @@
compose.yml
*.compose.yml
config*/
certs*/
@ -20,3 +21,4 @@ todo.md
.*.swp
.aider*
mtrace.json
.env

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

4
go.mod
View file

@ -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

4
go.sum
View file

@ -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=

View file

@ -66,22 +66,23 @@
<body>
<script>
window.onload = async function () {
let result = await fetch(window.location.href, {
let resp = await fetch(window.location.href, {
headers: {
{{ range $key, $value := .RequestHeaders }}
'{{ $key }}' : {{ $value }}
{{ end }}
"{{.CheckRedirectHeader}}": "1",
},
}).then((resp) => resp.text())
.catch((err) => {
document.getElementById("message").innerText = err;
});
if (result) {
document.documentElement.innerHTML = result
});
if (resp.ok) {
window.location.href = resp.url;
} else {
document.getElementById("message").innerText =
await resp.text();
document
.getElementById("spinner")
.classList.replace("spinner", "error");
}
};
</script>
<div class="{{.SpinnerClass}}"></div>
<div class="message">{{.Message}}</div>
<div id="spinner" class="spinner"></div>
<div id="message" class="message">{{.Message}}</div>
</body>
</html>

View file

@ -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", "<br>")
data.Message = strings.ReplaceAll(data.Message, " ", "&ensp;")
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()
}

View file

@ -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())
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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)
}