diff --git a/coderd/workspaceapps/db_test.go b/coderd/workspaceapps/db_test.go index eccc96d0080b4..e8c7464f88ff1 100644 --- a/coderd/workspaceapps/db_test.go +++ b/coderd/workspaceapps/db_test.go @@ -40,6 +40,7 @@ func Test_ResolveRequest(t *testing.T) { // Users can access unhealthy and initializing apps (as of 2024-02). appNameUnhealthy = "app-unhealthy" appNameInitializing = "app-initializing" + appNameEndsInS = "app-ends-in-s" // This agent will never connect, so it will never become "connected". // Users cannot access unhealthy agents. @@ -166,6 +167,12 @@ func Test_ResolveRequest(t *testing.T) { Threshold: 1000, }, }, + { + Slug: appNameEndsInS, + DisplayName: appNameEndsInS, + SharingLevel: proto.AppSharingLevel_OWNER, + Url: appURL, + }, }, }, { @@ -644,6 +651,67 @@ func Test_ResolveRequest(t *testing.T) { require.Equal(t, "http://127.0.0.1:9090", token.AppURL) }) + t.Run("PortSubdomainHTTPSS", func(t *testing.T) { + t.Parallel() + + req := (workspaceapps.Request{ + AccessMethod: workspaceapps.AccessMethodSubdomain, + BasePath: "/", + UsernameOrID: me.Username, + WorkspaceNameOrID: workspace.Name, + AgentNameOrID: agentName, + AppSlugOrPort: "9090ss", + }).Normalize() + + rw := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken()) + + _, ok := workspaceapps.ResolveRequest(rw, r, workspaceapps.ResolveRequestOptions{ + Logger: api.Logger, + SignedTokenProvider: api.WorkspaceAppsProvider, + DashboardURL: api.AccessURL, + PathAppBaseURL: api.AccessURL, + AppHostname: api.AppHostname, + AppRequest: req, + }) + // should parse as app and fail to find app "9090ss" + require.False(t, ok) + w := rw.Result() + _ = w.Body.Close() + b, err := io.ReadAll(w.Body) + require.NoError(t, err) + require.Contains(t, string(b), "404 - Application Not Found") + }) + + t.Run("SubdomainEndsInS", func(t *testing.T) { + t.Parallel() + + req := (workspaceapps.Request{ + AccessMethod: workspaceapps.AccessMethodSubdomain, + BasePath: "/", + UsernameOrID: me.Username, + WorkspaceNameOrID: workspace.Name, + AgentNameOrID: agentName, + AppSlugOrPort: appNameEndsInS, + }).Normalize() + + rw := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken()) + + token, ok := workspaceapps.ResolveRequest(rw, r, workspaceapps.ResolveRequestOptions{ + Logger: api.Logger, + SignedTokenProvider: api.WorkspaceAppsProvider, + DashboardURL: api.AccessURL, + PathAppBaseURL: api.AccessURL, + AppHostname: api.AppHostname, + AppRequest: req, + }) + require.True(t, ok) + require.Equal(t, req.AppSlugOrPort, token.AppSlugOrPort) + }) + t.Run("Terminal", func(t *testing.T) { t.Parallel() diff --git a/coderd/workspaceapps/request.go b/coderd/workspaceapps/request.go index 0f3eddf6cbd9a..d0fba4256cf03 100644 --- a/coderd/workspaceapps/request.go +++ b/coderd/workspaceapps/request.go @@ -287,12 +287,20 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR // whether the app is a slug or a port and whether there are multiple agents // in the workspace or not. var ( - agentNameOrID = r.AgentNameOrID - appURL string - appSharingLevel database.AppSharingLevel - portUint, portUintErr = strconv.ParseUint(r.AppSlugOrPort, 10, 16) + agentNameOrID = r.AgentNameOrID + appURL string + appSharingLevel database.AppSharingLevel + // First check if it's a port-based URL with an optional "s" suffix for HTTPS. + potentialPortStr = strings.TrimSuffix(r.AppSlugOrPort, "s") + portUint, portUintErr = strconv.ParseUint(potentialPortStr, 10, 16) ) + //nolint:nestif if portUintErr == nil { + protocol := "http" + if strings.HasSuffix(r.AppSlugOrPort, "s") { + protocol = "https" + } + if r.AccessMethod != AccessMethodSubdomain { // TODO(@deansheather): this should return a 400 instead of a 500. return nil, xerrors.New("port-based URLs are only supported for subdomain-based applications") @@ -309,10 +317,10 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR } // If the app slug is a port number, then route to the port as an - // "anonymous app". We only support HTTP for port-based URLs. + // "anonymous app". // // This is only supported for subdomain-based applications. - appURL = fmt.Sprintf("http://127.0.0.1:%d", portUint) + appURL = fmt.Sprintf("%s://127.0.0.1:%d", protocol, portUint) appSharingLevel = database.AppSharingLevelOwner // Port sharing authorization diff --git a/coderd/workspaceapps/request_test.go b/coderd/workspaceapps/request_test.go index 7240937a06d9f..b6e4bb7a2e65f 100644 --- a/coderd/workspaceapps/request_test.go +++ b/coderd/workspaceapps/request_test.go @@ -57,6 +57,26 @@ func Test_RequestValidate(t *testing.T) { AppSlugOrPort: "baz", }, }, + { + name: "OK5", + req: workspaceapps.Request{ + AccessMethod: workspaceapps.AccessMethodSubdomain, + BasePath: "/", + UsernameOrID: "foo", + WorkspaceNameOrID: "bar", + AppSlugOrPort: "8080", + }, + }, + { + name: "OK6", + req: workspaceapps.Request{ + AccessMethod: workspaceapps.AccessMethodSubdomain, + BasePath: "/", + UsernameOrID: "foo", + WorkspaceNameOrID: "bar", + AppSlugOrPort: "8080s", + }, + }, { name: "NoAccessMethod", req: workspaceapps.Request{ diff --git a/coderd/workspaceapps/token_test.go b/coderd/workspaceapps/token_test.go index 06ab8a2acd4b2..c656ae2ab77b8 100644 --- a/coderd/workspaceapps/token_test.go +++ b/coderd/workspaceapps/token_test.go @@ -222,6 +222,54 @@ func Test_TokenMatchesRequest(t *testing.T) { }, want: false, }, + { + name: "PortPortocolHTTP", + req: workspaceapps.Request{ + AccessMethod: workspaceapps.AccessMethodSubdomain, + Prefix: "yolo--", + BasePath: "/", + UsernameOrID: "foo", + WorkspaceNameOrID: "bar", + AgentNameOrID: "baz", + AppSlugOrPort: "8080", + }, + token: workspaceapps.SignedToken{ + Request: workspaceapps.Request{ + AccessMethod: workspaceapps.AccessMethodSubdomain, + Prefix: "yolo--", + BasePath: "/", + UsernameOrID: "foo", + WorkspaceNameOrID: "bar", + AgentNameOrID: "baz", + AppSlugOrPort: "8080", + }, + }, + want: true, + }, + { + name: "PortPortocolHTTPS", + req: workspaceapps.Request{ + AccessMethod: workspaceapps.AccessMethodSubdomain, + Prefix: "yolo--", + BasePath: "/", + UsernameOrID: "foo", + WorkspaceNameOrID: "bar", + AgentNameOrID: "baz", + AppSlugOrPort: "8080s", + }, + token: workspaceapps.SignedToken{ + Request: workspaceapps.Request{ + AccessMethod: workspaceapps.AccessMethodSubdomain, + Prefix: "yolo--", + BasePath: "/", + UsernameOrID: "foo", + WorkspaceNameOrID: "bar", + AgentNameOrID: "baz", + AppSlugOrPort: "8080s", + }, + }, + want: true, + }, } for _, c := range cases {