Documentation
¶
Index ¶
- Constants
- func AppConnectSessionTokenCookieName(accessMethod AccessMethod) string
- func AppConnectSessionTokenFromRequest(r *http.Request, accessMethod AccessMethod) string
- func WebsocketNetConn(ctx context.Context, conn *websocket.Conn, msgType websocket.MessageType) (context.Context, net.Conn)
- func WriteWorkspaceApp404(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, ...)
- func WriteWorkspaceApp500(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, ...)
- func WriteWorkspaceAppOffline(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, ...)
- func WriteWorkspaceOffline(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, ...)
- type AccessMethod
- type AgentProvider
- type DBTokenProvider
- type EncryptedAPIKeyPayload
- type IssueTokenRequest
- type Request
- type ResolveRequestOptions
- type Server
- type SignedToken
- type SignedTokenProvider
- type StatsCollector
- type StatsCollectorOptions
- type StatsReport
- type StatsReporter
Constants ¶
const ( // TODO(@deansheather): configurable expiry DefaultTokenExpiry = time.Minute // RedirectURIQueryParam is the query param for the app URL to be passed // back to the API auth endpoint on the main access URL. RedirectURIQueryParam = "redirect_uri" )
const ( DefaultStatsCollectorReportInterval = 30 * time.Second DefaultStatsCollectorRollupWindow = 1 * time.Minute DefaultStatsDBReporterBatchSize = 1024 )
const ( // This needs to be a super unique query parameter because we don't want to // conflict with query parameters that users may use. //nolint:gosec SubdomainProxyAPIKeyParam = "coder_application_connect_api_key_35e783" )
Variables ¶
This section is empty.
Functions ¶
func AppConnectSessionTokenCookieName ¶ added in v2.1.5
func AppConnectSessionTokenCookieName(accessMethod AccessMethod) string
AppConnectSessionTokenCookieName returns the cookie name for the session token for the given access method.
func AppConnectSessionTokenFromRequest ¶ added in v2.1.5
func AppConnectSessionTokenFromRequest(r *http.Request, accessMethod AccessMethod) string
AppConnectSessionTokenFromRequest returns the session token from the request if it exists. The access method is used to determine which cookie name to use.
We use different cookie names for path apps and for subdomain apps to avoid both being set and sent to the server at the same time and the server using the wrong value.
We use different cookie names for: - path apps on primary access URL: coder_session_token - path apps on proxies: coder_path_app_session_token - subdomain apps: coder_subdomain_app_session_token
First we try the default function to get a token from request, which supports query parameters, the Coder-Session-Token header and the coder_session_token cookie.
Then we try the specific cookie name for the access method.
func WebsocketNetConn ¶
func WebsocketNetConn(ctx context.Context, conn *websocket.Conn, msgType websocket.MessageType) (context.Context, net.Conn)
WebsocketNetConn wraps websocket.NetConn and returns a context that is tied to the parent context and the lifetime of the conn. Any error during read or write will cancel the context, but not close the conn. Close should be called to release context resources.
func WriteWorkspaceApp404 ¶
func WriteWorkspaceApp404(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, appReq *Request, warnings []string, details string)
WriteWorkspaceApp404 writes a HTML 404 error page for a workspace app. If appReq is not nil, it will be used to log the request details at debug level.
The 'warnings' parameter is sent to the user, 'details' is only shown in the logs.
func WriteWorkspaceApp500 ¶
func WriteWorkspaceApp500(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, appReq *Request, err error, msg string)
WriteWorkspaceApp500 writes a HTML 500 error page for a workspace app. If appReq is not nil, it's fields will be added to the logged error message.
func WriteWorkspaceAppOffline ¶
func WriteWorkspaceAppOffline(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, appReq *Request, msg string)
WriteWorkspaceAppOffline writes a HTML 502 error page for a workspace app. If appReq is not nil, it will be used to log the request details at debug level.
func WriteWorkspaceOffline ¶ added in v2.7.0
func WriteWorkspaceOffline(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, appReq *Request)
WriteWorkspaceOffline writes a HTML 400 error page for a workspace app. If appReq is not nil, it will be used to log the request details at debug level.
Types ¶
type AccessMethod ¶
type AccessMethod string
const ( AccessMethodPath AccessMethod = "path" AccessMethodSubdomain AccessMethod = "subdomain" // AccessMethodTerminal is special since it's not a real app and only // applies to the PTY endpoint on the API. AccessMethodTerminal AccessMethod = "terminal" )
type AgentProvider ¶
type AgentProvider interface { // ReverseProxy returns an httputil.ReverseProxy for proxying HTTP requests // to the specified agent. ReverseProxy(targetURL, dashboardURL *url.URL, agentID uuid.UUID, app appurl.ApplicationURL, wildcardHost string) *httputil.ReverseProxy // AgentConn returns a new connection to the specified agent. AgentConn(ctx context.Context, agentID uuid.UUID) (_ *workspacesdk.AgentConn, release func(), _ error) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) Close() error }
type DBTokenProvider ¶
type DBTokenProvider struct { Logger slog.Logger // DashboardURL is the main dashboard access URL for error pages. DashboardURL *url.URL Authorizer rbac.Authorizer Auditor *atomic.Pointer[audit.Auditor] Database database.Store DeploymentValues *codersdk.DeploymentValues OAuth2Configs *httpmw.OAuth2Configs WorkspaceAgentInactiveTimeout time.Duration WorkspaceAppAuditSessionTimeout time.Duration Keycache cryptokeys.SigningKeycache }
DBTokenProvider provides authentication and authorization for workspace apps by querying the database if the request is missing a valid token.
func (*DBTokenProvider) FromRequest ¶
func (p *DBTokenProvider) FromRequest(r *http.Request) (*SignedToken, bool)
func (*DBTokenProvider) Issue ¶
func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r *http.Request, issueReq IssueTokenRequest) (*SignedToken, string, bool)
type EncryptedAPIKeyPayload ¶
type EncryptedAPIKeyPayload struct { jwtutils.RegisteredClaims APIKey string `json:"api_key"` }
func (*EncryptedAPIKeyPayload) Fill ¶ added in v2.17.0
func (e *EncryptedAPIKeyPayload) Fill(now time.Time)
type IssueTokenRequest ¶
type IssueTokenRequest struct { AppRequest Request `json:"app_request"` // PathAppBaseURL is required. PathAppBaseURL string `json:"path_app_base_url"` // AppHostname is the optional hostname for subdomain apps on the external // proxy. It must start with an asterisk. AppHostname string `json:"app_hostname"` // AppPath is the path of the user underneath the app base path. AppPath string `json:"app_path"` // AppQuery is the query parameters the user provided in the app request. AppQuery string `json:"app_query"` // SessionToken is the session token provided by the user. SessionToken string `json:"session_token"` }
func (IssueTokenRequest) AppBaseURL ¶
func (r IssueTokenRequest) AppBaseURL() (*url.URL, error)
AppBaseURL returns the base URL of this specific app request. An error is returned if a subdomain app hostname is not provided but the app is a subdomain app.
type Request ¶
type Request struct { AccessMethod AccessMethod `json:"access_method"` // BasePath of the app. For path apps, this is the path prefix in the router // for this particular app. For subdomain apps, this should be "/". This is // used for setting the cookie path. BasePath string `json:"base_path"` // Prefix is the prefix of the subdomain app URL. Prefix should have a // trailing "---" if set. Prefix string `json:"app_prefix"` // For the following fields, if the AccessMethod is AccessMethodTerminal, // then only AgentNameOrID may be set and it must be a UUID. The other // fields must be left blank. UsernameOrID string `json:"username_or_id"` // WorkspaceAndAgent xor WorkspaceNameOrID are required. WorkspaceAndAgent string `json:"-"` // "workspace" or "workspace.agent" WorkspaceNameOrID string `json:"workspace_name_or_id"` // AgentNameOrID is not required if the workspace has only one agent. AgentNameOrID string `json:"agent_name_or_id"` AppSlugOrPort string `json:"app_slug_or_port"` }
type ResolveRequestOptions ¶
type ResolveRequestOptions struct { Logger slog.Logger SignedTokenProvider SignedTokenProvider DashboardURL *url.URL PathAppBaseURL *url.URL AppHostname string AppRequest Request // TODO: Replace these 2 fields with a "BrowserURL" field which is used for // redirecting the user back to their initial request after authenticating. // AppPath is the path under the app that was hit. AppPath string // AppQuery is the raw query of the request. AppQuery string }
type Server ¶
type Server struct { Logger slog.Logger // DashboardURL should be a url to the coderd dashboard. This can be the // same as the AccessURL if the Server is embedded. DashboardURL *url.URL AccessURL *url.URL // Hostname should be the wildcard hostname to use for workspace // applications INCLUDING the asterisk, (optional) suffix and leading dot. // It will use the same scheme and port number as the access URL. // E.g. "*.apps.coder.com" or "*-apps.coder.com". Hostname string // HostnameRegex contains the regex version of Hostname as generated by // appurl.CompileHostnamePattern(). It MUST be set if Hostname is set. HostnameRegex *regexp.Regexp RealIPConfig *httpmw.RealIPConfig SignedTokenProvider SignedTokenProvider APIKeyEncryptionKeycache cryptokeys.EncryptionKeycache // DisablePathApps disables path-based apps. This is a security feature as path // based apps share the same cookie as the dashboard, and are susceptible to XSS // by a malicious workspace app. // // Subdomain apps are safer with their cookies scoped to the subdomain, and XSS // calls to the dashboard are not possible due to CORs. DisablePathApps bool SecureAuthCookie bool AgentProvider AgentProvider StatsCollector *StatsCollector // contains filtered or unexported fields }
Server serves workspace apps endpoints, including: - Path-based apps - Subdomain app middleware - Workspace reconnecting-pty (aka. web terminal)
func (*Server) Close ¶
Close waits for all reconnecting-pty WebSocket connections to drain before returning.
func (*Server) HandleSubdomain ¶
func (s *Server) HandleSubdomain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler
HandleSubdomain handles subdomain-based application proxy requests (aka. DevURLs in Coder V1).
There are a lot of paths here:
- If api.Hostname is not set then we pass on.
- If we can't read the request hostname then we return a 400.
- If the request hostname matches api.AccessURL then we pass on.
- We split the subdomain into the subdomain and the "rest". If there are no periods in the hostname then we pass on.
- We parse the subdomain into a appurl.ApplicationURL struct. If we encounter an error: a. If the "rest" does not match api.Hostname then we pass on; b. Otherwise, we return a 400.
- Finally, we verify that the "rest" matches api.Hostname, else we return a 404.
Rationales for each of the above steps:
- We pass on if api.Hostname is not set to avoid returning any errors if `--app-hostname` is not configured.
- Every request should have a valid Host header anyways.
- We pass on if the request hostname matches api.AccessURL so we can support having the access URL be at the same level as the application base hostname.
- We pass on if there are no periods in the hostname as application URLs must be a subdomain of a hostname, which implies there must be at least one period.
- a. If the request subdomain is not a valid application URL, and the "rest" does not match api.Hostname, then it is very unlikely that the request was intended for this handler. We pass on. b. If the request subdomain is not a valid application URL, but the "rest" matches api.Hostname, then we return a 400 because the request is probably a typo or something.
- We finally verify that the "rest" matches api.Hostname for security purposes regarding re-authentication and application proxy session tokens.
type SignedToken ¶
type SignedToken struct { jwtutils.RegisteredClaims // Request details. Request `json:"request"` UserID uuid.UUID `json:"user_id"` WorkspaceID uuid.UUID `json:"workspace_id"` AgentID uuid.UUID `json:"agent_id"` AppURL string `json:"app_url"` }
SignedToken is the struct data contained inside a workspace app JWE. It contains the details of the workspace app that the token is valid for to avoid database queries.
func FromRequest ¶
func FromRequest(r *http.Request, mgr cryptokeys.SigningKeycache) (*SignedToken, bool)
FromRequest returns the signed token from the request, if it exists and is valid. The caller must check that the token matches the request.
func ResolveRequest ¶
func ResolveRequest(rw http.ResponseWriter, r *http.Request, opts ResolveRequestOptions) (*SignedToken, bool)
func (SignedToken) MatchesRequest ¶
func (t SignedToken) MatchesRequest(req Request) bool
MatchesRequest returns true if the token matches the request. Any token that does not match the request should be considered invalid.
type SignedTokenProvider ¶
type SignedTokenProvider interface { // FromRequest returns a parsed token from the request. If the request does // not contain a signed app token or is is invalid (expired, invalid // signature, etc.), it returns false. FromRequest(r *http.Request) (*SignedToken, bool) // Issue mints a new token for the given app request. It uses the long-lived // session token in the HTTP request to authenticate and authorize the // client for the given workspace app. The token is returned in struct and // string form. The string form should be written as a cookie. // // If the request is invalid or the user is not authorized to access the // app, false is returned. An error page is written to the response writer // in this case. Issue(ctx context.Context, rw http.ResponseWriter, r *http.Request, appReq IssueTokenRequest) (*SignedToken, string, bool) }
SignedTokenProvider provides signed workspace app tokens (aka. app tickets).
func NewDBTokenProvider ¶
func NewDBTokenProvider(log slog.Logger, accessURL *url.URL, authz rbac.Authorizer, auditor *atomic.Pointer[audit.Auditor], db database.Store, cfg *codersdk.DeploymentValues, oauth2Cfgs *httpmw.OAuth2Configs, workspaceAgentInactiveTimeout time.Duration, workspaceAppAuditSessionTimeout time.Duration, signer cryptokeys.SigningKeycache, ) SignedTokenProvider
type StatsCollector ¶
type StatsCollector struct {
// contains filtered or unexported fields
}
StatsCollector collects workspace app StatsReports and reports them in batches, stats compaction is performed for short-lived sessions.
func NewStatsCollector ¶
func NewStatsCollector(opts StatsCollectorOptions) *StatsCollector
func (*StatsCollector) Close ¶
func (sc *StatsCollector) Close() error
func (*StatsCollector) Collect ¶
func (sc *StatsCollector) Collect(report StatsReport)
Collect the given StatsReport for later reporting (non-blocking).
type StatsCollectorOptions ¶
type StatsCollectorOptions struct { Logger *slog.Logger Reporter StatsReporter // ReportInterval is the interval at which stats are reported, both partial // and fully formed stats. ReportInterval time.Duration // RollupWindow is the window size for rolling up stats, session shorter // than this will be rolled up and longer than this will be tracked // individually. RollupWindow time.Duration // Options for tests. Flush <-chan chan<- struct{} Now func() time.Time }
type StatsReport ¶
type StatsReport struct { UserID uuid.UUID `json:"user_id"` WorkspaceID uuid.UUID `json:"workspace_id"` AgentID uuid.UUID `json:"agent_id"` AccessMethod AccessMethod `json:"access_method"` SlugOrPort string `json:"slug_or_port"` SessionID uuid.UUID `json:"session_id"` SessionStartedAt time.Time `json:"session_started_at"` SessionEndedAt time.Time `json:"session_ended_at"` // Updated periodically while app is in use active and when the last connection is closed. Requests int `json:"requests"` // contains filtered or unexported fields }
StatsReport is a report of a workspace app session.
type StatsReporter ¶
type StatsReporter interface {
ReportAppStats(context.Context, []StatsReport) error
}
StatsReporter reports workspace app StatsReports.