diff --git a/deno.json b/deno.json index 0dc26e4..695531c 100644 --- a/deno.json +++ b/deno.json @@ -1,7 +1,7 @@ { "name": "@andyburke/serverus", "description": "A flexible HTTP server for mixed content. Throw static files, markdown, Typescript and (hopefully, eventually) more into a directory and serverus can serve it up a bit more like old-school CGI.", - "version": "0.12.2", + "version": "0.12.3", "license": "MIT", "exports": { ".": "./serverus.ts", diff --git a/handlers/static.ts b/handlers/static.ts index e6b0bcc..bd10e53 100644 --- a/handlers/static.ts +++ b/handlers/static.ts @@ -9,8 +9,9 @@ import * as media_types from '@std/media-types'; import { PRECHECK, SERVER } from '../server.ts'; import { getCookies } from '@std/http/cookie'; -let PUT_PATHS_ALLOWED: string[] | undefined = undefined; -let DELETE_PATHS_ALLOWED: string[] | undefined = undefined; +function get_allowed_paths(env_var: string) { + return (Deno.env.get(env_var) ?? '').split(';').filter((p) => typeof p === 'string' && p.length > 0).map((p) => path.resolve(p)); +} export type HTTP_METHOD = 'GET' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS'; export type HANDLER_METHOD = ( @@ -81,10 +82,8 @@ export const HANDLERS: Partial> = { }, PUT: async (request: Request, normalized_path: string, server: SERVER): Promise => { - PUT_PATHS_ALLOWED = PUT_PATHS_ALLOWED ?? - (Deno.env.get('SERVERUS_PUT_PATHS_ALLOWED') ?? '').split(';').map((p) => path.resolve(p)); - - const allowed = PUT_PATHS_ALLOWED.some((allowed_put_path: string) => normalized_path.startsWith(allowed_put_path)); + const allowed_paths = get_allowed_paths('SERVERUS_PUT_PATHS_ALLOWED'); + const allowed = allowed_paths.some((allowed_path: string) => normalized_path.startsWith(allowed_path)); if (!allowed) { return new Response('Permission Denied', { @@ -186,10 +185,8 @@ export const HANDLERS: Partial> = { }, DELETE: async (request: Request, normalized_path: string, server: SERVER): Promise => { - DELETE_PATHS_ALLOWED = DELETE_PATHS_ALLOWED ?? - (Deno.env.get('SERVERUS_DELETE_PATHS_ALLOWED') ?? '').split(';').map((p) => path.resolve(p)); - - const allowed = DELETE_PATHS_ALLOWED.some((allowed_delete_path: string) => normalized_path.startsWith(allowed_delete_path)); + const allowed_paths = get_allowed_paths('SERVERUS_DELETE_PATHS_ALLOWED'); + const allowed = allowed_paths.some((allowed_path: string) => normalized_path.startsWith(allowed_path)); if (!allowed) { return new Response('Permission Denied', { @@ -248,17 +245,13 @@ export const HANDLERS: Partial> = { OPTIONS: (_request: Request, normalized_path: string): Response | undefined => { const allowed = ['GET', 'HEAD', 'OPTIONS']; - PUT_PATHS_ALLOWED = PUT_PATHS_ALLOWED ?? - (Deno.env.get('SERVERUS_PUT_PATHS_ALLOWED') ?? '').split(';').map((p) => path.resolve(p)); - - if (PUT_PATHS_ALLOWED.some((allowed_put_path: string) => normalized_path.startsWith(allowed_put_path))) { + const allowed_put_paths = get_allowed_paths('SERVERUS_PUT_PATHS_ALLOWED'); + if (allowed_put_paths.some((allowed_path: string) => normalized_path.startsWith(allowed_path))) { allowed.push('PUT'); } - DELETE_PATHS_ALLOWED = DELETE_PATHS_ALLOWED ?? - (Deno.env.get('SERVERUS_DELETE_PATHS_ALLOWED') ?? '').split(';').map((p) => path.resolve(p)); - - if (DELETE_PATHS_ALLOWED.some((allowed_delete_path: string) => normalized_path.startsWith(allowed_delete_path))) { + const allowed_delete_paths = get_allowed_paths('SERVERUS_DELETE_PATHS_ALLOWED'); + if (allowed_delete_paths.some((allowed_path: string) => normalized_path.startsWith(allowed_path))) { allowed.push('DELETE'); } diff --git a/tests/01_static_files.test.ts b/tests/01_static_files.test.ts index 0e0f097..e0607f2 100644 --- a/tests/01_static_files.test.ts +++ b/tests/01_static_files.test.ts @@ -82,8 +82,11 @@ Deno.test({ let test_server_info: EPHEMERAL_SERVER | null = null; const cwd = Deno.cwd(); + const PREVIOUS_PUT_PATHS_ALLOWED = Deno.env.get('SERVERUS_PUT_PATHS_ALLOWED'); + const PREVIOUS_DELETE_PATHS_ALLOWED = Deno.env.get('SERVERUS_DELETE_PATHS_ALLOWED'); try { Deno.chdir('./tests/www'); + Deno.env.delete('SERVERUS_PUT_PATHS_ALLOWED'); test_server_info = await get_ephemeral_listen_server(); const response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/test.txt`, { @@ -94,9 +97,85 @@ Deno.test({ asserts.assert(response.ok); asserts.assert(response.headers); - asserts.assertEquals(response.headers.get('Allow'), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT'].join(',')); + asserts.assertEquals(response.headers.get('Allow'), ['GET', 'HEAD', 'OPTIONS'].join(',')); + + await test_server_info.server.stop(); + + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', '.'); + Deno.env.set('SERVERUS_DELETE_PATHS_ALLOWED', '.'); + + test_server_info = await get_ephemeral_listen_server(); + + const expanded_response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/test.txt`, { + method: 'OPTIONS' + }); + + await expanded_response.text(); + + asserts.assert(expanded_response.ok); + asserts.assert(expanded_response.headers); + asserts.assertEquals(expanded_response.headers.get('Allow'), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT'].join(',')); } finally { Deno.chdir(cwd); + if (PREVIOUS_PUT_PATHS_ALLOWED) { + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', PREVIOUS_PUT_PATHS_ALLOWED); + } else { + Deno.env.delete('SERVERUS_PUT_PATHS_ALLOWED'); + } + if (PREVIOUS_DELETE_PATHS_ALLOWED) { + Deno.env.set('SERVERUS_DELETE_PATHS_ALLOWED', PREVIOUS_DELETE_PATHS_ALLOWED); + } else { + Deno.env.delete('SERVERUS_DELETE_PATHS_ALLOWED'); + } + if (test_server_info) { + await test_server_info?.server?.stop(); + } + } + } +}); + +Deno.test({ + name: 'DISallow PUT to static files if SERVERUS_PUT_PATHS_ALLOWED is UNset', + permissions: { + env: true, + read: true, + write: true, + net: true + }, + + sanitizeResources: false, + sanitizeOps: false, + + fn: async () => { + let test_server_info: EPHEMERAL_SERVER | null = null; + const cwd = Deno.cwd(); + + const PREVIOUS_PUT_PATHS_ALLOWED = Deno.env.get('SERVERUS_PUT_PATHS_ALLOWED'); + try { + Deno.chdir('./tests/www'); + Deno.env.delete('SERVERUS_PUT_PATHS_ALLOWED'); + + test_server_info = await get_ephemeral_listen_server(); + + const put_body = new FormData(); + put_body.append('file', new File(['this is a test PUT upload'], 'test_put_upload.txt')); + + // Sending a single file + const put_response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/files/test_put_upload.txt`, { + method: 'PUT', + body: put_body + }); + + asserts.assert(!put_response.ok); + asserts.assertEquals(put_response.status, 400); + } finally { + Deno.chdir(cwd); + if (PREVIOUS_PUT_PATHS_ALLOWED) { + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', PREVIOUS_PUT_PATHS_ALLOWED); + } else { + Deno.env.delete('SERVERUS_PUT_PATHS_ALLOWED'); + } + if (test_server_info) { await test_server_info?.server?.stop(); } @@ -311,9 +390,11 @@ Deno.test({ let test_server_info: EPHEMERAL_SERVER | null = null; const cwd = Deno.cwd(); + const PREVIOUS_PUT_PATHS_ALLOWED = Deno.env.get('SERVERUS_PUT_PATHS_ALLOWED'); const PREVIOUS_DELETE_PATHS_ALLOWED = Deno.env.get('SERVERUS_DELETE_PATHS_ALLOWED'); try { Deno.chdir('./tests/www'); + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', path.join(Deno.cwd(), 'files')); Deno.env.set('SERVERUS_DELETE_PATHS_ALLOWED', path.join(Deno.cwd(), 'files')); test_server_info = await get_ephemeral_listen_server(); @@ -381,6 +462,11 @@ Deno.test({ asserts.assert(!fs.existsSync(local_upload_path)); } finally { Deno.chdir(cwd); + if (PREVIOUS_PUT_PATHS_ALLOWED) { + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', PREVIOUS_PUT_PATHS_ALLOWED); + } else { + Deno.env.delete('SERVERUS_PUT_PATHS_ALLOWED'); + } if (PREVIOUS_DELETE_PATHS_ALLOWED) { Deno.env.set('SERVERUS_DELETE_PATHS_ALLOWED', PREVIOUS_DELETE_PATHS_ALLOWED); } else { @@ -410,9 +496,11 @@ Deno.test({ let test_server_info: EPHEMERAL_SERVER | null = null; const cwd = Deno.cwd(); + const PREVIOUS_PUT_PATHS_ALLOWED = Deno.env.get('SERVERUS_PUT_PATHS_ALLOWED'); const PREVIOUS_DELETE_PATHS_ALLOWED = Deno.env.get('SERVERUS_DELETE_PATHS_ALLOWED'); try { Deno.chdir('./tests/www'); + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', path.join(Deno.cwd(), 'files')); Deno.env.set('SERVERUS_DELETE_PATHS_ALLOWED', path.join(Deno.cwd(), 'files')); test_server_info = await get_ephemeral_listen_server(); @@ -464,6 +552,11 @@ Deno.test({ asserts.assert(!fs.existsSync(path.dirname(local_upload_path))); } finally { Deno.chdir(cwd); + if (PREVIOUS_PUT_PATHS_ALLOWED) { + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', PREVIOUS_PUT_PATHS_ALLOWED); + } else { + Deno.env.delete('SERVERUS_PUT_PATHS_ALLOWED'); + } if (PREVIOUS_DELETE_PATHS_ALLOWED) { Deno.env.set('SERVERUS_DELETE_PATHS_ALLOWED', PREVIOUS_DELETE_PATHS_ALLOWED); } else { @@ -495,8 +588,13 @@ Deno.test({ const static_file_handler = await import('../handlers/static.ts'); const PREVIOUS_PRECHECKS = static_file_handler.PRECHECKS.PUT?.slice(0); + const PREVIOUS_PUT_PATHS_ALLOWED = Deno.env.get('SERVERUS_PUT_PATHS_ALLOWED'); + const PREVIOUS_DELETE_PATHS_ALLOWED = Deno.env.get('SERVERUS_DELETE_PATHS_ALLOWED'); + try { Deno.chdir('./tests/www'); + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', path.join(Deno.cwd(), 'files')); + Deno.env.set('SERVERUS_DELETE_PATHS_ALLOWED', path.join(Deno.cwd(), 'files')); test_server_info = await get_ephemeral_listen_server(); const PRECHECKS = static_file_handler.PRECHECKS.PUT = PREVIOUS_PRECHECKS ? [...PREVIOUS_PRECHECKS] : []; @@ -584,6 +682,16 @@ Deno.test({ if (PREVIOUS_PRECHECKS) { static_file_handler.PRECHECKS.PUT = PREVIOUS_PRECHECKS; } + if (PREVIOUS_PUT_PATHS_ALLOWED) { + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', PREVIOUS_PUT_PATHS_ALLOWED); + } else { + Deno.env.delete('SERVERUS_PUT_PATHS_ALLOWED'); + } + if (PREVIOUS_DELETE_PATHS_ALLOWED) { + Deno.env.set('SERVERUS_DELETE_PATHS_ALLOWED', PREVIOUS_DELETE_PATHS_ALLOWED); + } else { + Deno.env.delete('SERVERUS_DELETE_PATHS_ALLOWED'); + } Deno.chdir(cwd); if (test_server_info) { await test_server_info?.server?.stop(); diff --git a/tests/10_test_preloaders.test.ts b/tests/10_test_preloaders.test.ts index 2782c57..062eaaa 100644 --- a/tests/10_test_preloaders.test.ts +++ b/tests/10_test_preloaders.test.ts @@ -2,7 +2,7 @@ import * as asserts from '@std/assert'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server } from './helpers.ts'; Deno.test({ - name: 'check that _preload.ts files work', + name: 'check that _pre.ts files work', permissions: { env: true, read: true,