Make it possible to request absolute paths

This commit is contained in:
Asher 2019-07-08 15:27:46 -05:00
parent a20fa4a97a
commit fe1d609d1a
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
1 changed files with 102 additions and 66 deletions

168
server.ts
View File

@ -61,6 +61,12 @@ export interface Options {
CONNECTION_AUTH_TOKEN: string; CONNECTION_AUTH_TOKEN: string;
} }
export interface Response {
content?: string | Buffer;
code?: number;
headers: http.OutgoingHttpHeaders;
}
export class HttpError extends Error { export class HttpError extends Error {
public constructor(message: string, public readonly code: number) { public constructor(message: string, public readonly code: number) {
super(message); super(message);
@ -87,16 +93,26 @@ export abstract class Server {
} }
const parsedUrl = url.parse(request.url || "", true); const parsedUrl = url.parse(request.url || "", true);
const requestPath = parsedUrl.pathname || "/";
const [content, headers] = await this.handleRequest(request, parsedUrl, requestPath); const fullPath = decodeURIComponent(parsedUrl.pathname || "/");
response.writeHead(HttpCode.Ok, { const match = fullPath.match(/^(\/?[^/]*)(.*)$/);
const [, base, requestPath] = match
? match.map((p) => p !== "/" ? p.replace(/\/$/, "") : p)
: ["", "", ""];
const { content, headers, code } = await this.handleRequest(
request, parsedUrl, base, requestPath,
);
response.writeHead(code || HttpCode.Ok, {
"Cache-Control": "max-age=86400", "Cache-Control": "max-age=86400",
// TODO: ETag? // TODO: ETag?
...headers, ...headers,
}); });
response.end(content); response.end(content);
} catch (error) { } catch (error) {
if (error.code === "ENOENT" || error.code === "EISDIR") {
error = new HttpError("Not found", HttpCode.NotFound);
}
response.writeHead(typeof error.code === "number" ? error.code : 500); response.writeHead(typeof error.code === "number" ? error.code : 500);
response.end(error.message); response.end(error.message);
} }
@ -106,8 +122,9 @@ export abstract class Server {
protected abstract handleRequest( protected abstract handleRequest(
request: http.IncomingMessage, request: http.IncomingMessage,
parsedUrl: url.UrlWithParsedQuery, parsedUrl: url.UrlWithParsedQuery,
base: string,
requestPath: string, requestPath: string,
): Promise<[string | Buffer, http.OutgoingHttpHeaders]>; ): Promise<Response>;
public listen(): Promise<string> { public listen(): Promise<string> {
if (!this.listenPromise) { if (!this.listenPromise) {
@ -146,7 +163,11 @@ export class MainServer extends Server {
private readonly services = new ServiceCollection(); private readonly services = new ServiceCollection();
public constructor(port: number, private readonly webviewServer: WebviewServer, args: ParsedArgs) { public constructor(
port: number,
private readonly webviewServer: WebviewServer,
args: ParsedArgs,
) {
super(port); super(port);
this.server.on("upgrade", async (request, socket) => { this.server.on("upgrade", async (request, socket) => {
@ -163,7 +184,6 @@ export class MainServer extends Server {
this.ipc.registerChannel("loglevel", new LogLevelSetterChannel(logService)); this.ipc.registerChannel("loglevel", new LogLevelSetterChannel(logService));
const router = new StaticRouter((context: any) => { const router = new StaticRouter((context: any) => {
console.log("static router", context);
return context.clientId === "renderer"; return context.clientId === "renderer";
}); });
@ -194,72 +214,88 @@ export class MainServer extends Server {
protected async handleRequest( protected async handleRequest(
request: http.IncomingMessage, request: http.IncomingMessage,
parsedUrl: url.UrlWithParsedQuery, parsedUrl: url.UrlWithParsedQuery,
base: string,
requestPath: string, requestPath: string,
): Promise<[string | Buffer, http.OutgoingHttpHeaders]> { ): Promise<Response> {
if (requestPath === "/") { switch (base) {
const htmlPath = path.join( case "/":
this.rootPath, return this.getRoot(request, parsedUrl);
'out/vs/code/browser/workbench/workbench.html', case "/node_modules":
); case "/out":
return this.getResource(path.join(this.rootPath, base, requestPath));
let html = await util.promisify(fs.readFile)(htmlPath, "utf8"); // TODO: this setup means you can't request anything from the root if it
// starts with /node_modules or /out, although that's probably low risk.
const remoteAuthority = request.headers.host as string; // There doesn't seem to be a really good way to solve this since some
const transformer = getUriTransformer(remoteAuthority); // resources are requested by the browser (like the extension icon) and
// some by the file provider (like the extension README). Maybe add a
const webviewEndpoint = await this.webviewServer.listen(); // /resource prefix and a file provider that strips that prefix?
default:
const cwd = process.env.VSCODE_CWD || process.cwd(); return this.getResource(path.join(base, requestPath));
const workspacePath = parsedUrl.query.workspace as string | undefined;
const folderPath = !workspacePath ? parsedUrl.query.folder as string | undefined || cwd: undefined;
const options: Options = {
WORKBENCH_WEB_CONGIGURATION: {
workspaceUri: workspacePath
? transformer.transformOutgoing(URI.file(sanitizeFilePath(workspacePath, cwd)))
: undefined,
folderUri: folderPath
? transformer.transformOutgoing(URI.file(sanitizeFilePath(folderPath, cwd)))
: undefined,
remoteAuthority,
webviewEndpoint,
},
REMOTE_USER_DATA_URI: transformer.transformOutgoing(
(this.services.get(IEnvironmentService) as EnvironmentService).webUserDataHome,
),
PRODUCT_CONFIGURATION: product,
CONNECTION_AUTH_TOKEN: "",
};
Object.keys(options).forEach((key) => {
html = html.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key])}'`);
});
html = html.replace('{{WEBVIEW_ENDPOINT}}', webviewEndpoint);
return [html, {
"Content-Type": "text/html",
}];
} }
}
try { private async getRoot(request: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery): Promise<Response> {
const content = await util.promisify(fs.readFile)( const htmlPath = path.join(
path.join(this.rootPath, requestPath), this.rootPath,
); 'out/vs/code/browser/workbench/workbench.html',
return [content, { );
"Content-Type": getMediaMime(requestPath) || {
let content = await util.promisify(fs.readFile)(htmlPath, "utf8");
const remoteAuthority = request.headers.host as string;
const transformer = getUriTransformer(remoteAuthority);
const webviewEndpoint = await this.webviewServer.listen();
const cwd = process.env.VSCODE_CWD || process.cwd();
const workspacePath = parsedUrl.query.workspace as string | undefined;
const folderPath = !workspacePath ? parsedUrl.query.folder as string | undefined || cwd: undefined;
const options: Options = {
WORKBENCH_WEB_CONGIGURATION: {
workspaceUri: workspacePath
? transformer.transformOutgoing(URI.file(sanitizeFilePath(workspacePath, cwd)))
: undefined,
folderUri: folderPath
? transformer.transformOutgoing(URI.file(sanitizeFilePath(folderPath, cwd)))
: undefined,
remoteAuthority,
webviewEndpoint,
},
REMOTE_USER_DATA_URI: transformer.transformOutgoing(
(this.services.get(IEnvironmentService) as EnvironmentService).webUserDataHome,
),
PRODUCT_CONFIGURATION: product,
CONNECTION_AUTH_TOKEN: "",
};
Object.keys(options).forEach((key) => {
content = content.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key])}'`);
});
content = content.replace('{{WEBVIEW_ENDPOINT}}', webviewEndpoint);
return {
content,
headers: {
"Content-Type": "text/html",
},
}
}
private async getResource(filePath: string): Promise<Response> {
const content = await util.promisify(fs.readFile)(filePath);
return {
content,
headers: {
"Content-Type": getMediaMime(filePath) || {
".css": "text/css", ".css": "text/css",
".html": "text/html", ".html": "text/html",
".js": "text/javascript", ".js": "text/javascript",
".json": "application/json", ".json": "application/json",
}[extname(requestPath)] || "text/plain", }[extname(filePath)] || "text/plain",
}]; },
} catch (error) { };
if (error.code === "ENOENT" || error.code === "EISDIR") {
throw new HttpError("Not found", HttpCode.NotFound);
}
throw error;
}
} }
private createProtocol(request: http.IncomingMessage, socket: net.Socket): Protocol { private createProtocol(request: http.IncomingMessage, socket: net.Socket): Protocol {
@ -356,7 +392,7 @@ export class MainServer extends Server {
} }
export class WebviewServer extends Server { export class WebviewServer extends Server {
protected async handleRequest(): Promise<[string | Buffer, http.OutgoingHttpHeaders]> { protected async handleRequest(): Promise<Response> {
throw new Error("not implemented"); throw new Error("not implemented");
} }
} }