mirror of
https://github.com/Noratrieb/h2.js.git
synced 2026-01-14 09:55:03 +01:00
do things
This commit is contained in:
parent
9adad6436c
commit
7faf8fc164
2 changed files with 375 additions and 58 deletions
5
Caddyfile
Normal file
5
Caddyfile
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# caddy run --config Caddyfile
|
||||||
|
|
||||||
|
http://localhost:8081 {
|
||||||
|
reverse_proxy * h2c://localhost:8080
|
||||||
|
}
|
||||||
428
h2.mjs
428
h2.mjs
|
|
@ -1,5 +1,9 @@
|
||||||
import * as net from "node:net";
|
import * as net from "node:net";
|
||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const debug = process.env.DEBUG?.includes("h2.js");
|
||||||
|
|
||||||
const buildHuffmanTree = (values) => {
|
const buildHuffmanTree = (values) => {
|
||||||
const root = [];
|
const root = [];
|
||||||
|
|
@ -285,8 +289,6 @@ const HUFFMAN_TREE = buildHuffmanTree([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
class HPackCtx {
|
class HPackCtx {
|
||||||
#dynamicTable;
|
|
||||||
|
|
||||||
static #STATIC_TABLE = {
|
static #STATIC_TABLE = {
|
||||||
1: [":authority", ""],
|
1: [":authority", ""],
|
||||||
2: [":method", "GET"],
|
2: [":method", "GET"],
|
||||||
|
|
@ -350,16 +352,42 @@ class HPackCtx {
|
||||||
60: ["via", ""],
|
60: ["via", ""],
|
||||||
61: ["www-authenticate", ""],
|
61: ["www-authenticate", ""],
|
||||||
};
|
};
|
||||||
static #STATIC_TABLE_MAX = 61;
|
static #STATIC_TABLE_MAX = 62;
|
||||||
|
|
||||||
|
#maxDynamicTableLength;
|
||||||
|
#dynamicTable;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.#dynamicTable = {};
|
this.#dynamicTable = [];
|
||||||
|
this.#maxDynamicTableLength = 4096;
|
||||||
}
|
}
|
||||||
|
|
||||||
#indexTable = (index) => {
|
#indexTable = (index) => {
|
||||||
return index < HPackCtx.#STATIC_TABLE_MAX
|
const value =
|
||||||
? HPackCtx.#STATIC_TABLE[index]
|
index < HPackCtx.#STATIC_TABLE_MAX
|
||||||
: this.#dynamicTable[index - HPackCtx.#STATIC_TABLE_MAX];
|
? HPackCtx.#STATIC_TABLE[index]
|
||||||
|
: this.#dynamicTable[index - HPackCtx.#STATIC_TABLE_MAX];
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Failed to find index in HPACK tables ${index}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
#insertDynamicTable = (value) => {
|
||||||
|
this.#dynamicTable.splice(0, 0, value);
|
||||||
|
|
||||||
|
let tableSize = 0;
|
||||||
|
const tooBigIndex = this.#dynamicTable.findIndex((entry) => {
|
||||||
|
const entrySize = entry[0].length + entry[1].lenght + 32;
|
||||||
|
tableSize += entrySize;
|
||||||
|
return tableSize > this.#maxDynamicTableLength;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tooBigIndex !== -1) {
|
||||||
|
this.#dynamicTable.length = tooBigIndex;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
decode = (block) => {
|
decode = (block) => {
|
||||||
|
|
@ -376,12 +404,25 @@ class HPackCtx {
|
||||||
let field;
|
let field;
|
||||||
|
|
||||||
const decodeInteger = (mask) => {
|
const decodeInteger = (mask) => {
|
||||||
const int = block[size] & (0xff >> mask);
|
let int = block[size] & (0xff >> mask);
|
||||||
|
|
||||||
|
size += 1;
|
||||||
|
|
||||||
if (int === 0xff >> mask) {
|
if (int === 0xff >> mask) {
|
||||||
throw new Error("long integer, todo");
|
// large int
|
||||||
|
|
||||||
|
let next;
|
||||||
|
let m = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
next = block[size++];
|
||||||
|
const val = next & 0b0111_1111;
|
||||||
|
int = int + val * 2 ** m;
|
||||||
|
m += 7;
|
||||||
|
} while (next & 0b1000_0000);
|
||||||
|
|
||||||
|
return int;
|
||||||
}
|
}
|
||||||
size += 1;
|
|
||||||
return int;
|
return int;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -432,7 +473,6 @@ class HPackCtx {
|
||||||
// Indexed Header Field Representation
|
// Indexed Header Field Representation
|
||||||
if (firstBit) {
|
if (firstBit) {
|
||||||
const index = decodeInteger(1);
|
const index = decodeInteger(1);
|
||||||
|
|
||||||
const tabled = this.#indexTable(index);
|
const tabled = this.#indexTable(index);
|
||||||
|
|
||||||
field = tabled;
|
field = tabled;
|
||||||
|
|
@ -451,8 +491,28 @@ class HPackCtx {
|
||||||
const headerValue = decodeString();
|
const headerValue = decodeString();
|
||||||
|
|
||||||
field = [headerName, headerValue];
|
field = [headerName, headerValue];
|
||||||
|
|
||||||
|
this.#insertDynamicTable(field);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("some other encoding");
|
if (thirdBit) {
|
||||||
|
throw new Error("dynamic thing");
|
||||||
|
} else {
|
||||||
|
// Literal Header Field Never Indexed
|
||||||
|
// Literal Header Field without Indexing
|
||||||
|
|
||||||
|
const index = decodeInteger(4);
|
||||||
|
|
||||||
|
let headerName;
|
||||||
|
if (index === 0) {
|
||||||
|
headerName = decodeString();
|
||||||
|
} else {
|
||||||
|
headerName = this.#indexTable(index)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerValue = decodeString();
|
||||||
|
|
||||||
|
field = [headerName, headerValue];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -474,6 +534,10 @@ class HPackCtx {
|
||||||
|
|
||||||
// TODO: squimsh the bytes
|
// TODO: squimsh the bytes
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
|
if (typeof field[0] !== "string" || typeof field[1] !== "string") {
|
||||||
|
throw new Error("invalid type of header");
|
||||||
|
}
|
||||||
|
|
||||||
// let's just pick 6.2.1. Literal Header Field with Incremental Indexing
|
// let's just pick 6.2.1. Literal Header Field with Incremental Indexing
|
||||||
block = Buffer.concat([block, Buffer.from([64])]);
|
block = Buffer.concat([block, Buffer.from([64])]);
|
||||||
|
|
||||||
|
|
@ -499,6 +563,7 @@ const reverseMap = (c) =>
|
||||||
|
|
||||||
const FRAME_HEADER_SIZE = 3 + 1 + 1 + 4;
|
const FRAME_HEADER_SIZE = 3 + 1 + 1 + 4;
|
||||||
|
|
||||||
|
const PREFANCE_RECEIVED_FRAME_TYPE = Symbol("PREFACE_RECEIVED");
|
||||||
const FRAME_TYPE = {
|
const FRAME_TYPE = {
|
||||||
DATA: 0x0,
|
DATA: 0x0,
|
||||||
HEADERS: 0x1,
|
HEADERS: 0x1,
|
||||||
|
|
@ -531,6 +596,31 @@ const HEADERS_FLAG = {
|
||||||
};
|
};
|
||||||
const HEADERS_FLAG_NAME = reverseMap(HEADERS_FLAG);
|
const HEADERS_FLAG_NAME = reverseMap(HEADERS_FLAG);
|
||||||
|
|
||||||
|
const DATA_FLAG = {
|
||||||
|
PADDED: 0x08,
|
||||||
|
END_STREAM: 0x01,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ERROR_CODE = {
|
||||||
|
NO_ERROR: 0,
|
||||||
|
PROTOCOL_ERROR: 0x01,
|
||||||
|
INTERNAL_ERROR: 0x02,
|
||||||
|
FLOW_CONTROL_ERROR: 0x03,
|
||||||
|
SETTINGS_TIMEOUT: 0x04,
|
||||||
|
STREAM_CLOSED: 0x05,
|
||||||
|
FRAME_SIZE_ERROR: 0x06,
|
||||||
|
REFUSED_STREAM: 0x07,
|
||||||
|
CANCEL: 0x08,
|
||||||
|
COMPRESSION_ERROR: 0x09,
|
||||||
|
CONNECT_ERROR: 0x0a,
|
||||||
|
ENHANCE_YOUR_CALM: 0x0b,
|
||||||
|
INADEQUATE_SECURITY: 0x0c,
|
||||||
|
HTTP_1_1_REQUIRED: 0x0d,
|
||||||
|
};
|
||||||
|
const ERROR_CODE_NAME = reverseMap(ERROR_CODE);
|
||||||
|
|
||||||
|
class Http11Error extends Error {}
|
||||||
|
|
||||||
const frameReader = (frameCb) => {
|
const frameReader = (frameCb) => {
|
||||||
const STATE = {
|
const STATE = {
|
||||||
PREFACE: 0,
|
PREFACE: 0,
|
||||||
|
|
@ -558,10 +648,29 @@ const frameReader = (frameCb) => {
|
||||||
const preface = buf.subarray(0, 24);
|
const preface = buf.subarray(0, 24);
|
||||||
buf = buf.subarray(24);
|
buf = buf.subarray(24);
|
||||||
if (Buffer.compare(preface, CONNECTION_PREFACE) !== 0) {
|
if (Buffer.compare(preface, CONNECTION_PREFACE) !== 0) {
|
||||||
frameCb(new Error(`invalid preface from ${peer}`));
|
try {
|
||||||
|
const decoded = new TextDecoder().decode(preface);
|
||||||
|
if (
|
||||||
|
decoded.includes("HTTP/1.1") ||
|
||||||
|
/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|TRACE) /.test(decoded)
|
||||||
|
) {
|
||||||
|
frameCb(new Http11Error());
|
||||||
|
} else {
|
||||||
|
frameCb(new Error("invalid preface"));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
frameCb(new Error("invalid preface"));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frameCb(null, {
|
||||||
|
length: 0,
|
||||||
|
type: PREFANCE_RECEIVED_FRAME_TYPE,
|
||||||
|
flags: 0,
|
||||||
|
streamIdentifier: 0,
|
||||||
|
});
|
||||||
|
|
||||||
state = STATE.FRAME_HEAD;
|
state = STATE.FRAME_HEAD;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -654,12 +763,32 @@ const encodeFrame = (frame) => {
|
||||||
* @typedef Request
|
* @typedef Request
|
||||||
* @type {object}
|
* @type {object}
|
||||||
* @property {string} method
|
* @property {string} method
|
||||||
|
* @property {string} path
|
||||||
|
* @property {URLSearchParams} query
|
||||||
|
* @property {URL} url
|
||||||
|
* @property {string[][]} headers
|
||||||
|
* @property {object} peer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef Response
|
* @typedef ResponseWriteHeaders
|
||||||
|
* @type {string[][]}
|
||||||
|
*
|
||||||
|
* @typedef ResponseWriteHeadParam
|
||||||
* @type {object}
|
* @type {object}
|
||||||
* @property {number} status
|
* @property {number} status
|
||||||
|
* @property {ResponseWriteHeaders} headers
|
||||||
|
*
|
||||||
|
* @callback ResponseWriteHead
|
||||||
|
* @param {ResponseWriteHeadParam} head
|
||||||
|
*
|
||||||
|
* @callback ResponseWriteBody
|
||||||
|
* @param {Buffer|string} body
|
||||||
|
*
|
||||||
|
* @typedef ResponseWriter
|
||||||
|
* @type {object}
|
||||||
|
* @property {ResponseWriteHead} writeHead
|
||||||
|
* @property {ResponseWriteBody} body
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const buildRequest = (rawH2Request) => {
|
const buildRequest = (rawH2Request) => {
|
||||||
|
|
@ -703,13 +832,16 @@ const buildRequest = (rawH2Request) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this is probably bad
|
||||||
|
const url = new URL(`${scheme}://${authority}${path}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
request: {
|
request: {
|
||||||
method,
|
method,
|
||||||
authority,
|
path: url.pathname,
|
||||||
path,
|
query: url.searchParams,
|
||||||
scheme,
|
url,
|
||||||
headers: rawH2Request.fields
|
headers: rawH2Request.fields
|
||||||
.filter((f) => !f[0].startsWith(":"))
|
.filter((f) => !f[0].startsWith(":"))
|
||||||
.map(([name, value]) => [name.toLowerCase(), value]),
|
.map(([name, value]) => [name.toLowerCase(), value]),
|
||||||
|
|
@ -742,24 +874,37 @@ const handleConnection =
|
||||||
const peerSettings = new Map();
|
const peerSettings = new Map();
|
||||||
const streams = new Map();
|
const streams = new Map();
|
||||||
|
|
||||||
socket.write(
|
|
||||||
encodeFrame({
|
|
||||||
type: FRAME_TYPE.SETTINGS,
|
|
||||||
flags: 0,
|
|
||||||
streamIdentifier: 0,
|
|
||||||
payload: Buffer.from([]),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const onData = frameReader((err, frame) => {
|
const onData = frameReader((err, frame) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
if (err instanceof Http11Error) {
|
||||||
|
socket.write(
|
||||||
|
"HTTP/1.1 505 HTTP Version Not Supported\r\nConnection: close\r\n\r\n"
|
||||||
|
);
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.warn("error from frame layer", err);
|
console.warn("error from frame layer", err);
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("received frame", FRAME_TYPE_NAME[frame.type], frame);
|
|
||||||
|
if (debug) {
|
||||||
|
console.log("received frame", FRAME_TYPE_NAME[frame.type], frame);
|
||||||
|
}
|
||||||
|
|
||||||
switch (frame.type) {
|
switch (frame.type) {
|
||||||
|
case PREFANCE_RECEIVED_FRAME_TYPE: {
|
||||||
|
socket.write(
|
||||||
|
encodeFrame({
|
||||||
|
type: FRAME_TYPE.SETTINGS,
|
||||||
|
flags: 0,
|
||||||
|
streamIdentifier: 0,
|
||||||
|
payload: Buffer.from([]),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case FRAME_TYPE.HEADERS: {
|
case FRAME_TYPE.HEADERS: {
|
||||||
if (!streams.has(frame.streamIdentifier)) {
|
if (!streams.has(frame.streamIdentifier)) {
|
||||||
streams.set(frame.streamIdentifier, {
|
streams.set(frame.streamIdentifier, {
|
||||||
|
|
@ -802,9 +947,14 @@ const handleConnection =
|
||||||
|
|
||||||
if (streams.get(frame.streamIdentifier).endHeaders) {
|
if (streams.get(frame.streamIdentifier).endHeaders) {
|
||||||
const fieldBlockFragement = payload;
|
const fieldBlockFragement = payload;
|
||||||
const fields = hpackDecode.decode(fieldBlockFragement);
|
let fields;
|
||||||
|
try {
|
||||||
console.log("headers", fields);
|
fields = hpackDecode.decode(fieldBlockFragement);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("failed to decode HPACK block", e);
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rawH2Request = {
|
const rawH2Request = {
|
||||||
peer: {
|
peer: {
|
||||||
|
|
@ -816,25 +966,69 @@ const handleConnection =
|
||||||
|
|
||||||
const request = buildRequest(rawH2Request);
|
const request = buildRequest(rawH2Request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {ResponseWriter}
|
||||||
|
*/
|
||||||
|
const resWriter = {
|
||||||
|
writeHead: (param) => {
|
||||||
|
const hasBody = param.contentLength;
|
||||||
|
|
||||||
|
const responseBlock = hpackEncode.encode([
|
||||||
|
[":status", String(param.status)],
|
||||||
|
...(hasBody
|
||||||
|
? [["content-length", String(param.contentLength)]]
|
||||||
|
: []),
|
||||||
|
...(param.headers
|
||||||
|
? param.headers.filter(
|
||||||
|
(h) =>
|
||||||
|
!h[0].startsWith(":") &&
|
||||||
|
h[0].toLowerCase() !== "content-length"
|
||||||
|
)
|
||||||
|
: []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
socket.write(
|
||||||
|
encodeFrame({
|
||||||
|
type: FRAME_TYPE.HEADERS,
|
||||||
|
flags:
|
||||||
|
HEADERS_FLAG.END_HEADERS |
|
||||||
|
(hasBody ? 0 : HEADERS_FLAG.END_STREAM),
|
||||||
|
streamIdentifier: frame.streamIdentifier,
|
||||||
|
payload: responseBlock,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
bodyPart: (body) => {
|
||||||
|
socket.write(
|
||||||
|
encodeFrame({
|
||||||
|
type: FRAME_TYPE.DATA,
|
||||||
|
flags: 0,
|
||||||
|
payload: Buffer.from(body),
|
||||||
|
streamIdentifier: frame.streamIdentifier,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
end: (body) => {
|
||||||
|
socket.write(
|
||||||
|
encodeFrame({
|
||||||
|
type: FRAME_TYPE.DATA,
|
||||||
|
flags: DATA_FLAG.END_STREAM,
|
||||||
|
payload: Buffer.from(body),
|
||||||
|
streamIdentifier: frame.streamIdentifier,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// friends, we got a request!
|
// friends, we got a request!
|
||||||
|
|
||||||
if (false && request.ok) {
|
if (request.ok) {
|
||||||
server.emit("request", request.request);
|
server.emit("request", request.request, resWriter);
|
||||||
} else {
|
} else {
|
||||||
const responseBlock = hpackEncode.encode([
|
resWriter.writeHead({
|
||||||
[":status", "400"],
|
status: 400,
|
||||||
["date", new Date().toUTCString()],
|
headers: [["date", new Date().toUTCString()]],
|
||||||
["server", "h2.js"],
|
});
|
||||||
]);
|
|
||||||
|
|
||||||
socket.write(
|
|
||||||
encodeFrame({
|
|
||||||
type: FRAME_TYPE.HEADERS,
|
|
||||||
flags: HEADERS_FLAG.END_STREAM | HEADERS_FLAG.END_HEADERS,
|
|
||||||
streamIdentifier: frame.streamIdentifier,
|
|
||||||
payload: responseBlock,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("expecting CONTINUATION is not yet supported");
|
throw new Error("expecting CONTINUATION is not yet supported");
|
||||||
|
|
@ -868,22 +1062,37 @@ const handleConnection =
|
||||||
for (let i = 0; i < frame.length; i += 6) {
|
for (let i = 0; i < frame.length; i += 6) {
|
||||||
const identifier = frame.payload.readUint16BE(i);
|
const identifier = frame.payload.readUint16BE(i);
|
||||||
const value = frame.payload.readUint32BE(i + 2);
|
const value = frame.payload.readUint32BE(i + 2);
|
||||||
console.log(
|
if (debug) {
|
||||||
"SETTINGS setting",
|
console.log(
|
||||||
SETTING_NAME[identifier],
|
"SETTINGS setting",
|
||||||
"=",
|
SETTING_NAME[identifier],
|
||||||
value
|
"=",
|
||||||
);
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
peerSettings[SETTING_NAME[identifier]] = value;
|
peerSettings[SETTING_NAME[identifier]] = value;
|
||||||
|
|
||||||
|
if (identifier === SETTING.SETTINGS_HEADER_TABLE_SIZE) {
|
||||||
|
throw new Error("cannot change table size yet");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FRAME_TYPE.WINDOW_UPDATE: {
|
case FRAME_TYPE.WINDOW_UPDATE: {
|
||||||
// whatever
|
// whatever
|
||||||
const increment = frame.payload.readUint32BE();
|
// const increment = frame.payload.readUint32BE();
|
||||||
console.log("incrementing transfer window by", increment);
|
break;
|
||||||
|
}
|
||||||
|
case FRAME_TYPE.GOAWAY: {
|
||||||
|
const errorCode = frame.payload.readUint32BE(4);
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
console.log("goaway with code", ERROR_CODE_NAME[errorCode]);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.destroy();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
@ -937,10 +1146,113 @@ const { server, onConnection } = createH2Server();
|
||||||
server.on(
|
server.on(
|
||||||
"request",
|
"request",
|
||||||
/**
|
/**
|
||||||
* @param {Request} request
|
* @param {Request} req
|
||||||
|
* @param {ResponseWriter} res
|
||||||
*/
|
*/
|
||||||
(request) => {
|
(req, res) => {
|
||||||
console.log(request);
|
console.log("received a request", req.method, req.path);
|
||||||
|
|
||||||
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||||
|
res.writeHead({ status: 405 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filepath = path.join(".", path.normalize(req.path));
|
||||||
|
|
||||||
|
console.log("opening", filepath);
|
||||||
|
|
||||||
|
fs.stat(filepath, (err, stat) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
res.writeHead({ status: 404 });
|
||||||
|
} else {
|
||||||
|
console.error("failed to open", err);
|
||||||
|
res.writeHead({ status: 500 });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
fs.readdir(filepath, (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("failed to read dir", err);
|
||||||
|
res.writeHead({ status: 500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = `<ul>${files
|
||||||
|
.map(
|
||||||
|
(file) =>
|
||||||
|
`<li><a href="${encodeURIComponent(file)}">${file}</a></li>`
|
||||||
|
)
|
||||||
|
.join("\n")}</ul>`;
|
||||||
|
|
||||||
|
res.writeHead({
|
||||||
|
status: 200,
|
||||||
|
contentLength: html.length,
|
||||||
|
headers: [["content-type", "text/html; charset=utf-8"]],
|
||||||
|
});
|
||||||
|
res.end(html);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.isFile()) {
|
||||||
|
fs.open(filepath, (err, fd) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("failed to read", err);
|
||||||
|
res.writeHead({ status: 500 });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = stat.size;
|
||||||
|
|
||||||
|
const contentType =
|
||||||
|
{
|
||||||
|
".html": "text/html; charset=utf-8",
|
||||||
|
".css": "text/css; charset=utf-8",
|
||||||
|
".js": "text/javascript; charset=utf-8",
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".avif": "image/avif",
|
||||||
|
".woff": "font/woff",
|
||||||
|
".woff2": "font/woff2",
|
||||||
|
".txt": "text/plain",
|
||||||
|
}[path.extname(filepath)] ?? "application/octet-stream";
|
||||||
|
|
||||||
|
res.writeHead({
|
||||||
|
status: 200,
|
||||||
|
contentLength: size,
|
||||||
|
headers: [["content-type", contentType]],
|
||||||
|
});
|
||||||
|
|
||||||
|
let remainingSize = size;
|
||||||
|
|
||||||
|
const readDataPart = () =>
|
||||||
|
fs.read(fd, (err, bytesRead, buffer) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("error reading from file", err);
|
||||||
|
res.end("");
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingSize -= bytesRead;
|
||||||
|
if (remainingSize === 0) {
|
||||||
|
res.end(buffer.subarray(0, bytesRead));
|
||||||
|
} else {
|
||||||
|
res.bodyPart(buffer.subarray(0, bytesRead));
|
||||||
|
readDataPart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
readDataPart();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`File ${filepath} is neither a directory nor regular file`);
|
||||||
|
res.writeHead({ status: 404 });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue