mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-20 04:42:33 +02:00
improved idlewatcher and content type matching, update CI
This commit is contained in:
parent
d89155a6ee
commit
ef83ed0596
5 changed files with 126 additions and 19 deletions
4
.github/workflows/docker-image.yml
vendored
4
.github/workflows/docker-image.yml
vendored
|
@ -11,7 +11,7 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build multi-platform Docker image
|
name: Build multi-platform Docker image
|
||||||
runs-on: self-hosted
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
@ -85,7 +85,7 @@ jobs:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
merge:
|
merge:
|
||||||
runs-on: self-hosted
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
permissions:
|
permissions:
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||||
|
@ -38,6 +39,7 @@ func (w *Waker) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
func (w *Waker) wake(next http.HandlerFunc, rw http.ResponseWriter, r *http.Request) {
|
func (w *Waker) wake(next http.HandlerFunc, rw http.ResponseWriter, r *http.Request) {
|
||||||
// pass through if container is ready
|
// pass through if container is ready
|
||||||
if w.ready.Load() {
|
if w.ready.Load() {
|
||||||
|
w.resetIdleTimer()
|
||||||
next(rw, r)
|
next(rw, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -45,11 +47,23 @@ func (w *Waker) wake(next http.HandlerFunc, rw http.ResponseWriter, r *http.Requ
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), w.WakeTimeout)
|
ctx, cancel := context.WithTimeout(r.Context(), w.WakeTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
isCheckRedirect := r.Header.Get(headerCheckRedirect) != ""
|
accept := gphttp.GetAccept(r.Header)
|
||||||
|
acceptHTML := accept.AcceptHTML() || accept.IsEmpty()
|
||||||
|
|
||||||
|
if !acceptHTML {
|
||||||
|
w.l.Debugf("Accept %v", accept)
|
||||||
|
}
|
||||||
|
|
||||||
|
isCheckRedirect := r.Header.Get(headerCheckRedirect) != "" && acceptHTML
|
||||||
if !isCheckRedirect {
|
if !isCheckRedirect {
|
||||||
// Send a loading response to the client
|
// Send a loading response to the client
|
||||||
|
body := w.makeRespBody("%s waking up...", w.ContainerName)
|
||||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
rw.Write(w.makeRespBody("%s waking up...", w.ContainerName))
|
rw.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||||
|
rw.Header().Add("Cache-Control", "no-cache")
|
||||||
|
rw.Header().Add("Cache-Control", "no-store")
|
||||||
|
rw.Header().Add("Cache-Control", "must-revalidate")
|
||||||
|
rw.Write(body)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +110,11 @@ func (w *Waker) wake(next http.HandlerFunc, rw http.ResponseWriter, r *http.Requ
|
||||||
_, err = w.client.Do(wakeReq)
|
_, err = w.client.Do(wakeReq)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
w.ready.Store(true)
|
w.ready.Store(true)
|
||||||
|
if isCheckRedirect {
|
||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
} else {
|
||||||
|
next(rw, r)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ type (
|
||||||
|
|
||||||
wakeCh chan struct{}
|
wakeCh chan struct{}
|
||||||
wakeDone chan E.NestedError
|
wakeDone chan E.NestedError
|
||||||
|
ticker *time.Ticker
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
@ -79,8 +80,9 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||||
ReverseProxyEntry: entry,
|
ReverseProxyEntry: entry,
|
||||||
client: client,
|
client: client,
|
||||||
refCount: &sync.WaitGroup{},
|
refCount: &sync.WaitGroup{},
|
||||||
wakeCh: make(chan struct{}),
|
wakeCh: make(chan struct{}, 1),
|
||||||
wakeDone: make(chan E.NestedError),
|
wakeDone: make(chan E.NestedError),
|
||||||
|
ticker: time.NewTicker(entry.IdleTimeout),
|
||||||
l: logger.WithField("container", entry.ContainerName),
|
l: logger.WithField("container", entry.ContainerName),
|
||||||
}
|
}
|
||||||
w.refCount.Add(1)
|
w.refCount.Add(1)
|
||||||
|
@ -116,7 +118,6 @@ func Start() {
|
||||||
w.watchUntilCancel()
|
w.watchUntilCancel()
|
||||||
w.refCount.Wait() // wait for 0 ref count
|
w.refCount.Wait() // wait for 0 ref count
|
||||||
|
|
||||||
w.client.Close()
|
|
||||||
delete(watcherMap, w.ContainerID)
|
delete(watcherMap, w.ContainerID)
|
||||||
w.l.Debug("unregistered")
|
w.l.Debug("unregistered")
|
||||||
mainLoopWg.Done()
|
mainLoopWg.Done()
|
||||||
|
@ -207,10 +208,14 @@ func (w *watcher) getStopCallback() StopCallback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *watcher) resetIdleTimer() {
|
||||||
|
w.ticker.Reset(w.IdleTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
func (w *watcher) watchUntilCancel() {
|
func (w *watcher) watchUntilCancel() {
|
||||||
defer close(w.wakeCh)
|
defer close(w.wakeCh)
|
||||||
|
|
||||||
w.ctx, w.cancel = context.WithCancel(context.Background())
|
w.ctx, w.cancel = context.WithCancel(mainLoopCtx)
|
||||||
|
|
||||||
dockerWatcher := W.NewDockerWatcherWithClient(w.client)
|
dockerWatcher := W.NewDockerWatcherWithClient(w.client)
|
||||||
dockerEventCh, dockerEventErrCh := dockerWatcher.EventsWithOptions(w.ctx, W.DockerListOptions{
|
dockerEventCh, dockerEventErrCh := dockerWatcher.EventsWithOptions(w.ctx, W.DockerListOptions{
|
||||||
|
@ -225,14 +230,11 @@ func (w *watcher) watchUntilCancel() {
|
||||||
W.DockerFilterUnpause,
|
W.DockerFilterUnpause,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
defer w.ticker.Stop()
|
||||||
ticker := time.NewTicker(w.IdleTimeout)
|
defer w.client.Close()
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-mainLoopCtx.Done():
|
|
||||||
w.cancel()
|
|
||||||
case <-w.ctx.Done():
|
case <-w.ctx.Done():
|
||||||
w.l.Debug("stopped")
|
w.l.Debug("stopped")
|
||||||
return
|
return
|
||||||
|
@ -244,22 +246,24 @@ func (w *watcher) watchUntilCancel() {
|
||||||
switch {
|
switch {
|
||||||
// create / start / unpause
|
// create / start / unpause
|
||||||
case e.Action.IsContainerWake():
|
case e.Action.IsContainerWake():
|
||||||
ticker.Reset(w.IdleTimeout)
|
w.ContainerRunning = true
|
||||||
|
w.resetIdleTimer()
|
||||||
w.l.Info(e)
|
w.l.Info(e)
|
||||||
default: // stop / pause / kill
|
default: // stop / pause / kil
|
||||||
ticker.Stop()
|
w.ContainerRunning = false
|
||||||
|
w.ticker.Stop()
|
||||||
w.ready.Store(false)
|
w.ready.Store(false)
|
||||||
w.l.Info(e)
|
w.l.Info(e)
|
||||||
}
|
}
|
||||||
case <-ticker.C:
|
case <-w.ticker.C:
|
||||||
w.l.Debug("idle timeout")
|
w.l.Debug("idle timeout")
|
||||||
ticker.Stop()
|
w.ticker.Stop()
|
||||||
if err := w.stopByMethod(); err != nil && err.IsNot(context.Canceled) {
|
if err := w.stopByMethod(); err != nil && err.IsNot(context.Canceled) {
|
||||||
w.l.Error(E.FailWith("stop", err).Extraf("stop method: %s", w.StopMethod))
|
w.l.Error(E.FailWith("stop", err).Extraf("stop method: %s", w.StopMethod))
|
||||||
}
|
}
|
||||||
case <-w.wakeCh:
|
case <-w.wakeCh:
|
||||||
w.l.Debug("wake signal received")
|
w.l.Debug("wake signal received")
|
||||||
ticker.Reset(w.IdleTimeout)
|
w.resetIdleTimer()
|
||||||
err := w.wakeIfStopped()
|
err := w.wakeIfStopped()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.l.Error(E.FailWith("wake", err))
|
w.l.Error(E.FailWith("wake", err))
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContentType string
|
type ContentType string
|
||||||
|
type AcceptContentType []ContentType
|
||||||
|
|
||||||
func GetContentType(h http.Header) ContentType {
|
func GetContentType(h http.Header) ContentType {
|
||||||
ct := h.Get("Content-Type")
|
ct := h.Get("Content-Type")
|
||||||
|
@ -19,6 +20,18 @@ func GetContentType(h http.Header) ContentType {
|
||||||
return ContentType(ct)
|
return ContentType(ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetAccept(h http.Header) AcceptContentType {
|
||||||
|
var accepts []ContentType
|
||||||
|
for _, v := range h["Accept"] {
|
||||||
|
ct, _, err := mime.ParseMediaType(v)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
accepts = append(accepts, ContentType(ct))
|
||||||
|
}
|
||||||
|
return accepts
|
||||||
|
}
|
||||||
|
|
||||||
func (ct ContentType) IsHTML() bool {
|
func (ct ContentType) IsHTML() bool {
|
||||||
return ct == "text/html" || ct == "application/xhtml+xml"
|
return ct == "text/html" || ct == "application/xhtml+xml"
|
||||||
}
|
}
|
||||||
|
@ -30,3 +43,34 @@ func (ct ContentType) IsJSON() bool {
|
||||||
func (ct ContentType) IsPlainText() bool {
|
func (ct ContentType) IsPlainText() bool {
|
||||||
return ct == "text/plain"
|
return ct == "text/plain"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (act AcceptContentType) IsEmpty() bool {
|
||||||
|
return len(act) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (act AcceptContentType) AcceptHTML() bool {
|
||||||
|
for _, v := range act {
|
||||||
|
if v.IsHTML() || v == "text/*" || v == "*/*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (act AcceptContentType) AcceptJSON() bool {
|
||||||
|
for _, v := range act {
|
||||||
|
if v.IsJSON() || v == "*/*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (act AcceptContentType) AcceptPlainText() bool {
|
||||||
|
for _, v := range act {
|
||||||
|
if v.IsPlainText() || v == "text/*" || v == "*/*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
41
internal/net/http/content_type_test.go
Normal file
41
internal/net/http/content_type_test.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContentTypes(t *testing.T) {
|
||||||
|
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsHTML())
|
||||||
|
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/html; charset=utf-8"}}).IsHTML())
|
||||||
|
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"application/xhtml+xml"}}).IsHTML())
|
||||||
|
ExpectFalse(t, GetContentType(http.Header{"Content-Type": {"text/plain"}}).IsHTML())
|
||||||
|
|
||||||
|
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"application/json"}}).IsJSON())
|
||||||
|
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"application/json; charset=utf-8"}}).IsJSON())
|
||||||
|
ExpectFalse(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsJSON())
|
||||||
|
|
||||||
|
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/plain"}}).IsPlainText())
|
||||||
|
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/plain; charset=utf-8"}}).IsPlainText())
|
||||||
|
ExpectFalse(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsPlainText())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcceptContentTypes(t *testing.T) {
|
||||||
|
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain"}}).AcceptPlainText())
|
||||||
|
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain; charset=utf-8"}}).AcceptPlainText())
|
||||||
|
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain"}}).AcceptHTML())
|
||||||
|
ExpectTrue(t, GetAccept(http.Header{"Accept": {"application/json"}}).AcceptJSON())
|
||||||
|
ExpectTrue(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptPlainText())
|
||||||
|
ExpectTrue(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptHTML())
|
||||||
|
ExpectTrue(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptJSON())
|
||||||
|
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptPlainText())
|
||||||
|
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptHTML())
|
||||||
|
|
||||||
|
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/plain"}}).AcceptHTML())
|
||||||
|
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/plain; charset=utf-8"}}).AcceptHTML())
|
||||||
|
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/html"}}).AcceptPlainText())
|
||||||
|
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/html"}}).AcceptJSON())
|
||||||
|
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptJSON())
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue