Skip to content

Commit 6b00721

Browse files
committed
[dashboard, ws-proxy, supervisor] Break potential DDOS cycle by disabling autostart
When triggered: a) inFrame or b) when redirect from IDE url (by ws-proxy)
1 parent 2da01c7 commit 6b00721

File tree

7 files changed

+101
-21
lines changed

7 files changed

+101
-21
lines changed

components/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"js-cookie": "^3.0.1",
1010
"moment": "^2.29.1",
1111
"monaco-editor": "^0.25.2",
12+
"query-string": "^7.1.1",
1213
"react": "^17.0.1",
1314
"react-dom": "^17.0.1",
1415
"react-router-dom": "^5.2.0",

components/dashboard/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { settingsPathAccount, settingsPathIntegrations, settingsPathMain, settin
2727
import { projectsPathInstallGitHubApp, projectsPathMain, projectsPathMainWithParams, projectsPathNew } from './projects/projects.routes';
2828
import { refreshSearchData } from './components/RepositoryFinder';
2929
import { StartWorkspaceModal } from './workspaces/StartWorkspaceModal';
30+
import { StartWorkspaceParameters } from './start/StartWorkspace';
3031

3132
const Setup = React.lazy(() => import(/* webpackPrefetch: true */ './Setup'));
3233
const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ './workspaces/Workspaces'));
@@ -412,7 +413,8 @@ function App() {
412413
} else if (isCreation) {
413414
toRender = <CreateWorkspace contextUrl={hash} />;
414415
} else if (isWsStart) {
415-
toRender = <StartWorkspace workspaceId={hash} />;
416+
const params = StartWorkspaceParameters.parse(window.location.search);
417+
toRender = <StartWorkspace workspaceId={hash} parameters={params} />;
416418
} else if (/^(github|gitlab)\.com\/.+?/i.test(window.location.pathname)) {
417419
let url = new URL(window.location.href)
418420
url.hash = url.pathname

components/dashboard/src/start/StartWorkspace.tsx

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ContextURL, DisposableCollection, GitpodServer, RateLimiterError, Start
88
import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
99
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
1010
import EventEmitter from "events";
11+
import * as queryString from "query-string";
1112
import React, { Suspense, useEffect } from "react";
1213
import { v4 } from 'uuid';
1314
import Arrow from "../components/Arrow";
@@ -21,10 +22,33 @@ const sessionId = v4();
2122
const WorkspaceLogs = React.lazy(() => import('../components/WorkspaceLogs'));
2223

2324
export interface StartWorkspaceProps {
24-
workspaceId: string;
25+
workspaceId: string,
26+
parameters?: StartWorkspaceParameters,
27+
}
28+
29+
export interface StartWorkspaceParameters {
30+
/**
31+
* The last start of this workspace experienced in error: ws-proxy did not find it within the workspace cluster.
32+
*/
33+
not_found?: boolean,
34+
}
35+
36+
export namespace StartWorkspaceParameters {
37+
export function parse(search: string): StartWorkspaceParameters | undefined {
38+
try {
39+
return queryString.parse(search, {parseBooleans: true});
40+
} catch (err) {
41+
console.error("/start: error parsing search params", err);
42+
return undefined;
43+
}
44+
}
2545
}
2646

2747
export interface StartWorkspaceState {
48+
/**
49+
* This is set to the istanceId we started (think we started on).
50+
* We only receive updates for this particular instance, or none if not set.
51+
*/
2852
startedInstanceId?: string;
2953
workspaceInstance?: WorkspaceInstance;
3054
workspace?: Workspace;
@@ -34,8 +58,12 @@ export interface StartWorkspaceState {
3458
link: string
3559
label: string
3660
clientID?: string
37-
}
38-
ideOptions?: IDEOptions
61+
};
62+
ideOptions?: IDEOptions;
63+
/**
64+
* This flag is used to break the autostart-cycle explained in https://github.com/gitpod-io/gitpod/issues/8043
65+
*/
66+
dontAutostart?: boolean;
3967
}
4068

