Skip to content

Commit

Permalink
fix: fix security issue and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
learosema committed Oct 19, 2024
1 parent 5b53fdd commit fac2cb6
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 30 deletions.
69 changes: 39 additions & 30 deletions src/httpd.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import EventEmitter from 'events';
import { readFile } from 'fs';
import { createServer, IncomingMessage, ServerResponse } from 'http';
import { createServer, IncomingMessage, ServerResponse } from 'node:http';
import path from 'node:path';
import url from "node:url";

import { getMime } from './mimes.js'

Expand Down Expand Up @@ -61,38 +60,48 @@ function serverSentEvents(req, res, eventEmitter) {
req.on('close', () => eventEmitter.off('watch-event', watchListener));
}

export function serve(eventEmitter = null, wwwRoot = 'dist') {
const port = process.env.PORT || 8000;
console.log(`[http]\tServer listening on http://localhost:${port}/`);
createServer((req, res) => {
const uri = url.parse(req.url).pathname;
const { send, sendError } = sendFactory(req, res);
if (eventEmitter && uri === '/_dev-events') {
serverSentEvents(req, res, eventEmitter);
return;
}
if (uri === '/_dev-events.js') {
send(200, DEVSERVER_JS, 'text/javascript');
return;
}
const dir = path.resolve(process.cwd(), wwwRoot);
const resourcePath = path.normalize(uri + (uri.endsWith('/') ? 'index.html' : ''));
if (resourcePath.split('/').includes('..')) {
sendError(404, 'Not Found');
return;
}
const filePath = path.join(dir, resourcePath);
readFile(filePath, (err, data) => {
if (err) {
export function serve(eventEmitter = null, wwwRoot = 'dist', listenOptions) {
return new Promise((resolve) => {
const host = listenOptions?.host ?? process.env.HOST ?? 'localhost';
const port = listenOptions?.port ?? parseInt(process.env.PORT ?? '8000', 10);
const server = createServer((req, res) => {
const url = new URL(`http://${host}${port !== 80?`:${port}`:''}${req.url}`);
const { send, sendError } = sendFactory(req, res);
if (eventEmitter && url.pathname === '/_dev-events') {
serverSentEvents(req, res, eventEmitter);
return;
}
if (url.pathname === '/_dev-events.js') {
send(200, DEVSERVER_JS, 'text/javascript');
return;
}
const dir = path.resolve(process.cwd(), wwwRoot);
const resourcePath = path.normalize(url.pathname + (url.pathname.endsWith('/') ? 'index.html' : ''));
if (resourcePath.split('/').includes('..')) {
sendError(404, 'Not Found');
return;
}
const mime = getMime(resourcePath);
if (data && mime === 'text/html') {
send(200, data.toString().replace('</body>', '<script src="/_dev-events.js"></script></body>'), mime);
const filePath = path.join(dir, path.normalize(resourcePath));
if (! filePath.startsWith(dir)) {
sendError(404, 'Not Found');
return;
}
send(200, data, mime);
readFile(filePath, (err, data) => {
if (err) {
sendError(404, 'Not Found');
return;
}
const mime = getMime(resourcePath);
if (data && mime === 'text/html') {
send(200, data.toString().replace('</body>', '<script src="/_dev-events.js"></script></body>'), mime);
return;
}
send(200, data, mime);
});
});
}).listen(port);
server.listen({port, host, ...(listenOptions ?? {})}, () => {
console.log(`[http]\tServer listening on http://${host}:${port}/`);
resolve(server);
});
});
}
1 change: 1 addition & 0 deletions tests/fixtures/httpd/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/fixtures/httpd/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>It works!</h1>
64 changes: 64 additions & 0 deletions tests/httpd.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { serve } from '../src/httpd.js';



describe('httpd tests', () => {

let counter = 0;

function getListenOptions() {
const port = 11551 + counter++; // rolled a dice.
const host = 'localhost';
const killSwitch = new AbortController();
return [killSwitch, {host, port, signal: killSwitch.signal}]
}

test('basic functionality', async () => {
const [killSwitch, options] = getListenOptions();
await serve(null, 'tests/fixtures/httpd', options);

const requestTests = [
{
url: '/',
expected: [200, 'text/html', '<h1>It works!</h1>\n'],
},
{
url: '/favicon.svg',
expected: [200, 'image/svg+xml', '<svg></svg>\n'],
},
{
url: '/../../../../etc/passwd',
expected: [404, 'text/html', '404 Not Found'],
},
{
url: '/_dev-events.js',
expected: [200, 'text/javascript'],
},
{
url: '/_dev-events',
expected: [404, 'text/html', '404 Not Found'],
}
];

for (const reqTest of requestTests) {
const response = await fetch(`http://${options.host}:${options.port}${reqTest.url}`);
const text = await response.text();
reqTest.actual = [response.status, response.headers.get('content-type'), text];
}

killSwitch.abort();

for (const reqTest of requestTests) {
const [actualCode, actualType, actualBody] = reqTest.actual;
const [expectedCode, expectedType, expectedBody] = reqTest.expected;

assert.equal(actualCode, expectedCode, new Error(`request ${reqTest.url} should return ${expectedCode}, is ${actualCode}`));
assert.equal(actualType, expectedType);
if (expectedBody) {
assert.equal(actualBody, expectedBody);
}
}
});
});

0 comments on commit fac2cb6

Please sign in to comment.