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
internal/api/v1
Implements the v1 REST API handlers for GoDoxy, exposing endpoints for managing routes, Docker containers, certificates, metrics, and system configuration.
Overview
The internal/api/v1 package implements the HTTP handlers that power GoDoxy's REST API. It uses the Gin web framework and provides endpoints for route management, container operations, certificate handling, system metrics, and configuration.
Primary Consumers
- WebUI: The homepage dashboard and admin interface consume these endpoints
Non-goals
- Authentication and authorization logic (delegated to
internal/auth) - Route proxying and request handling (handled by
internal/route) - Docker container lifecycle management (delegated to
internal/docker) - Certificate issuance and storage (handled by
internal/autocert)
Stability
This package is stable. Public API endpoints follow semantic versioning for request/response contracts. Internal implementation may change between minor versions.
Public API
Exported Types
Types are defined in goutils/apitypes:
| Type | Purpose |
|---|---|
apitypes.ErrorResponse |
Standard error response format |
apitypes.SuccessResponse |
Standard success response format |
Handler Subpackages
| Package | Purpose |
|---|---|
route |
Route listing, details, and playground testing |
docker |
Docker container management and monitoring |
cert |
Certificate information and renewal |
metrics |
System metrics and uptime information |
homepage |
Homepage items and category management |
file |
Configuration file read/write operations |
auth |
Authentication and session management |
agent |
Remote agent creation and management |
proxmox |
Proxmox API management and monitoring |
Architecture
Handler Organization
Package structure mirrors the API endpoint paths (e.g., auth/login.go handles /auth/login).
Request Flow
sequenceDiagram
participant Client
participant GinRouter
participant Handler
participant Service
participant Response
Client->>GinRouter: HTTP Request
GinRouter->>Handler: Route to handler
Handler->>Service: Call service layer
Service-->>Handler: Data or error
Handler->>Response: Format JSON response
Response-->>Client: JSON or redirect
Configuration Surface
API listening address is configured with GODOXY_API_ADDR environment variable.
Dependency and Integration Map
Internal Dependencies
| Package | Purpose |
|---|---|
internal/route/routes |
Route storage and iteration |
internal/docker |
Docker client management |
internal/config |
Configuration access |
internal/metrics |
System metrics collection |
internal/homepage |
Homepage item generation |
internal/agentpool |
Remote agent management |
internal/auth |
Authentication services |
internal/proxmox |
Proxmox API management and monitoring |
External Dependencies
| Package | Purpose |
|---|---|
github.com/gin-gonic/gin |
HTTP routing and middleware |
github.com/gorilla/websocket |
WebSocket support |
github.com/moby/moby/client |
Docker API client |
Observability
Logs
Handlers log at INFO level for requests and ERROR level for failures. Logs include:
- Request path and method
- Response status code
- Error details (when applicable)
Metrics
No dedicated metrics exposed by handlers. Request metrics collected by middleware.
Security Considerations
- All endpoints (except
/api/v1/version) require authentication - Input validation using Gin binding tags
- File read/write handlers are rooted per file type (
config/orconfig/middlewares/) to prevent traversal into sibling paths - WebSocket connections use same auth middleware as HTTP
Failure Modes and Recovery
| Failure | Behavior |
|---|---|
| Docker host unreachable | Returns partial results with errors logged |
| Certificate provider not configured | Returns 404 |
| Invalid request body | Returns 400 with error details |
| Authentication failure | Returns 302 redirect to login |
| Agent not found | Returns 404 |
Usage Examples
Listing All Routes via WebSocket
import (
"github.com/gorilla/websocket"
)
func watchRoutes(provider string) error {
url := "ws://localhost:8888/api/v1/route/list"
if provider != "" {
url += "?provider=" + provider
}
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return err
}
defer conn.Close()
for {
_, message, err := conn.ReadMessage()
if err != nil {
return err
}
// message contains JSON array of routes
processRoutes(message)
}
}
Getting Container Status
import (
"encoding/json"
"net/http"
)
type Container struct {
Server string `json:"server"`
Name string `json:"name"`
ID string `json:"id"`
Image string `json:"image"`
}
func listContainers() ([]Container, error) {
resp, err := http.Get("http://localhost:8888/api/v1/docker/containers")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var containers []Container
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
return nil, err
}
return containers, nil
}
Health Check
curl http://localhost:8888/health