mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-18 15:23:51 +01:00
159 lines
4.0 KiB
Go
159 lines
4.0 KiB
Go
package idlewatcher
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/yusing/godoxy/internal/homepage"
|
|
httputils "github.com/yusing/goutils/http"
|
|
"github.com/yusing/goutils/http/httpheaders"
|
|
|
|
_ "unsafe"
|
|
)
|
|
|
|
type ForceCacheControl struct {
|
|
expires string
|
|
http.ResponseWriter
|
|
}
|
|
|
|
func (f *ForceCacheControl) WriteHeader(code int) {
|
|
f.ResponseWriter.Header().Set("Cache-Control", "must-revalidate")
|
|
f.ResponseWriter.Header().Set("Expires", f.expires)
|
|
f.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
func (f *ForceCacheControl) Unwrap() http.ResponseWriter {
|
|
return f.ResponseWriter
|
|
}
|
|
|
|
// ServeHTTP implements http.Handler.
|
|
func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|
shouldNext := w.wakeFromHTTP(rw, r)
|
|
if !shouldNext {
|
|
return
|
|
}
|
|
select {
|
|
case <-r.Context().Done():
|
|
return
|
|
default:
|
|
f := &ForceCacheControl{expires: w.expires().Format(http.TimeFormat), ResponseWriter: rw}
|
|
w.rp.ServeHTTP(f, r)
|
|
}
|
|
}
|
|
|
|
func isFaviconPath(path string) bool {
|
|
return path == "/favicon.ico"
|
|
}
|
|
|
|
func (w *Watcher) redirectToStartEndpoint(rw http.ResponseWriter, r *http.Request) {
|
|
uri := "/"
|
|
if w.cfg.StartEndpoint != "" {
|
|
uri = w.cfg.StartEndpoint
|
|
}
|
|
http.Redirect(rw, r, uri, http.StatusTemporaryRedirect)
|
|
}
|
|
|
|
func (w *Watcher) getFavIcon(ctx context.Context) (result homepage.FetchResult, err error) {
|
|
r := w.route
|
|
hp := r.HomepageItem()
|
|
if hp.Icon != nil {
|
|
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
|
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL)
|
|
} else {
|
|
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
|
}
|
|
} else {
|
|
// try extract from "link[rel=icon]"
|
|
result, err = homepage.FindIcon(ctx, r, "/")
|
|
}
|
|
if result.StatusCode == 0 {
|
|
result.StatusCode = http.StatusOK
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldNext bool) {
|
|
w.resetIdleTimer()
|
|
|
|
// pass through if container is already ready
|
|
if w.ready() {
|
|
return true
|
|
}
|
|
|
|
// handle favicon request
|
|
if isFaviconPath(r.URL.Path) {
|
|
result, err := w.getFavIcon(r.Context())
|
|
if err != nil {
|
|
rw.WriteHeader(result.StatusCode)
|
|
fmt.Fprint(rw, err)
|
|
return false
|
|
}
|
|
rw.Header().Set("Content-Type", result.ContentType())
|
|
rw.WriteHeader(result.StatusCode)
|
|
rw.Write(result.Icon)
|
|
return false
|
|
}
|
|
|
|
// Check if start endpoint is configured and request path matches
|
|
if w.cfg.StartEndpoint != "" && r.URL.Path != w.cfg.StartEndpoint {
|
|
http.Error(rw, "Forbidden: Container can only be started via configured start endpoint", http.StatusForbidden)
|
|
return false
|
|
}
|
|
|
|
accept := httputils.GetAccept(r.Header)
|
|
acceptHTML := (r.Method == http.MethodGet && accept.AcceptHTML() || r.RequestURI == "/" && accept.IsEmpty())
|
|
|
|
isCheckRedirect := r.Header.Get(httpheaders.HeaderGoDoxyCheckRedirect) != ""
|
|
if !isCheckRedirect && acceptHTML {
|
|
// Send a loading response to the client
|
|
body := w.makeLoadingPageBody()
|
|
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
rw.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
|
rw.Header().Set("Cache-Control", "no-cache")
|
|
rw.Header().Add("Cache-Control", "no-store")
|
|
rw.Header().Add("Cache-Control", "must-revalidate")
|
|
rw.Header().Add("Connection", "close")
|
|
if _, err := rw.Write(body); err != nil {
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
ctx := r.Context()
|
|
if w.canceled(ctx) {
|
|
w.redirectToStartEndpoint(rw, r)
|
|
return false
|
|
}
|
|
|
|
w.l.Trace().Msg("signal received")
|
|
err := w.Wake(ctx)
|
|
if err != nil {
|
|
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
|
|
httputils.LogError(r).Msg(fmt.Sprintf("failed to wake: %v", err))
|
|
return false
|
|
}
|
|
|
|
// Wait for route to be started
|
|
if !w.waitStarted(ctx) {
|
|
return false
|
|
}
|
|
|
|
// Wait for container to become ready
|
|
if !w.waitForReady(ctx) {
|
|
if w.canceled(ctx) {
|
|
w.redirectToStartEndpoint(rw, r)
|
|
}
|
|
return false
|
|
}
|
|
|
|
if isCheckRedirect {
|
|
w.l.Debug().Stringer("url", w.hc.URL()).Msg("container is ready, redirecting")
|
|
rw.WriteHeader(http.StatusOK)
|
|
return false
|
|
}
|
|
w.l.Debug().Stringer("url", w.hc.URL()).Msg("container is ready, passing through")
|
|
return true
|
|
}
|