feature: allow for static file uploads and deletions
feature: add HEAD and OPTIONS support to static files
This commit is contained in:
parent
582636ab5a
commit
3ef936d2d6
7 changed files with 900 additions and 120 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
data/
|
data/
|
||||||
tests/data
|
tests/data
|
||||||
serverus
|
serverus
|
||||||
|
tests/www/files
|
||||||
|
|
|
@ -66,6 +66,8 @@ listening.
|
||||||
|
|
||||||
- `SERVERUS_ROOT`: set the root, aka --root on the command line
|
- `SERVERUS_ROOT`: set the root, aka --root on the command line
|
||||||
- `SERVERUS_HANDLERS`: a list of ;-separated directories to look for handlers in
|
- `SERVERUS_HANDLERS`: a list of ;-separated directories to look for handlers in
|
||||||
|
- `SERVERUS_PUT_PATHS_ALLOWED`: a list of ;-separated directories for which file uploads via PUT are allowed
|
||||||
|
- `SERVERUS_DELETE_PATHS_ALLOWED`: a list of ;-separated directories for which file deletions via DELETE are allowed
|
||||||
|
|
||||||
### Typescript Handling
|
### Typescript Handling
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@andyburke/serverus",
|
"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.",
|
"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.10.0",
|
"version": "0.11.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./serverus.ts",
|
".": "./serverus.ts",
|
||||||
|
|
|
@ -3,8 +3,275 @@
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as fs from '@std/fs';
|
||||||
import * as path from '@std/path';
|
import * as path from '@std/path';
|
||||||
import * as media_types from '@std/media-types';
|
import * as media_types from '@std/media-types';
|
||||||
|
import { SERVER } from '@andyburke/serverus/server';
|
||||||
|
|
||||||
|
let PUT_PATHS_ALLOWED: string[] | undefined = undefined;
|
||||||
|
let DELETE_PATHS_ALLOWED: string[] | undefined = undefined;
|
||||||
|
|
||||||
|
export type HTTP_METHOD = 'GET' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
||||||
|
export type HANDLER_METHOD = (
|
||||||
|
request: Request,
|
||||||
|
normalized_path: string,
|
||||||
|
server: SERVER
|
||||||
|
) => Promise<Response | undefined> | Response | undefined;
|
||||||
|
export type PRECHECK = (request: Request) => Promise<Response> | Response | undefined;
|
||||||
|
export const PRECHECKS: Partial<Record<HTTP_METHOD, PRECHECK[]>> = {};
|
||||||
|
export const HANDLERS: Partial<Record<HTTP_METHOD, HANDLER_METHOD>> = {
|
||||||
|
HEAD: async (_request: Request, normalized_path: string, _server: SERVER): Promise<Response | undefined> => {
|
||||||
|
try {
|
||||||
|
const stat = await Deno.stat(normalized_path);
|
||||||
|
|
||||||
|
if (stat.isFile) {
|
||||||
|
const extension = path.extname(normalized_path)?.slice(1)?.toLowerCase() ?? '';
|
||||||
|
|
||||||
|
const content_type = media_types.contentType(extension) ?? 'application/octet-stream';
|
||||||
|
return new Response('', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': content_type,
|
||||||
|
'Content-Length': `${stat.size}`,
|
||||||
|
'Last-Modified': `${stat.mtime ?? stat.ctime}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Deno.errors.NotFound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Deno.errors.PermissionDenied) {
|
||||||
|
return new Response('Permission Denied', {
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
GET: async (_request: Request, normalized_path: string, _server: SERVER): Promise<Response | undefined> => {
|
||||||
|
try {
|
||||||
|
const stat = await Deno.stat(normalized_path);
|
||||||
|
|
||||||
|
if (stat.isFile) {
|
||||||
|
const extension = path.extname(normalized_path)?.slice(1)?.toLowerCase() ?? '';
|
||||||
|
|
||||||
|
const content_type = media_types.contentType(extension) ?? 'application/octet-stream';
|
||||||
|
return new Response(await Deno.readFile(normalized_path), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': content_type
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Deno.errors.NotFound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Deno.errors.PermissionDenied) {
|
||||||
|
return new Response('Permission Denied', {
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
PUT: async (request: Request, normalized_path: string, server: SERVER): Promise<Response | undefined> => {
|
||||||
|
PUT_PATHS_ALLOWED = PUT_PATHS_ALLOWED ??
|
||||||
|
(Deno.env.get('SERVERUS_PUT_PATHS_ALLOWED') ?? '').split(';');
|
||||||
|
|
||||||
|
const allowed = PUT_PATHS_ALLOWED.some((allowed_put_path: string) => normalized_path.startsWith(allowed_put_path));
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
return new Response('Permission Denied', {
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = Deno.cwd();
|
||||||
|
|
||||||
|
const body = await request.formData();
|
||||||
|
const files: File[] = Array.from(body.entries()).map(([_field, value]) => value).filter((value) => value instanceof File);
|
||||||
|
const errors = [];
|
||||||
|
const written = [];
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return Response.json({
|
||||||
|
error: {
|
||||||
|
message: 'You must send at least one file in the form data with a static file PUT request.',
|
||||||
|
cause: 'missing_file'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const upload_directory = files.length > 1 ? normalized_path : path.dirname(normalized_path);
|
||||||
|
const all_files_have_names = files.every((file: File) => !!file.name);
|
||||||
|
|
||||||
|
if (files.length > 1 && !all_files_have_names) {
|
||||||
|
return Response.json({
|
||||||
|
error: {
|
||||||
|
message: 'You must specify all filenames when uploading to a directory.',
|
||||||
|
cause: 'missing_filename'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const file of files) {
|
||||||
|
const filename: string = file.name ?? path.basename(normalized_path);
|
||||||
|
const resolved_upload_name = path.resolve(path.join(upload_directory, filename));
|
||||||
|
|
||||||
|
if (!resolved_upload_name.startsWith(root)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Deno.mkdir(upload_directory, {
|
||||||
|
recursive: true,
|
||||||
|
mode: 0o755
|
||||||
|
});
|
||||||
|
|
||||||
|
const temp_upload_name = path.join(upload_directory, `.${filename}.${new Date().toISOString()}`);
|
||||||
|
await Deno.writeFile(temp_upload_name, file.stream(), {
|
||||||
|
mode: 0o755,
|
||||||
|
create: true,
|
||||||
|
createNew: true,
|
||||||
|
signal: request.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
await file.stream().cancel();
|
||||||
|
|
||||||
|
const resolved_path_exists = await fs.exists(resolved_upload_name);
|
||||||
|
|
||||||
|
if (resolved_path_exists) {
|
||||||
|
await Deno.remove(resolved_upload_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Deno.rename(temp_upload_name, resolved_upload_name);
|
||||||
|
|
||||||
|
const base_url = request.url.toString();
|
||||||
|
const ends_with_filename = base_url.endsWith(filename);
|
||||||
|
const url = base_url + (ends_with_filename ? '' : filename);
|
||||||
|
written.push(url);
|
||||||
|
|
||||||
|
server.emit('static.put', {
|
||||||
|
url,
|
||||||
|
normalized_path
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return Response.json({
|
||||||
|
errors
|
||||||
|
}, {
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
written
|
||||||
|
}, {
|
||||||
|
status: 201
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
DELETE: async (request: Request, normalized_path: string, server: SERVER): Promise<Response | undefined> => {
|
||||||
|
DELETE_PATHS_ALLOWED = DELETE_PATHS_ALLOWED ??
|
||||||
|
(Deno.env.get('SERVERUS_DELETE_PATHS_ALLOWED') ?? '').split(';');
|
||||||
|
|
||||||
|
const allowed = DELETE_PATHS_ALLOWED.some((allowed_delete_path: string) => normalized_path.startsWith(allowed_delete_path));
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
return new Response('Permission Denied', {
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stat = await Deno.lstat(normalized_path);
|
||||||
|
|
||||||
|
if (stat.isDirectory) {
|
||||||
|
await Deno.remove(normalized_path, {
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
} else if (stat.isFile || stat.isSymlink) {
|
||||||
|
await Deno.remove(normalized_path);
|
||||||
|
const directory = path.dirname(normalized_path);
|
||||||
|
const is_empty = Array.from(Deno.readDirSync(directory)).length === 0;
|
||||||
|
|
||||||
|
if (is_empty) {
|
||||||
|
await Deno.remove(directory, {
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new Response('Permission Denied', {
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
server.emit('static.delete', {
|
||||||
|
url: request.url.toString(),
|
||||||
|
normalized_path
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Deno.errors.NotFound) {
|
||||||
|
return Response.json({
|
||||||
|
error: 'Not Found'
|
||||||
|
}, {
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
error
|
||||||
|
}, {
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({}, {
|
||||||
|
status: 200
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
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((put_path) => path.resolve(path.join(Deno.cwd(), put_path)));
|
||||||
|
|
||||||
|
if (PUT_PATHS_ALLOWED.some((allowed_put_path: string) => normalized_path.startsWith(allowed_put_path))) {
|
||||||
|
allowed.push('PUT');
|
||||||
|
}
|
||||||
|
|
||||||
|
DELETE_PATHS_ALLOWED = DELETE_PATHS_ALLOWED ??
|
||||||
|
(Deno.env.get('SERVERUS_DELETE_PATHS_ALLOWED') ?? '').split(';').map((delete_path) =>
|
||||||
|
path.resolve(path.join(Deno.cwd(), delete_path))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (DELETE_PATHS_ALLOWED.some((allowed_delete_path: string) => normalized_path.startsWith(allowed_delete_path))) {
|
||||||
|
allowed.push('DELETE');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('', {
|
||||||
|
headers: {
|
||||||
|
'Allow': allowed.sort().join(','),
|
||||||
|
'Access-Control-Allow-Origin': Deno.env.get('SERVERUS_ACCESS_CONTROL_ALLOW_ORIGIN') ?? '*'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles requests for static files.
|
* Handles requests for static files.
|
||||||
|
@ -12,45 +279,30 @@ import * as media_types from '@std/media-types';
|
||||||
* @param request The incoming HTTP request
|
* @param request The incoming HTTP request
|
||||||
* @returns Either a response (a static file was requested and returned properly) or undefined if unhandled.
|
* @returns Either a response (a static file was requested and returned properly) or undefined if unhandled.
|
||||||
*/
|
*/
|
||||||
export default async function handle_static_files(request: Request): Promise<Response | undefined> {
|
export default async function handle_static_files(request: Request, server: SERVER): Promise<Response | undefined> {
|
||||||
// we only handle GET on static files
|
const method: HTTP_METHOD = request.method.toUpperCase() as HTTP_METHOD;
|
||||||
if (request.method.toUpperCase() !== 'GET') {
|
const handler: HANDLER_METHOD | undefined = HANDLERS[method];
|
||||||
|
|
||||||
|
if (!handler) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const normalized_path = path.resolve(path.normalize(url.pathname).replace(/^\/+/, ''));
|
const normalized_path = path.resolve(path.normalize(url.pathname).replace(/^\/+/, ''));
|
||||||
|
|
||||||
|
// if they're requesting something outside the working dir, just bail
|
||||||
if (!normalized_path.startsWith(Deno.cwd())) {
|
if (!normalized_path.startsWith(Deno.cwd())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const prechecks: PRECHECK[] = PRECHECKS[method] ?? [];
|
||||||
const stat = await Deno.stat(normalized_path);
|
|
||||||
|
|
||||||
if (stat.isFile) {
|
for await (const precheck of prechecks) {
|
||||||
const extension = path.extname(normalized_path)?.slice(1)?.toLowerCase() ?? '';
|
const error_response: Response | undefined = await precheck(request);
|
||||||
|
if (error_response) {
|
||||||
const content_type = media_types.contentType(extension) ?? 'application/octet-stream';
|
return error_response;
|
||||||
return new Response(await Deno.readFile(normalized_path), {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': content_type
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Deno.errors.NotFound) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Deno.errors.PermissionDenied) {
|
|
||||||
return new Response('Permission Denied', {
|
|
||||||
status: 400,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/plain'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await handler(request, normalized_path, server);
|
||||||
}
|
}
|
||||||
|
|
105
server.ts
105
server.ts
|
@ -10,20 +10,26 @@ const EVENTS_TO_SHUTDOWN_ON: Deno.Signal[] = ['SIGTERM', 'SIGINT'];
|
||||||
|
|
||||||
const DEFAULT_HANDLER_DIRECTORIES = [import.meta.resolve('./handlers')];
|
const DEFAULT_HANDLER_DIRECTORIES = [import.meta.resolve('./handlers')];
|
||||||
|
|
||||||
/** A `HANDLER` must take a `Request` and return a `Response` if it can handle it. */
|
/**
|
||||||
type HANDLER = (request: Request) => Promise<Response | null | undefined> | Response | null | undefined;
|
* @type HANDLER Takes a `Request` and returns a `Response` if it can properly handle the request.
|
||||||
|
*/
|
||||||
|
type HANDLER = (request: Request, server: SERVER) => Promise<Response | null | undefined> | Response | null | undefined;
|
||||||
|
|
||||||
/** A `LOGGER` must take a `Request`, a `Response`, and a `processing_time` and log it. */
|
/**
|
||||||
|
* @type LOGGER Takes a `Request`, `Response`, and a `processing_time` in ms to be logged.
|
||||||
|
*/
|
||||||
type LOGGER = (request: Request, response: Response, processing_time: number) => void | Promise<void>;
|
type LOGGER = (request: Request, response: Response, processing_time: number) => void | Promise<void>;
|
||||||
|
|
||||||
/** A `HANDLER_MODULE` must export a default method and may export an unload method to be called at shutdown. */
|
/**
|
||||||
|
* @interface HANDLER_MODULE Handler modules should export a default method (handler) and an optional `unload` method to be called at shutdown.
|
||||||
|
*/
|
||||||
interface HANDLER_MODULE {
|
interface HANDLER_MODULE {
|
||||||
default: HANDLER;
|
default: HANDLER;
|
||||||
unload?: () => void | Promise<void>;
|
unload?: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface defining the configuration for a serverus server
|
* @type SERVER_OPTIONS Specifies options for creating the SERVERUS server.
|
||||||
*
|
*
|
||||||
* @property {string} [hostname='localhost'] - hostname to bind to
|
* @property {string} [hostname='localhost'] - hostname to bind to
|
||||||
* @property {number} [port=8000] - port to bind to
|
* @property {number} [port=8000] - port to bind to
|
||||||
|
@ -51,7 +57,7 @@ export const DEFAULT_SERVER_OPTIONS: SERVER_OPTIONS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default logger
|
* @method LOG_REQUEST Default request logger.
|
||||||
*
|
*
|
||||||
* @param {Request} request - the incoming request
|
* @param {Request} request - the incoming request
|
||||||
* @param {Response} response - the outgoing response
|
* @param {Response} response - the outgoing response
|
||||||
|
@ -71,17 +77,20 @@ function LOG_REQUEST(request: Request, response: Response, time: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* serverus SERVER
|
* @class SERVER Loads all handlers found in the [semi-]colon separated list of directories in `SERVERUS_ROOT`.
|
||||||
*
|
|
||||||
* Loads all handlers found in the [semi-]colon separated list of directories in
|
|
||||||
*/
|
*/
|
||||||
export class SERVER {
|
export class SERVER {
|
||||||
private options: SERVER_OPTIONS;
|
private options: SERVER_OPTIONS;
|
||||||
private server: Deno.HttpServer | undefined;
|
private server: Deno.HttpServer | undefined;
|
||||||
private controller: AbortController | undefined;
|
private controller: AbortController | undefined;
|
||||||
private shutdown_binding: (() => void) | undefined;
|
private shutdown_binding: (() => void) | undefined;
|
||||||
private handlers: HANDLER_MODULE[];
|
|
||||||
private original_directory: string | undefined;
|
private original_directory: string | undefined;
|
||||||
|
private event_listeners: Record<string, []>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @member handlers The HANDLER_MODULEs loaded for this server.
|
||||||
|
*/
|
||||||
|
public handlers: HANDLER_MODULE[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {SERVER_OPTIONS} (optional) options to configure the server
|
* @param {SERVER_OPTIONS} (optional) options to configure the server
|
||||||
|
@ -92,6 +101,7 @@ export class SERVER {
|
||||||
...(options ?? {})
|
...(options ?? {})
|
||||||
};
|
};
|
||||||
this.handlers = [];
|
this.handlers = [];
|
||||||
|
this.event_listeners = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -163,7 +173,7 @@ export class SERVER {
|
||||||
: (this.options.logging ? LOG_REQUEST : undefined);
|
: (this.options.logging ? LOG_REQUEST : undefined);
|
||||||
|
|
||||||
for (const handler_module of this.handlers) {
|
for (const handler_module of this.handlers) {
|
||||||
const response = await handler_module.default(request);
|
const response = await handler_module.default(request, this);
|
||||||
if (response) {
|
if (response) {
|
||||||
logger?.(request, response, Date.now() - request_time);
|
logger?.(request, response, Date.now() - request_time);
|
||||||
return response;
|
return response;
|
||||||
|
@ -189,6 +199,8 @@ export class SERVER {
|
||||||
// Deno.watchFs;
|
// Deno.watchFs;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
this.emit('started', {});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,6 +208,8 @@ export class SERVER {
|
||||||
* Stop the server
|
* Stop the server
|
||||||
*/
|
*/
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
|
this.emit('stopping', {});
|
||||||
|
|
||||||
if (this.server) {
|
if (this.server) {
|
||||||
this.server.finished.finally(() => {
|
this.server.finished.finally(() => {
|
||||||
if (this.shutdown_binding) {
|
if (this.shutdown_binding) {
|
||||||
|
@ -226,5 +240,74 @@ export class SERVER {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.handlers = [];
|
this.handlers = [];
|
||||||
|
|
||||||
|
this.emit('stopped', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an event listener.
|
||||||
|
*
|
||||||
|
* @param {string} event The event to listen for.
|
||||||
|
* @param {(event_data: any) => void} handler The handler for the event.
|
||||||
|
*/
|
||||||
|
public on(event: string, handler: (event_data: any) => void) {
|
||||||
|
const listeners: ((event: any) => void)[] = this.event_listeners[event] = this.event_listeners[event] ?? [];
|
||||||
|
if (!listeners.includes(handler)) {
|
||||||
|
listeners.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Deno.env.get('SERVERUS_LOG_EVENTS')) {
|
||||||
|
console.dir({
|
||||||
|
on: {
|
||||||
|
event,
|
||||||
|
handler
|
||||||
|
},
|
||||||
|
listeners
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an event listener.
|
||||||
|
*
|
||||||
|
* @param {string} event The event that was listened to.
|
||||||
|
* @param {(event_data: any) => void} handler The handler that was registered that should be removed.
|
||||||
|
*/
|
||||||
|
public off(event: string, handler: (event_data: any) => void) {
|
||||||
|
const listeners: ((event: any) => void)[] = this.event_listeners[event] = this.event_listeners[event] ?? [];
|
||||||
|
if (listeners.includes(handler)) {
|
||||||
|
listeners.splice(listeners.indexOf(handler), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Deno.env.get('SERVERUS_LOG_EVENTS')) {
|
||||||
|
console.dir({
|
||||||
|
off: {
|
||||||
|
event: event,
|
||||||
|
handler
|
||||||
|
},
|
||||||
|
listeners
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit(event_name: string, event_data: any) {
|
||||||
|
const listeners: ((event: any) => void)[] = this.event_listeners[event_name] = this.event_listeners[event_name] ?? [];
|
||||||
|
const wildcard_listeners: ((event: any) => void)[] = this.event_listeners['*'] = this.event_listeners['*'] ?? [];
|
||||||
|
const all_listeners: ((event: any) => void)[] = [...listeners, ...wildcard_listeners];
|
||||||
|
|
||||||
|
if (Deno.env.get('SERVERUS_LOG_EVENTS')) {
|
||||||
|
console.dir({
|
||||||
|
emitting: {
|
||||||
|
event_name,
|
||||||
|
event_data,
|
||||||
|
listeners,
|
||||||
|
wildcard_listeners
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listener of all_listeners) {
|
||||||
|
listener(event_data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
import * as asserts from '@std/assert';
|
|
||||||
import { EPHEMERAL_SERVER, get_ephemeral_listen_server } from './helpers.ts';
|
|
||||||
|
|
||||||
Deno.test({
|
|
||||||
name: 'get static file',
|
|
||||||
permissions: {
|
|
||||||
env: true,
|
|
||||||
read: true,
|
|
||||||
write: true,
|
|
||||||
net: true
|
|
||||||
},
|
|
||||||
fn: async () => {
|
|
||||||
let test_server_info: EPHEMERAL_SERVER | null = null;
|
|
||||||
const cwd = Deno.cwd();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Deno.chdir('./tests/www');
|
|
||||||
test_server_info = await get_ephemeral_listen_server();
|
|
||||||
|
|
||||||
const response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/test.txt`, {
|
|
||||||
method: 'GET'
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = await response.text();
|
|
||||||
|
|
||||||
asserts.assert(response.ok);
|
|
||||||
asserts.assert(body);
|
|
||||||
asserts.assertEquals(body, 'this is a test\n');
|
|
||||||
} finally {
|
|
||||||
Deno.chdir(cwd);
|
|
||||||
if (test_server_info) {
|
|
||||||
await test_server_info?.server?.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test({
|
|
||||||
name: 'other methods than GET should not work on static files',
|
|
||||||
permissions: {
|
|
||||||
env: true,
|
|
||||||
read: true,
|
|
||||||
write: true,
|
|
||||||
net: true
|
|
||||||
},
|
|
||||||
fn: async () => {
|
|
||||||
let test_server_info: EPHEMERAL_SERVER | null = null;
|
|
||||||
const cwd = Deno.cwd();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Deno.chdir('./tests/www');
|
|
||||||
test_server_info = await get_ephemeral_listen_server();
|
|
||||||
|
|
||||||
for await (const method of ['POST', 'PUT', 'PATCH', 'DELETE']) {
|
|
||||||
const response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/test.txt`, {
|
|
||||||
method,
|
|
||||||
body: method === 'DELETE' ? undefined : JSON.stringify({})
|
|
||||||
});
|
|
||||||
|
|
||||||
asserts.assert(!response.ok);
|
|
||||||
|
|
||||||
const body = await response.json();
|
|
||||||
asserts.assert(body);
|
|
||||||
|
|
||||||
asserts.assertEquals(body, {
|
|
||||||
error: {
|
|
||||||
cause: 'not_found',
|
|
||||||
message: 'Not found'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Deno.chdir(cwd);
|
|
||||||
if (test_server_info) {
|
|
||||||
await test_server_info?.server?.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
521
tests/01_static_files.test.ts
Normal file
521
tests/01_static_files.test.ts
Normal file
|
@ -0,0 +1,521 @@
|
||||||
|
import * as asserts from '@std/assert';
|
||||||
|
import * as fs from '@std/fs';
|
||||||
|
import * as path from '@std/path';
|
||||||
|
import { EPHEMERAL_SERVER, get_ephemeral_listen_server } from './helpers.ts';
|
||||||
|
import { ensureFile } from '@std/fs/ensure-file';
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: 'GET static file',
|
||||||
|
permissions: {
|
||||||
|
env: true,
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
net: true
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
let test_server_info: EPHEMERAL_SERVER | null = null;
|
||||||
|
const cwd = Deno.cwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Deno.chdir('./tests/www');
|
||||||
|
test_server_info = await get_ephemeral_listen_server();
|
||||||
|
|
||||||
|
const response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/test.txt`, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await response.text();
|
||||||
|
|
||||||
|
asserts.assert(response.ok);
|
||||||
|
asserts.assert(body);
|
||||||
|
asserts.assertEquals(body, 'this is a test\n');
|
||||||
|
} finally {
|
||||||
|
Deno.chdir(cwd);
|
||||||
|
if (test_server_info) {
|
||||||
|
await test_server_info?.server?.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: 'HEAD static file',
|
||||||
|
permissions: {
|
||||||
|
env: true,
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
net: true
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
let test_server_info: EPHEMERAL_SERVER | null = null;
|
||||||
|
const cwd = Deno.cwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Deno.chdir('./tests/www');
|
||||||
|
test_server_info = await get_ephemeral_listen_server();
|
||||||
|
|
||||||
|
const response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/test.txt`, {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
|
||||||
|
asserts.assert(response.ok);
|
||||||
|
asserts.assert(response.headers);
|
||||||
|
asserts.assertEquals(response.headers.get('Content-Length'), '15');
|
||||||
|
} finally {
|
||||||
|
Deno.chdir(cwd);
|
||||||
|
if (test_server_info) {
|
||||||
|
await test_server_info?.server?.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: 'OPTIONS static file',
|
||||||
|
permissions: {
|
||||||
|
env: true,
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
net: true
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
let test_server_info: EPHEMERAL_SERVER | null = null;
|
||||||
|
const cwd = Deno.cwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Deno.chdir('./tests/www');
|
||||||
|
test_server_info = await get_ephemeral_listen_server();
|
||||||
|
|
||||||
|
const response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/test.txt`, {
|
||||||
|
method: 'OPTIONS'
|
||||||
|
});
|
||||||
|
|
||||||
|
await response.text();
|
||||||
|
|
||||||
|
asserts.assert(response.ok);
|
||||||
|
asserts.assert(response.headers);
|
||||||
|
asserts.assertEquals(response.headers.get('Allow'), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT'].join(','));
|
||||||
|
} finally {
|
||||||
|
Deno.chdir(cwd);
|
||||||
|
if (test_server_info) {
|
||||||
|
await test_server_info?.server?.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: 'allow PUT to static files if SERVERUS_PUT_PATHS_ALLOWED is set',
|
||||||
|
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.set('SERVERUS_PUT_PATHS_ALLOWED', path.join(Deno.cwd(), 'files'));
|
||||||
|
|
||||||
|
test_server_info = await get_ephemeral_listen_server();
|
||||||
|
|
||||||
|
const put_body = new FormData();
|
||||||
|
let test_file: File | undefined = new File(['this is a test PUT upload'], 'test_put_upload.txt');
|
||||||
|
put_body.append('file', test_file);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
const put_response_body = await put_response.json();
|
||||||
|
asserts.assert(put_response_body);
|
||||||
|
asserts.assert(put_response_body.written);
|
||||||
|
asserts.assertEquals(
|
||||||
|
put_response_body.written?.[0],
|
||||||
|
`http://${test_server_info.hostname}:${test_server_info.port}/files/test_put_upload.txt`
|
||||||
|
);
|
||||||
|
|
||||||
|
put_body.delete('file');
|
||||||
|
test_file = undefined;
|
||||||
|
|
||||||
|
const get_response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/files/test_put_upload.txt`, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
asserts.assert(get_response.ok);
|
||||||
|
asserts.assert(get_response.body);
|
||||||
|
|
||||||
|
const local_download_path = path.join(Deno.cwd(), 'files', 'test_put_upload.txt-downloaded');
|
||||||
|
await ensureFile(local_download_path);
|
||||||
|
|
||||||
|
const file = await Deno.open(local_download_path, { truncate: true, write: true });
|
||||||
|
await get_response.body.pipeTo(file.writable);
|
||||||
|
|
||||||
|
const download_content = await Deno.readTextFile(local_download_path);
|
||||||
|
asserts.assert(download_content);
|
||||||
|
asserts.assertEquals(download_content, 'this is a test PUT upload');
|
||||||
|
|
||||||
|
await Deno.remove(local_download_path);
|
||||||
|
asserts.assert(!fs.existsSync(local_download_path));
|
||||||
|
|
||||||
|
const local_upload_path = path.join(Deno.cwd(), 'files', 'test_put_upload.txt');
|
||||||
|
asserts.assert(fs.existsSync(local_upload_path));
|
||||||
|
|
||||||
|
const stat = await Deno.lstat(local_upload_path);
|
||||||
|
asserts.assert(stat);
|
||||||
|
asserts.assert(stat.isFile);
|
||||||
|
asserts.assertEquals(stat.size, 25);
|
||||||
|
asserts.assertEquals(stat.mode! & 0o777, 0o755);
|
||||||
|
|
||||||
|
await Deno.remove(local_upload_path);
|
||||||
|
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 (test_server_info) {
|
||||||
|
await test_server_info?.server?.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: 'allow PUT to multiple files in a directory',
|
||||||
|
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.set('SERVERUS_PUT_PATHS_ALLOWED', path.join(Deno.cwd(), 'files'));
|
||||||
|
|
||||||
|
test_server_info = await get_ephemeral_listen_server();
|
||||||
|
|
||||||
|
const put_body = new FormData();
|
||||||
|
|
||||||
|
for (const i of [1, 2, 3]) {
|
||||||
|
put_body.append('file', new File([`this is a test PUT upload ${i}`], `test_put_upload_${i}.txt`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const put_response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/files/test_multiple/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: put_body
|
||||||
|
});
|
||||||
|
|
||||||
|
asserts.assert(put_response.ok);
|
||||||
|
|
||||||
|
const put_response_body = await put_response.json();
|
||||||
|
asserts.assert(put_response_body);
|
||||||
|
asserts.assert(put_response_body.written);
|
||||||
|
|
||||||
|
for await (const i of [1, 2, 3]) {
|
||||||
|
const url = put_response_body.written?.[i - 1];
|
||||||
|
|
||||||
|
asserts.assertEquals(
|
||||||
|
url,
|
||||||
|
`http://${test_server_info.hostname}:${test_server_info.port}/files/test_multiple/test_put_upload_${i}.txt`
|
||||||
|
);
|
||||||
|
|
||||||
|
const get_response = await fetch(url, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
asserts.assert(get_response.ok);
|
||||||
|
asserts.assert(get_response.body);
|
||||||
|
|
||||||
|
const local_download_path = path.join(Deno.cwd(), 'files', 'test_multiple', `test_put_upload_${i}.txt-downloaded`);
|
||||||
|
await ensureFile(local_download_path);
|
||||||
|
|
||||||
|
const file = await Deno.open(local_download_path, { truncate: true, write: true });
|
||||||
|
await get_response.body.pipeTo(file.writable);
|
||||||
|
|
||||||
|
const download_content = await Deno.readTextFile(local_download_path);
|
||||||
|
asserts.assert(download_content);
|
||||||
|
asserts.assertEquals(download_content, `this is a test PUT upload ${i}`);
|
||||||
|
|
||||||
|
await Deno.remove(local_download_path);
|
||||||
|
asserts.assert(!fs.existsSync(local_download_path));
|
||||||
|
|
||||||
|
const local_upload_path = path.join(Deno.cwd(), 'files', 'test_multiple', `test_put_upload_${i}.txt`);
|
||||||
|
asserts.assert(fs.existsSync(local_upload_path));
|
||||||
|
|
||||||
|
const stat = await Deno.lstat(local_upload_path);
|
||||||
|
asserts.assert(stat);
|
||||||
|
asserts.assert(stat.isFile);
|
||||||
|
asserts.assertEquals(stat.size, 27);
|
||||||
|
asserts.assertEquals(stat.mode! & 0o777, 0o755);
|
||||||
|
|
||||||
|
await Deno.remove(local_upload_path);
|
||||||
|
asserts.assert(!fs.existsSync(local_upload_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploads_directory = path.join(Deno.cwd(), 'files', 'test_multiple');
|
||||||
|
await Deno.remove(uploads_directory);
|
||||||
|
asserts.assert(!fs.existsSync(uploads_directory));
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: 'allow DELETE to static files if SERVERUS_DELETE_PATHS_ALLOWED is set',
|
||||||
|
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_DELETE_PATHS_ALLOWED = Deno.env.get('SERVERUS_DELETE_PATHS_ALLOWED');
|
||||||
|
try {
|
||||||
|
Deno.chdir('./tests/www');
|
||||||
|
Deno.env.set('SERVERUS_DELETE_PATHS_ALLOWED', path.join(Deno.cwd(), 'files'));
|
||||||
|
|
||||||
|
test_server_info = await get_ephemeral_listen_server();
|
||||||
|
|
||||||
|
const put_body = new FormData();
|
||||||
|
let test_file: File | undefined = new File(['this is a test DELETE upload'], 'test_delete_upload.txt');
|
||||||
|
put_body.append('file', test_file);
|
||||||
|
|
||||||
|
const put_response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/files/test_delete_upload.txt`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: put_body
|
||||||
|
});
|
||||||
|
|
||||||
|
asserts.assert(put_response.ok);
|
||||||
|
|
||||||
|
const put_response_body = await put_response.json();
|
||||||
|
asserts.assert(put_response_body);
|
||||||
|
asserts.assert(put_response_body.written);
|
||||||
|
asserts.assertEquals(
|
||||||
|
put_response_body.written?.[0],
|
||||||
|
`http://${test_server_info.hostname}:${test_server_info.port}/files/test_delete_upload.txt`
|
||||||
|
);
|
||||||
|
|
||||||
|
put_body.delete('file');
|
||||||
|
test_file = undefined;
|
||||||
|
|
||||||
|
const get_response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/files/test_delete_upload.txt`, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
asserts.assert(get_response.ok);
|
||||||
|
asserts.assert(get_response.body);
|
||||||
|
|
||||||
|
const local_download_path = path.join(Deno.cwd(), 'files', 'test_delete_upload.txt-downloaded');
|
||||||
|
await ensureFile(local_download_path);
|
||||||
|
|
||||||
|
const file = await Deno.open(local_download_path, { truncate: true, write: true });
|
||||||
|
await get_response.body.pipeTo(file.writable);
|
||||||
|
|
||||||
|
const download_content = await Deno.readTextFile(local_download_path);
|
||||||
|
asserts.assert(download_content);
|
||||||
|
asserts.assertEquals(download_content, 'this is a test DELETE upload');
|
||||||
|
|
||||||
|
await Deno.remove(local_download_path);
|
||||||
|
asserts.assert(!fs.existsSync(local_download_path));
|
||||||
|
|
||||||
|
const local_upload_path = path.join(Deno.cwd(), 'files', 'test_delete_upload.txt');
|
||||||
|
asserts.assert(fs.existsSync(local_upload_path));
|
||||||
|
|
||||||
|
const stat = await Deno.lstat(local_upload_path);
|
||||||
|
asserts.assert(stat);
|
||||||
|
asserts.assert(stat.isFile);
|
||||||
|
asserts.assertEquals(stat.size, 28);
|
||||||
|
asserts.assertEquals(stat.mode! & 0o777, 0o755);
|
||||||
|
|
||||||
|
const delete_response = await fetch(
|
||||||
|
`http://${test_server_info.hostname}:${test_server_info.port}/files/test_delete_upload.txt`,
|
||||||
|
{
|
||||||
|
method: 'DELETE'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
asserts.assert(delete_response.ok);
|
||||||
|
|
||||||
|
asserts.assert(!fs.existsSync(local_upload_path));
|
||||||
|
} finally {
|
||||||
|
Deno.chdir(cwd);
|
||||||
|
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: 'allow DELETE directory',
|
||||||
|
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_DELETE_PATHS_ALLOWED = Deno.env.get('SERVERUS_DELETE_PATHS_ALLOWED');
|
||||||
|
try {
|
||||||
|
Deno.chdir('./tests/www');
|
||||||
|
Deno.env.set('SERVERUS_DELETE_PATHS_ALLOWED', path.join(Deno.cwd(), 'files'));
|
||||||
|
|
||||||
|
test_server_info = await get_ephemeral_listen_server();
|
||||||
|
|
||||||
|
const put_body = new FormData();
|
||||||
|
let test_file: File | undefined = new File(['this is a test DELETE upload'], 'test_delete_directory_upload.txt');
|
||||||
|
put_body.append('file', test_file);
|
||||||
|
|
||||||
|
const put_response = await fetch(
|
||||||
|
`http://${test_server_info.hostname}:${test_server_info.port}/files/delete_directory_test/test_delete_directory_upload.txt`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: put_body
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
asserts.assert(put_response.ok);
|
||||||
|
|
||||||
|
const put_response_body = await put_response.json();
|
||||||
|
asserts.assert(put_response_body);
|
||||||
|
asserts.assert(put_response_body.written);
|
||||||
|
asserts.assertEquals(
|
||||||
|
put_response_body.written?.[0],
|
||||||
|
`http://${test_server_info.hostname}:${test_server_info.port}/files/delete_directory_test/test_delete_directory_upload.txt`
|
||||||
|
);
|
||||||
|
|
||||||
|
put_body.delete('file');
|
||||||
|
test_file = undefined;
|
||||||
|
|
||||||
|
const local_upload_path = path.join(Deno.cwd(), 'files', 'delete_directory_test', 'test_delete_directory_upload.txt');
|
||||||
|
asserts.assert(fs.existsSync(local_upload_path));
|
||||||
|
|
||||||
|
const stat = await Deno.lstat(local_upload_path);
|
||||||
|
asserts.assert(stat);
|
||||||
|
asserts.assert(stat.isFile);
|
||||||
|
asserts.assertEquals(stat.size, 28);
|
||||||
|
asserts.assertEquals(stat.mode! & 0o777, 0o755);
|
||||||
|
|
||||||
|
const delete_response = await fetch(
|
||||||
|
`http://${test_server_info.hostname}:${test_server_info.port}/files/delete_directory_test`,
|
||||||
|
{
|
||||||
|
method: 'DELETE'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
asserts.assert(delete_response.ok);
|
||||||
|
|
||||||
|
asserts.assert(!fs.existsSync(local_upload_path));
|
||||||
|
asserts.assert(!fs.existsSync(path.dirname(local_upload_path)));
|
||||||
|
} finally {
|
||||||
|
Deno.chdir(cwd);
|
||||||
|
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: 'these methods should not work on static files',
|
||||||
|
permissions: {
|
||||||
|
env: true,
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
net: true
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
let test_server_info: EPHEMERAL_SERVER | null = null;
|
||||||
|
const cwd = Deno.cwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Deno.chdir('./tests/www');
|
||||||
|
test_server_info = await get_ephemeral_listen_server();
|
||||||
|
|
||||||
|
for await (const method of ['POST', 'PATCH']) {
|
||||||
|
const response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/test.txt`, {
|
||||||
|
method,
|
||||||
|
body: method === 'TRACE' ? undefined : JSON.stringify({})
|
||||||
|
});
|
||||||
|
|
||||||
|
asserts.assert(!response.ok);
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
asserts.assert(body);
|
||||||
|
|
||||||
|
asserts.assertEquals(body, {
|
||||||
|
error: {
|
||||||
|
cause: 'not_found',
|
||||||
|
message: 'Not found'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Deno.chdir(cwd);
|
||||||
|
if (test_server_info) {
|
||||||
|
await test_server_info?.server?.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue