Add authentication
This commit is contained in:
parent
2b2aa9a211
commit
97167e75ff
@ -73,7 +73,7 @@ yarn patch:apply
|
|||||||
yarn
|
yarn
|
||||||
yarn watch
|
yarn watch
|
||||||
# Wait for the initial compilation to complete (it will say "Finished compilation").
|
# Wait for the initial compilation to complete (it will say "Finished compilation").
|
||||||
yarn start
|
yarn start --allow-http --no-auth
|
||||||
# Visit http://localhost:8443
|
# Visit http://localhost:8443
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"ensure-in-vscode": "bash ./scripts/tasks.bash ensure-in-vscode",
|
"ensure-in-vscode": "bash ./scripts/tasks.bash ensure-in-vscode",
|
||||||
"preinstall": "yarn ensure-in-vscode && cd ../../../ && yarn || true",
|
"preinstall": "yarn ensure-in-vscode && cd ../../../ && yarn || true",
|
||||||
"postinstall": "rm -rf node_modules/@types/node",
|
"postinstall": "rm -rf node_modules/@types/node",
|
||||||
"start": "yarn ensure-in-vscode && nodemon ../../../out/vs/server/main.js --watch ../../../out --verbose",
|
"start": "yarn ensure-in-vscode && nodemon --watch ../../../out --verbose ../../../out/vs/server/main.js",
|
||||||
"watch": "yarn ensure-in-vscode && cd ../../../ && yarn watch",
|
"watch": "yarn ensure-in-vscode && cd ../../../ && yarn watch",
|
||||||
"build": "bash ./scripts/tasks.bash build",
|
"build": "bash ./scripts/tasks.bash build",
|
||||||
"package": "bash ./scripts/tasks.bash package",
|
"package": "bash ./scripts/tasks.bash package",
|
||||||
|
49
src/cli.ts
49
src/cli.ts
@ -1,4 +1,5 @@
|
|||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
import { validatePaths } from "vs/code/node/paths";
|
import { validatePaths } from "vs/code/node/paths";
|
||||||
import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper";
|
import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper";
|
||||||
@ -9,16 +10,16 @@ import pkg from "vs/platform/product/node/package";
|
|||||||
|
|
||||||
import { MainServer, WebviewServer } from "vs/server/src/server";
|
import { MainServer, WebviewServer } from "vs/server/src/server";
|
||||||
import "vs/server/src/tar";
|
import "vs/server/src/tar";
|
||||||
import { generateCertificate } from "vs/server/src/util";
|
import { generateCertificate, generatePassword } from "vs/server/src/util";
|
||||||
|
|
||||||
interface Args extends ParsedArgs {
|
interface Args extends ParsedArgs {
|
||||||
"allow-http"?: boolean;
|
"allow-http"?: boolean;
|
||||||
|
auth?: boolean;
|
||||||
cert?: string;
|
cert?: string;
|
||||||
"cert-key"?: string;
|
"cert-key"?: string;
|
||||||
"extra-builtin-extensions-dir"?: string;
|
"extra-builtin-extensions-dir"?: string;
|
||||||
"extra-extensions-dir"?: string;
|
"extra-extensions-dir"?: string;
|
||||||
host?: string;
|
host?: string;
|
||||||
"no-auth"?: boolean;
|
|
||||||
open?: string;
|
open?: string;
|
||||||
port?: string;
|
port?: string;
|
||||||
socket?: string;
|
socket?: string;
|
||||||
@ -58,7 +59,7 @@ options.push({ id: "cert-key", type: "string", cat: "o", description: "Path to c
|
|||||||
options.push({ id: "extra-builtin-extensions-dir", type: "string", cat: "o", description: "Path to extra builtin extension directory." });
|
options.push({ id: "extra-builtin-extensions-dir", type: "string", cat: "o", description: "Path to extra builtin extension directory." });
|
||||||
options.push({ id: "extra-extensions-dir", type: "string", cat: "o", description: "Path to extra user extension directory." });
|
options.push({ id: "extra-extensions-dir", type: "string", cat: "o", description: "Path to extra user extension directory." });
|
||||||
options.push({ id: "host", type: "string", cat: "o", description: "Host for the main and webview servers." });
|
options.push({ id: "host", type: "string", cat: "o", description: "Host for the main and webview servers." });
|
||||||
options.push({ id: "no-auth", type: "string", cat: "o", description: "Disable password authentication." });
|
options.push({ id: "no-auth", type: "boolean", cat: "o", description: "Disable password authentication." });
|
||||||
options.push({ id: "open", type: "boolean", cat: "o", description: "Open in the browser on startup." });
|
options.push({ id: "open", type: "boolean", cat: "o", description: "Open in the browser on startup." });
|
||||||
options.push({ id: "port", type: "string", cat: "o", description: "Port for the main server." });
|
options.push({ id: "port", type: "string", cat: "o", description: "Port for the main server." });
|
||||||
options.push({ id: "socket", type: "string", cat: "o", description: "Listen on a socket instead of host:port." });
|
options.push({ id: "socket", type: "string", cat: "o", description: "Listen on a socket instead of host:port." });
|
||||||
@ -115,17 +116,32 @@ const main = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
host: args["host"]
|
host: args.host,
|
||||||
|| (args["no-auth"] || args["allow-http"] ? "localhost" : "0.0.0.0"),
|
|
||||||
allowHttp: args["allow-http"],
|
allowHttp: args["allow-http"],
|
||||||
cert: args["cert"],
|
cert: args.cert,
|
||||||
certKey: args["cert"],
|
certKey: args["cert-key"],
|
||||||
|
auth: typeof args.auth !== "undefined" ? args.auth : true,
|
||||||
|
password: process.env.PASSWORD,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!options.host) {
|
||||||
|
options.host = !options.auth || options.allowHttp
|
||||||
|
? "localhost"
|
||||||
|
: "0.0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
let usingGeneratedCert = false;
|
||||||
if (!options.allowHttp && (!options.cert || !options.certKey)) {
|
if (!options.allowHttp && (!options.cert || !options.certKey)) {
|
||||||
const { cert, certKey } = await generateCertificate();
|
const { cert, certKey } = await generateCertificate();
|
||||||
options.cert = cert;
|
options.cert = cert;
|
||||||
options.certKey = certKey;
|
options.certKey = certKey;
|
||||||
|
usingGeneratedCert = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let usingGeneratedPassword = false;
|
||||||
|
if (options.auth && !options.password) {
|
||||||
|
options.password = await generatePassword();
|
||||||
|
usingGeneratedPassword = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const webviewPort = typeof args["webview-port"] !== "undefined"
|
const webviewPort = typeof args["webview-port"] !== "undefined"
|
||||||
@ -149,6 +165,25 @@ const main = async (): Promise<void> => {
|
|||||||
]);
|
]);
|
||||||
console.log(`Main server listening on ${serverAddress}`);
|
console.log(`Main server listening on ${serverAddress}`);
|
||||||
console.log(`Webview server listening on ${webviewAddress}`);
|
console.log(`Webview server listening on ${webviewAddress}`);
|
||||||
|
|
||||||
|
if (usingGeneratedPassword) {
|
||||||
|
console.log(" - Password is", options.password);
|
||||||
|
console.log(" - To use your own password, set the PASSWORD environment variable");
|
||||||
|
} else if (options.auth) {
|
||||||
|
console.log(" - Using custom password for authentication");
|
||||||
|
} else {
|
||||||
|
console.log(" - No authentication");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.allowHttp && options.cert && options.certKey) {
|
||||||
|
console.log(
|
||||||
|
usingGeneratedCert
|
||||||
|
? ` - Using generated certificate and key in ${path.dirname(options.cert)} for HTTPS`
|
||||||
|
: " - Using provided certificate and key for HTTPS",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(" - Not serving HTTPS");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
|
BIN
src/favicon/favicon.ico
Normal file
BIN
src/favicon/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
94
src/login/login.css
Normal file
94
src/login/login.css
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *:before, *:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
font-family: "monospace";
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 18px 80px 10px rgba(69, 65, 78, 0.08);
|
||||||
|
color: #575962;
|
||||||
|
margin-top: -10%;
|
||||||
|
max-width: 328px;
|
||||||
|
padding: 40px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form > .title {
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
line-height: 15px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form > .subtitle {
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 25px;
|
||||||
|
margin-bottom: 45px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form > .field {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #797E84;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form > .field > .input {
|
||||||
|
background: none !important;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form > .button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 12px 17px 2px rgba(171,173,163,0.14), 0 5px 22px 4px rgba(171,173,163,0.12), 0 7px 8px -4px rgba(171,173,163,0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
padding: 15px 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form > .button:hover {
|
||||||
|
background-color: rgb(0, 122, 204);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-display {
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #bb2d0f;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 12px;
|
||||||
|
padding: 20px 8px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
26
src/login/login.html
Normal file
26
src/login/login.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
|
||||||
|
<title>Authenticate: code-server</title>
|
||||||
|
<link href="/login/login.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form class="login-form" action="/login" method="post">
|
||||||
|
<h4 class="title">code-server</h4>
|
||||||
|
<h2 class="subtitle">
|
||||||
|
Enter server password
|
||||||
|
</h2>
|
||||||
|
<div class="field">
|
||||||
|
<!-- The onfocus code places the cursor at the end of the value. -->
|
||||||
|
<input name="password" type="password" class="input" value=""
|
||||||
|
required autofocus
|
||||||
|
onfocus="const value=this.value;this.value='';this.value=value;">
|
||||||
|
</div>
|
||||||
|
<button class="button" type="submit">
|
||||||
|
<span class="label">Enter IDE</span>
|
||||||
|
</button>
|
||||||
|
<div class="error-display" style="display:none">{{ERROR}}</div>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
302
src/server.ts
302
src/server.ts
@ -6,11 +6,10 @@ import * as path from "path";
|
|||||||
import * as tls from "tls";
|
import * as tls from "tls";
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
|
import * as querystring from "querystring";
|
||||||
|
|
||||||
import { Emitter } from "vs/base/common/event";
|
import { Emitter } from "vs/base/common/event";
|
||||||
import { sanitizeFilePath } from "vs/base/common/extpath";
|
import { sanitizeFilePath } from "vs/base/common/extpath";
|
||||||
import { getMediaMime } from "vs/base/common/mime";
|
|
||||||
import { extname } from "vs/base/common/path";
|
|
||||||
import { UriComponents, URI } from "vs/base/common/uri";
|
import { UriComponents, URI } from "vs/base/common/uri";
|
||||||
import { IPCServer, ClientConnectionEvent, StaticRouter } from "vs/base/parts/ipc/common/ipc";
|
import { IPCServer, ClientConnectionEvent, StaticRouter } from "vs/base/parts/ipc/common/ipc";
|
||||||
import { mkdirp } from "vs/base/node/pfs";
|
import { mkdirp } from "vs/base/node/pfs";
|
||||||
@ -49,12 +48,16 @@ import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
|
|||||||
import { Connection, ManagementConnection, ExtensionHostConnection } from "vs/server/src/connection";
|
import { Connection, ManagementConnection, ExtensionHostConnection } from "vs/server/src/connection";
|
||||||
import { ExtensionEnvironmentChannel, FileProviderChannel , } from "vs/server/src/channel";
|
import { ExtensionEnvironmentChannel, FileProviderChannel , } from "vs/server/src/channel";
|
||||||
import { Protocol } from "vs/server/src/protocol";
|
import { Protocol } from "vs/server/src/protocol";
|
||||||
import { getUriTransformer, useHttpsTransformer } from "vs/server/src/util";
|
import { getMediaMime, getUriTransformer, useHttpsTransformer } from "vs/server/src/util";
|
||||||
|
|
||||||
export enum HttpCode {
|
export enum HttpCode {
|
||||||
Ok = 200,
|
Ok = 200,
|
||||||
|
Redirect = 302,
|
||||||
NotFound = 404,
|
NotFound = 404,
|
||||||
BadRequest = 400,
|
BadRequest = 400,
|
||||||
|
Unauthorized = 401,
|
||||||
|
LargePayload = 413,
|
||||||
|
ServerError = 500,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
@ -65,9 +68,15 @@ export interface Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Response {
|
export interface Response {
|
||||||
content?: string | Buffer;
|
|
||||||
code?: number;
|
code?: number;
|
||||||
headers: http.OutgoingHttpHeaders;
|
content?: string | Buffer;
|
||||||
|
filePath?: string;
|
||||||
|
headers?: http.OutgoingHttpHeaders;
|
||||||
|
redirect?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginPayload {
|
||||||
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HttpError extends Error {
|
export class HttpError extends Error {
|
||||||
@ -80,19 +89,21 @@ export class HttpError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerOptions {
|
export interface ServerOptions {
|
||||||
readonly port: number;
|
readonly port?: number;
|
||||||
readonly host: string;
|
readonly host?: string;
|
||||||
readonly socket?: string;
|
readonly socket?: string;
|
||||||
readonly allowHttp?: boolean;
|
readonly allowHttp?: boolean;
|
||||||
readonly cert?: string;
|
readonly cert?: string;
|
||||||
readonly certKey?: string;
|
readonly certKey?: string;
|
||||||
|
readonly auth?: boolean;
|
||||||
|
readonly password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class Server {
|
export abstract class Server {
|
||||||
// The underlying web server.
|
// The underlying web server.
|
||||||
protected readonly server: http.Server | https.Server;
|
protected readonly server: http.Server | https.Server;
|
||||||
|
|
||||||
protected rootPath = path.resolve(__dirname, "../../..");
|
protected rootPath = path.resolve(__dirname, "../../../..");
|
||||||
|
|
||||||
private listenPromise: Promise<string> | undefined;
|
private listenPromise: Promise<string> | undefined;
|
||||||
|
|
||||||
@ -113,7 +124,7 @@ export abstract class Server {
|
|||||||
if (!this.listenPromise) {
|
if (!this.listenPromise) {
|
||||||
this.listenPromise = new Promise((resolve, reject) => {
|
this.listenPromise = new Promise((resolve, reject) => {
|
||||||
this.server.on("error", reject);
|
this.server.on("error", reject);
|
||||||
const onListen = () => resolve(this.address(this.server, this.options.allowHttp));
|
const onListen = () => resolve(this.address());
|
||||||
if (this.options.socket) {
|
if (this.options.socket) {
|
||||||
this.server.listen(this.options.socket, onListen);
|
this.server.listen(this.options.socket, onListen);
|
||||||
} else {
|
} else {
|
||||||
@ -124,6 +135,22 @@ export abstract class Server {
|
|||||||
return this.listenPromise;
|
return this.listenPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The local address of the server. If you pass in a request, it will use the
|
||||||
|
* request's host if listening on a port (rather than a socket). This enables
|
||||||
|
* accessing the webview server from the same host as the main server.
|
||||||
|
*/
|
||||||
|
public address(request?: http.IncomingMessage): string {
|
||||||
|
const address = this.server.address();
|
||||||
|
const endpoint = typeof address !== "string"
|
||||||
|
? (request
|
||||||
|
? request.headers.host!.split(":", 1)[0]
|
||||||
|
: (address.address === "::" ? "localhost" : address.address)
|
||||||
|
) + ":" + address.port
|
||||||
|
: address;
|
||||||
|
return `${this.options.allowHttp ? "http" : "https"}://${endpoint}`;
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract handleRequest(
|
protected abstract handleRequest(
|
||||||
base: string,
|
base: string,
|
||||||
requestPath: string,
|
requestPath: string,
|
||||||
@ -133,68 +160,192 @@ export abstract class Server {
|
|||||||
|
|
||||||
protected async getResource(filePath: string): Promise<Response> {
|
protected async getResource(filePath: string): Promise<Response> {
|
||||||
const content = await util.promisify(fs.readFile)(filePath);
|
const content = await util.promisify(fs.readFile)(filePath);
|
||||||
return {
|
return { content, filePath };
|
||||||
content,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": getMediaMime(filePath) || {
|
|
||||||
".css": "text/css",
|
|
||||||
".html": "text/html",
|
|
||||||
".js": "text/javascript",
|
|
||||||
".json": "application/json",
|
|
||||||
}[extname(filePath)] || "text/plain",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
|
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
|
||||||
const secure = (request.connection as tls.TLSSocket).encrypted;
|
|
||||||
if (!this.options.allowHttp && !secure) {
|
|
||||||
response.writeHead(302, {
|
|
||||||
Location: "https://" + request.headers.host + request.url,
|
|
||||||
});
|
|
||||||
return response.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (request.method !== "GET") {
|
const payload = await this.preHandleRequest(request);
|
||||||
throw new HttpError(
|
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
||||||
`Unsupported method ${request.method}`,
|
"Cache-Control": "max-age=86400", // TODO: ETag?
|
||||||
HttpCode.BadRequest,
|
"Content-Type": getMediaMime(payload.filePath),
|
||||||
);
|
...(payload.redirect ? { Location: payload.redirect } : {}),
|
||||||
}
|
...payload.headers,
|
||||||
|
|
||||||
const parsedUrl = url.parse(request.url || "", true);
|
|
||||||
|
|
||||||
const fullPath = decodeURIComponent(parsedUrl.pathname || "/");
|
|
||||||
const match = fullPath.match(/^(\/?[^/]*)(.*)$/);
|
|
||||||
const [, base, requestPath] = match
|
|
||||||
? match.map((p) => p !== "/" ? p.replace(/\/$/, "") : p)
|
|
||||||
: ["", "", ""];
|
|
||||||
|
|
||||||
const { content, headers, code } = await this.handleRequest(
|
|
||||||
base, requestPath, parsedUrl, request,
|
|
||||||
);
|
|
||||||
response.writeHead(code || HttpCode.Ok, {
|
|
||||||
"Cache-Control": "max-age=86400",
|
|
||||||
// TODO: ETag?
|
|
||||||
...headers,
|
|
||||||
});
|
});
|
||||||
response.end(content);
|
response.end(payload.content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
||||||
error = new HttpError("Not found", HttpCode.NotFound);
|
error = new HttpError("Not found", HttpCode.NotFound);
|
||||||
}
|
}
|
||||||
response.writeHead(typeof error.code === "number" ? error.code : 500);
|
response.writeHead(typeof error.code === "number" ? error.code : HttpCode.ServerError);
|
||||||
response.end(error.message);
|
response.end(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private address(server: net.Server, http?: boolean): string {
|
private async preHandleRequest(request: http.IncomingMessage): Promise<Response> {
|
||||||
const address = server.address();
|
const secure = (request.connection as tls.TLSSocket).encrypted;
|
||||||
const endpoint = typeof address !== "string"
|
if (!this.options.allowHttp && !secure) {
|
||||||
? ((address.address === "::" ? "localhost" : address.address) + ":" + address.port)
|
return { redirect: "https://" + request.headers.host + request.url };
|
||||||
: address;
|
}
|
||||||
return `${http ? "http" : "https"}://${endpoint}`;
|
|
||||||
|
const parsedUrl = url.parse(request.url || "", true);
|
||||||
|
const fullPath = decodeURIComponent(parsedUrl.pathname || "/");
|
||||||
|
const match = fullPath.match(/^(\/?[^/]*)(.*)$/);
|
||||||
|
let [, base, requestPath] = match
|
||||||
|
? match.map((p) => p.replace(/\/$/, ""))
|
||||||
|
: ["", "", ""];
|
||||||
|
if (base.indexOf(".") !== -1) { // Assume it's a file at the root.
|
||||||
|
requestPath = base;
|
||||||
|
base = "/";
|
||||||
|
} else if (base === "") { // Happens if it's a plain `domain.com`.
|
||||||
|
base = "/";
|
||||||
|
}
|
||||||
|
if (requestPath === "/") { // Trailing slash, like `domain.com/login/`.
|
||||||
|
requestPath = "";
|
||||||
|
} else if (requestPath !== "") { // "" will become "." with normalize.
|
||||||
|
requestPath = path.normalize(requestPath);
|
||||||
|
}
|
||||||
|
base = path.normalize(base);
|
||||||
|
|
||||||
|
switch (base) {
|
||||||
|
case "/":
|
||||||
|
this.ensureGet(request);
|
||||||
|
if (!this.authenticate(request)) {
|
||||||
|
return { redirect: "https://" + request.headers.host + "/login" };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "/login":
|
||||||
|
if (!this.options.auth) {
|
||||||
|
throw new HttpError("Not found", HttpCode.NotFound);
|
||||||
|
}
|
||||||
|
if (requestPath === "") {
|
||||||
|
return this.tryLogin(request);
|
||||||
|
}
|
||||||
|
this.ensureGet(request);
|
||||||
|
return this.getResource(path.join(this.rootPath, "/out/vs/server/src/login", requestPath));
|
||||||
|
case "/favicon.ico":
|
||||||
|
this.ensureGet(request);
|
||||||
|
return this.getResource(path.join(this.rootPath, "/out/vs/server/src/favicon", base));
|
||||||
|
default:
|
||||||
|
this.ensureGet(request);
|
||||||
|
if (!this.authenticate(request)) {
|
||||||
|
throw new HttpError(`Unauthorized`, HttpCode.Unauthorized);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.handleRequest(base, requestPath, parsedUrl, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async tryLogin(request: http.IncomingMessage): Promise<Response> {
|
||||||
|
if (this.authenticate(request)) {
|
||||||
|
this.ensureGet(request);
|
||||||
|
return { redirect: "https://" + request.headers.host + "/" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === "POST") {
|
||||||
|
const data = await this.getData<LoginPayload>(request);
|
||||||
|
if (this.authenticate(request, data)) {
|
||||||
|
return {
|
||||||
|
redirect: "https://" + request.headers.host + "/",
|
||||||
|
headers: {
|
||||||
|
"Set-Cookie": `password=${data.password}`,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let userAgent = request.headers["user-agent"];
|
||||||
|
const timestamp = Math.floor(new Date().getTime() / 1000);
|
||||||
|
if (Array.isArray(userAgent)) {
|
||||||
|
userAgent = userAgent.join(", ");
|
||||||
|
}
|
||||||
|
console.error("Failed login attempt", JSON.stringify({
|
||||||
|
xForwardedFor: request.headers["x-forwarded-for"],
|
||||||
|
remoteAddress: request.connection.remoteAddress,
|
||||||
|
userAgent,
|
||||||
|
timestamp,
|
||||||
|
}));
|
||||||
|
return this.getLogin("Invalid password", data);
|
||||||
|
}
|
||||||
|
this.ensureGet(request);
|
||||||
|
return this.getLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLogin(error: string = "", payload?: LoginPayload): Promise<Response> {
|
||||||
|
const filePath = path.join(this.rootPath, "out/vs/server/src/login/login.html");
|
||||||
|
let content = await util.promisify(fs.readFile)(filePath, "utf8");
|
||||||
|
if (error) {
|
||||||
|
content = content.replace("{{ERROR}}", error)
|
||||||
|
.replace("display:none", "display:block");
|
||||||
|
}
|
||||||
|
if (payload && payload.password) {
|
||||||
|
content = content.replace('value=""', `value="${payload.password}"`);
|
||||||
|
}
|
||||||
|
return { content, filePath };
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureGet(request: http.IncomingMessage): void {
|
||||||
|
if (request.method !== "GET") {
|
||||||
|
throw new HttpError(
|
||||||
|
`Unsupported method ${request.method}`,
|
||||||
|
HttpCode.BadRequest,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getData<T extends object>(request: http.IncomingMessage): Promise<T> {
|
||||||
|
return request.method === "POST"
|
||||||
|
? new Promise<T>((resolve, reject) => {
|
||||||
|
let body = "";
|
||||||
|
const onEnd = (): void => {
|
||||||
|
off();
|
||||||
|
resolve(querystring.parse(body) as T);
|
||||||
|
};
|
||||||
|
const onError = (error: Error): void => {
|
||||||
|
off();
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
const onData = (d: Buffer): void => {
|
||||||
|
body += d;
|
||||||
|
if (body.length > 1e6) {
|
||||||
|
onError(new HttpError(
|
||||||
|
"Payload is too large",
|
||||||
|
HttpCode.LargePayload,
|
||||||
|
));
|
||||||
|
request.connection.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const off = (): void => {
|
||||||
|
request.off("error", onError);
|
||||||
|
request.off("data", onError);
|
||||||
|
request.off("end", onEnd);
|
||||||
|
};
|
||||||
|
request.on("error", onError);
|
||||||
|
request.on("data", onData);
|
||||||
|
request.on("end", onEnd);
|
||||||
|
})
|
||||||
|
: Promise.resolve({} as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
private authenticate(request: http.IncomingMessage, payload?: LoginPayload): boolean {
|
||||||
|
if (!this.options.auth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const safeCompare = require.__$__nodeRequire(path.resolve(__dirname, "../node_modules/safe-compare/index")) as typeof import("safe-compare");
|
||||||
|
if (typeof payload === "undefined") {
|
||||||
|
payload = this.parseCookies<LoginPayload>(request);
|
||||||
|
}
|
||||||
|
return !!this.options.password && safeCompare(payload.password || "", this.options.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseCookies<T extends object>(request: http.IncomingMessage): T {
|
||||||
|
const cookies: { [key: string]: string } = {};
|
||||||
|
if (request.headers.cookie) {
|
||||||
|
request.headers.cookie.split(";").forEach((keyValue) => {
|
||||||
|
const [key, value] = keyValue.split("=", 2);
|
||||||
|
cookies[key.trim()] = decodeURI(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cookies as T;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,8 +432,7 @@ export class MainServer extends Server {
|
|||||||
request: http.IncomingMessage,
|
request: http.IncomingMessage,
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
switch (base) {
|
switch (base) {
|
||||||
case "/":
|
case "/": return this.getRoot(request, parsedUrl);
|
||||||
return this.getRoot(request, parsedUrl);
|
|
||||||
case "/node_modules":
|
case "/node_modules":
|
||||||
case "/out":
|
case "/out":
|
||||||
return this.getResource(path.join(this.rootPath, base, requestPath));
|
return this.getResource(path.join(this.rootPath, base, requestPath));
|
||||||
@ -292,23 +442,19 @@ export class MainServer extends Server {
|
|||||||
// resources are requested by the browser (like the extension icon) and
|
// resources are requested by the browser (like the extension icon) and
|
||||||
// some by the file provider (like the extension README). Maybe add a
|
// some by the file provider (like the extension README). Maybe add a
|
||||||
// /resource prefix and a file provider that strips that prefix?
|
// /resource prefix and a file provider that strips that prefix?
|
||||||
default:
|
default: return this.getResource(path.join(base, requestPath));
|
||||||
return this.getResource(path.join(base, requestPath));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getRoot(request: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery): Promise<Response> {
|
private async getRoot(request: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery): Promise<Response> {
|
||||||
const htmlPath = path.join(
|
const filePath = path.join(this.rootPath, "out/vs/code/browser/workbench/workbench.html");
|
||||||
this.rootPath,
|
let content = await util.promisify(fs.readFile)(filePath, "utf8");
|
||||||
'out/vs/code/browser/workbench/workbench.html',
|
|
||||||
);
|
|
||||||
|
|
||||||
let content = await util.promisify(fs.readFile)(htmlPath, "utf8");
|
|
||||||
|
|
||||||
const remoteAuthority = request.headers.host as string;
|
const remoteAuthority = request.headers.host as string;
|
||||||
const transformer = getUriTransformer(remoteAuthority);
|
const transformer = getUriTransformer(remoteAuthority);
|
||||||
|
|
||||||
const webviewEndpoint = await this.webviewServer.listen();
|
await this.webviewServer.listen();
|
||||||
|
const webviewEndpoint = this.webviewServer.address(request);
|
||||||
|
|
||||||
const cwd = process.env.VSCODE_CWD || process.cwd();
|
const cwd = process.env.VSCODE_CWD || process.cwd();
|
||||||
const workspacePath = parsedUrl.query.workspace as string | undefined;
|
const workspacePath = parsedUrl.query.workspace as string | undefined;
|
||||||
@ -338,12 +484,7 @@ export class MainServer extends Server {
|
|||||||
|
|
||||||
content = content.replace('{{WEBVIEW_ENDPOINT}}', webviewEndpoint);
|
content = content.replace('{{WEBVIEW_ENDPOINT}}', webviewEndpoint);
|
||||||
|
|
||||||
return {
|
return { content, filePath };
|
||||||
content,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "text/html",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createProtocol(request: http.IncomingMessage, socket: net.Socket): Protocol {
|
private createProtocol(request: http.IncomingMessage, socket: net.Socket): Protocol {
|
||||||
@ -444,15 +585,10 @@ export class WebviewServer extends Server {
|
|||||||
base: string,
|
base: string,
|
||||||
requestPath: string,
|
requestPath: string,
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const webviewPath = path.join(
|
const webviewPath = path.join(this.rootPath, "out/vs/workbench/contrib/webview/browser/pre");
|
||||||
this.rootPath,
|
if (requestPath === "") {
|
||||||
"out/vs/workbench/contrib/webview/browser/pre",
|
requestPath = "/index.html";
|
||||||
);
|
|
||||||
|
|
||||||
if (base === "/") {
|
|
||||||
base = "/index.html";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getResource(path.join(webviewPath, base, requestPath));
|
return this.getResource(path.join(webviewPath, base, requestPath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
src/util.ts
18
src/util.ts
@ -1,9 +1,12 @@
|
|||||||
|
import * as crypto from "crypto";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
|
|
||||||
import { getPathFromAmdModule } from "vs/base/common/amd";
|
import { getPathFromAmdModule } from "vs/base/common/amd";
|
||||||
|
import { getMediaMime as vsGetMediaMime } from "vs/base/common/mime";
|
||||||
|
import { extname } from "vs/base/common/path";
|
||||||
import { URITransformer, IRawURITransformer } from "vs/base/common/uriIpc";
|
import { URITransformer, IRawURITransformer } from "vs/base/common/uriIpc";
|
||||||
import { mkdirp } from "vs/base/node/pfs";
|
import { mkdirp } from "vs/base/node/pfs";
|
||||||
|
|
||||||
@ -58,3 +61,18 @@ export const getUriTransformer = (remoteAuthority: string): URITransformer => {
|
|||||||
const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority);
|
const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority);
|
||||||
return new URITransformer(rawURITransformer);
|
return new URITransformer(rawURITransformer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generatePassword = async (length: number = 24): Promise<string> => {
|
||||||
|
const buffer = Buffer.alloc(Math.ceil(length / 2));
|
||||||
|
await util.promisify(crypto.randomFill)(buffer);
|
||||||
|
return buffer.toString("hex").substring(0, length);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMediaMime = (filePath?: string): string => {
|
||||||
|
return filePath && (vsGetMediaMime(filePath) || {
|
||||||
|
".css": "text/css",
|
||||||
|
".html": "text/html",
|
||||||
|
".js": "text/javascript",
|
||||||
|
".json": "application/json",
|
||||||
|
}[extname(filePath)]) || "text/plain";
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user