This commit is contained in:
nora 2025-08-16 13:39:08 +02:00
parent 2ca0fea9a7
commit 9adad6436c

240
h2.mjs
View file

@ -1,4 +1,5 @@
import * as net from "node:net";
import { EventEmitter } from "node:events";
const buildHuffmanTree = (values) => {
const root = [];
@ -420,8 +421,11 @@ class HPackCtx {
if (huffman) {
return decodeHuffman(length);
} else {
const s = new TextDecoder().decode(
block.subarray(size, size + length)
);
size += length;
return new TextDecoder().decode(block.subarray(size, length));
return s;
}
};
@ -465,8 +469,35 @@ class HPackCtx {
return fields;
};
encode = () => {};
encode = (fields) => {
let block = Buffer.from([]);
// TODO: squimsh the bytes
for (const field of fields) {
// let's just pick 6.2.1. Literal Header Field with Incremental Indexing
block = Buffer.concat([block, Buffer.from([64])]);
const encodeString = (s) => {
const length = s.length;
if (length > 126) {
throw new Error("long header not implemented");
}
block = Buffer.concat([block, Buffer.from([length /*huffman false*/])]);
block = Buffer.concat([block, new TextEncoder().encode(s)]);
};
encodeString(field[0]);
encodeString(field[1]);
}
return block;
};
}
const reverseMap = (c) =>
Object.fromEntries(Object.entries(c).map(([k, v]) => [v, k]));
const FRAME_HEADER_SIZE = 3 + 1 + 1 + 4;
const FRAME_TYPE = {
DATA: 0x0,
@ -480,9 +511,7 @@ const FRAME_TYPE = {
WINDOW_UPDATE: 0x08,
CONTINUATION: 0x09,
};
const FRAME_TYPE_NAME = Object.fromEntries(
Object.entries(FRAME_TYPE).map(([k, v]) => [v, k])
);
const FRAME_TYPE_NAME = reverseMap(FRAME_TYPE);
const SETTING = {
SETTINGS_HEADER_TABLE_SIZE: 0x01,
@ -492,10 +521,15 @@ const SETTING = {
SETTINGS_MAX_FRAME_SIZE: 0x05,
SETTINGS_MAX_HEADER_LIST_SIZE: 0x06,
};
const SETTING_NAME = Object.fromEntries(
Object.entries(SETTING).map(([k, v]) => [v, k])
);
const FRAME_HEADER_SIZE = 3 + 1 + 1 + 4;
const SETTING_NAME = reverseMap(SETTING);
const HEADERS_FLAG = {
END_HEADERS: 0x04,
END_STREAM: 0x01,
PRIORITY: 0x20,
PADDED: 0x08,
};
const HEADERS_FLAG_NAME = reverseMap(HEADERS_FLAG);
const frameReader = (frameCb) => {
const STATE = {
@ -604,7 +638,12 @@ const encodeFrame = (frame) => {
throw new Error(`Frame flags do not fit in a byte: ${frame.flags}`);
}
buffer[4] = frame.flags;
buffer.writeUint32BE(length, 5);
if (typeof frame.streamIdentifier !== "number") {
throw new Error(
`Frame stream identifier is not a number: ${frame.streamIdentifier}`
);
}
buffer.writeUint32BE(frame.streamIdentifier, 5);
frame.payload.copy(buffer, FRAME_HEADER_SIZE);
@ -612,9 +651,88 @@ const encodeFrame = (frame) => {
};
/**
* @typedef Request
* @type {object}
* @property {string} method
*/
/**
* @typedef Response
* @type {object}
* @property {number} status
*/
const buildRequest = (rawH2Request) => {
const getField = (name) => {
const fields = rawH2Request.fields.filter((f) => f[0] === name);
if (fields.length === 0) {
return undefined;
}
if (fields.length === 1) {
return fields[0][1];
}
return fields.map((f) => f[1]).join(", ");
};
const method = getField(":method");
if (!method) {
return {
ok: false,
error: "Missing :method",
};
}
const scheme = getField(":scheme");
if (!scheme) {
return {
ok: false,
error: "Missing :scheme",
};
}
const authority = getField(":authority");
if (!scheme) {
return {
ok: false,
error: "Missing :scheme",
};
}
const path = getField(":path");
if (!path) {
return {
ok: false,
error: "Missing :path",
};
}
return {
ok: true,
request: {
method,
authority,
path,
scheme,
headers: rawH2Request.fields
.filter((f) => !f[0].startsWith(":"))
.map(([name, value]) => [name.toLowerCase(), value]),
peer: rawH2Request.peer,
},
};
};
/**
* @param {Response} response
*/
const serializeResponseFieldBlock = (fields) => {};
/**
* @param {EventEmitter} server
*/
const handleConnection =
(server) =>
/**
*
* @param {net.Socket} socket
*/
const handleConnection = (socket) => {
(socket) => {
const peer = `${socket.remoteAddress}:${socket.remotePort}`;
console.log(`received connection from ${peer}`);
@ -628,6 +746,7 @@ const handleConnection = (socket) => {
encodeFrame({
type: FRAME_TYPE.SETTINGS,
flags: 0,
streamIdentifier: 0,
payload: Buffer.from([]),
})
);
@ -650,14 +769,14 @@ const handleConnection = (socket) => {
}
// END_HEADERS
if ((frame.flags & 0x04) !== 0) {
if ((frame.flags & HEADERS_FLAG.END_HEADERS) !== 0) {
streams.get(frame.streamIdentifier).endHeaders = true;
}
// PRIORITY
const priorityFlag = (frame.flags & 0x20) !== 0;
const priorityFlag = (frame.flags & HEADERS_FLAG.PRIORITY) !== 0;
// PADDED
const paddedFlag = (frame.flags & 0x08) !== 0;
const paddedFlag = (frame.flags & HEADERS_FLAG.PADDED) !== 0;
let payload = frame.payload;
@ -687,7 +806,36 @@ const handleConnection = (socket) => {
console.log("headers", fields);
// we got a request!!!
const rawH2Request = {
peer: {
address: socket.remoteAddress,
port: socket.remotePort,
},
fields,
};
const request = buildRequest(rawH2Request);
// friends, we got a request!
if (false && request.ok) {
server.emit("request", request.request);
} else {
const responseBlock = hpackEncode.encode([
[":status", "400"],
["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 {
throw new Error("expecting CONTINUATION is not yet supported");
}
@ -720,7 +868,12 @@ const handleConnection = (socket) => {
for (let i = 0; i < frame.length; i += 6) {
const identifier = frame.payload.readUint16BE(i);
const value = frame.payload.readUint32BE(i + 2);
console.log("SETTINGS setting", SETTING_NAME[identifier], "=", value);
console.log(
"SETTINGS setting",
SETTING_NAME[identifier],
"=",
value
);
peerSettings[SETTING_NAME[identifier]] = value;
}
@ -735,7 +888,9 @@ const handleConnection = (socket) => {
}
default: {
console.warn(
`unsupported frame type ${FRAME_TYPE_NAME[frame.type] ?? frame.type}`
`unsupported frame type ${
FRAME_TYPE_NAME[frame.type] ?? frame.type
}`
);
}
}
@ -744,18 +899,59 @@ const handleConnection = (socket) => {
socket.on("data", onData);
socket.on("error", (err) => {
console.warn(`error from ${peer}:`, err);
server.emit("error", err);
});
socket.on("close", () => {
console.log(`connection closed for ${peer}`);
server.emit("close");
});
};
const server = net.createServer(handleConnection).on("error", (err) => {
/**
* @callback onConnectionCallback
* @param {net.Socket} socket
* @returns {void}
*/
/**
* @typedef Http2ServerReturn
* @type {object}
* @property {EventEmitter} server
* @property {onConnectionCallback} onConnection
*/
/**
* @returns {Http2ServerReturn}
*/
export const createH2Server = () => {
const server = new EventEmitter();
return {
server,
onConnection: handleConnection(server),
};
};
const { server, onConnection } = createH2Server();
server.on(
"request",
/**
* @param {Request} request
*/
(request) => {
console.log(request);
}
);
server.on("error", (err) => {
console.log("error", err);
});
const tcpServer = net.createServer(onConnection).on("error", (err) => {
console.error(`error: ${err}`);
});
server.listen(8080, () => {
console.log("Listening on", server.address());
tcpServer.listen(8080, () => {
console.log("Listening on", tcpServer.address());
});