mirror of https://github.com/coder/coder.git
634 lines
19 KiB
Go
634 lines
19 KiB
Go
package coderd
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog"
|
|
agpl "github.com/coder/coder/coderd"
|
|
"github.com/coder/coder/coderd/audit"
|
|
"github.com/coder/coder/coderd/database"
|
|
"github.com/coder/coder/coderd/database/dbauthz"
|
|
"github.com/coder/coder/coderd/httpapi"
|
|
"github.com/coder/coder/coderd/httpmw"
|
|
"github.com/coder/coder/coderd/rbac"
|
|
"github.com/coder/coder/coderd/workspaceapps"
|
|
"github.com/coder/coder/codersdk"
|
|
"github.com/coder/coder/cryptorand"
|
|
"github.com/coder/coder/enterprise/coderd/proxyhealth"
|
|
"github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
|
|
)
|
|
|
|
// forceWorkspaceProxyHealthUpdate forces an update of the proxy health.
|
|
// This is useful when a proxy is created or deleted. Errors will be logged.
|
|
func (api *API) forceWorkspaceProxyHealthUpdate(ctx context.Context) {
|
|
if err := api.ProxyHealth.ForceUpdate(ctx); err != nil {
|
|
api.Logger.Error(ctx, "force proxy health update", slog.Error(err))
|
|
}
|
|
}
|
|
|
|
// NOTE: this doesn't need a swagger definition since AGPL already has one, and
|
|
// this route overrides the AGPL one.
|
|
func (api *API) regions(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
//nolint:gocritic // this route intentionally requests resources that users
|
|
// cannot usually access in order to give them a full list of available
|
|
// regions.
|
|
ctx = dbauthz.AsSystemRestricted(ctx)
|
|
|
|
primaryRegion, err := api.AGPL.PrimaryRegion(ctx)
|
|
if err != nil {
|
|
httpapi.InternalServerError(rw, err)
|
|
return
|
|
}
|
|
regions := []codersdk.Region{primaryRegion}
|
|
|
|
proxies, err := api.Database.GetWorkspaceProxies(ctx)
|
|
if err != nil {
|
|
httpapi.InternalServerError(rw, err)
|
|
return
|
|
}
|
|
|
|
// Only add additional regions if the proxy health is enabled.
|
|
// If it is nil, it is because the moons feature flag is not on.
|
|
// By default, we still want to return the primary region.
|
|
if api.ProxyHealth != nil {
|
|
proxyHealth := api.ProxyHealth.HealthStatus()
|
|
for _, proxy := range proxies {
|
|
if proxy.Deleted {
|
|
continue
|
|
}
|
|
|
|
health := proxyHealth[proxy.ID]
|
|
regions = append(regions, codersdk.Region{
|
|
ID: proxy.ID,
|
|
Name: proxy.Name,
|
|
DisplayName: proxy.DisplayName,
|
|
IconURL: proxy.Icon,
|
|
Healthy: health.Status == proxyhealth.Healthy,
|
|
PathAppURL: proxy.Url,
|
|
WildcardHostname: proxy.WildcardHostname,
|
|
})
|
|
}
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.RegionsResponse{
|
|
Regions: regions,
|
|
})
|
|
}
|
|
|
|
// @Summary Update workspace proxy
|
|
// @ID update-workspace-proxy
|
|
// @Security CoderSessionToken
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Param workspaceproxy path string true "Proxy ID or name" format(uuid)
|
|
// @Param request body codersdk.PatchWorkspaceProxy true "Update workspace proxy request"
|
|
// @Success 200 {object} codersdk.WorkspaceProxy
|
|
// @Router /workspaceproxies/{workspaceproxy} [patch]
|
|
func (api *API) patchWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
ctx = r.Context()
|
|
proxy = httpmw.WorkspaceProxyParam(r)
|
|
auditor = api.AGPL.Auditor.Load()
|
|
aReq, commitAudit = audit.InitRequest[database.WorkspaceProxy](rw, &audit.RequestParams{
|
|
Audit: *auditor,
|
|
Log: api.Logger,
|
|
Request: r,
|
|
Action: database.AuditActionWrite,
|
|
})
|
|
)
|
|
aReq.Old = proxy
|
|
defer commitAudit()
|
|
|
|
var req codersdk.PatchWorkspaceProxy
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
var hashedSecret []byte
|
|
var fullToken string
|
|
if req.RegenerateToken {
|
|
var err error
|
|
fullToken, hashedSecret, err = generateWorkspaceProxyToken(proxy.ID)
|
|
if err != nil {
|
|
httpapi.InternalServerError(rw, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
updatedProxy, err := api.Database.UpdateWorkspaceProxy(ctx, database.UpdateWorkspaceProxyParams{
|
|
Name: req.Name,
|
|
DisplayName: req.DisplayName,
|
|
Icon: req.Icon,
|
|
ID: proxy.ID,
|
|
// If hashedSecret is nil or empty, this will not update the secret.
|
|
TokenHashedSecret: hashedSecret,
|
|
})
|
|
if httpapi.Is404Error(err) {
|
|
httpapi.ResourceNotFound(rw)
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpapi.InternalServerError(rw, err)
|
|
return
|
|
}
|
|
|
|
aReq.New = updatedProxy
|
|
status, ok := api.ProxyHealth.HealthStatus()[updatedProxy.ID]
|
|
if !ok {
|
|
// The proxy should have some status, but just in case.
|
|
status.Status = proxyhealth.Unknown
|
|
}
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UpdateWorkspaceProxyResponse{
|
|
Proxy: convertProxy(updatedProxy, status),
|
|
ProxyToken: fullToken,
|
|
})
|
|
|
|
// Update the proxy cache.
|
|
go api.forceWorkspaceProxyHealthUpdate(api.ctx)
|
|
}
|
|
|
|
// @Summary Delete workspace proxy
|
|
// @ID delete-workspace-proxy
|
|
// @Security CoderSessionToken
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Param workspaceproxy path string true "Proxy ID or name" format(uuid)
|
|
// @Success 200 {object} codersdk.Response
|
|
// @Router /workspaceproxies/{workspaceproxy} [delete]
|
|
func (api *API) deleteWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
ctx = r.Context()
|
|
proxy = httpmw.WorkspaceProxyParam(r)
|
|
auditor = api.AGPL.Auditor.Load()
|
|
aReq, commitAudit = audit.InitRequest[database.WorkspaceProxy](rw, &audit.RequestParams{
|
|
Audit: *auditor,
|
|
Log: api.Logger,
|
|
Request: r,
|
|
Action: database.AuditActionDelete,
|
|
})
|
|
)
|
|
aReq.Old = proxy
|
|
defer commitAudit()
|
|
|
|
err := api.Database.UpdateWorkspaceProxyDeleted(ctx, database.UpdateWorkspaceProxyDeletedParams{
|
|
ID: proxy.ID,
|
|
Deleted: true,
|
|
})
|
|
if httpapi.Is404Error(err) {
|
|
httpapi.ResourceNotFound(rw)
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpapi.InternalServerError(rw, err)
|
|
return
|
|
}
|
|
|
|
aReq.New = database.WorkspaceProxy{}
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
|
|
Message: "Proxy has been deleted!",
|
|
})
|
|
|
|
// Update the proxy health cache to remove this proxy.
|
|
go api.forceWorkspaceProxyHealthUpdate(api.ctx)
|
|
}
|
|
|
|
// @Summary Get workspace proxy
|
|
// @ID get-workspace-proxy
|
|
// @Security CoderSessionToken
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Param workspaceproxy path string true "Proxy ID or name" format(uuid)
|
|
// @Success 200 {object} codersdk.WorkspaceProxy
|
|
// @Router /workspaceproxies/{workspaceproxy} [get]
|
|
func (api *API) workspaceProxy(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
ctx = r.Context()
|
|
proxy = httpmw.WorkspaceProxyParam(r)
|
|
)
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, convertProxy(proxy, api.ProxyHealth.HealthStatus()[proxy.ID]))
|
|
}
|
|
|
|
// @Summary Create workspace proxy
|
|
// @ID create-workspace-proxy
|
|
// @Security CoderSessionToken
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Param request body codersdk.CreateWorkspaceProxyRequest true "Create workspace proxy request"
|
|
// @Success 201 {object} codersdk.WorkspaceProxy
|
|
// @Router /workspaceproxies [post]
|
|
func (api *API) postWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
ctx = r.Context()
|
|
auditor = api.AGPL.Auditor.Load()
|
|
aReq, commitAudit = audit.InitRequest[database.WorkspaceProxy](rw, &audit.RequestParams{
|
|
Audit: *auditor,
|
|
Log: api.Logger,
|
|
Request: r,
|
|
Action: database.AuditActionCreate,
|
|
})
|
|
)
|
|
defer commitAudit()
|
|
|
|
var req codersdk.CreateWorkspaceProxyRequest
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
if strings.ToLower(req.Name) == "primary" {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: `The name "primary" is reserved for the primary region.`,
|
|
Detail: "Cannot name a workspace proxy 'primary'.",
|
|
Validations: []codersdk.ValidationError{
|
|
{
|
|
Field: "name",
|
|
Detail: "Reserved name",
|
|
},
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
id := uuid.New()
|
|
fullToken, hashedSecret, err := generateWorkspaceProxyToken(id)
|
|
if err != nil {
|
|
httpapi.InternalServerError(rw, err)
|
|
return
|
|
}
|
|
|
|
proxy, err := api.Database.InsertWorkspaceProxy(ctx, database.InsertWorkspaceProxyParams{
|
|
ID: id,
|
|
Name: req.Name,
|
|
DisplayName: req.DisplayName,
|
|
Icon: req.Icon,
|
|
TokenHashedSecret: hashedSecret[:],
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
})
|
|
if database.IsUniqueViolation(err) {
|
|
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
|
Message: fmt.Sprintf("Workspace proxy with name %q already exists.", req.Name),
|
|
})
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpapi.InternalServerError(rw, err)
|
|
return
|
|
}
|
|
|
|
aReq.New = proxy
|
|
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.UpdateWorkspaceProxyResponse{
|
|
Proxy: convertProxy(proxy, proxyhealth.ProxyStatus{
|
|
Proxy: proxy,
|
|
CheckedAt: time.Now(),
|
|
Status: proxyhealth.Unregistered,
|
|
}),
|
|
ProxyToken: fullToken,
|
|
})
|
|
|
|
// Update the proxy health cache to include this new proxy.
|
|
go api.forceWorkspaceProxyHealthUpdate(api.ctx)
|
|
}
|
|
|
|
// nolint:revive
|
|
func validateProxyURL(u string) error {
|
|
p, err := url.Parse(u)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if p.Scheme != "http" && p.Scheme != "https" {
|
|
return xerrors.New("scheme must be http or https")
|
|
}
|
|
if !(p.Path == "/" || p.Path == "") {
|
|
return xerrors.New("path must be empty or /")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// @Summary Get workspace proxies
|
|
// @ID get-workspace-proxies
|
|
// @Security CoderSessionToken
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Success 200 {array} codersdk.WorkspaceProxy
|
|
// @Router /workspaceproxies [get]
|
|
func (api *API) workspaceProxies(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
proxies, err := api.Database.GetWorkspaceProxies(ctx)
|
|
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
|
httpapi.InternalServerError(rw, err)
|
|
return
|
|
}
|
|
|
|
statues := api.ProxyHealth.HealthStatus()
|
|
httpapi.Write(ctx, rw, http.StatusOK, convertProxies(proxies, statues))
|
|
}
|
|
|
|
// @Summary Issue signed workspace app token
|
|
// @ID issue-signed-workspace-app-token
|
|
// @Security CoderSessionToken
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Param request body workspaceapps.IssueTokenRequest true "Issue signed app token request"
|
|
// @Success 201 {object} wsproxysdk.IssueSignedAppTokenResponse
|
|
// @Router /workspaceproxies/me/issue-signed-app-token [post]
|
|
// @x-apidocgen {"skip": true}
|
|
func (api *API) workspaceProxyIssueSignedAppToken(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// NOTE: this endpoint will return JSON on success, but will (usually)
|
|
// return a self-contained HTML error page on failure. The external proxy
|
|
// should forward any non-201 response to the client.
|
|
|
|
var req workspaceapps.IssueTokenRequest
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
// userReq is a http request from the user on the other side of the proxy.
|
|
// Although the workspace proxy is making this call, we want to use the user's
|
|
// authorization context to create the token.
|
|
//
|
|
// We can use the existing request context for all tracing/logging purposes.
|
|
// Any workspace proxy auth uses different context keys so we don't need to
|
|
// worry about that.
|
|
userReq, err := http.NewRequestWithContext(ctx, "GET", req.AppRequest.BasePath, nil)
|
|
if err != nil {
|
|
// This should never happen
|
|
httpapi.InternalServerError(rw, xerrors.Errorf("[DEV ERROR] new request: %w", err))
|
|
return
|
|
}
|
|
userReq.Header.Set(codersdk.SessionTokenHeader, req.SessionToken)
|
|
|
|
// Exchange the token.
|
|
token, tokenStr, ok := api.AGPL.WorkspaceAppsProvider.Issue(ctx, rw, userReq, req)
|
|
if !ok {
|
|
return
|
|
}
|
|
if token == nil {
|
|
httpapi.InternalServerError(rw, xerrors.New("nil token after calling token provider"))
|
|
return
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusCreated, wsproxysdk.IssueSignedAppTokenResponse{
|
|
SignedTokenStr: tokenStr,
|
|
})
|
|
}
|
|
|
|
// workspaceProxyRegister is used to register a new workspace proxy. When a proxy
|
|
// comes online, it will announce itself to this endpoint. This updates its values
|
|
// in the database and returns a signed token that can be used to authenticate
|
|
// tokens.
|
|
//
|
|
// @Summary Register workspace proxy
|
|
// @ID register-workspace-proxy
|
|
// @Security CoderSessionToken
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Param request body wsproxysdk.RegisterWorkspaceProxyRequest true "Issue signed app token request"
|
|
// @Success 201 {object} wsproxysdk.RegisterWorkspaceProxyResponse
|
|
// @Router /workspaceproxies/me/register [post]
|
|
// @x-apidocgen {"skip": true}
|
|
func (api *API) workspaceProxyRegister(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
ctx = r.Context()
|
|
proxy = httpmw.WorkspaceProxy(r)
|
|
// TODO: This audit log does not work because it has no user id
|
|
// associated with it. The audit log commitAudit() function ignores
|
|
// the audit log if there is no user id. We should find a solution
|
|
// to make sure this event is tracked.
|
|
// auditor = api.AGPL.Auditor.Load()
|
|
// aReq, commitAudit = audit.InitRequest[database.WorkspaceProxy](rw, &audit.RequestParams{
|
|
// Audit: *auditor,
|
|
// Log: api.Logger,
|
|
// Request: r,
|
|
// Action: database.AuditActionWrite,
|
|
// })
|
|
)
|
|
// aReq.Old = proxy
|
|
// defer commitAudit()
|
|
|
|
var req wsproxysdk.RegisterWorkspaceProxyRequest
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
if err := validateProxyURL(req.AccessURL); err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "URL is invalid.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
if req.WildcardHostname != "" {
|
|
if _, err := httpapi.CompileHostnamePattern(req.WildcardHostname); err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Wildcard URL is invalid.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
_, err := api.Database.RegisterWorkspaceProxy(ctx, database.RegisterWorkspaceProxyParams{
|
|
ID: proxy.ID,
|
|
Url: req.AccessURL,
|
|
WildcardHostname: req.WildcardHostname,
|
|
})
|
|
if httpapi.Is404Error(err) {
|
|
httpapi.ResourceNotFound(rw)
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpapi.InternalServerError(rw, err)
|
|
return
|
|
}
|
|
|
|
// aReq.New = updatedProxy
|
|
httpapi.Write(ctx, rw, http.StatusCreated, wsproxysdk.RegisterWorkspaceProxyResponse{
|
|
AppSecurityKey: api.AppSecurityKey.String(),
|
|
})
|
|
|
|
go api.forceWorkspaceProxyHealthUpdate(api.ctx)
|
|
}
|
|
|
|
// workspaceProxyGoingAway is used to tell coderd that the workspace proxy is
|
|
// shutting down and going away. The main purpose of this function is for the
|
|
// health status of the workspace proxy to be more quickly updated when we know
|
|
// that the proxy is going to be unhealthy. This does not delete the workspace
|
|
// or cause any other side effects.
|
|
// If the workspace proxy comes back online, even without a register, it will
|
|
// be found healthy again by the normal checks.
|
|
// @Summary Workspace proxy going away
|
|
// @ID workspace-proxy-going-away
|
|
// @Security CoderSessionToken
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Success 201 {object} codersdk.Response
|
|
// @Router /workspaceproxies/me/goingaway [post]
|
|
// @x-apidocgen {"skip": true}
|
|
func (api *API) workspaceProxyGoingAway(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Force a health update to happen immediately. The proxy should
|
|
// not return a successful response if it is going away.
|
|
go api.forceWorkspaceProxyHealthUpdate(api.ctx)
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
|
|
Message: "OK",
|
|
})
|
|
}
|
|
|
|
// reconnectingPTYSignedToken issues a signed app token for use when connecting
|
|
// to the reconnecting PTY websocket on an external workspace proxy. This is set
|
|
// by the client as a query parameter when connecting.
|
|
//
|
|
// @Summary Issue signed app token for reconnecting PTY
|
|
// @ID issue-signed-app-token-for-reconnecting-pty
|
|
// @Security CoderSessionToken
|
|
// @Tags Applications Enterprise
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body codersdk.IssueReconnectingPTYSignedTokenRequest true "Issue reconnecting PTY signed token request"
|
|
// @Success 200 {object} codersdk.IssueReconnectingPTYSignedTokenResponse
|
|
// @Router /applications/reconnecting-pty-signed-token [post]
|
|
// @x-apidocgen {"skip": true}
|
|
func (api *API) reconnectingPTYSignedToken(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
apiKey := httpmw.APIKey(r)
|
|
if !api.Authorize(r, rbac.ActionCreate, apiKey) {
|
|
httpapi.ResourceNotFound(rw)
|
|
return
|
|
}
|
|
|
|
var req codersdk.IssueReconnectingPTYSignedTokenRequest
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
u, err := url.Parse(req.URL)
|
|
if err == nil && u.Scheme != "ws" && u.Scheme != "wss" {
|
|
err = xerrors.Errorf("invalid URL scheme %q, expected 'ws' or 'wss'", u.Scheme)
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Invalid URL.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Assert the URL is a valid reconnecting-pty URL.
|
|
expectedPath := fmt.Sprintf("/api/v2/workspaceagents/%s/pty", req.AgentID.String())
|
|
if u.Path != expectedPath {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Invalid URL path.",
|
|
Detail: "The provided URL is not a valid reconnecting PTY endpoint URL.",
|
|
})
|
|
return
|
|
}
|
|
|
|
scheme, err := api.AGPL.ValidWorkspaceAppHostname(ctx, u.Host, agpl.ValidWorkspaceAppHostnameOpts{
|
|
// Only allow the proxy access URL as a hostname since we don't need a
|
|
// ticket for the primary dashboard URL terminal.
|
|
AllowPrimaryAccessURL: false,
|
|
AllowPrimaryWildcard: false,
|
|
AllowProxyAccessURL: true,
|
|
AllowProxyWildcard: false,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to verify hostname in URL.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
if scheme == "" {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Invalid hostname in URL.",
|
|
Detail: "The hostname must be the primary wildcard app hostname, a workspace proxy access URL or a workspace proxy wildcard app hostname.",
|
|
})
|
|
return
|
|
}
|
|
|
|
_, tokenStr, ok := api.AGPL.WorkspaceAppsProvider.Issue(ctx, rw, r, workspaceapps.IssueTokenRequest{
|
|
AppRequest: workspaceapps.Request{
|
|
AccessMethod: workspaceapps.AccessMethodTerminal,
|
|
BasePath: u.Path,
|
|
AgentNameOrID: req.AgentID.String(),
|
|
},
|
|
SessionToken: httpmw.APITokenFromRequest(r),
|
|
// The following fields aren't required as long as the request is authed
|
|
// with a valid API key.
|
|
PathAppBaseURL: "",
|
|
AppHostname: "",
|
|
// The following fields are empty for terminal apps.
|
|
AppPath: "",
|
|
AppQuery: "",
|
|
})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.IssueReconnectingPTYSignedTokenResponse{
|
|
SignedToken: tokenStr,
|
|
})
|
|
}
|
|
|
|
func generateWorkspaceProxyToken(id uuid.UUID) (token string, hashed []byte, err error) {
|
|
secret, err := cryptorand.HexString(64)
|
|
if err != nil {
|
|
return "", nil, xerrors.Errorf("generate token: %w", err)
|
|
}
|
|
hashedSecret := sha256.Sum256([]byte(secret))
|
|
fullToken := fmt.Sprintf("%s:%s", id, secret)
|
|
return fullToken, hashedSecret[:], nil
|
|
}
|
|
|
|
func convertProxies(p []database.WorkspaceProxy, statuses map[uuid.UUID]proxyhealth.ProxyStatus) []codersdk.WorkspaceProxy {
|
|
resp := make([]codersdk.WorkspaceProxy, 0, len(p))
|
|
for _, proxy := range p {
|
|
resp = append(resp, convertProxy(proxy, statuses[proxy.ID]))
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func convertProxy(p database.WorkspaceProxy, status proxyhealth.ProxyStatus) codersdk.WorkspaceProxy {
|
|
if status.Status == "" {
|
|
status.Status = proxyhealth.Unknown
|
|
}
|
|
return codersdk.WorkspaceProxy{
|
|
ID: p.ID,
|
|
Name: p.Name,
|
|
DisplayName: p.DisplayName,
|
|
Icon: p.Icon,
|
|
URL: p.Url,
|
|
WildcardHostname: p.WildcardHostname,
|
|
CreatedAt: p.CreatedAt,
|
|
UpdatedAt: p.UpdatedAt,
|
|
Deleted: p.Deleted,
|
|
Status: codersdk.WorkspaceProxyStatus{
|
|
Status: codersdk.ProxyHealthStatus(status.Status),
|
|
Report: status.Report,
|
|
CheckedAt: status.CheckedAt,
|
|
},
|
|
}
|
|
}
|