Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-protocol/src/util/gitpod-host-url.ts
2500 views
1
/**
2
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
const URL = require("url").URL || window.URL;
8
import { log } from "./logging";
9
10
export interface UrlChange {
11
(old: URL): Partial<URL>;
12
}
13
export type UrlUpdate = UrlChange | Partial<URL>;
14
15
const baseWorkspaceIDRegex =
16
"(([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}))";
17
18
// this pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21)
19
const workspaceIDRegex = RegExp(`^(?:debug-)?${baseWorkspaceIDRegex}$`);
20
21
// this pattern matches URL prefixes of workspaces
22
const workspaceUrlPrefixRegex = RegExp(`^(([0-9]{4,6}|debug)-)?${baseWorkspaceIDRegex}\\.`);
23
24
export class GitpodHostUrl {
25
readonly url: URL;
26
27
constructor(url: string) {
28
//HACK - we don't want clients to pass in a URL object, but we need to use it internally
29
const urlParam = url as any;
30
if (typeof urlParam === "string") {
31
// public constructor
32
this.url = new URL(url);
33
this.url.search = "";
34
this.url.hash = "";
35
this.url.pathname = "";
36
} else if (urlParam instanceof URL) {
37
// internal constructor, see with
38
this.url = urlParam;
39
} else {
40
log.error("Unexpected urlParam", { urlParam });
41
}
42
}
43
44
withWorkspacePrefix(workspaceId: string, region: string) {
45
return this.withDomainPrefix(`${workspaceId}.ws-${region}.`);
46
}
47
48
withDomainPrefix(prefix: string): GitpodHostUrl {
49
return this.with((url) => ({ host: prefix + url.host }));
50
}
51
52
withoutWorkspacePrefix(): GitpodHostUrl {
53
if (!this.url.host.match(workspaceUrlPrefixRegex)) {
54
// URL has no workspace prefix
55
return this;
56
}
57
58
return this.withoutDomainPrefix(2);
59
}
60
61
withoutDomainPrefix(removeSegmentsCount: number): GitpodHostUrl {
62
return this.with((url) => ({ host: url.host.split(".").splice(removeSegmentsCount).join(".") }));
63
}
64
65
with(urlUpdate: UrlUpdate) {
66
const update = typeof urlUpdate === "function" ? urlUpdate(this.url) : urlUpdate;
67
const addSlashToPath = update.pathname && update.pathname.length > 0 && !update.pathname.startsWith("/");
68
if (addSlashToPath) {
69
update.pathname = "/" + update.pathname;
70
}
71
const result = Object.assign(new URL(this.toString()), update);
72
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
73
return new GitpodHostUrl(result);
74
}
75
76
withApi(urlUpdate?: UrlUpdate) {
77
const updated = urlUpdate ? this.with(urlUpdate) : this;
78
return updated.with((url) => ({ pathname: `/api${url.pathname}` }));
79
}
80
81
withContext(
82
contextUrl: string,
83
startOptions?: { showOptions?: boolean; editor?: string; workspaceClass?: string },
84
) {
85
const searchParams = new URLSearchParams();
86
if (startOptions?.showOptions) {
87
searchParams.append("showOptions", "true");
88
}
89
return this.with((url) => ({ hash: contextUrl, search: searchParams.toString() }));
90
}
91
92
asWebsocket(): GitpodHostUrl {
93
return this.with((url) => ({ protocol: url.protocol === "https:" ? "wss:" : "ws:" }));
94
}
95
96
asWorkspacePage(): GitpodHostUrl {
97
return this.with((url) => ({ pathname: "/workspaces" }));
98
}
99
100
asDashboard(): GitpodHostUrl {
101
return this.with((url) => ({ pathname: "/" }));
102
}
103
104
asBilling(): GitpodHostUrl {
105
return this.with((url) => ({ pathname: "/user/billing" }));
106
}
107
108
asLogin(): GitpodHostUrl {
109
return this.with((url) => ({ pathname: "/login" }));
110
}
111
112
asAccessControl(): GitpodHostUrl {
113
return this.with((url) => ({ pathname: "/user/integrations" }));
114
}
115
116
asSettings(): GitpodHostUrl {
117
return this.with((url) => ({ pathname: "/user/account" }));
118
}
119
120
asPreferences(): GitpodHostUrl {
121
return this.with((url) => ({ pathname: "/user/preferences" }));
122
}
123
124
asStart(workspaceId = this.workspaceId): GitpodHostUrl {
125
return this.with({
126
pathname: "/start/",
127
hash: "#" + workspaceId,
128
});
129
}
130
131
asWorkspaceAuth(instanceID: string): GitpodHostUrl {
132
return this.with((url) => ({
133
pathname: `/api/auth/workspace-cookie/${instanceID}`,
134
}));
135
}
136
137
toString() {
138
return this.url.toString();
139
}
140
141
toStringWoRootSlash() {
142
let result = this.toString();
143
if (result.endsWith("/")) {
144
result = result.slice(0, result.length - 1);
145
}
146
return result;
147
}
148
149
get debugWorkspace(): boolean {
150
return this.url.host.match(workspaceUrlPrefixRegex)?.[2] === "debug";
151
}
152
153
get workspaceId(): string | undefined {
154
const hostSegs = this.url.host.split(".");
155
if (hostSegs.length > 1) {
156
const matchResults = hostSegs[0].match(workspaceIDRegex);
157
if (matchResults) {
158
// URL has a workspace prefix
159
// port prefixes are excluded
160
return matchResults[1];
161
}
162
}
163
164
const pathSegs = this.url.pathname.split("/");
165
if (pathSegs.length > 3 && pathSegs[1] === "workspace") {
166
return pathSegs[2];
167
}
168
169
const cleanHash = this.url.hash.replace(/^#/, "");
170
if (this.url.pathname == "/start/" && cleanHash.match(workspaceIDRegex)) {
171
return cleanHash;
172
}
173
174
return undefined;
175
}
176
177
get blobServe(): boolean {
178
const hostSegments = this.url.host.split(".");
179
if (hostSegments[0] === "blobserve") {
180
return true;
181
}
182
183
const pathSegments = this.url.pathname.split("/");
184
return pathSegments[0] === "blobserve";
185
}
186
187
asSorry(message: string) {
188
return this.with({ pathname: "/sorry", hash: message });
189
}
190
191
asApiLogout(): GitpodHostUrl {
192
return this.withApi((url) => ({ pathname: "/logout/" }));
193
}
194
195
asIDEProxy(): GitpodHostUrl {
196
const hostSegments = this.url.host.split(".");
197
if (hostSegments[0] === "ide") {
198
return this;
199
}
200
return this.with((url) => ({ host: "ide." + url.host }));
201
}
202
203
asPublicServices(): GitpodHostUrl {
204
const hostSegments = this.url.host.split(".");
205
if (hostSegments[0] === "services") {
206
return this;
207
}
208
return this.with((url) => ({ host: "services." + url.host }));
209
}
210
211
asIDEMetrics(): GitpodHostUrl {
212
let newUrl: GitpodHostUrl = this;
213
const hostSegments = this.url.host.split(".");
214
if (hostSegments[0] !== "ide") {
215
newUrl = newUrl.asIDEProxy();
216
}
217
return newUrl.with((url) => ({ pathname: "/metrics-api" }));
218
}
219
220
asLoginWithOTS(userId: string, key: string, returnToUrl?: string) {
221
const result = this.withApi({ pathname: `/login/ots/${userId}/${key}` });
222
if (returnToUrl) {
223
return result.with({ search: `returnTo=${encodeURIComponent(returnToUrl)}` });
224
}
225
return result;
226
}
227
}
228
229