4169
export default class StartWorkspace extends React.Component<StartWorkspaceProps, StartWorkspaceState> {
@@ -75,8 +103,35 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
75103
this.setState({ error });
76104
}
77105

106+
// we're coming back from a workspace cluster/IDE URL where we expected a workspace to be running but it wasn't because either:
107+
// - this is a (very) old tab and the workspace already timed out
108+
// - due to a start error our workspace terminated very quickly between:
109+
// a) us being redirected to that IDEUrl (based on the first ws-manager update) and
110+
// b) our requests being validated by ws-proxy
111+
// we break this potentially DDOS cycle by showing a modal "Restart workspace?" instead of auto-restarting said workspace.
112+
const params = this.props.parameters;
113+
if (!!params?.not_found) {
114+
this.setState({ dontAutostart: true });
115+
this.fetchWorkspaceInfo(undefined);
116+
return;
117+
}
118+
119+
// IDE case:
120+
// - we assume the workspace has already been started for us
121+
// - we don't know it's instanceId
122+
// we could also rely on "startWorkspace" to return us the "running" instance, but that opens the door for
123+
// self-DDOSing as we found in the past (cmp. https://github.com/gitpod-io/gitpod/issues/8043)
124+
if (this.runsInIFrame()) {
125+
// fetch current state display (incl. potential errors)
126+
this.setState({ dontAutostart: true });
127+
this.fetchWorkspaceInfo(undefined);
128+
return;
129+
}
130+
131+
// dashboard case (no previous errors): start workspace as quickly as possible
78132
this.startWorkspace();
79-
getGitpodService().server.getIDEOptions().then(ideOptions => this.setState({ ideOptions }))
133+
// query IDE options so we can show them if necessary once the workspace is running
134+
getGitpodService().server.getIDEOptions().then(ideOptions => this.setState({ ideOptions }));
80135
}
81136

82137
componentWillUnmount() {
@@ -134,10 +189,9 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
134189
this.redirectTo(result.workspaceURL);
135190
return;
136191
}
137-
this.setState({ startedInstanceId: result.instanceID });
138-
// Explicitly query state to guarantee we get at least one update
192+
// Start listening too instance updates - and explicitly query state once to guarantee we get at least one update
139193
// (needed for already started workspaces, and not hanging in 'Starting ...' for too long)
140-
this.fetchWorkspaceInfo();
194+
this.fetchWorkspaceInfo(result.instanceID);
141195
} catch (error) {
142196
console.error(error);
143197
if (typeof error === 'string') {
@@ -180,15 +234,28 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
180234
}
181235
}
182236

