Documentation
¶
Index ¶
- Variables
- func CSRFMiddleware(next http.Handler) http.Handler
- func DeviceIDFromContext(ctx context.Context) (string, bool)
- func GeneratePairingCode() string
- func HashCode(code string) []byte
- func IsLoopback(r *http.Request) bool
- func NewWebAuthMiddleware(signer *SessionSigner, store *Store) func(http.Handler) http.Handler
- func WithDeviceID(ctx context.Context, deviceID string) context.Context
- type ConnectionRegistry
- type Device
- type DeviceInfo
- type PairRequest
- type PairResponse
- type PairingManager
- type RateLimiter
- type SessionSigner
- type Store
- func (s *Store) ConsumePairingCode(ctx context.Context, codeHash []byte) (label string, err error)
- func (s *Store) DeleteRevokedDevices(ctx context.Context, dryRun bool) (int64, error)
- func (s *Store) GetDevice(ctx context.Context, id string) (*Device, error)
- func (s *Store) HasAnyDevice(ctx context.Context) (bool, error)
- func (s *Store) InsertDevice(ctx context.Context, id, label string, cookieHash []byte) error
- func (s *Store) InsertPairingCode(ctx context.Context, codeHash []byte, label string, expiresAt time.Time) error
- func (s *Store) ListDevices(ctx context.Context) ([]*Device, error)
- func (s *Store) RevokeAllDevices(ctx context.Context) error
- func (s *Store) RevokeDevice(ctx context.Context, id string) error
- func (s *Store) UpdateDeviceLastSeen(ctx context.Context, id string, t time.Time) error
Constants ¶
This section is empty.
Variables ¶
var ( ErrCodeNotFound = errors.New("pairing code not found") ErrCodeExpired = errors.New("pairing code expired") ErrCodeConsumed = errors.New("pairing code already consumed") ErrDeviceNotFound = errors.New("device not found") )
var ErrInvalidSession = errors.New("invalid session")
Functions ¶
func CSRFMiddleware ¶
CSRFMiddleware implements double-submit cookie CSRF protection.
GET/HEAD/OPTIONS/TRACE: issues csrf_token cookie if absent, then passes. POST/PUT/PATCH/DELETE: compares the cookie with the X-CSRF-Token header (for HTMX / fetch callers) or the _csrf form field (for plain HTML forms); 403 on mismatch.
Exempt paths:
- /auth and /auth/* (protected by one-time pairing token)
- /api/* (programmatic CLI access via UNIX socket)
func DeviceIDFromContext ¶
DeviceIDFromContext returns the authenticated device ID stored in ctx by NewWebAuthMiddleware. Returns ("", false) for unauthenticated requests.
func GeneratePairingCode ¶
func GeneratePairingCode() string
func IsLoopback ¶
IsLoopback reports whether the request came from a loopback address. UNIX domain socket connections (RemoteAddr == "" or "@") are also considered loopback since they are only reachable from the local machine.
If the request carries upstream-proxy headers (X-Forwarded-For, CF-Connecting-IP, Forwarded), the request is treated as NOT loopback even when RemoteAddr is 127.0.0.1 — because such a request arrived via a reverse proxy / tunnel (e.g. cloudflared forwarding to localhost:8080) and must not benefit from the bootstrap exemption granted to real local sessions. Header values themselves are not trusted (they can be spoofed); only the presence of the header is used as a "I came through a proxy" signal.
func NewWebAuthMiddleware ¶
NewWebAuthMiddleware returns middleware that enforces session cookie auth for web UI routes.
Exempt paths (pass through without any check): /login, /auth*, /static/*.
For non-exempt paths the logic is:
no cookie + loopback + no devices registered → warn + pass (bootstrap mode) no cookie + anything else → 302 /login invalid cookie → clear cookie + 302 /login valid cookie → pass (Verify updates last_seen, deviceID stored in ctx)
Types ¶
type ConnectionRegistry ¶
type ConnectionRegistry struct {
// contains filtered or unexported fields
}
ConnectionRegistry maps device IDs to the set of revoke channels for active long-lived connections (SSE / WebSocket). Calling RevokeDevice closes all channels registered for that device, causing handlers to unblock and return.
func NewConnectionRegistry ¶
func NewConnectionRegistry() *ConnectionRegistry
func (*ConnectionRegistry) Register ¶
func (r *ConnectionRegistry) Register(deviceID string) (<-chan struct{}, func())
Register records a long-lived connection for deviceID and returns a channel that is closed when the device is revoked, plus a release function that must be deferred by the caller to clean up when the connection ends normally.
func (*ConnectionRegistry) RevokeAll ¶
func (r *ConnectionRegistry) RevokeAll()
RevokeAll closes all revoke channels for every registered device.
func (*ConnectionRegistry) RevokeDevice ¶
func (r *ConnectionRegistry) RevokeDevice(deviceID string)
RevokeDevice closes all revoke channels registered for deviceID.
type DeviceInfo ¶
type DeviceInfo struct {
ID string `json:"id"`
Label string `json:"label,omitempty"`
LastSeenAt time.Time `json:"last_seen_at"`
CreatedAt time.Time `json:"created_at"`
}
DeviceInfo is the API response representation of a paired web device.
type PairRequest ¶
type PairRequest struct {
Label string `json:"label,omitempty"`
}
PairRequest is the body for POST /api/web/pair.
type PairResponse ¶
type PairResponse struct {
Code string `json:"code"`
URL string `json:"url,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
}
PairResponse is returned by POST /api/web/pair.
type PairingManager ¶
type PairingManager struct {
// contains filtered or unexported fields
}
func NewPairingManager ¶
func NewPairingManager(store *Store) *PairingManager
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
func NewRateLimiter ¶
func NewRateLimiter(now func() time.Time) *RateLimiter
func (*RateLimiter) Allow ¶
func (rl *RateLimiter) Allow(ip string) bool
func (*RateLimiter) Allowed ¶
func (rl *RateLimiter) Allowed(ip string) bool
Allowed reports whether ip is currently not rate-limited (read-only, no side effects).
func (*RateLimiter) RecordFailure ¶
func (rl *RateLimiter) RecordFailure(ip string)
RecordFailure records a failed attempt for ip and locks it if the threshold is exceeded.
type SessionSigner ¶
type SessionSigner struct {
// contains filtered or unexported fields
}
func NewSessionSigner ¶
func NewSessionSigner(secret []byte, store *Store) *SessionSigner
func (*SessionSigner) Clear ¶
func (s *SessionSigner) Clear(w http.ResponseWriter)
func (*SessionSigner) Issue ¶
func (s *SessionSigner) Issue(w http.ResponseWriter, deviceID string) error
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
func (*Store) ConsumePairingCode ¶
func (*Store) DeleteRevokedDevices ¶
func (*Store) GetDevice ¶
GetDevice returns the device with the given id, or nil if not found or revoked.