From 91ff91093dba45c9696d71e74323487cfa4ce406 Mon Sep 17 00:00:00 2001 From: Alberto Ricart Date: Wed, 12 Aug 2020 05:55:38 -0500 Subject: [PATCH] feat(std): added TLS serve abilities to file_server (#6962) --- std/http/file_server.ts | 111 +++++++++++++++++++++++------------ std/http/file_server_test.ts | 89 ++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 36 deletions(-) mode change 100755 => 100644 std/http/file_server.ts diff --git a/std/http/file_server.ts b/std/http/file_server.ts old mode 100755 new mode 100644 index ffcf31e5..0ece6056 --- a/std/http/file_server.ts +++ b/std/http/file_server.ts @@ -7,7 +7,13 @@ // https://github.com/indexzero/http-server/blob/master/test/http-server-test.js import { posix, extname } from "../path/mod.ts"; -import { listenAndServe, ServerRequest, Response } from "./server.ts"; +import { + listenAndServe, + listenAndServeTLS, + ServerRequest, + Response, + HTTPSOptions, +} from "./server.ts"; import { parse } from "../flags/mod.ts"; import { assert } from "../_util/assert.ts"; @@ -28,6 +34,14 @@ interface FileServerArgs { // -h --help h: boolean; help: boolean; + // --host + host: string; + // -c --cert + c: string; + cert: string; + // -k --key + k: string; + key: string; } const encoder = new TextEncoder(); @@ -49,6 +63,7 @@ const MEDIA_TYPES: Record = { ".gz": "application/gzip", ".css": "text/css", ".wasm": "application/wasm", + ".mjs": "application/javascript", }; /** Returns the content-type based on the extension of a path. */ @@ -306,7 +321,19 @@ function html(strings: TemplateStringsArray, ...values: unknown[]): string { function main(): void { const CORSEnabled = serverArgs.cors ? true : false; - const addr = `0.0.0.0:${serverArgs.port ?? serverArgs.p ?? 4507}`; + const port = serverArgs.port ?? serverArgs.p ?? 4507; + const host = serverArgs.host ?? "0.0.0.0"; + const addr = `${host}:${port}`; + const tlsOpts = {} as HTTPSOptions; + tlsOpts.certFile = serverArgs.cert ?? serverArgs.c ?? ""; + tlsOpts.keyFile = serverArgs.key ?? serverArgs.k ?? ""; + + if (tlsOpts.keyFile || tlsOpts.certFile) { + if (tlsOpts.keyFile === "" || tlsOpts.certFile === "") { + console.log("--key and --cert are required for TLS"); + serverArgs.h = true; + } + } if (serverArgs.h ?? serverArgs.help) { console.log(`Deno File Server @@ -321,50 +348,62 @@ function main(): void { OPTIONS: -h, --help Prints help information -p, --port Set port - --cors Enable CORS via the "Access-Control-Allow-Origin" header`); + --cors Enable CORS via the "Access-Control-Allow-Origin" header + --host Hostname (default is 0.0.0.0) + -c, --cert TLS certificate file (enables TLS) + -k, --key TLS key file (enables TLS) + + All TLS options are required when one is provided.`); + Deno.exit(); } - listenAndServe( - addr, - async (req): Promise => { - let normalizedUrl = posix.normalize(req.url); - try { - normalizedUrl = decodeURIComponent(normalizedUrl); - } catch (e) { - if (!(e instanceof URIError)) { - throw e; - } + const handler = async (req: ServerRequest): Promise => { + let normalizedUrl = posix.normalize(req.url); + try { + normalizedUrl = decodeURIComponent(normalizedUrl); + } catch (e) { + if (!(e instanceof URIError)) { + throw e; } - const fsPath = posix.join(target, normalizedUrl); + } + const fsPath = posix.join(target, normalizedUrl); - let response: Response | undefined; + let response: Response | undefined; + try { + const fileInfo = await Deno.stat(fsPath); + if (fileInfo.isDirectory) { + response = await serveDir(req, fsPath); + } else { + response = await serveFile(req, fsPath); + } + } catch (e) { + console.error(e.message); + response = await serveFallback(req, e); + } finally { + if (CORSEnabled) { + assert(response); + setCORS(response); + } + serverLog(req, response!); try { - const fileInfo = await Deno.stat(fsPath); - if (fileInfo.isDirectory) { - response = await serveDir(req, fsPath); - } else { - response = await serveFile(req, fsPath); - } + await req.respond(response!); } catch (e) { console.error(e.message); - response = await serveFallback(req, e); - } finally { - if (CORSEnabled) { - assert(response); - setCORS(response); - } - serverLog(req, response!); - try { - await req.respond(response!); - } catch (e) { - console.error(e.message); - } } - }, - ); + } + }; - console.log(`HTTP server listening on http://${addr}/`); + let proto = "http"; + if (tlsOpts.keyFile || tlsOpts.certFile) { + proto += "s"; + tlsOpts.hostname = host; + tlsOpts.port = port; + listenAndServeTLS(tlsOpts, handler); + } else { + listenAndServe(addr, handler); + } + console.log(`${proto.toUpperCase()} server listening on ${proto}://${addr}/`); } if (import.meta.main) { diff --git a/std/http/file_server_test.ts b/std/http/file_server_test.ts index 486bc514..a25c13a7 100644 --- a/std/http/file_server_test.ts +++ b/std/http/file_server_test.ts @@ -199,3 +199,92 @@ Deno.test("file_server running as library", async function (): Promise { await killFileServer(); } }); + +async function startTlsFileServer({ + target = ".", + port = 4577, +}: FileServerCfg = {}): Promise { + fileServer = Deno.run({ + cmd: [ + Deno.execPath(), + "run", + "--allow-read", + "--allow-net", + "http/file_server.ts", + target, + "--host", + "localhost", + "--cert", + "./http/testdata/tls/localhost.crt", + "--key", + "./http/testdata/tls/localhost.key", + "--cors", + "-p", + `${port}`, + ], + stdout: "piped", + stderr: "null", + }); + // Once fileServer is ready it will write to its stdout. + assert(fileServer.stdout != null); + const r = new TextProtoReader(new BufReader(fileServer.stdout)); + const s = await r.readLine(); + assert(s !== null && s.includes("server listening")); +} + +Deno.test("serveDirectory TLS", async function (): Promise { + await startTlsFileServer(); + try { + // Valid request after invalid + const conn = await Deno.connectTls({ + hostname: "localhost", + port: 4577, + certFile: "./http/testdata/tls/RootCA.pem", + }); + + await Deno.writeAll( + conn, + new TextEncoder().encode("GET /http HTTP/1.0\r\n\r\n"), + ); + const res = new Uint8Array(128 * 1024); + const nread = await conn.read(res); + assert(nread !== null); + conn.close(); + const page = new TextDecoder().decode(res.subarray(0, nread)); + assert(page.includes("Deno File Server")); + } finally { + await killFileServer(); + } +}); + +Deno.test("partial TLS arguments fail", async function (): Promise { + fileServer = Deno.run({ + cmd: [ + Deno.execPath(), + "run", + "--allow-read", + "--allow-net", + "http/file_server.ts", + ".", + "--host", + "localhost", + "--cert", + "./http/testdata/tls/localhost.crt", + "-p", + `4578`, + ], + stdout: "piped", + stderr: "null", + }); + try { + // Once fileServer is ready it will write to its stdout. + assert(fileServer.stdout != null); + const r = new TextProtoReader(new BufReader(fileServer.stdout)); + const s = await r.readLine(); + assert( + s !== null && s.includes("--key and --cert are required for TLS"), + ); + } finally { + await killFileServer(); + } +}); -- GitLab