Skip to content

Commit 4cc181c

Browse files
committed
Make routing base path agnostic
1 parent a149c5f commit 4cc181c

File tree

13 files changed

+192
-215
lines changed

13 files changed

+192
-215
lines changed

src/browser/api.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getBasepath } from "hookrouter"
12
import { Application, ApplicationsResponse, CreateSessionResponse, FilesResponse, RecentResponse } from "../common/api"
23
import { ApiEndpoint, HttpCode, HttpError } from "../common/http"
34

@@ -18,7 +19,7 @@ export function setAuthed(authed: boolean): void {
1819
* Also set authed to false if the request returns unauthorized.
1920
*/
2021
const tryRequest = async (endpoint: string, options?: RequestInit): Promise<Response> => {
21-
const response = await fetch("/api" + endpoint + "/", options)
22+
const response = await fetch(getBasepath() + "/api" + endpoint + "/", options)
2223
if (response.status === HttpCode.Unauthorized) {
2324
setAuthed(false)
2425
}
@@ -33,14 +34,9 @@ const tryRequest = async (endpoint: string, options?: RequestInit): Promise<Resp
3334
* Try authenticating.
3435
*/
3536
export const authenticate = async (body?: AuthBody): Promise<void> => {
36-
let formBody: URLSearchParams | undefined
37-
if (body) {
38-
formBody = new URLSearchParams()
39-
formBody.append("password", body.password)
40-
}
4137
const response = await tryRequest(ApiEndpoint.login, {
4238
method: "POST",
43-
body: formBody,
39+
body: JSON.stringify({ ...body, basePath: getBasepath() }),
4440
headers: {
4541
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
4642
},

src/browser/app.tsx

+20-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getBasepath, navigate } from "hookrouter"
1+
import { getBasepath, navigate, setBasepath } from "hookrouter"
22
import * as React from "react"
33
import { Application, isExecutableApplication } from "../common/api"
44
import { HttpError } from "../common/http"
@@ -11,25 +11,36 @@ export interface AppProps {
1111
}
1212

1313
const App: React.FunctionComponent<AppProps> = (props) => {
14-
const [authed, setAuthed] = React.useState<boolean>(!!props.options.authed)
14+
const [authed, setAuthed] = React.useState<boolean>(props.options.authed)
1515
const [app, setApp] = React.useState<Application | undefined>(props.options.app)
1616
const [error, setError] = React.useState<HttpError | Error | string>()
1717

18-
React.useEffect(() => {
19-
if (app && !isExecutableApplication(app)) {
20-
navigate(normalize(`${getBasepath()}/${app.path}/`, true))
21-
}
22-
}, [app])
23-
2418
if (typeof window !== "undefined") {
19+
const url = new URL(window.location.origin + window.location.pathname + props.options.basePath)
20+
setBasepath(normalize(url.pathname))
21+
2522
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2623
;(window as any).setAuthed = (a: boolean): void => {
2724
if (authed !== a) {
2825
setAuthed(a)
26+
// TEMP: Remove when no longer auto-loading VS Code.
27+
if (a && !app) {
28+
setApp({
29+
name: "VS Code",
30+
path: "/",
31+
embedPath: "/vscode-embed",
32+
})
33+
}
2934
}
3035
}
3136
}
3237

38+
React.useEffect(() => {
39+
if (app && !isExecutableApplication(app)) {
40+
navigate(normalize(`${getBasepath()}/${app.path}/`, true))
41+
}
42+
}, [app])
43+
3344
return (
3445
<>
3546
{!app || !app.loaded ? (
@@ -41,7 +52,7 @@ const App: React.FunctionComponent<AppProps> = (props) => {
4152
)}
4253
<Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} />
4354
{authed && app && app.embedPath ? (
44-
<iframe id="iframe" src={normalize(`${getBasepath()}/${app.embedPath}/`, true)}></iframe>
55+
<iframe id="iframe" src={normalize(`./${app.embedPath}/`, true)}></iframe>
4556
) : (
4657
undefined
4758
)}

src/browser/components/modal.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
128128
<aside className="sidebar-nav">
129129
<nav className="links">
130130
{props.authed ? (
131-
// TEMP: Remove once we don't auto-load vscode.
132131
<>
133132
<button className="link" onClick={(): void => setSection(Section.Recent)}>
134133
Recent

src/browser/index.html

+6-6
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
<head>
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
6-
<!-- <meta http-equiv="Content-Security-Policy" content="font-src 'self'; connect-src 'self'; default-src ws: wss:; style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;"> -->
6+
<meta http-equiv="Content-Security-Policy" content="font-src 'self' fonts.gstatic.com; connect-src 'self'; default-src ws: wss: 'self'; style-src 'self' fonts.googleapis.com; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;">
77
<title>code-server</title>
8-
<link rel="icon" href="/static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
9-
<link rel="manifest" href="/static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
10-
<link rel="apple-touch-icon" href="/static-{{COMMIT}}/src/browser/media/code-server.png" />
8+
<link rel="icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
9+
<link rel="manifest" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
10+
<link rel="apple-touch-icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/code-server.png" />
1111
<link href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans&display=swap" rel="stylesheet" />
12-
<link href="/static-{{COMMIT}}/dist/index.css" rel="stylesheet">
12+
<link href="{{BASE}}/static-{{COMMIT}}/dist/index.css" rel="stylesheet">
1313
<meta id="coder-options" data-settings="{{OPTIONS}}">
1414
</head>
1515
<body>
1616
<div id="root">{{COMPONENT}}</div>
17-
<script src="/static-{{COMMIT}}/dist/index.js"></script>
17+
<script src="{{BASE}}/static-{{COMMIT}}/dist/index.js"></script>
1818
</body>
1919
</html>

src/common/api.ts

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export enum SessionError {
2222
Unknown,
2323
}
2424

25+
export interface LoginRequest {
26+
password: string
27+
basePath: string
28+
}
29+
2530
export interface LoginResponse {
2631
success: boolean
2732
}

src/common/util.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { Application } from "../common/api"
33

44
export interface Options {
55
app?: Application
6-
authed?: boolean
7-
logLevel?: number
6+
authed: boolean
7+
basePath: string
8+
logLevel: number
89
}
910

1011
/**

src/node/api/server.ts

+41-39
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { field, logger } from "@coder/logger"
22
import * as http from "http"
33
import * as net from "net"
4-
import * as querystring from "querystring"
54
import * as ws from "ws"
6-
import { ApplicationsResponse, ClientMessage, FilesResponse, LoginResponse, ServerMessage } from "../../common/api"
5+
import {
6+
ApplicationsResponse,
7+
ClientMessage,
8+
FilesResponse,
9+
LoginRequest,
10+
LoginResponse,
11+
ServerMessage,
12+
} from "../../common/api"
713
import { ApiEndpoint, HttpCode } from "../../common/http"
8-
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, PostData } from "../http"
14+
import { normalize } from "../../common/util"
15+
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
916
import { hash } from "../util"
1017

11-
interface LoginPayload extends PostData {
12-
password?: string | string[]
13-
}
14-
1518
/**
1619
* API HTTP provider.
1720
*/
@@ -22,13 +25,8 @@ export class ApiHttpProvider extends HttpProvider {
2225
super(options)
2326
}
2427

25-
public async handleRequest(
26-
base: string,
27-
_requestPath: string,
28-
_query: querystring.ParsedUrlQuery,
29-
request: http.IncomingMessage
30-
): Promise<HttpResponse | undefined> {
31-
switch (base) {
28+
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
29+
switch (route.base) {
3230
case ApiEndpoint.login:
3331
if (request.method === "POST") {
3432
return this.login(request)
@@ -38,7 +36,7 @@ export class ApiHttpProvider extends HttpProvider {
3836
if (!this.authenticated(request)) {
3937
return { code: HttpCode.Unauthorized }
4038
}
41-
switch (base) {
39+
switch (route.base) {
4240
case ApiEndpoint.applications:
4341
return this.applications()
4442
case ApiEndpoint.files:
@@ -49,9 +47,7 @@ export class ApiHttpProvider extends HttpProvider {
4947
}
5048

5149
public async handleWebSocket(
52-
_base: string,
53-
_requestPath: string,
54-
_query: querystring.ParsedUrlQuery,
50+
_route: Route,
5551
request: http.IncomingMessage,
5652
socket: net.Socket,
5753
head: Buffer
@@ -93,39 +89,45 @@ export class ApiHttpProvider extends HttpProvider {
9389
* unauthorized.
9490
*/
9591
private async login(request: http.IncomingMessage): Promise<HttpResponse<LoginResponse>> {
96-
const ok = (password: string | true): HttpResponse<LoginResponse> => {
97-
return {
98-
content: {
99-
success: true,
100-
},
101-
cookie: typeof password === "string" ? { key: "key", value: password } : undefined,
102-
}
103-
}
104-
10592
// Already authenticated via cookies?
10693
const providedPassword = this.authenticated(request)
10794
if (providedPassword) {
108-
return ok(providedPassword)
95+
return { code: HttpCode.Ok }
10996
}
11097

11198
const data = await this.getData(request)
112-
const payload: LoginPayload = data ? querystring.parse(data) : {}
99+
const payload: LoginRequest = data ? JSON.parse(data) : {}
113100
const password = this.authenticated(request, {
114101
key: typeof payload.password === "string" ? [hash(payload.password)] : undefined,
115102
})
116103
if (password) {
117-
return ok(password)
104+
return {
105+
content: {
106+
success: true,
107+
},
108+
cookie:
109+
typeof password === "string"
110+
? {
111+
key: "key",
112+
value: password,
113+
path: normalize(payload.basePath),
114+
}
115+
: undefined,
116+
}
118117
}
119118

120-
console.error(
121-
"Failed login attempt",
122-
JSON.stringify({
123-
xForwardedFor: request.headers["x-forwarded-for"],
124-
remoteAddress: request.connection.remoteAddress,
125-
userAgent: request.headers["user-agent"],
126-
timestamp: Math.floor(new Date().getTime() / 1000),
127-
})
128-
)
119+
// Only log if it was an actual login attempt.
120+
if (payload && payload.password) {
121+
console.error(
122+
"Failed login attempt",
123+
JSON.stringify({
124+
xForwardedFor: request.headers["x-forwarded-for"],
125+
remoteAddress: request.connection.remoteAddress,
126+
userAgent: request.headers["user-agent"],
127+
timestamp: Math.floor(new Date().getTime() / 1000),
128+
})
129+
)
130+
}
129131

130132
return { code: HttpCode.Unauthorized }
131133
}

src/node/app/server.tsx

+34-31
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,53 @@
11
import { logger } from "@coder/logger"
22
import * as http from "http"
3-
import * as querystring from "querystring"
43
import * as React from "react"
54
import * as ReactDOMServer from "react-dom/server"
65
import App from "../../browser/app"
76
import { Options } from "../../common/util"
8-
import { HttpProvider, HttpResponse } from "../http"
7+
import { HttpProvider, HttpResponse, Route } from "../http"
98

109
/**
1110
* Top-level and fallback HTTP provider.
1211
*/
1312
export class MainHttpProvider extends HttpProvider {
14-
public async handleRequest(
15-
base: string,
16-
requestPath: string,
17-
_query: querystring.ParsedUrlQuery,
18-
request: http.IncomingMessage
19-
): Promise<HttpResponse | undefined> {
20-
if (base === "/static") {
21-
const response = await this.getResource(this.rootPath, requestPath)
22-
if (!this.isDev) {
23-
response.cache = true
13+
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
14+
switch (route.base) {
15+
case "/static": {
16+
const response = await this.getResource(this.rootPath, route.requestPath)
17+
if (!this.isDev) {
18+
response.cache = true
19+
}
20+
return response
2421
}
25-
return response
26-
}
2722

28-
// TEMP: Auto-load VS Code for now. In future versions we'll need to check
29-
// the URL for the appropriate application to load, if any.
30-
const app = {
31-
name: "VS Code",
32-
path: "/",
33-
embedPath: "/vscode-embed",
34-
}
23+
case "/": {
24+
const options: Options = {
25+
authed: !!this.authenticated(request),
26+
basePath: this.base(route),
27+
logLevel: logger.level,
28+
}
29+
30+
if (options.authed) {
31+
// TEMP: Auto-load VS Code for now. In future versions we'll need to check
32+
// the URL for the appropriate application to load, if any.
33+
options.app = {
34+
name: "VS Code",
35+
path: "/",
36+
embedPath: "/vscode-embed",
37+
}
38+
}
3539

36-
const options: Options = {
37-
app,
38-
authed: !!this.authenticated(request),
39-
logLevel: logger.level,
40+
const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html")
41+
response.content = response.content
42+
.replace(/{{COMMIT}}/g, this.options.commit)
43+
.replace(/{{BASE}}/g, this.base(route))
44+
.replace(/"{{OPTIONS}}"/g, `'${JSON.stringify(options)}'`)
45+
.replace(/{{COMPONENT}}/g, ReactDOMServer.renderToString(<App options={options} />))
46+
return response
47+
}
4048
}
4149

42-
const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html")
43-
response.content = response.content
44-
.replace(/{{COMMIT}}/g, this.options.commit)
45-
.replace(/"{{OPTIONS}}"/g, `'${JSON.stringify(options)}'`)
46-
.replace(/{{COMPONENT}}/g, ReactDOMServer.renderToString(<App options={options} />))
47-
return response
50+
return undefined
4851
}
4952

5053
public async handleWebSocket(): Promise<undefined> {

0 commit comments

Comments
 (0)