Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ const nextConfig: NextConfig = {
images: {
remotePatterns: [{ protocol: 'https', hostname: 'avatars.githubusercontent.com', pathname: '/u/**' }],
},
logging: {
incomingRequests: {
// Noisy route - VSC makes a lot of requests.
ignore: [/^\/api\/auth-vsc\//],
},
},
}

export default nextConfig
2 changes: 2 additions & 0 deletions nginx.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ worker_processes auto;
daemon off;
pid $NGINX_PID_PATH;
error_log $NGINX_ERROR_LOG_PATH;
# Run worker processes as root so that they can access root-owned Unix domain sockets.
user root;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit scary. On the other hand restricting nginx workers only feels a bit security-theater to me: if we do that, we should make all processes (next.js, the nginx root process, the bwraps) run as unprivileged users as well.


events {
worker_connections 1024;
Expand Down
2 changes: 1 addition & 1 deletion src/app/admin/components/SessionRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function SessionRow({ info }: { info: EditorSessionInfo }) {
<a href={`/${info.ownerUsername}/${info.projectName}`}>{info.projectName}</a>
</div>
<div className='actions'>
<span style={{ fontSize: '0.8rem', color: '#90a4ae' }}>port {info.port}</span>
<span style={{ fontSize: '0.8rem', color: '#90a4ae' }}>UUID {info.sessionId}</span>
<button className='delete' disabled={killPending} onClick={() => startTransition(killAction)}>
Kill
</button>
Expand Down
6 changes: 4 additions & 2 deletions src/lib/server/collabServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ export class CollabServerHandle {
// prettier-ignore
[
...BWRAP_ARGS,
// We don't need internet access.
'--unshare-net',
'--ro-bind', getCollabServerDir(), '/workspace/.collab-server',
'--bind', this.socketDir, '/workspace/.collab-sockets',
'--bind', this.socketDir, '/workspace/.sockets/collab-server',
// Mount project files as writable for the collaboration server.
'--bind', this.projectDir, '/workspace/project',
'/usr/bin/node',
'/workspace/.collab-server/server.ts',
`/workspace/.collab-sockets/${COLLAB_SOCKET_FILENAME}`,
`/workspace/.sockets/collab-server/${COLLAB_SOCKET_FILENAME}`,
'/workspace/project',
],
{ stdio: 'inherit' },
Expand Down
14 changes: 1 addition & 13 deletions src/lib/server/editorSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@ export interface EditorSessionInfo {
ownerUsername: string
projectId: string
projectName: string
port: number
}

const BASE_PORT = 3010
const MAX_PORT = 3999

export class EditorSessionManager {
/** projectId ↦ [servers for that project]
*
Expand All @@ -42,8 +38,6 @@ export class EditorSessionManager {
* Same invariant as in {@link vscServers}. */
private collabServers = new Map<string, CollabServerHandle>()

private availablePorts = new Set<number>(Array.from({ length: MAX_PORT - BASE_PORT + 1 }, (_, i) => BASE_PORT + i))

constructor() {
this.vscServerEvents.addListener('close', s => {
if ((this.vscServers.get(s.project.id) ?? []).length === 0) {
Expand Down Expand Up @@ -81,20 +75,15 @@ export class EditorSessionManager {
const projectSessions = this.vscServers.get(project.id) ?? []
let session = projectSessions.find(s => s.viewer.id === viewer.id)
if (!session) {
const port = this.availablePorts.values().next().value
if (port === undefined) throw new Error('no available ports')
this.availablePorts.delete(port)

const projectDir = path.join(getWorkspacesDir(), owner.name, project.id)
const collabServer = this.findCollabServer(project, projectDir)
session = new VscodeServerHandle(viewer, owner, project, projectDir, collabServer.socketDir, port)
session = new VscodeServerHandle(viewer, owner, project, projectDir, collabServer.socketDir)
session.addDisposable(async () => {
this.vscServers.set(
project.id,
(this.vscServers.get(project.id) ?? []).filter(s => s !== session),
)
this.vscServerEvents.emit('close', session!)
this.availablePorts.add(port)
})
// Insertion happens in same transaction as failed lookup (before any `await`)
this.vscServers.set(project.id, [...(this.vscServers.get(project.id) ?? []), session])
Expand Down Expand Up @@ -154,7 +143,6 @@ export class EditorSessionManager {
ownerUsername: project.user.name,
projectId,
projectName: project.name,
port: s.port,
})
}
}
Expand Down
22 changes: 15 additions & 7 deletions src/lib/server/vscodeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,19 @@ async function waitForNginxRoute(path: string, timeoutMs = 10_000): Promise<void
throw new Error(`Timeout waiting for Nginx route ${path} (last HTTP status=${status})`)
}

/** Name of the `openvscode-server` UDS file. */
const VSCODE_SOCKET_FILENAME = 'client.sock'

/** Manages an `openvscode-server` instance.
* Non-reusable; construct a new handle to start a new server. */
export class VscodeServerHandle {
/** Unique ID of this VSCode server instance. */
readonly uuid = crypto.randomUUID()
/** Route that Nginx exposes the VSCode server on. */
readonly vscodeIframePath = `/_vs/${this.uuid}/`
/** Directory in which `openvscode-server` places its UDS file. */
readonly socketDir = `/tmp/vsc-${this.uuid}/`
private readonly socketPath = `${this.socketDir}/${VSCODE_SOCKET_FILENAME}`

constructor(
readonly viewer: User,
Expand All @@ -80,9 +86,6 @@ export class VscodeServerHandle {
readonly projectDir: string,
/** `collab-server` UDS directory. */
readonly collabSocketDir: string,
/** Port on which `openvscode-server` listens. */
// TODO(security): use UDS via `--socket-path` instead.
readonly port: number,
) {}

/** The `bwrap` process. Defined iff the process is running. */
Expand All @@ -105,7 +108,7 @@ export class VscodeServerHandle {
private async writeNginxUserRoute() {
const conf = `location ${this.vscodeIframePath} {
auth_request /api/auth-vsc/${this.uuid};
proxy_pass http://127.0.0.1:${this.port};
proxy_pass http://unix:${this.socketPath};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
Expand Down Expand Up @@ -158,6 +161,11 @@ export class VscodeServerHandle {
throw new Error(`Could not open project directory '${this.projectDir}': ${String(err)}`)
}

await fs.mkdir(this.socketDir, { recursive: true })
this.disposables.defer(async () => {
await fs.rm(this.socketDir, { recursive: true, force: true })
})

// Every user gets their own VSCode server configuration, and set of installed extensions.
// Openvscode-server derives --user-data-dir and --extensions-dir from --server-data-dir:
// https://github.com/gitpod-io/openvscode-server/blob/2bfb814c5215c51a10e80c2cb1b58ed91068ad8b/src/vs/server/node/server.main.ts
Expand Down Expand Up @@ -193,7 +201,8 @@ export class VscodeServerHandle {
// but users can still write files directly if needed.
// Lake and other CLI tools do such writes.
'--bind', this.projectDir, sandboxProjectDir,
'--bind', this.collabSocketDir, '/workspace/.collab-sockets',
'--bind', this.collabSocketDir, '/workspace/.sockets/collab-server',
'--bind', this.socketDir, '/workspace/.sockets/openvscode-server',
...overlayArgs,
'--setenv', 'HOME', '/workspace',
'--setenv', 'ELAN_HOME', '/workspace/.elan',
Expand All @@ -208,8 +217,7 @@ export class VscodeServerHandle {
...devArgs,
'--',
'/workspace/.openvscode-server/bin/openvscode-server',
'--host', '127.0.0.1',
'--port', String(this.port),
'--socket-path', `/workspace/.sockets/openvscode-server/${VSCODE_SOCKET_FILENAME}`,
'--without-connection-token',
`--server-base-path=${this.vscodeIframePath}`,
'--server-data-dir', '/workspace/.vscode-remote',
Expand Down
Loading