183-
async fetchWorkspaceInfo() {
237+
/**
238+
* Fetches initial WorkspaceInfo from the server. If there is a WorkspaceInstance for workspaceId, we feed it
239+
* into "onInstanceUpdate" and start accepting further updates.
240+
*
241+
* @param startedInstanceId The instanceId we want to listen on
242+
*/
243+
async fetchWorkspaceInfo(startedInstanceId: string | undefined) {
244+
// this ensures we're receiving updates for this instance
245+
if (startedInstanceId) {
246+
this.setState({ startedInstanceId });
247+
}
248+
184249
const { workspaceId } = this.props;
185250
try {
186251
const info = await getGitpodService().server.getWorkspace(workspaceId);
187252
if (info.latestInstance) {
188-
this.setState({
189-
workspace: info.workspace
190-
});
191-
this.onInstanceUpdate(info.latestInstance);
253+
const instance = info.latestInstance;
254+
this.setState((s) => ({
255+
workspace: info.workspace,
256+
startedInstanceId: s.startedInstanceId || instance.id, // note: here's a potential mismatch between startedInstanceId and instance.id. TODO(gpl) How to handle this?
257+
}));
258+
this.onInstanceUpdate(instance);
192259
}
193260
} catch (error) {
194261
console.error(error);
@@ -197,7 +264,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
197264
}
198265

199266
notifyDidOpenConnection() {
200-
this.fetchWorkspaceInfo();
267+
this.fetchWorkspaceInfo(undefined);
201268
}
202269

203270
async onInstanceUpdate(workspaceInstance: WorkspaceInstance) {
@@ -210,7 +277,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
210277

211278
// Redirect to workspaceURL if we are not yet running in an iframe.
212279
// It happens this late if we were waiting for a docker build.
213-
if (!this.runsInIFrame() && workspaceInstance.ideUrl) {
280+
if (!this.runsInIFrame() && workspaceInstance.ideUrl && !this.state?.dontAutostart) {
214281
this.redirectTo(workspaceInstance.ideUrl);
215282
return;
216283
}

components/supervisor/frontend/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ const toStop = new DisposableCollection();
153153
// reload the page if the workspace was restarted to ensure:
154154
// - graceful reconnection of IDEs
155155
// - new owner token is set
156-
window.location.href = startUrl.toString();
156+
window.location.href = startUrl.with({ search: "error=true" }).toString();
157157
}
158158
}
159159
return loading.frame;

components/ws-proxy/pkg/proxy/routes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ func workspaceMustExistHandler(config *Config, infoProvider WorkspaceInfoProvide
550550
info := infoProvider.WorkspaceInfo(coords.ID)
551551
if info == nil {
552552
log.WithFields(log.OWI("", coords.ID, "")).Info("no workspace info found - redirecting to start")
553-
redirectURL := fmt.Sprintf("%s://%s/start/#%s", config.GitpodInstallation.Scheme, config.GitpodInstallation.HostName, coords.ID)
553+
redirectURL := fmt.Sprintf("%s://%s/start/?not_found=true#%s", config.GitpodInstallation.Scheme, config.GitpodInstallation.HostName, coords.ID)
554554
http.Redirect(resp, req, redirectURL, http.StatusFound)
555555
return
556556
}

components/ws-proxy/pkg/proxy/routes_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,10 @@ func TestRoutes(t *testing.T) {
414414
Status: http.StatusFound,
415415
Header: http.Header{
416416
"Content-Type": {"text/html; charset=utf-8"},
417-
"Location": {"https://test-domain.com/start/#blabla-smelt-9ba20cc1"},
417+
"Location": {"https://test-domain.com/start/?not_found=true#blabla-smelt-9ba20cc1"},
418418
"Vary": {"Accept-Encoding"},
419419
},
420-
Body: ("<a href=\"https://test-domain.com/start/#blabla-smelt-9ba20cc1\">Found</a>.\n\n"),
420+
Body: ("<a href=\"https://test-domain.com/start/?not_found=true#blabla-smelt-9ba20cc1\">Found</a>.\n\n"),
421421
},
422422
},
423423
{
@@ -429,10 +429,10 @@ func TestRoutes(t *testing.T) {
429429
Status: http.StatusFound,
430430
Header: http.Header{
431431
"Content-Type": {"text/html; charset=utf-8"},
432-
"Location": {"https://test-domain.com/start/#blabla-smelt-9ba20cc1"},
432+
"Location": {"https://test-domain.com/start/?not_found=true#blabla-smelt-9ba20cc1"},
433433
"Vary": {"Accept-Encoding"},
434434
},
435-
Body: ("<a href=\"https://test-domain.com/start/#blabla-smelt-9ba20cc1\">Found</a>.\n\n"),
435+
Body: ("<a href=\"https://test-domain.com/start/?not_found=true#blabla-smelt-9ba20cc1\">Found</a>.\n\n"),
436436
},
437437
},
438438
{

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14397,6 +14397,16 @@ query-string@^6.13.3:
1439714397
split-on-first "^1.0.0"
1439814398
strict-uri-encode "^2.0.0"
1439914399

14400+
query-string@^7.1.1:
14401+
version "7.1.1"
14402+
resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1"
14403+
integrity sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==
14404+
dependencies:
14405+
decode-uri-component "^0.2.0"
14406+
filter-obj "^1.1.0"
14407+
split-on-first "^1.0.0"
14408+
strict-uri-encode "^2.0.0"
14409+
1440014410
querystring-es3@^0.2.0:
1440114411
version "0.2.1"
1440214412
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"

0 commit comments

Comments
 (0)