diff --git a/components/gitpod-protocol/src/util/generate-workspace-id.spec.ts b/components/gitpod-protocol/src/util/generate-workspace-id.spec.ts index 679cbd06b95cfc..e83e4b4d282898 100644 --- a/components/gitpod-protocol/src/util/generate-workspace-id.spec.ts +++ b/components/gitpod-protocol/src/util/generate-workspace-id.spec.ts @@ -27,5 +27,21 @@ const expect = chai.expect expect(longestName.length <= 36, `"${longestName}" is longer than 36 chars (${longestName.length})`).to.be.true; } + @test public async testCustomName() { + const data = [ + ['foo','bar','foo-bar-'], + ['f','bar','.{2,16}-bar-'], + ['gitpod-io','gitpod','gitpodio-gitpod-'], + ['this is rather long and has some "ยง$"% special chars','also here pretty long and needs abbreviation','thisisratherlon-alsohere-'], + ['breatheco-de', 'python-flask-api-tutorial', 'breathecode-pythonflaska-'], + ] + for (const d of data) { + const id = await generateWorkspaceID(d[0], d[1]); + expect(id).match(new RegExp("^"+d[2])); + expect(new GitpodHostUrl().withWorkspacePrefix(id, "eu").workspaceId).to.equal(id); + expect(id.length <= 36, `"${id}" is longer than 36 chars (${id.length})`).to.be.true; + } + } + } module.exports = new TestGenerateWorkspaceId() diff --git a/components/gitpod-protocol/src/util/generate-workspace-id.ts b/components/gitpod-protocol/src/util/generate-workspace-id.ts index a9e8e6f7e0fbb2..7164ce9002b0ae 100644 --- a/components/gitpod-protocol/src/util/generate-workspace-id.ts +++ b/components/gitpod-protocol/src/util/generate-workspace-id.ts @@ -5,8 +5,25 @@ */ import randomNumber = require("random-number-csprng"); -export async function generateWorkspaceID(): Promise { - return (await random(colors))+'-'+(await random(animals))+'-'+(await random(characters, 8)); +export async function generateWorkspaceID(firstSegment?: string, secondSegment?: string): Promise { + const firstSeg = clean(firstSegment) || await random(colors); + const secSeg = clean(secondSegment, Math.min(15, 23 - firstSeg.length)) || await random(animals); + return firstSeg+'-'+secSeg+'-'+(await random(characters, 11)); +} + +function clean(segment: string | undefined, maxChars: number = 15) { + if (!segment) { + return undefined; + } + let result = ''; + for (let i =0; i < segment.length; i++) { + if (characters.indexOf(segment[i]) !== -1) { + result += segment[i]; + } + } + if (result.length >= 2) { + return result.substring(0, maxChars); + } } async function random(array: string[], length: number = 1): Promise { diff --git a/components/gitpod-protocol/src/util/gitpod-host-url.ts b/components/gitpod-protocol/src/util/gitpod-host-url.ts index 69920ac7b9a66f..4ea49bf00ec5c2 100644 --- a/components/gitpod-protocol/src/util/gitpod-host-url.ts +++ b/components/gitpod-protocol/src/util/gitpod-host-url.ts @@ -12,13 +12,13 @@ export interface UrlChange { } export type UrlUpdate = UrlChange | Partial; -const basewoWkspaceIDRegex = "(([a-f][0-9a-f]{7}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|([0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8}))"; +const baseWorkspaceIDRegex = "(([a-f][0-9a-f]{7}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|([0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8,11}))"; // this pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21) -const workspaceIDRegex = RegExp(`^${basewoWkspaceIDRegex}$`); +const workspaceIDRegex = RegExp(`^${baseWorkspaceIDRegex}$`); // this pattern matches URL prefixes of workspaces -const workspaceUrlPrefixRegex = RegExp(`^([0-9]{4,6}-)?${basewoWkspaceIDRegex}\\.`); +const workspaceUrlPrefixRegex = RegExp(`^([0-9]{4,6}-)?${baseWorkspaceIDRegex}\\.`); export class GitpodHostUrl { readonly url: URL; diff --git a/components/gitpod-protocol/src/util/parse-workspace-id.ts b/components/gitpod-protocol/src/util/parse-workspace-id.ts index 68459465115c75..99e63beeaf1c0b 100644 --- a/components/gitpod-protocol/src/util/parse-workspace-id.ts +++ b/components/gitpod-protocol/src/util/parse-workspace-id.ts @@ -4,7 +4,7 @@ * See License-AGPL.txt in the project root for license information. */ -const REGEX_WORKSPACE_ID = /[0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8}/; +const REGEX_WORKSPACE_ID = /[0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8,11}/; const REGEX_WORKSPACE_ID_EXACT = new RegExp(`^${REGEX_WORKSPACE_ID.source}$`); // We need to parse the workspace id precisely here to get the case '--.ws.' right const REGEX_WORKSPACE_ID_FROM_HOSTNAME = new RegExp(`(${REGEX_WORKSPACE_ID.source})\.ws`); diff --git a/components/local-app/main.go b/components/local-app/main.go index e439f9ab861f49..4a6251453d09be 100644 --- a/components/local-app/main.go +++ b/components/local-app/main.go @@ -162,7 +162,7 @@ func run(origin, sshConfig string, apiPort int, allowCORSFromPort bool, autoTunn return err } wsHostRegex := "(\\.[^.]+)\\." + strings.ReplaceAll(originURL.Host, ".", "\\.") - wsHostRegex = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8})" + wsHostRegex + wsHostRegex = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8,11})" + wsHostRegex if allowCORSFromPort { wsHostRegex = "([0-9]+)-" + wsHostRegex } diff --git a/components/server/ee/src/workspace/workspace-factory.ts b/components/server/ee/src/workspace/workspace-factory.ts index 451680d52ab632..b888106e1566f8 100644 --- a/components/server/ee/src/workspace/workspace-factory.ts +++ b/components/server/ee/src/workspace/workspace-factory.ts @@ -14,7 +14,6 @@ import { LicenseEvaluator } from '@gitpod/licensor/lib'; import { Feature } from '@gitpod/licensor/lib/api'; import { ResponseError } from 'vscode-jsonrpc'; import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error'; -import { generateWorkspaceID } from '@gitpod/gitpod-protocol/lib/util/generate-workspace-id'; import { HostContextProvider } from '../../../src/auth/host-context-provider'; import { RepoURL } from '../../../src/repohost'; @@ -220,7 +219,7 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { } } - const id = await generateWorkspaceID(); + const id = await this.generateWorkspaceID(context); const newWs: Workspace = { id, type: "regular", diff --git a/components/server/src/workspace/workspace-factory.ts b/components/server/src/workspace/workspace-factory.ts index 07a13a267e9324..fe9127fbce9903 100644 --- a/components/server/src/workspace/workspace-factory.ts +++ b/components/server/src/workspace/workspace-factory.ts @@ -5,13 +5,14 @@ */ import { DBWithTracing, TracedWorkspaceDB, WorkspaceDB, ProjectDB, TeamDB } from '@gitpod/gitpod-db/lib'; -import { AdditionalContentContext, CommitContext, IssueContext, PullRequestContext, Repository, SnapshotContext, User, Workspace, WorkspaceConfig, WorkspaceContext, WorkspaceProbeContext } from '@gitpod/gitpod-protocol'; +import { AdditionalContentContext, CommitContext, IssueContext, PrebuiltWorkspaceContext, PullRequestContext, Repository, SnapshotContext, User, Workspace, WorkspaceConfig, WorkspaceContext, WorkspaceProbeContext } from '@gitpod/gitpod-protocol'; import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error'; import { generateWorkspaceID } from '@gitpod/gitpod-protocol/lib/util/generate-workspace-id'; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; import { inject, injectable } from 'inversify'; import { ResponseError } from 'vscode-jsonrpc'; +import { RepoURL } from '../repohost'; import { ConfigProvider } from './config-provider'; import { ImageSourceProvider } from './image-source-provider'; @@ -55,7 +56,7 @@ export class WorkspaceFactory { // Basically we're using the raw alpine image bait-and-switch style without adding the GP layer. const imageSource = await this.imageSourceProvider.getImageSource(ctx, user, null as any, config); - const id = await generateWorkspaceID(); + const id = await this.generateWorkspaceID(context); const date = new Date().toISOString(); const newWs: Workspace = { id, @@ -94,7 +95,7 @@ export class WorkspaceFactory { throw new Error(`The original workspace has been deleted - cannot open this snapshot.`); } - const id = await generateWorkspaceID(); + const id = await this.generateWorkspaceID(context); const date = new Date().toISOString(); const newWs = { id, @@ -169,7 +170,7 @@ export class WorkspaceFactory { } } - const id = await generateWorkspaceID(); + const id = await this.generateWorkspaceID(context); const newWs: Workspace = { id, type: "regular", @@ -210,4 +211,16 @@ export class WorkspaceFactory { return context.title; } + protected async generateWorkspaceID(context: WorkspaceContext): Promise { + let ctx = context; + if (PrebuiltWorkspaceContext.is(context)) { + ctx = context.originalContext; + } + if (CommitContext.is(ctx)) { + const parsed = RepoURL.parseRepoUrl(ctx.repository.cloneUrl); + return await generateWorkspaceID(parsed?.owner, parsed?.repo); + } + return await generateWorkspaceID(); + } + } \ No newline at end of file diff --git a/components/ws-proxy/pkg/proxy/workspacerouter.go b/components/ws-proxy/pkg/proxy/workspacerouter.go index 4d29f4785f309b..21abdc0a37f838 100644 --- a/components/ws-proxy/pkg/proxy/workspacerouter.go +++ b/components/ws-proxy/pkg/proxy/workspacerouter.go @@ -31,7 +31,7 @@ const ( forwardedHostnameHeader = "x-wsproxy-host" // This pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21). - workspaceIDRegex = "(?P<" + workspaceIDIdentifier + ">[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8})" + workspaceIDRegex = "(?P<" + workspaceIDIdentifier + ">[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8,11})" workspacePortRegex = "(?P<" + workspacePortIdentifier + ">[0-9]+)-" )