Make routing base path agnostic
This commit is contained in:
parent
a149c5fc60
commit
4cc181cedc
@ -1,3 +1,4 @@
|
||||
import { getBasepath } from "hookrouter"
|
||||
import { Application, ApplicationsResponse, CreateSessionResponse, FilesResponse, RecentResponse } from "../common/api"
|
||||
import { ApiEndpoint, HttpCode, HttpError } from "../common/http"
|
||||
|
||||
@ -18,7 +19,7 @@ export function setAuthed(authed: boolean): void {
|
||||
* Also set authed to false if the request returns unauthorized.
|
||||
*/
|
||||
const tryRequest = async (endpoint: string, options?: RequestInit): Promise<Response> => {
|
||||
const response = await fetch("/api" + endpoint + "/", options)
|
||||
const response = await fetch(getBasepath() + "/api" + endpoint + "/", options)
|
||||
if (response.status === HttpCode.Unauthorized) {
|
||||
setAuthed(false)
|
||||
}
|
||||
@ -33,14 +34,9 @@ const tryRequest = async (endpoint: string, options?: RequestInit): Promise<Resp
|
||||
* Try authenticating.
|
||||
*/
|
||||
export const authenticate = async (body?: AuthBody): Promise<void> => {
|
||||
let formBody: URLSearchParams | undefined
|
||||
if (body) {
|
||||
formBody = new URLSearchParams()
|
||||
formBody.append("password", body.password)
|
||||
}
|
||||
const response = await tryRequest(ApiEndpoint.login, {
|
||||
method: "POST",
|
||||
body: formBody,
|
||||
body: JSON.stringify({ ...body, basePath: getBasepath() }),
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getBasepath, navigate } from "hookrouter"
|
||||
import { getBasepath, navigate, setBasepath } from "hookrouter"
|
||||
import * as React from "react"
|
||||
import { Application, isExecutableApplication } from "../common/api"
|
||||
import { HttpError } from "../common/http"
|
||||
@ -11,25 +11,36 @@ export interface AppProps {
|
||||
}
|
||||
|
||||
const App: React.FunctionComponent<AppProps> = (props) => {
|
||||
const [authed, setAuthed] = React.useState<boolean>(!!props.options.authed)
|
||||
const [authed, setAuthed] = React.useState<boolean>(props.options.authed)
|
||||
const [app, setApp] = React.useState<Application | undefined>(props.options.app)
|
||||
const [error, setError] = React.useState<HttpError | Error | string>()
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const url = new URL(window.location.origin + window.location.pathname + props.options.basePath)
|
||||
setBasepath(normalize(url.pathname))
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(window as any).setAuthed = (a: boolean): void => {
|
||||
if (authed !== a) {
|
||||
setAuthed(a)
|
||||
// TEMP: Remove when no longer auto-loading VS Code.
|
||||
if (a && !app) {
|
||||
setApp({
|
||||
name: "VS Code",
|
||||
path: "/",
|
||||
embedPath: "/vscode-embed",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (app && !isExecutableApplication(app)) {
|
||||
navigate(normalize(`${getBasepath()}/${app.path}/`, true))
|
||||
}
|
||||
}, [app])
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(window as any).setAuthed = (a: boolean): void => {
|
||||
if (authed !== a) {
|
||||
setAuthed(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!app || !app.loaded ? (
|
||||
@ -41,7 +52,7 @@ const App: React.FunctionComponent<AppProps> = (props) => {
|
||||
)}
|
||||
<Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} />
|
||||
{authed && app && app.embedPath ? (
|
||||
<iframe id="iframe" src={normalize(`${getBasepath()}/${app.embedPath}/`, true)}></iframe>
|
||||
<iframe id="iframe" src={normalize(`./${app.embedPath}/`, true)}></iframe>
|
||||
) : (
|
||||
undefined
|
||||
)}
|
||||
|
@ -128,7 +128,6 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
|
||||
<aside className="sidebar-nav">
|
||||
<nav className="links">
|
||||
{props.authed ? (
|
||||
// TEMP: Remove once we don't auto-load vscode.
|
||||
<>
|
||||
<button className="link" onClick={(): void => setSection(Section.Recent)}>
|
||||
Recent
|
||||
|
@ -3,17 +3,17 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
|
||||
<!-- <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:;"> -->
|
||||
<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:;">
|
||||
<title>code-server</title>
|
||||
<link rel="icon" href="/static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="/static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
|
||||
<link rel="apple-touch-icon" href="/static-{{COMMIT}}/src/browser/media/code-server.png" />
|
||||
<link rel="icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
|
||||
<link rel="apple-touch-icon" href="{{BASE}}/static-{{COMMIT}}/src/browser/media/code-server.png" />
|
||||
<link href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans&display=swap" rel="stylesheet" />
|
||||
<link href="/static-{{COMMIT}}/dist/index.css" rel="stylesheet">
|
||||
<link href="{{BASE}}/static-{{COMMIT}}/dist/index.css" rel="stylesheet">
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">{{COMPONENT}}</div>
|
||||
<script src="/static-{{COMMIT}}/dist/index.js"></script>
|
||||
<script src="{{BASE}}/static-{{COMMIT}}/dist/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -22,6 +22,11 @@ export enum SessionError {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
password: string
|
||||
basePath: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean
|
||||
}
|
||||
|
@ -3,8 +3,9 @@ import { Application } from "../common/api"
|
||||
|
||||
export interface Options {
|
||||
app?: Application
|
||||
authed?: boolean
|
||||
logLevel?: number
|
||||
authed: boolean
|
||||
basePath: string
|
||||
logLevel: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,17 +1,20 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as http from "http"
|
||||
import * as net from "net"
|
||||
import * as querystring from "querystring"
|
||||
import * as ws from "ws"
|
||||
import { ApplicationsResponse, ClientMessage, FilesResponse, LoginResponse, ServerMessage } from "../../common/api"
|
||||
import {
|
||||
ApplicationsResponse,
|
||||
ClientMessage,
|
||||
FilesResponse,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
ServerMessage,
|
||||
} from "../../common/api"
|
||||
import { ApiEndpoint, HttpCode } from "../../common/http"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, PostData } from "../http"
|
||||
import { normalize } from "../../common/util"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
|
||||
import { hash } from "../util"
|
||||
|
||||
interface LoginPayload extends PostData {
|
||||
password?: string | string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* API HTTP provider.
|
||||
*/
|
||||
@ -22,13 +25,8 @@ export class ApiHttpProvider extends HttpProvider {
|
||||
super(options)
|
||||
}
|
||||
|
||||
public async handleRequest(
|
||||
base: string,
|
||||
_requestPath: string,
|
||||
_query: querystring.ParsedUrlQuery,
|
||||
request: http.IncomingMessage
|
||||
): Promise<HttpResponse | undefined> {
|
||||
switch (base) {
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
|
||||
switch (route.base) {
|
||||
case ApiEndpoint.login:
|
||||
if (request.method === "POST") {
|
||||
return this.login(request)
|
||||
@ -38,7 +36,7 @@ export class ApiHttpProvider extends HttpProvider {
|
||||
if (!this.authenticated(request)) {
|
||||
return { code: HttpCode.Unauthorized }
|
||||
}
|
||||
switch (base) {
|
||||
switch (route.base) {
|
||||
case ApiEndpoint.applications:
|
||||
return this.applications()
|
||||
case ApiEndpoint.files:
|
||||
@ -49,9 +47,7 @@ export class ApiHttpProvider extends HttpProvider {
|
||||
}
|
||||
|
||||
public async handleWebSocket(
|
||||
_base: string,
|
||||
_requestPath: string,
|
||||
_query: querystring.ParsedUrlQuery,
|
||||
_route: Route,
|
||||
request: http.IncomingMessage,
|
||||
socket: net.Socket,
|
||||
head: Buffer
|
||||
@ -93,39 +89,45 @@ export class ApiHttpProvider extends HttpProvider {
|
||||
* unauthorized.
|
||||
*/
|
||||
private async login(request: http.IncomingMessage): Promise<HttpResponse<LoginResponse>> {
|
||||
const ok = (password: string | true): HttpResponse<LoginResponse> => {
|
||||
return {
|
||||
content: {
|
||||
success: true,
|
||||
},
|
||||
cookie: typeof password === "string" ? { key: "key", value: password } : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Already authenticated via cookies?
|
||||
const providedPassword = this.authenticated(request)
|
||||
if (providedPassword) {
|
||||
return ok(providedPassword)
|
||||
return { code: HttpCode.Ok }
|
||||
}
|
||||
|
||||
const data = await this.getData(request)
|
||||
const payload: LoginPayload = data ? querystring.parse(data) : {}
|
||||
const payload: LoginRequest = data ? JSON.parse(data) : {}
|
||||
const password = this.authenticated(request, {
|
||||
key: typeof payload.password === "string" ? [hash(payload.password)] : undefined,
|
||||
})
|
||||
if (password) {
|
||||
return ok(password)
|
||||
return {
|
||||
content: {
|
||||
success: true,
|
||||
},
|
||||
cookie:
|
||||
typeof password === "string"
|
||||
? {
|
||||
key: "key",
|
||||
value: password,
|
||||
path: normalize(payload.basePath),
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
console.error(
|
||||
"Failed login attempt",
|
||||
JSON.stringify({
|
||||
xForwardedFor: request.headers["x-forwarded-for"],
|
||||
remoteAddress: request.connection.remoteAddress,
|
||||
userAgent: request.headers["user-agent"],
|
||||
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||
})
|
||||
)
|
||||
// Only log if it was an actual login attempt.
|
||||
if (payload && payload.password) {
|
||||
console.error(
|
||||
"Failed login attempt",
|
||||
JSON.stringify({
|
||||
xForwardedFor: request.headers["x-forwarded-for"],
|
||||
remoteAddress: request.connection.remoteAddress,
|
||||
userAgent: request.headers["user-agent"],
|
||||
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return { code: HttpCode.Unauthorized }
|
||||
}
|
||||
|
@ -1,50 +1,53 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import * as http from "http"
|
||||
import * as querystring from "querystring"
|
||||
import * as React from "react"
|
||||
import * as ReactDOMServer from "react-dom/server"
|
||||
import App from "../../browser/app"
|
||||
import { Options } from "../../common/util"
|
||||
import { HttpProvider, HttpResponse } from "../http"
|
||||
import { HttpProvider, HttpResponse, Route } from "../http"
|
||||
|
||||
/**
|
||||
* Top-level and fallback HTTP provider.
|
||||
*/
|
||||
export class MainHttpProvider extends HttpProvider {
|
||||
public async handleRequest(
|
||||
base: string,
|
||||
requestPath: string,
|
||||
_query: querystring.ParsedUrlQuery,
|
||||
request: http.IncomingMessage
|
||||
): Promise<HttpResponse | undefined> {
|
||||
if (base === "/static") {
|
||||
const response = await this.getResource(this.rootPath, requestPath)
|
||||
if (!this.isDev) {
|
||||
response.cache = true
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
|
||||
switch (route.base) {
|
||||
case "/static": {
|
||||
const response = await this.getResource(this.rootPath, route.requestPath)
|
||||
if (!this.isDev) {
|
||||
response.cache = true
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
case "/": {
|
||||
const options: Options = {
|
||||
authed: !!this.authenticated(request),
|
||||
basePath: this.base(route),
|
||||
logLevel: logger.level,
|
||||
}
|
||||
|
||||
if (options.authed) {
|
||||
// TEMP: Auto-load VS Code for now. In future versions we'll need to check
|
||||
// the URL for the appropriate application to load, if any.
|
||||
options.app = {
|
||||
name: "VS Code",
|
||||
path: "/",
|
||||
embedPath: "/vscode-embed",
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html")
|
||||
response.content = response.content
|
||||
.replace(/{{COMMIT}}/g, this.options.commit)
|
||||
.replace(/{{BASE}}/g, this.base(route))
|
||||
.replace(/"{{OPTIONS}}"/g, `'${JSON.stringify(options)}'`)
|
||||
.replace(/{{COMPONENT}}/g, ReactDOMServer.renderToString(<App options={options} />))
|
||||
return response
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// TEMP: Auto-load VS Code for now. In future versions we'll need to check
|
||||
// the URL for the appropriate application to load, if any.
|
||||
const app = {
|
||||
name: "VS Code",
|
||||
path: "/",
|
||||
embedPath: "/vscode-embed",
|
||||
}
|
||||
|
||||
const options: Options = {
|
||||
app,
|
||||
authed: !!this.authenticated(request),
|
||||
logLevel: logger.level,
|
||||
}
|
||||
|
||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html")
|
||||
response.content = response.content
|
||||
.replace(/{{COMMIT}}/g, this.options.commit)
|
||||
.replace(/"{{OPTIONS}}"/g, `'${JSON.stringify(options)}'`)
|
||||
.replace(/{{COMPONENT}}/g, ReactDOMServer.renderToString(<App options={options} />))
|
||||
return response
|
||||
return undefined
|
||||
}
|
||||
|
||||
public async handleWebSocket(): Promise<undefined> {
|
||||
|
126
src/node/http.ts
126
src/node/http.ts
@ -47,8 +47,9 @@ export interface HttpResponse<T = string | Buffer | object> {
|
||||
content?: T
|
||||
/**
|
||||
* Cookie to write with the response.
|
||||
* NOTE: Cookie paths must be absolute. The default is /.
|
||||
*/
|
||||
cookie?: { key: string; value: string }
|
||||
cookie?: { key: string; value: string; path?: string }
|
||||
/**
|
||||
* Used to automatically determine the appropriate mime type.
|
||||
*/
|
||||
@ -64,7 +65,7 @@ export interface HttpResponse<T = string | Buffer | object> {
|
||||
/**
|
||||
* Redirect to this path. Will rewrite against the base path but NOT the
|
||||
* provider endpoint so you must include it. This allows redirecting outside
|
||||
* of your endpoint. Use `withBase()` to redirect within your endpoint.
|
||||
* of your endpoint.
|
||||
*/
|
||||
redirect?: string
|
||||
/**
|
||||
@ -87,9 +88,12 @@ export interface HttpStringFileResponse extends HttpResponse {
|
||||
filePath: string
|
||||
}
|
||||
|
||||
export interface RedirectResponse extends HttpResponse {
|
||||
redirect: string
|
||||
}
|
||||
|
||||
export interface HttpServerOptions {
|
||||
readonly auth?: AuthType
|
||||
readonly basePath?: string
|
||||
readonly cert?: string
|
||||
readonly certKey?: string
|
||||
readonly commit?: string
|
||||
@ -99,15 +103,18 @@ export interface HttpServerOptions {
|
||||
readonly socket?: string
|
||||
}
|
||||
|
||||
interface ProviderRoute {
|
||||
export interface Route {
|
||||
base: string
|
||||
requestPath: string
|
||||
query: querystring.ParsedUrlQuery
|
||||
provider: HttpProvider
|
||||
fullPath: string
|
||||
originalPath: string
|
||||
}
|
||||
|
||||
interface ProviderRoute extends Route {
|
||||
provider: HttpProvider
|
||||
}
|
||||
|
||||
export interface HttpProviderOptions {
|
||||
readonly base: string
|
||||
readonly auth: AuthType
|
||||
@ -132,9 +139,7 @@ export abstract class HttpProvider {
|
||||
* Handle web sockets on the registered endpoint.
|
||||
*/
|
||||
public abstract handleWebSocket(
|
||||
base: string,
|
||||
requestPath: string,
|
||||
query: querystring.ParsedUrlQuery,
|
||||
route: Route,
|
||||
request: http.IncomingMessage,
|
||||
socket: net.Socket,
|
||||
head: Buffer
|
||||
@ -143,24 +148,20 @@ export abstract class HttpProvider {
|
||||
/**
|
||||
* Handle requests to the registered endpoint.
|
||||
*/
|
||||
public abstract handleRequest(
|
||||
base: string,
|
||||
requestPath: string,
|
||||
query: querystring.ParsedUrlQuery,
|
||||
request: http.IncomingMessage
|
||||
): Promise<HttpResponse | undefined>
|
||||
public abstract handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse | undefined>
|
||||
|
||||
/**
|
||||
* Get the base relative to the provided route.
|
||||
*/
|
||||
public base(route: Route): string {
|
||||
const depth = route.fullPath ? (route.fullPath.match(/\//g) || []).length : 1
|
||||
return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
|
||||
}
|
||||
|
||||
protected get isDev(): boolean {
|
||||
return this.options.commit === "development"
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the specified path with the base path prepended.
|
||||
*/
|
||||
protected withBase(path: string): string {
|
||||
return normalize(`${this.options.base}/${path}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a file resource.
|
||||
* TODO: Would a stream be faster, at least for large files?
|
||||
@ -346,19 +347,14 @@ export class HttpServer {
|
||||
private listenPromise: Promise<string | null> | undefined
|
||||
public readonly protocol: "http" | "https"
|
||||
private readonly providers = new Map<string, HttpProvider>()
|
||||
private readonly options: HttpServerOptions
|
||||
private readonly heart: Heart
|
||||
|
||||
public constructor(options: HttpServerOptions) {
|
||||
public constructor(private readonly options: HttpServerOptions) {
|
||||
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
|
||||
const connections = await this.getConnections()
|
||||
logger.trace(`${connections} active connection${plural(connections)}`)
|
||||
return connections !== 0
|
||||
})
|
||||
this.options = {
|
||||
...options,
|
||||
basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "",
|
||||
}
|
||||
this.protocol = this.options.cert ? "https" : "http"
|
||||
if (this.protocol === "https") {
|
||||
this.server = httpolyglot.createServer(
|
||||
@ -452,30 +448,19 @@ export class HttpServer {
|
||||
try {
|
||||
this.heart.beat()
|
||||
const route = this.parseUrl(request)
|
||||
const payload =
|
||||
this.maybeRedirect(request, route) ||
|
||||
(await route.provider.handleRequest(route.base, route.requestPath, route.query, request))
|
||||
const payload = this.maybeRedirect(request, route) || (await route.provider.handleRequest(route, request))
|
||||
if (!payload) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
const basePath = this.options.basePath || "/"
|
||||
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
||||
"Content-Type": payload.mime || getMediaMime(payload.filePath),
|
||||
...(payload.redirect
|
||||
? {
|
||||
Location: this.constructRedirect(
|
||||
request.headers.host as string,
|
||||
route.fullPath,
|
||||
normalize(`${basePath}/${payload.redirect}`) + "/",
|
||||
{ ...route.query, ...(payload.query || {}) }
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(request.headers["service-worker"] ? { "Service-Worker-Allowed": basePath } : {}),
|
||||
...(payload.redirect ? { Location: payload.redirect } : {}),
|
||||
...(request.headers["service-worker"] ? { "Service-Worker-Allowed": route.provider.base(route) } : {}),
|
||||
...(payload.cache ? { "Cache-Control": "public, max-age=31536000" } : {}),
|
||||
...(payload.cookie
|
||||
? {
|
||||
"Set-Cookie": `${payload.cookie.key}=${payload.cookie.value}; Path=${basePath}; HttpOnly; SameSite=strict`,
|
||||
"Set-Cookie": `${payload.cookie.key}=${payload.cookie.value}; Path=${payload.cookie.path ||
|
||||
"/"}; HttpOnly; SameSite=strict`,
|
||||
}
|
||||
: {}),
|
||||
...payload.headers,
|
||||
@ -497,9 +482,8 @@ export class HttpServer {
|
||||
let e = error
|
||||
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
||||
e = new HttpError("Not found", HttpCode.NotFound)
|
||||
} else {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
logger.debug(error.stack)
|
||||
response.writeHead(typeof e.code === "number" ? e.code : HttpCode.ServerError)
|
||||
response.end(error.message)
|
||||
}
|
||||
@ -509,14 +493,29 @@ export class HttpServer {
|
||||
* Return any necessary redirection before delegating to a provider.
|
||||
*/
|
||||
private maybeRedirect(request: http.IncomingMessage, route: ProviderRoute): HttpResponse | undefined {
|
||||
// Redirect to HTTPS.
|
||||
if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) {
|
||||
return { redirect: route.fullPath }
|
||||
const redirect = (path: string): string => {
|
||||
Object.keys(route.query).forEach((key) => {
|
||||
if (typeof route.query[key] === "undefined") {
|
||||
delete route.query[key]
|
||||
}
|
||||
})
|
||||
// If we're handling TLS ensure all requests are redirected to HTTPS.
|
||||
return this.options.cert
|
||||
? `${this.protocol}://${request.headers.host}`
|
||||
: "" +
|
||||
normalize(`${route.provider.base(route)}/${path}`, true) +
|
||||
(Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : "")
|
||||
}
|
||||
// Redirect indexes to a trailing slash so relative paths will operate
|
||||
// against the provider.
|
||||
if (route.requestPath === "/index.html" && !route.originalPath.endsWith("/")) {
|
||||
return { redirect: route.fullPath } // Redirect always includes a trailing slash.
|
||||
|
||||
// Redirect to HTTPS if we're handling the TLS.
|
||||
if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) {
|
||||
return { redirect: redirect(route.fullPath) }
|
||||
}
|
||||
|
||||
// Redirect our indexes to a trailing slash so relative paths in the served
|
||||
// HTML will operate against the base path properly.
|
||||
if (route.requestPath === "/index.html" && !route.originalPath.endsWith("/") && this.providers.has(route.base)) {
|
||||
return { redirect: redirect(route.fullPath + "/") }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@ -534,12 +533,12 @@ export class HttpServer {
|
||||
throw new HttpError("HTTP/1.1 400 Bad Request", HttpCode.BadRequest)
|
||||
}
|
||||
|
||||
const { base, requestPath, query, provider } = this.parseUrl(request)
|
||||
if (!provider) {
|
||||
const route = this.parseUrl(request)
|
||||
if (!route.provider) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
if (!(await provider.handleWebSocket(base, requestPath, query, request, socket, head))) {
|
||||
if (!(await route.provider.handleWebSocket(route, request, socket, head))) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
} catch (error) {
|
||||
@ -593,21 +592,4 @@ export class HttpServer {
|
||||
}
|
||||
return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the request URL with the specified base and new path.
|
||||
*/
|
||||
private constructRedirect(host: string, oldPath: string, newPath: string, query: Query): string {
|
||||
if (oldPath && oldPath !== "/" && !query.to && /\/login(\/|$)/.test(newPath) && !/\/login(\/|$)/.test(oldPath)) {
|
||||
query.to = oldPath
|
||||
}
|
||||
Object.keys(query).forEach((key) => {
|
||||
if (typeof query[key] === "undefined") {
|
||||
delete query[key]
|
||||
}
|
||||
})
|
||||
return (
|
||||
`${this.protocol}://${host}${newPath}` + (Object.keys(query).length > 0 ? `?${querystring.stringify(query)}` : "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
|
||||
<!-- <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:;"> -->
|
||||
<meta http-equiv="Content-Security-Policy" content="font-src 'self' fonts.gstatic.com; connect-src 'self'; default-src ws: wss: 'self'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;">
|
||||
<title>code-server</title>
|
||||
<link rel="icon" href="./static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="./static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
|
||||
<link rel="apple-touch-icon" href="./static-{{COMMIT}}/src/browser/media/code-server.png" />
|
||||
<link href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans&display=swap" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" style="color:#f4f4f4;padding:20px;max-width:700px;">
|
||||
|
@ -4,7 +4,6 @@ import * as crypto from "crypto"
|
||||
import * as http from "http"
|
||||
import * as net from "net"
|
||||
import * as path from "path"
|
||||
import * as querystring from "querystring"
|
||||
import {
|
||||
CodeServerMessage,
|
||||
Settings,
|
||||
@ -13,7 +12,7 @@ import {
|
||||
WorkbenchOptions,
|
||||
} from "../../../lib/vscode/src/vs/server/ipc"
|
||||
import { generateUuid } from "../../common/util"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse } from "../http"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { SettingsProvider } from "../settings"
|
||||
import { xdgLocalDir } from "../util"
|
||||
|
||||
@ -76,13 +75,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
return this._vscode
|
||||
}
|
||||
|
||||
public async handleWebSocket(
|
||||
_base: string,
|
||||
_requestPath: string,
|
||||
query: querystring.ParsedUrlQuery,
|
||||
request: http.IncomingMessage,
|
||||
socket: net.Socket
|
||||
): Promise<true> {
|
||||
public async handleWebSocket(route: Route, request: http.IncomingMessage, socket: net.Socket): Promise<true> {
|
||||
if (!this.authenticated(request)) {
|
||||
throw new Error("not authenticated")
|
||||
}
|
||||
@ -105,7 +98,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
)
|
||||
|
||||
const vscode = await this._vscode
|
||||
this.send({ type: "socket", query }, vscode, socket)
|
||||
this.send({ type: "socket", query: route.query }, vscode, socket)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -116,27 +109,20 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
vscode.send(message, socket)
|
||||
}
|
||||
|
||||
public async handleRequest(
|
||||
base: string,
|
||||
requestPath: string,
|
||||
query: querystring.ParsedUrlQuery,
|
||||
request: http.IncomingMessage
|
||||
): Promise<HttpResponse | undefined> {
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
|
||||
this.ensureGet(request)
|
||||
switch (base) {
|
||||
this.ensureAuthenticated(request)
|
||||
switch (route.base) {
|
||||
case "/":
|
||||
if (!this.authenticated(request)) {
|
||||
return { redirect: "/login" }
|
||||
}
|
||||
try {
|
||||
return await this.getRoot(request, query)
|
||||
return await this.getRoot(request, route)
|
||||
} catch (error) {
|
||||
return this.getErrorRoot(error)
|
||||
}
|
||||
case "/static": {
|
||||
switch (requestPath) {
|
||||
switch (route.requestPath) {
|
||||
case "/out/vs/workbench/services/extensions/worker/extensionHostWorkerMain.js": {
|
||||
const response = await this.getUtf8Resource(this.vsRootPath, requestPath)
|
||||
const response = await this.getUtf8Resource(this.vsRootPath, route.requestPath)
|
||||
response.content = response.content.replace(
|
||||
/{{COMMIT}}/g,
|
||||
this.workbenchOptions ? this.workbenchOptions.commit : ""
|
||||
@ -145,40 +131,37 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
return response
|
||||
}
|
||||
}
|
||||
const response = await this.getResource(this.vsRootPath, requestPath)
|
||||
const response = await this.getResource(this.vsRootPath, route.requestPath)
|
||||
response.cache = true
|
||||
return response
|
||||
}
|
||||
case "/resource":
|
||||
case "/vscode-remote-resource":
|
||||
this.ensureAuthenticated(request)
|
||||
if (typeof query.path === "string") {
|
||||
return this.getResource(query.path)
|
||||
if (typeof route.query.path === "string") {
|
||||
return this.getResource(route.query.path)
|
||||
}
|
||||
break
|
||||
case "/tar":
|
||||
this.ensureAuthenticated(request)
|
||||
if (typeof query.path === "string") {
|
||||
return this.getTarredResource(query.path)
|
||||
if (typeof route.query.path === "string") {
|
||||
return this.getTarredResource(route.query.path)
|
||||
}
|
||||
break
|
||||
case "/webview":
|
||||
this.ensureAuthenticated(request)
|
||||
if (/^\/vscode-resource/.test(requestPath)) {
|
||||
return this.getResource(requestPath.replace(/^\/vscode-resource(\/file)?/, ""))
|
||||
if (/^\/vscode-resource/.test(route.requestPath)) {
|
||||
return this.getResource(route.requestPath.replace(/^\/vscode-resource(\/file)?/, ""))
|
||||
}
|
||||
return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", requestPath)
|
||||
return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", route.requestPath)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
private async getRoot(request: http.IncomingMessage, query: querystring.ParsedUrlQuery): Promise<HttpResponse> {
|
||||
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
|
||||
const settings = await this.settings.read()
|
||||
const [response, options] = await Promise.all([
|
||||
await this.getUtf8Resource(this.rootPath, `src/node/vscode/workbench${!this.isDev ? "-build" : ""}.html`),
|
||||
this.initialize({
|
||||
args: this.args,
|
||||
query,
|
||||
query: route.query,
|
||||
remoteAuthority: request.headers.host as string,
|
||||
settings,
|
||||
}),
|
||||
|
@ -19,10 +19,10 @@
|
||||
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}">
|
||||
|
||||
<!-- Workbench Icon/Manifest/CSS -->
|
||||
<link rel="icon" href="./static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="./static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="./static-{{COMMIT}}/out/vs/workbench/workbench.web.api.css">
|
||||
<link rel="apple-touch-icon" href="./static-{{COMMIT}}/src/browser/media/code-server.png" />
|
||||
<link rel="icon" href="../static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="../static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="../static-{{COMMIT}}/out/vs/workbench/workbench.web.api.css">
|
||||
<link rel="apple-touch-icon" href="../static-{{COMMIT}}/src/browser/media/code-server.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
<!-- Prefetch to avoid waterfall -->
|
||||
|
@ -19,9 +19,9 @@
|
||||
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}">
|
||||
|
||||
<!-- Workbench Icon/Manifest/CSS -->
|
||||
<link rel="icon" href="./static/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="./static/src/browser/media/manifest.json" crossorigin="use-credentials">
|
||||
<link rel="apple-touch-icon" href="./static/src/browser/media/code-server.png" />
|
||||
<link rel="icon" href="../static/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="../static/src/browser/media/manifest.json" crossorigin="use-credentials">
|
||||
<link rel="apple-touch-icon" href="../static/src/browser/media/code-server.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
</head>
|
||||
|
||||
|
Reference in New Issue
Block a user