Replace chained level checks with a validLevels list and includes().
Coalesce addConsoleLine call sites. Default missing payload.message to an
empty string so the console does not show undefined.
LXCGetIPsWithStatus reads net config for stopped and suspended guests instead
of the interfaces API. UpdateResources reuses IP lists for 30s when the
resource status is unchanged, caps concurrent LXC IP lookups, and sets
MaxConnsPerHost on the HTTP transport to match.
Add httptest coverage for offline IPs, second-poll IP reuse, and status
transitions. Replace math.Pow session backoff with fixed-duration retries.
Restyle the idle watcher loading UI with design tokens, light and dark
variants via `prefers-color-scheme`, and system font stacks (drop
Google Fonts). Tighten layout, animations, and console presentation.
In `loading.js`, pass event `level` into `addConsoleLine` and apply
`level-*` classes so log lines can reflect debug, info, warn, and
error. On the HTML side, set `color-scheme`, add an empty `alt` on the
logo, and mark the loading dots as `aria-hidden` for assistive
technology.
Clarify OpenAPI and the `Route` field: HTTP-based routes only, must
match a configured `inbound_mtls_profiles` entry, and ignored when
`entrypoint.inbound_mtls_profile` is set. Reorder `load_avg_5m` in the
host metrics schema alongside the other load average fields.
Update go.mod/sum across the root module, agent, internal/dnsproviders, and
socket-proxy (lego, docker/cli, Moby API/client, xsync, zerolog, goutils,
DNS SDKs, genproto RPC). Agent and dnsproviders take godoxy v0.28.0 and newer
replace pseudo-versions.
Switch the Dockerfile minify-stage Bun COPY to oven/bun:1-alpine instead of a
pinned patch tag.
Relax response-body gating so HTML and XHTML can be buffered when
Transfer-Encoding is chunked-only, or when Content-Length is missing,
while still rejecting non-identity encodings that are not chunked HTML
and other non-HTML cases.
Update modifyHTML to cap reads for unknown length, splice the original
stream back when the cap is hit, and document the behavior in the
package README. Extend tests for themed middleware and the rewrite gate.
Point go:embed at block_page.min.html, loading_page.min.html,
loading.min.js, and captcha.min.html. Update the minified-files
gitignore glob from **/*-min.* to **/*.min.*.
Remove the xsync map cache and add a 1000-entry goutils keyed cache in
lookup.go. Rrate-limit failure logs, and populate IPInfo.City.
Adjust ACL Geo matchers to the new signature.
Remove the ForceCacheControl wrapper from the ready ServeHTTP path so
proxied responses keep upstream Cache-Control and Expires.
Centralize strong no-store headers via setNoStoreHeaders for the loading
page, static CSS/JS, favicon, and SSE wake events. Add tests covering
loading responses and upstream header preservation.
Added details on request-variable substitution, explaining how it
reads fields from the active outbound request and resolves upstream
variables from the current route context.
Replace the xsync map plus manual expiry on checkCache with
cache.NewKeyFunc(evaluateIP).WithTTL. Move deny/allow/default logic into
evaluateIP; wire getCachedCity and IPAllowed through the cache API.
Refresh README security notes and add tests showing cached decisions persist
across in-memory rule changes until TTL expires.
Add per-provider obtain serialization and an RWMutex around shared
provider fields. GetCert and SNI matching use snapshot helpers; rebuild
the matcher atomically; GetExpiries returns a cloned map.
Clone Config.HTTPClient (including Transport) per lego.Config so parallel
providers do not mutate shared client state.
Document the concurrency model in README.
Httptest and similar callers often leave Host unset; fall back to URL
for scheme, host, port, and addr substitution.
jsonstore drops the IsTest load short-circuit and duplicate loadNS map
registration; tests isolate storesPath. Skip MaxMind background updates
when IsTest. Tests restore APISkipOriginCheck, use app-scoped OIDC
state cookies, attach route context in middleware helpers, and use
locked buffers for concurrent log capture.
Header-only modifiers no longer use LazyResponseModifier with buffering
always enabled. They wrap the response with ModifyResponseWriter and
return after invoking the next handler.
Body modifiers still use LazyResponseModifier with canBufferAndModifyResponseBody.
Add a regression test that uses a 64MiB Content-Length with a small body.
Introduce reusable `inbound_mtls_profiles` in root config and support
`entrypoint.inbound_mtls_profile` to require client certificates for all
HTTPS traffic on an entrypoint. Profiles can trust the system CA store,
custom PEM CA files, or both, and are compiled into TLS client-auth
pools during entrypoint initialization.
Also add route-scoped `inbound_mtls_profile` support for HTTP-based
routes when no global entrypoint profile is configured. Route-level mTLS
selection is driven by TLS SNI, preserves existing behavior for open and
unmatched hosts, and returns the intended 421 response when secure
requests omit SNI or when Host and SNI resolve to different routes.
Add validation for missing profile references and unsupported non-HTTP
route usage, update config and route documentation/examples, expand
inbound mTLS handshake and routing regression coverage, and bump
`goutils` for HTTPS listener test support.
Sort proxy.* keys by dot depth, then name, before building the tree so
broader paths apply before deeper ones. When a new value would sit on a
node that is already a map, parse it as a YAML object (tabs normalized to
two spaces), deep-merge, and treat an empty string as an empty object.
Return clear errors when a scalar and a nested map disagree.
Drop the preallocated refPrefixes table in favor of refPrefix(n). Add
internal tests for parseLabelObject, mergeLabelMaps, key order, and
flatten; extend export tests for mixed OIDC-style labels and conflicts.
* refactor(docker): extract label parse and flatten helpers
Refactor ParseLabels by moving proxy label application into applyLabel,
descendLabelMap, and setLabelValue so traversal and leaf merge share one path
without labelLoop continues.
Add splitAliasLabel for ExpandWildcard so proxy.* prefix handling stays in one
place and uses CutPrefix/Cut consistently.
Deduplicate flattenMap and flattenMapAny value handling with flattenValue plus
joinLabelKey and stringifyLabelKey for flattened key construction.
* refactor(docker): structured errors for label type clashes
Replace ad hoc fmt.Errorf messages in descendLabelMap, setLabelValue, and
mergeLabelMaps with UnexpectedTypeError so wording is consistent and mapping
vs scalar conflicts stay explicit.
Hoist requireMap in label tests to a shared helper.
Normalize tabs to two spaces in expandYamlWildcard so wildcard YAML matches
the indentation used in the object-merge path.
* refactor(docker): optional UnexpectedTypeError message for merge conflicts
Extend UnexpectedTypeError with an optional Message field; when set, Error()
returns it instead of the default expect-versus-actual formatting.
mergeLabelMaps sets that message when a mapping would merge into an existing
scalar, so the error states the situation instead of only "expect scalar".
Update TestMergeLabelMaps to assert the new wording.
Refresh indirect and direct golang.org/x pins (crypto, net, sys, text,
arch, mod, tools) across the root module, agent, dnsproviders,
socket-proxy, and h2c_test_server.
Advance the goutils submodule and align pseudo-versions for reverseproxy,
websocket, and server; bump workspace replace commits for agent and
internal/dnsproviders.
Update vultr/govultr to v3.30.0 and mattn/go-isatty to v0.0.21 where they
appear in the graph.
Validate GODOXY_LOCAL_API_ADDR before starting the unauthenticated local
API. Loopback listeners still succeed by default; addresses that bind
all interfaces, unspecified IPs, LAN hosts, or non-loopback names need
GODOXY_LOCAL_API_ALLOW_NON_LOOPBACK=true.
When that opt-in is set and the host is not loopback, log a warning so
non-local exposure is obvious. Wire common.LocalAPIAllowNonLoopback from
LOCAL_API_ALLOW_NON_LOOPBACK and document it (with a risk note) in
.env.example.
Add TestValidateLocalAPIAddr for loopback, wildcard, LAN, and hostname
cases with the allow flag on and off.
When a path exists but reads as empty or whitespace-only, return nil
without touching dst, matching the no-file case. This avoids
unmarshaler errors on blank files and matches the updated doc comment.
Finish the file API traversal fix by rooting both GET and SET operations at the
actual file-type directory instead of the process working directory. This blocks
`..` escapes from `config/` and `config/middlewares/` while preserving valid
in-root reads and writes.
Also harden the optional unauthenticated local API listener so it only starts on
loopback addresses (`localhost`, `127.0.0.1`, `::1`). This preserves same-host
automation while preventing accidental exposure on wildcard, LAN, bridge, or
public interfaces.
Add regression tests for blocked traversal on GET and SET, valid in-root writes,
and loopback-only local API address validation. Fix an unrelated config test
cleanup panic so the touched package verification can run cleanly.
Constraint: `GODOXY_LOCAL_API_ADDR` is documented for local automation and must remain usable without adding a new auth flow
Constraint: File API behavior must keep valid config/provider/middleware edits working while blocking path escapes
Rejected: Mirror the previous GET `OpenInRoot(".", ...)` approach in SET | still allows escapes from `config/` to sibling paths under the working directory
Rejected: Keep unauthenticated non-loopback local API binds and document the risk | preserves a high-severity pre-auth network exposure
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Treat `LOCAL_API_ADDR` as same-host only; if non-loopback unauthenticated access is ever needed, gate it behind a separately named explicit insecure opt-in
Tested: `go test -count=1 -ldflags='-checklinkname=0' ./internal/api/v1/file -run 'Test(Get|Set)_PathTraversalBlocked' -v`
Tested: `go test -count=1 -ldflags='-checklinkname=0' ./internal/config -run '^TestValidateLocalAPIAddr$|^TestRouteValidateInboundMTLSProfile$' -v`
Tested: `go test -count=1 -ldflags='-checklinkname=0' ./internal/api/... ./internal/config/...`
Not-tested: End-to-end runtime verification of fsnotify reload behavior after a valid in-root provider edit
Update Docker builder images and all staged go.mod `go` lines to 1.26.2 for the
root module, agent, cli, bench_server, h2c_test_server, dnsproviders, and
socket-proxy.
Upgrade coreos/go-oidc, docker/cli, valyala/fasthttp, OpenTelemetry HTTP
instrumentation and SDK, Google Cloud auth and API clients, genproto RPC,
OCI DNS SDK, and pinned goutils/http packages; advance the goutils submodule
pointer.
Bump direct and indirect dependencies across the main module, agent,
dnsproviders package, and socket-proxy: zerolog, lego, validator, Docker CLI,
OpenTelemetry, cloud SDKs, DNS provider clients, and related transitives.
Advance goutils, go-proxmox, and gopsutil submodules; refresh internal
godoxy/agent and dnsproviders pseudo-versions. Extend Moby exclude list for
newer API and client releases to keep older daemon compatibility.
Add exclusion for all paths under /src/* and modify the websocket protocol header to 'vite-hmr' for improved compatibility with development environments.
Implement Signed Double Submit Cookie pattern to prevent CSRF attacks.
Adds CSRF token generation, validation, and middleware for API endpoints.
Safe methods (GET/HEAD/OPTIONS) automatically receive CSRF cookies, while
unsafe methods require X-CSRF-Token header matching the cookie value with
valid HMAC signature. Includes same-origin exemption for login/callback
endpoints to support browser-based authentication flows.
Use os.OpenRoot to restrict file access to the application root,
preventing directory traversal attacks through the file download endpoint.
Also add test to verify path traversal attempts are blocked.
Synthetic load balancer routes were created with SchemeNone and a zero
proxy port, so the embedded Route logic treated them as excluded routes.
That caused them to be keyed like excluded routes instead of by alias,
which broke HTTP route lookup in reverse proxy load balancer tests.
Override Key and ShouldExclude for synthetic load balancer routes so
they stay addressable through the HTTP route pool while preserving the
existing behavior for normal backend routes.
Also guard addToLoadBalancer against a nil Homepage on an existing
linked route, and update the reverse proxy test to use the in-memory
test entrypoint rather than depending on real listener setup.
- Update golang base image in Dockerfiles from 1.26.0 to 1.26.1
- Upgrade go.mod go version to 1.26.1 in all modules
- Update github.com/yusing/godoxy from v0.26.0 to v0.27.2
- Update gin from v1.11.0 to v1.12.0, docker/cli to v29.3.0, gotify to v2.9.1
- Update golang.org/x/{net,oauth2,sync,time,sys} to latest versions
- Update OpenTelemetry packages to v1.42.0/v0.67.0
- Update OpenJDK and other cloud provider SDKs
- Update goutils submodule to latest commit
Add `relay_proxy_protocol_header` configuration option for TCP routes that enables
forwarding the original client IP address to upstream services via PROXY protocol
v2 headers. This feature is only available for TCP routes and includes validation
to prevent misuse on UDP routes.
- Add RelayProxyProtocolHeader field to Route struct with JSON tag
- Implement writeProxyProtocolHeader in stream package to craft v2 headers
- Update TCPTCPStream to conditionally send PROXY header to upstream
- Add validation ensuring feature is TCP-only
- Include tests for both enabled/disabled states and incoming proxy header relay
Replace the rewrite requirement check with a BodyResponseModifier
marker and treat header and body modifiers separately.
This ensures header/status rewrites still apply when body rewriting is
blocked (for binary, encoded, or chunked responses), while body changes
are skipped safely. It also avoids body reset/close side effects and
returns early on passthrough responses.
Update middleware tests to cover split header/body behavior and themed
middleware body-skip scenarios.
Prevent response modifiers that require body rewriting from running when
the body rewrite gate blocks buffering (for example, chunked transfer
encoding).
Add an explicit `requiresBodyRewrite` capability and implement it for
HTML/theme/error-page modifiers, including bypass delegation.
Also add a regression test to ensure the original response body remains
readable and is not closed prematurely when rewrite is blocked.
This commit fixeds the "http: read on closed response body" with empty page error
happens when body-rewriting middleware (like themed) runs on responses where body rewrite is blocked (e.g. chunked),
then the gate restores an already-closed original body.
Keep only real disk-like mounts (/, /dev/*,
and /mnt/* excluding /mnt/ itself and /mnt/wsl) to avoid noisy or irrelevant entries in
disk metrics.
Normalize disk map keys to use mountpoints for empty/none and /dev/root
devices so usage data remains stable and accessible across environments.
treat lines ending with unquoted `|` or `&` as continued
conditions in `do` block headers so nested blocks parse correctly
across line breaks.
update `on` condition splitting to avoid breaking on newlines that
follow an unescaped trailing pipe, while still respecting quotes,
escapes, and bracket nesting.
add coverage for multiline `|`/`&` continuations in `do` parsing,
`splitAnd`, `parseOn`, and HTTP flow nested block behavior.
* fix(middleware): restore SSE streaming for POST endpoints
Regression introduced in 16935865 (v0.27.0).
Before that commit, LazyResponseModifier only buffered HTML responses and
let everything else pass through via the IsBuffered() early return. The
refactor replaced it with NewResponseModifier which unconditionally buffers
all writes until FlushRelease() fires after the handler returns. That kills
real-time streaming for any SSE endpoint that uses POST.
The existing bypass at ServeHTTP line 193 only fires when the *request*
carries Accept: text/event-stream. That works for browser EventSource (which
always sets that header) but not for programmatic fetch() calls, which set
Content-Type: application/json on the request and only emit
Content-Type: text/event-stream on the *response*.
Fix: introduce ssePassthroughWriter, a thin http.ResponseWriter wrapper that
sits in front of the ResponseModifier. It watches for Content-Type:
text/event-stream in the response headers at the moment WriteHeader or the
first Write is called. Once detected it copies the buffered headers to the
real writer and switches all subsequent writes to pass directly through with
an immediate Flush(), bypassing the ResponseModifier buffer entirely.
Also tighten the Accept header check from == to strings.Contains so that
Accept: text/event-stream, */* is handled correctly.
Reported against Dockhand (https://github.com/Finsys/dockhand) where
container update progress, image pull logs and vulnerability scan output all
stopped streaming after users upgraded to GoDoxy v0.27.0. GET SSE endpoints
(container logs) continued to work because browsers send Accept:
text/event-stream for EventSource connections.
* fix(middleware): make Content-Type SSE check case-insensitive
* refactor(middleware): extract Content-Type into a named constant
* fix(middleware): enhance safe guard to avoid buffering SSE, WS and large bodies
Reverts some changes in 16935865 and apply more rubust handling.
Use a lazy response modifier that buffers only when the response is safe
to mutate. This prevents middleware from intercepting websocket/SSE
streams, encoded payloads, and non-text or oversized responses.
Set a 4MB max buffered size and gate buffering via response headers
(content type, transfer/content encoding, and content length). Skip
mutation when a response is not buffered or mutation setup fails, and
simplify chained response modifiers to operate on the same response.
Also update the goutils submodule for max body limit support.
---------
Co-authored-by: yusing <yusing.wys@gmail.com>
Introduces a new `$redacted` dynamic variable that wraps its single
argument with `strutils.Redact`, allowing sensitive values (e.g.,
authorization headers, query parameters) to be masked in rule
expressions.
The variable accepts exactly one argument, which may itself be a nested
dynamic variable expression such as `$header(Authorization)` or
`$arg(token)`, enabling patterns like `$redacted($header(Authorization))`.
Adds corresponding tests covering plain string redaction, nested header
and query arg wrapping, and the error case when no argument is provided.
* chore(deps): update submodule goutils
* docs(http): remove default client from README.md
* refactor(rules): introduce block DSL, phase-based execution, and flow validation
- add block syntax parser/scanner with nested @blocks and elif/else support
- restructure rule execution into explicit pre/post phases with phase flags
- classify commands by phase and termination behavior
- enforce flow semantics (default rule handling, dead-rule detection)
- expand HTTP flow coverage with block + YAML parity tests and benches
- refresh rules README/spec and update playground/docs integration
- Default rules act as fallback handlers that execute only when no matching non-default rule exists in the pre phase
- IfElseBlockCommand now returns early when a condition matches with a nil Do block, instead of falling through to else blocks
- Add nil check for auth handler to allow requests when no auth is configured
* fix(rules): buffer log output before writing to stdout/stderr
* refactor(api/rules): remove IsResponseRule field from ParsedRule and related logic
* docs(rules): update examples to use block syntax
Refactor response body modification to only allow text-like content types
(JSON, YAML, XML, etc.) instead of all HTML responses.
Body modification is now
blocked for binary content and transfer/content encoded responses, while status
code and headers can still be modified.
This prevents issues with compressed or streaming responses while
maintaining the ability to modify text-based API responses.
Previously, up notifications were sent whenever a service recovered,
even if no down notification had been sent (e.g., when recovering
before the failure threshold was met). This could confuse users who
would receive "service is up" notifications without ever being
notified of a problem.
Now, recovery notifications are only sent when a prior down
notification exists, ensuring notification pairs are always complete.