Documentation
¶
Overview ¶
Package portkit is a Caddy plugin that gives every local dev server an HTTPS URL at <name>.localhost.
Caddy runs socket-activated as the user: launchd (macOS) or systemd (Linux) holds the loopback port 443 and the admin socket, and the "portkit" network hands those sockets to Caddy. Services live in the "portkit" app (apps.portkit.services); each one proxies to a fixed port, to a process the supervisor starts on demand, or to a process that `portkit run` or `portkit adopt` registered.
Index ¶
- Constants
- func AdminSocket() string
- func AssignPorts(svc *Service) (map[string]int, error)
- func CACertPath() string
- func ChildEnv(base []string, name string, svc *Service, ports map[string]int) []string
- func DescribeResult(host string, r Result) string
- func ExpandCommand(argv []string, ports map[string]int) []string
- func FreePort() (int, error)
- func Home() string
- func LogDir() string
- func Placeholders(argv []string) []string
- func PortFromLine(line string) (int, bool)
- func ProcessStartTime(pid int) (string, error)
- func ProcessUID(pid int) (int, error)
- func RunDetection(d *Detector, pgid int, stop <-chan struct{}, ...)
- func ServiceURL(name string) string
- func StripANSI(s string) string
- func ValidLabel(s string) bool
- func ValidateName(name string) error
- type APIHandler
- type AdminAPI
- type AdoptRequest
- type AdoptResponse
- type App
- type CrashError
- type Detector
- type DiscoveredService
- type HelpHandler
- type Info
- type Matcher
- type Owner
- type Permission
- type ProcState
- type Resolved
- type Result
- type Service
- type ServiceState
- type Socket
- type StartHandler
- type Subdomain
- type Target
- type Upstreams
Constants ¶
const ( ModePort = "port" ModeOnDemand = "on_demand" ModeForeground = "foreground" ModeAdopted = "adopted" )
Service modes.
Variables ¶
This section is empty.
Functions ¶
func AdminSocket ¶
func AdminSocket() string
AdminSocket is the path of the admin API socket that launchd or systemd holds.
func AssignPorts ¶
AssignPorts picks a free port for {port} ("") and for each placeholder subdomain.
func CACertPath ¶
func CACertPath() string
CACertPath is the root certificate of the portkit local CA. Caddy runs with XDG_DATA_HOME=$PORTKIT_HOME/data, so its storage is under there.
func ChildEnv ¶
ChildEnv builds a child's environment: base, then the service's saved variables, then the variables portkit adds.
func DescribeResult ¶
DescribeResult formats the line portkit writes once a port is chosen, e.g. "portkit: foo.localhost → 127.0.0.1:3000 (assigned port)".
func ExpandCommand ¶
ExpandCommand replaces {port} and {<sub>} with the assigned ports.
func Home ¶
func Home() string
Home is PORTKIT_HOME: the directory holding the binary, config, CA and logs.
func Placeholders ¶
Placeholders returns the {name} tokens in a command, in order of first use. "{{" and "}}" are literal braces.
func PortFromLine ¶
PortFromLine returns the port a log line names, if any. The line should already have ANSI escapes stripped.
func ProcessStartTime ¶
ProcessStartTime identifies a process across PID reuse: field 22 of /proc/<pid>/stat, in clock ticks since boot.
func ProcessUID ¶
ProcessUID returns the user ID a process runs as.
func RunDetection ¶
func RunDetection(d *Detector, pgid int, stop <-chan struct{}, report func(target string, r Result))
RunDetection steps d every 250 ms with the sockets of process group pgid until every target resolves or stop closes. report is called for each result as it resolves.
func ServiceURL ¶
ServiceURL is the HTTPS URL for a service name or <sub>.<name>.
func ValidLabel ¶
ValidLabel reports whether s is a single lowercase DNS label.
func ValidateName ¶
ValidateName checks a service name: one or more DNS labels joined by dots.
Types ¶
type APIHandler ¶
type APIHandler struct {
// contains filtered or unexported fields
}
APIHandler serves https://portkit.localhost: the discovery endpoint at /services and an index page at /.
func (APIHandler) CaddyModule ¶
func (APIHandler) CaddyModule() caddy.ModuleInfo
func (*APIHandler) ServeHTTP ¶
func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error
type AdminAPI ¶
type AdminAPI struct{}
AdminAPI adds runtime endpoints to Caddy's admin API, beside the standard /config/apps/portkit/... endpoints that hold the records:
GET /portkit/info Caddy's process, paths and activity
GET /portkit/state whether each service is running, and on which port
POST /portkit/stop {"name": ...}: stop an on-demand process
POST /portkit/adopt register a running server (see Adopt)
func (AdminAPI) CaddyModule ¶
func (AdminAPI) CaddyModule() caddy.ModuleInfo
func (AdminAPI) Routes ¶
func (AdminAPI) Routes() []caddy.AdminRoute
type AdoptRequest ¶
type AdoptRequest struct {
Name string `json:"name"`
Port int `json:"port"`
Cwd string `json:"cwd,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Subdomains map[string]int `json:"subdomains,omitempty"`
Owner Owner `json:"owner"`
}
AdoptRequest is the body of POST /portkit/adopt.
type AdoptResponse ¶
type AdoptResponse struct {
Name string `json:"name"`
URL string `json:"url"`
Action string `json:"action"` // "merged", "created" or "took over"
Record *Service `json:"record"`
}
AdoptResponse says what adopt did.
type App ¶
type App struct {
// IdleExit is how long Caddy waits with no connections and no running
// children before it exits: a duration such as "2h", or "never".
IdleExit string `json:"idle_exit,omitempty"`
// Services maps each name to its record.
Services map[string]*Service `json:"services,omitempty"`
// contains filtered or unexported fields
}
App is apps.portkit: the service registry and Caddy's idle exit.
func (App) CaddyModule ¶
func (App) CaddyModule() caddy.ModuleInfo
type CrashError ¶
CrashError is returned once a service exited quickly too many times.
func (*CrashError) Error ¶
func (e *CrashError) Error() string
type Detector ¶
type Detector struct {
// contains filtered or unexported fields
}
Detector resolves targets from snapshots of listening sockets and the lines a process prints. Step is called every 250 ms until every target is resolved; it doesn't touch the OS itself, so it can be tested.
func NewDetector ¶
NewDetector makes a detector. accepts reports whether a port outside the process group takes connections; nil uses probePort.
func (*Detector) Reset ¶
Reset forgets a target's result so detection runs again, for example after dials to it start failing.
type DiscoveredService ¶
type DiscoveredService struct {
Name string `json:"name"`
URL string `json:"url"`
Mode string `json:"mode"`
Cwd string `json:"cwd,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Subdomains map[string]string `json:"subdomains,omitempty"`
Running bool `json:"running"`
}
DiscoveredService is one entry of GET /services. Commands and environments are left out.
type HelpHandler ¶
type HelpHandler struct {
// contains filtered or unexported fields
}
HelpHandler serves the page for names that aren't registered.
func (HelpHandler) CaddyModule ¶
func (HelpHandler) CaddyModule() caddy.ModuleInfo
func (*HelpHandler) ServeHTTP ¶
func (h *HelpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error
type Info ¶
type Info struct {
PID int `json:"pid"`
Version string `json:"version"`
Started time.Time `json:"started"`
Home string `json:"home"`
CACert string `json:"ca_cert"`
Connections int `json:"connections"`
Children int `json:"children"`
IdleExit string `json:"idle_exit"`
}
Info is the response of GET /portkit/info.
type Matcher ¶
type Matcher struct {
// contains filtered or unexported fields
}
Matcher matches requests whose host is a registered service with a live process or port, and records the service for the handlers after it.
func (Matcher) CaddyModule ¶
func (Matcher) CaddyModule() caddy.ModuleInfo
type Owner ¶
Owner identifies a process by ID and start time, so a reused ID doesn't count as the same process.
type Permission ¶
type Permission struct {
// contains filtered or unexported fields
}
Permission allows on-demand certificates for any name under .localhost with up to four labels, registered or not, so the help page loads for unregistered names. At most 10 new unregistered names a minute get one, so a runaway script can't fill the disk.
func (Permission) CaddyModule ¶
func (Permission) CaddyModule() caddy.ModuleInfo
func (*Permission) CertificateAllowed ¶
func (p *Permission) CertificateAllowed(_ context.Context, host string) error
type ProcState ¶
type ProcState struct {
State string `json:"state"` // "running", "starting", "stopped" or "crashed"
PID int `json:"pid,omitempty"`
Addr string `json:"addr,omitempty"`
Subdomains map[string]string `json:"subdomains,omitempty"`
Started *time.Time `json:"started,omitempty"`
Error string `json:"error,omitempty"`
}
ProcState is the runtime state of an on-demand service.
type Service ¶
type Service struct {
Mode string `json:"mode"`
Port int `json:"port,omitempty"`
Command []string `json:"command,omitempty"`
Cwd string `json:"cwd,omitempty"`
Env map[string]string `json:"env,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
// Subdomains are served at <sub>.<name>.localhost.
Subdomains map[string]*Subdomain `json:"subdomains,omitempty"`
// IdleTimeout is a duration such as "30m", or "never".
IdleTimeout string `json:"idle_timeout,omitempty"`
// HostHeader replaces the Host header sent upstream, e.g. "localhost:{port}".
HostHeader string `json:"host_header,omitempty"`
// ListenPort skips port detection for a process whose port is known.
ListenPort int `json:"listen_port,omitempty"`
// Owner is the process a "foreground" or "adopted" record lives with.
Owner *Owner `json:"owner,omitempty"`
// Replaces is the record an adopter took the name from; it applies
// again when the owner exits.
Replaces *Service `json:"replaces,omitempty"`
// contains filtered or unexported fields
}
Service is one record in apps.portkit.services. The key is its name.
type ServiceState ¶
ServiceState is the runtime state of one service in GET /portkit/state.
type Socket ¶
Socket is one TCP socket in LISTEN state.
func ListeningSockets ¶
ListeningSockets returns the TCP sockets in LISTEN state owned by process group pgid: the socket inodes under /proc/<pid>/fd of every process in the group, matched against /proc/net/tcp and /proc/net/tcp6.
type StartHandler ¶
type StartHandler struct {
// contains filtered or unexported fields
}
StartHandler runs before reverse_proxy. For an on-demand service it starts the process and waits for its port, and counts the request as activity for the idle timeout. For the other modes it checks that something listens on the port. Either way it shows a page instead of proxying when the service can't be reached.
func (StartHandler) CaddyModule ¶
func (StartHandler) CaddyModule() caddy.ModuleInfo
func (*StartHandler) ServeHTTP ¶
func (h *StartHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error
type Subdomain ¶
type Subdomain struct {
Assign bool `json:"assign,omitempty"`
Port int `json:"port,omitempty"`
Match string `json:"match,omitempty"`
// contains filtered or unexported fields
}
Subdomain declares where a subdomain's port comes from: a placeholder in the command (Assign), a fixed Port, or the first output line that Matches.
type Target ¶
type Target struct {
Name string
Assigned int // assigned through PORT, {port} or {<sub>}
Fixed int // known in advance: --listen-port or --sub NAME=PORT
Match *regexp.Regexp // --sub NAME=/regex/
}
Target is one port to find: the main port ("") or a subdomain's.
type Upstreams ¶
type Upstreams struct {
// contains filtered or unexported fields
}
Upstreams is the reverse_proxy upstream source: it finds the service for the request's host, starts it if needed, waits until its port is known and returns the exact address the process listens on.
func (Upstreams) CaddyModule ¶
func (Upstreams) CaddyModule() caddy.ModuleInfo