Path: blob/main/components/gitpod-protocol/src/util/gitpod-host-url.ts
2500 views
/**1* Copyright (c) 2020 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56const URL = require("url").URL || window.URL;7import { log } from "./logging";89export interface UrlChange {10(old: URL): Partial<URL>;11}12export type UrlUpdate = UrlChange | Partial<URL>;1314const baseWorkspaceIDRegex =15"(([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}))";1617// this pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21)18const workspaceIDRegex = RegExp(`^(?:debug-)?${baseWorkspaceIDRegex}$`);1920// this pattern matches URL prefixes of workspaces21const workspaceUrlPrefixRegex = RegExp(`^(([0-9]{4,6}|debug)-)?${baseWorkspaceIDRegex}\\.`);2223export class GitpodHostUrl {24readonly url: URL;2526constructor(url: string) {27//HACK - we don't want clients to pass in a URL object, but we need to use it internally28const urlParam = url as any;29if (typeof urlParam === "string") {30// public constructor31this.url = new URL(url);32this.url.search = "";33this.url.hash = "";34this.url.pathname = "";35} else if (urlParam instanceof URL) {36// internal constructor, see with37this.url = urlParam;38} else {39log.error("Unexpected urlParam", { urlParam });40}41}4243withWorkspacePrefix(workspaceId: string, region: string) {44return this.withDomainPrefix(`${workspaceId}.ws-${region}.`);45}4647withDomainPrefix(prefix: string): GitpodHostUrl {48return this.with((url) => ({ host: prefix + url.host }));49}5051withoutWorkspacePrefix(): GitpodHostUrl {52if (!this.url.host.match(workspaceUrlPrefixRegex)) {53// URL has no workspace prefix54return this;55}5657return this.withoutDomainPrefix(2);58}5960withoutDomainPrefix(removeSegmentsCount: number): GitpodHostUrl {61return this.with((url) => ({ host: url.host.split(".").splice(removeSegmentsCount).join(".") }));62}6364with(urlUpdate: UrlUpdate) {65const update = typeof urlUpdate === "function" ? urlUpdate(this.url) : urlUpdate;66const addSlashToPath = update.pathname && update.pathname.length > 0 && !update.pathname.startsWith("/");67if (addSlashToPath) {68update.pathname = "/" + update.pathname;69}70const result = Object.assign(new URL(this.toString()), update);71// eslint-disable-next-line @typescript-eslint/no-unsafe-argument72return new GitpodHostUrl(result);73}7475withApi(urlUpdate?: UrlUpdate) {76const updated = urlUpdate ? this.with(urlUpdate) : this;77return updated.with((url) => ({ pathname: `/api${url.pathname}` }));78}7980withContext(81contextUrl: string,82startOptions?: { showOptions?: boolean; editor?: string; workspaceClass?: string },83) {84const searchParams = new URLSearchParams();85if (startOptions?.showOptions) {86searchParams.append("showOptions", "true");87}88return this.with((url) => ({ hash: contextUrl, search: searchParams.toString() }));89}9091asWebsocket(): GitpodHostUrl {92return this.with((url) => ({ protocol: url.protocol === "https:" ? "wss:" : "ws:" }));93}9495asWorkspacePage(): GitpodHostUrl {96return this.with((url) => ({ pathname: "/workspaces" }));97}9899asDashboard(): GitpodHostUrl {100return this.with((url) => ({ pathname: "/" }));101}102103asBilling(): GitpodHostUrl {104return this.with((url) => ({ pathname: "/user/billing" }));105}106107asLogin(): GitpodHostUrl {108return this.with((url) => ({ pathname: "/login" }));109}110111asAccessControl(): GitpodHostUrl {112return this.with((url) => ({ pathname: "/user/integrations" }));113}114115asSettings(): GitpodHostUrl {116return this.with((url) => ({ pathname: "/user/account" }));117}118119asPreferences(): GitpodHostUrl {120return this.with((url) => ({ pathname: "/user/preferences" }));121}122123asStart(workspaceId = this.workspaceId): GitpodHostUrl {124return this.with({125pathname: "/start/",126hash: "#" + workspaceId,127});128}129130asWorkspaceAuth(instanceID: string): GitpodHostUrl {131return this.with((url) => ({132pathname: `/api/auth/workspace-cookie/${instanceID}`,133}));134}135136toString() {137return this.url.toString();138}139140toStringWoRootSlash() {141let result = this.toString();142if (result.endsWith("/")) {143result = result.slice(0, result.length - 1);144}145return result;146}147148get debugWorkspace(): boolean {149return this.url.host.match(workspaceUrlPrefixRegex)?.[2] === "debug";150}151152get workspaceId(): string | undefined {153const hostSegs = this.url.host.split(".");154if (hostSegs.length > 1) {155const matchResults = hostSegs[0].match(workspaceIDRegex);156if (matchResults) {157// URL has a workspace prefix158// port prefixes are excluded159return matchResults[1];160}161}162163const pathSegs = this.url.pathname.split("/");164if (pathSegs.length > 3 && pathSegs[1] === "workspace") {165return pathSegs[2];166}167168const cleanHash = this.url.hash.replace(/^#/, "");169if (this.url.pathname == "/start/" && cleanHash.match(workspaceIDRegex)) {170return cleanHash;171}172173return undefined;174}175176get blobServe(): boolean {177const hostSegments = this.url.host.split(".");178if (hostSegments[0] === "blobserve") {179return true;180}181182const pathSegments = this.url.pathname.split("/");183return pathSegments[0] === "blobserve";184}185186asSorry(message: string) {187return this.with({ pathname: "/sorry", hash: message });188}189190asApiLogout(): GitpodHostUrl {191return this.withApi((url) => ({ pathname: "/logout/" }));192}193194asIDEProxy(): GitpodHostUrl {195const hostSegments = this.url.host.split(".");196if (hostSegments[0] === "ide") {197return this;198}199return this.with((url) => ({ host: "ide." + url.host }));200}201202asPublicServices(): GitpodHostUrl {203const hostSegments = this.url.host.split(".");204if (hostSegments[0] === "services") {205return this;206}207return this.with((url) => ({ host: "services." + url.host }));208}209210asIDEMetrics(): GitpodHostUrl {211let newUrl: GitpodHostUrl = this;212const hostSegments = this.url.host.split(".");213if (hostSegments[0] !== "ide") {214newUrl = newUrl.asIDEProxy();215}216return newUrl.with((url) => ({ pathname: "/metrics-api" }));217}218219asLoginWithOTS(userId: string, key: string, returnToUrl?: string) {220const result = this.withApi({ pathname: `/login/ots/${userId}/${key}` });221if (returnToUrl) {222return result.with({ search: `returnTo=${encodeURIComponent(returnToUrl)}` });223}224return result;225}226}227228229