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
*.compose.yml
config*/ config*/
certs*/ certs*/
@ -20,3 +21,4 @@ todo.md
.*.swp .*.swp
.aider* .aider*
mtrace.json mtrace.json
.env

View file

@ -1,5 +1,5 @@
# Stage 1: Builder # 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 RUN apk add --no-cache tzdata make
WORKDIR /src WORKDIR /src

View file

@ -30,6 +30,9 @@ get:
debug: debug:
make build && sudo GOPROXY_DEBUG=1 bin/go-proxy make build && sudo GOPROXY_DEBUG=1 bin/go-proxy
mtrace:
bin/go-proxy debug-ls-mtrace > mtrace.json
run-test: run-test:
make build && sudo GOPROXY_TEST=1 bin/go-proxy 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` 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 text editor (e.g. Visual Studio Code)
- With Web UI via `http://localhost:3000` or `https://gp.y.z` - 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)) - For more info, [See Wiki]([wiki](https://github.com/yusing/go-proxy/wiki))
[🔼Back to top](#table-of-content) [🔼Back to top](#table-of-content)
|
### Use JSON Schema in VSCode ### 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) # Make sure the value is same as `GOPROXY_API_ADDR` below (if you have changed it)
# #
# environment: # environment:
# NEXT_PUBLIC_GOPROXY_API_ADDR: 127.0.0.1:8888 # GOPROXY_API_ADDR: 127.0.0.1:8888
app: app:
image: ghcr.io/yusing/go-proxy:latest image: ghcr.io/yusing/go-proxy:latest
container_name: go-proxy container_name: go-proxy

View file

@ -7,7 +7,7 @@ services:
limits: limits:
memory: 256M memory: 256M
env_file: .env env_file: .env
image: docker.i.sh/danielszabo99/microbin:latest image: danielszabo99/microbin:latest
ports: ports:
- 8080 - 8080
restart: unless-stopped restart: unless-stopped

4
go.mod
View file

@ -1,13 +1,13 @@
module github.com/yusing/go-proxy module github.com/yusing/go-proxy
go 1.23.1 go 1.23.2
require ( require (
github.com/coder/websocket v1.8.12 github.com/coder/websocket v1.8.12
github.com/docker/cli v27.3.1+incompatible github.com/docker/cli v27.3.1+incompatible
github.com/docker/docker v27.3.1+incompatible github.com/docker/docker v27.3.1+incompatible
github.com/fsnotify/fsnotify v1.7.0 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/puzpuzpuz/xsync/v3 v3.4.0
github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/santhosh-tekuri/jsonschema v1.2.4
github.com/sirupsen/logrus v1.9.3 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/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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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.2 h1:Y8hrmMvWETdqzzkRly7m98xtPJJivWFsgWi8fcvZo+Y=
github.com/go-acme/lego/v4 v4.19.0/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ= 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 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= 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= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=

View file

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

View file

@ -4,84 +4,35 @@ import (
"bytes" "bytes"
_ "embed" _ "embed"
"fmt" "fmt"
"io"
"net/http"
"strings" "strings"
"text/template" "text/template"
) )
type templateData struct { type templateData struct {
CheckRedirectHeader string
Title string Title string
Message string Message string
RequestHeaders http.Header
SpinnerClass string
} }
//go:embed html/loading_page.html //go:embed html/loading_page.html
var loadingPage []byte var loadingPage []byte
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage))) var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
const ( const headerCheckRedirect = "X-GoProxy-Check-Redirect"
htmlContentType = "text/html; charset=utf-8"
errPrefix = "\u1000" func (w *watcher) makeRespBody(format string, args ...any) []byte {
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) {
msg := fmt.Sprintf(format, args...) msg := fmt.Sprintf(format, args...)
data := new(templateData) data := new(templateData)
data.CheckRedirectHeader = headerCheckRedirect
data.Title = w.ContainerName data.Title = w.ContainerName
data.Message = strings.ReplaceAll(msg, "\n", "<br>") data.Message = strings.ReplaceAll(msg, "\n", "<br>")
data.Message = strings.ReplaceAll(data.Message, " ", "&ensp;") 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 buf := bytes.NewBuffer(make([]byte, 128)) // more than enough
err := loadingPageTmpl.Execute(buf, data) err := loadingPageTmpl.Execute(buf, data)
if err != nil { // should never happen if err != nil { // should never happen in production
panic(err) panic(err)
} }
return &http.Response{ return buf.Bytes()
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
} }

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 ( import (
"context" "context"
"net/http"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -96,11 +95,9 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
return w, nil return w, nil
} }
func Unregister(entry *P.ReverseProxyEntry) { func (w *watcher) Unregister() {
if w, ok := watcherMap[entry.ContainerID]; ok {
w.refCount.Add(-1) w.refCount.Add(-1)
} }
}
func Start() { func Start() {
logger.Debug("started") logger.Debug("started")
@ -133,12 +130,6 @@ func Stop() {
mainLoopWg.Wait() 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 { func (w *watcher) containerStop() error {
return w.client.ContainerStop(w.ctx, w.ContainerID, container.StopOptions{ return w.client.ContainerStop(w.ctx, w.ContainerID, container.StopOptions{
Signal: string(w.StopSignal), Signal: string(w.StopSignal),
@ -253,11 +244,9 @@ func (w *watcher) watchUntilCancel() {
switch { switch {
// create / start / unpause // create / start / unpause
case e.Action.IsContainerWake(): case e.Action.IsContainerWake():
w.ContainerRunning = true
ticker.Reset(w.IdleTimeout) ticker.Reset(w.IdleTimeout)
w.l.Info(e) w.l.Info(e)
default: // stop / pause / kill default: // stop / pause / kill
w.ContainerRunning = false
ticker.Stop() ticker.Stop()
w.ready.Store(false) w.ready.Store(false)
w.l.Info(e) w.l.Info(e)
@ -272,13 +261,10 @@ func (w *watcher) watchUntilCancel() {
w.l.Debug("wake signal received") w.l.Debug("wake signal received")
ticker.Reset(w.IdleTimeout) ticker.Reset(w.IdleTimeout)
err := w.wakeIfStopped() err := w.wakeIfStopped()
if err != nil && err.IsNot(context.Canceled) { if err != nil {
w.l.Error(E.FailWith("wake", err)) w.l.Error(E.FailWith("wake", err))
} }
select { w.wakeDone <- err
case w.wakeDone <- err: // this is passed to roundtrip
default:
}
} }
} }
} }

View file

@ -10,7 +10,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/yusing/go-proxy/internal/api/v1/error_page" "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{ var CustomErrorPage = &Middleware{
@ -21,8 +21,8 @@ var CustomErrorPage = &Middleware{
}, },
modifyResponse: func(resp *Response) error { modifyResponse: func(resp *Response) error {
// only handles non-success status code and html/plain content type // only handles non-success status code and html/plain content type
contentType := gpHTTP.GetContentType(resp.Header) contentType := gphttp.GetContentType(resp.Header)
if !gpHTTP.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) { if !gphttp.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) {
errorPage, ok := error_page.GetErrorPageByStatus(resp.StatusCode) errorPage, ok := error_page.GetErrorPageByStatus(resp.StatusCode)
if ok { if ok {
errPageLogger.Debugf("error page for status %d loaded", resp.StatusCode) 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] != '/' { if path != "" && path[0] != '/' {
path = "/" + path path = "/" + path
} }
if strings.HasPrefix(path, gpHTTP.StaticFilePathPrefix) { if strings.HasPrefix(path, gphttp.StaticFilePathPrefix) {
filename := path[len(gpHTTP.StaticFilePathPrefix):] filename := path[len(gphttp.StaticFilePathPrefix):]
file, ok := error_page.GetStaticFile(filename) file, ok := error_page.GetStaticFile(filename)
if !ok { if !ok {
errPageLogger.Errorf("unable to load resource %s", filename) errPageLogger.Errorf("unable to load resource %s", filename)

View file

@ -14,7 +14,7 @@ import (
"time" "time"
E "github.com/yusing/go-proxy/internal/error" 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 ( type (
@ -58,7 +58,7 @@ func NewForwardAuthfunc(optsRaw OptionsRaw) (*Middleware, E.NestedError) {
if ok { if ok {
tr = tr.Clone() tr = tr.Clone()
} else { } else {
tr = gpHTTP.DefaultTransport.Clone() tr = gphttp.DefaultTransport.Clone()
} }
fa.client = http.Client{ 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) { func (fa *forwardAuth) forward(next http.HandlerFunc, w ResponseWriter, req *Request) {
gpHTTP.RemoveHop(req.Header) gphttp.RemoveHop(req.Header)
faReq, err := http.NewRequestWithContext( faReq, err := http.NewRequestWithContext(
req.Context(), req.Context(),
@ -86,10 +86,10 @@ func (fa *forwardAuth) forward(next http.HandlerFunc, w ResponseWriter, req *Req
return return
} }
gpHTTP.CopyHeader(faReq.Header, req.Header) gphttp.CopyHeader(faReq.Header, req.Header)
gpHTTP.RemoveHop(faReq.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.setAuthHeaders(req, faReq)
fa.m.AddTraceRequest("forward auth request", 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 { if faResp.StatusCode < http.StatusOK || faResp.StatusCode >= http.StatusMultipleChoices {
fa.m.AddTraceResponse("forward auth response", faResp) fa.m.AddTraceResponse("forward auth response", faResp)
gpHTTP.CopyHeader(w.Header(), faResp.Header) gphttp.CopyHeader(w.Header(), faResp.Header)
gpHTTP.RemoveHop(w.Header()) gphttp.RemoveHop(w.Header())
redirectURL, err := faResp.Location() redirectURL, err := faResp.Location()
if err != nil { if err != nil {
@ -148,7 +148,7 @@ func (fa *forwardAuth) forward(next http.HandlerFunc, w ResponseWriter, req *Req
return 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) fa.setAuthCookies(resp, authCookies)
return nil return nil
}), req) }), req)

View file

@ -6,15 +6,15 @@ import (
"net/http" "net/http"
E "github.com/yusing/go-proxy/internal/error" 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" U "github.com/yusing/go-proxy/internal/utils"
) )
type ( type (
Error = E.NestedError Error = E.NestedError
ReverseProxy = gpHTTP.ReverseProxy ReverseProxy = gphttp.ReverseProxy
ProxyRequest = gpHTTP.ProxyRequest ProxyRequest = gphttp.ProxyRequest
Request = http.Request Request = http.Request
Response = http.Response Response = http.Response
ResponseWriter = http.ResponseWriter ResponseWriter = http.ResponseWriter

View file

@ -11,7 +11,7 @@ import (
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error" 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 //go:embed test_data/sample_headers.json
@ -110,7 +110,7 @@ func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.N
} else { } else {
proxyURL, _ = url.Parse("https://" + testHost) // dummy url, no actual effect 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) mid, setOptErr := middleware.WithOptionsClone(args.middlewareOpt)
if setOptErr != nil { if setOptErr != nil {
return nil, setOptErr return nil, setOptErr

View file

@ -5,7 +5,7 @@ import (
"sync" "sync"
"time" "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" U "github.com/yusing/go-proxy/internal/utils"
) )
@ -36,7 +36,7 @@ func (tr *Trace) WithRequest(req *Request) *Trace {
return nil return nil
} }
tr.URL = req.RequestURI tr.URL = req.RequestURI
tr.ReqHeaders = gpHTTP.HeaderToMap(req.Header) tr.ReqHeaders = gphttp.HeaderToMap(req.Header)
return tr return tr
} }
@ -45,8 +45,8 @@ func (tr *Trace) WithResponse(resp *Response) *Trace {
return nil return nil
} }
tr.URL = resp.Request.RequestURI tr.URL = resp.Request.RequestURI
tr.ReqHeaders = gpHTTP.HeaderToMap(resp.Request.Header) tr.ReqHeaders = gphttp.HeaderToMap(resp.Request.Header)
tr.RespHeaders = gpHTTP.HeaderToMap(resp.Header) tr.RespHeaders = gphttp.HeaderToMap(resp.Header)
tr.RespStatus = resp.StatusCode tr.RespStatus = resp.StatusCode
return tr return tr
} }

View file

@ -26,11 +26,8 @@ type (
PathPatterns PT.PathPatterns `json:"path_patterns"` PathPatterns PT.PathPatterns `json:"path_patterns"`
entry *P.ReverseProxyEntry entry *P.ReverseProxyEntry
mux http.Handler handler http.Handler
handler *ReverseProxy rp *ReverseProxy
regIdleWatcher func() E.NestedError
unregIdleWatcher func()
} }
URL url.URL URL url.URL
@ -63,8 +60,6 @@ func SetFindMuxDomains(domains []string) {
func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) { func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) {
var trans *http.Transport var trans *http.Transport
var regIdleWatcher func() E.NestedError
var unregIdleWatcher func()
if entry.NoTLSVerify { if entry.NoTLSVerify {
trans = DefaultTransportNoTLS.Clone() trans = DefaultTransportNoTLS.Clone()
@ -81,26 +76,6 @@ 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() httpRoutesMu.Lock()
defer httpRoutesMu.Unlock() defer httpRoutesMu.Unlock()
@ -109,9 +84,7 @@ func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) {
TargetURL: (*URL)(entry.URL), TargetURL: (*URL)(entry.URL),
PathPatterns: entry.PathPatterns, PathPatterns: entry.PathPatterns,
entry: entry, entry: entry,
handler: rp, rp: rp,
regIdleWatcher: regIdleWatcher,
unregIdleWatcher: unregIdleWatcher,
} }
return r, nil return r, nil
} }
@ -121,60 +94,55 @@ func (r *HTTPRoute) String() string {
} }
func (r *HTTPRoute) Start() E.NestedError { func (r *HTTPRoute) Start() E.NestedError {
if r.mux != nil { if r.handler != nil {
return nil return nil
} }
httpRoutesMu.Lock() httpRoutesMu.Lock()
defer httpRoutesMu.Unlock() defer httpRoutesMu.Unlock()
if r.regIdleWatcher != nil { if r.entry.UseIdleWatcher() {
if err := r.regIdleWatcher(); err.HasError() { watcher, err := idlewatcher.Register(r.entry)
r.unregIdleWatcher = nil if err != nil {
return err return err
} }
} r.handler = idlewatcher.NewWaker(watcher, r.rp)
} else if r.entry.URL.Port() == "0" ||
if !r.entry.UseIdleWatcher() && (r.entry.URL.Port() == "0" || r.entry.IsDocker() && !r.entry.ContainerRunning {
r.entry.IsDocker() && !r.entry.ContainerRunning) {
// TODO: if it use idlewatcher, set mux to dummy mux
return nil return nil
} } else if len(r.PathPatterns) == 1 && r.PathPatterns[0] == "/" {
r.handler = ReverseProxyHandler{r.rp}
if len(r.PathPatterns) == 1 && r.PathPatterns[0] == "/" {
r.mux = ReverseProxyHandler{r.handler}
} else { } else {
mux := http.NewServeMux() mux := http.NewServeMux()
for _, p := range r.PathPatterns { 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) httpRoutes.Store(string(r.Alias), r)
return nil return nil
} }
func (r *HTTPRoute) Stop() E.NestedError { func (r *HTTPRoute) Stop() (_ E.NestedError) {
if r.mux == nil { if r.handler == nil {
return nil return
} }
httpRoutesMu.Lock() httpRoutesMu.Lock()
defer httpRoutesMu.Unlock() defer httpRoutesMu.Unlock()
if r.unregIdleWatcher != nil { if waker, ok := r.handler.(*idlewatcher.Waker); ok {
r.unregIdleWatcher() waker.Unregister()
r.unregIdleWatcher = nil
} }
r.mux = nil r.handler = nil
httpRoutes.Delete(string(r.Alias)) httpRoutes.Delete(string(r.Alias))
return nil return
} }
func (r *HTTPRoute) Started() bool { func (r *HTTPRoute) Started() bool {
return r.mux != nil return r.handler != nil
} }
func (u *URL) String() string { func (u *URL) String() string {
@ -214,7 +182,7 @@ func findMuxAnyDomain(host string) (http.Handler, error) {
} }
sd := strings.Join(hostSplit[:n-2], ".") sd := strings.Join(hostSplit[:n-2], ".")
if r, ok := httpRoutes.Load(sd); ok { if r, ok := httpRoutes.Load(sd); ok {
return r.mux, nil return r.handler, nil
} }
return nil, fmt.Errorf("no such route: %s", sd) 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) return nil, fmt.Errorf("%s does not match any base domain", host)
} }
if r, ok := httpRoutes.Load(subdomain); ok { if r, ok := httpRoutes.Load(subdomain); ok {
return r.mux, nil return r.handler, nil
} }
return nil, fmt.Errorf("no such route: %s", subdomain) return nil, fmt.Errorf("no such route: %s", subdomain)
} }