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

448
h2.mjs
View file

@ -1,4 +1,5 @@
import * as net from "node:net"; import * as net from "node:net";
import { EventEmitter } from "node:events";
const buildHuffmanTree = (values) => { const buildHuffmanTree = (values) => {
const root = []; const root = [];
@ -420,8 +421,11 @@ class HPackCtx {
if (huffman) { if (huffman) {
return decodeHuffman(length); return decodeHuffman(length);
} else { } else {
const s = new TextDecoder().decode(
block.subarray(size, size + length)
);
size += length; size += length;
return new TextDecoder().decode(block.subarray(size, length)); return s;
} }
}; };
@ -465,9 +469,36 @@ class HPackCtx {
return fields; 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 = { const FRAME_TYPE = {
DATA: 0x0, DATA: 0x0,
HEADERS: 0x1, HEADERS: 0x1,
@ -480,9 +511,7 @@ const FRAME_TYPE = {
WINDOW_UPDATE: 0x08, WINDOW_UPDATE: 0x08,
CONTINUATION: 0x09, CONTINUATION: 0x09,
}; };
const FRAME_TYPE_NAME = Object.fromEntries( const FRAME_TYPE_NAME = reverseMap(FRAME_TYPE);
Object.entries(FRAME_TYPE).map(([k, v]) => [v, k])
);
const SETTING = { const SETTING = {
SETTINGS_HEADER_TABLE_SIZE: 0x01, SETTINGS_HEADER_TABLE_SIZE: 0x01,
@ -492,10 +521,15 @@ const SETTING = {
SETTINGS_MAX_FRAME_SIZE: 0x05, SETTINGS_MAX_FRAME_SIZE: 0x05,
SETTINGS_MAX_HEADER_LIST_SIZE: 0x06, SETTINGS_MAX_HEADER_LIST_SIZE: 0x06,
}; };
const SETTING_NAME = Object.fromEntries( const SETTING_NAME = reverseMap(SETTING);
Object.entries(SETTING).map(([k, v]) => [v, k])
); const HEADERS_FLAG = {
const FRAME_HEADER_SIZE = 3 + 1 + 1 + 4; END_HEADERS: 0x04,
END_STREAM: 0x01,
PRIORITY: 0x20,
PADDED: 0x08,
};
const HEADERS_FLAG_NAME = reverseMap(HEADERS_FLAG);
const frameReader = (frameCb) => { const frameReader = (frameCb) => {
const STATE = { const STATE = {
@ -604,7 +638,12 @@ const encodeFrame = (frame) => {
throw new Error(`Frame flags do not fit in a byte: ${frame.flags}`); throw new Error(`Frame flags do not fit in a byte: ${frame.flags}`);
} }
buffer[4] = 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); frame.payload.copy(buffer, FRAME_HEADER_SIZE);
@ -612,150 +651,307 @@ const encodeFrame = (frame) => {
}; };
/** /**
* @param {net.Socket} socket * @typedef Request
* @type {object}
* @property {string} method
*/ */
const handleConnection = (socket) => {
const peer = `${socket.remoteAddress}:${socket.remotePort}`;
console.log(`received connection from ${peer}`); /**
* @typedef Response
* @type {object}
* @property {number} status
*/
const hpackDecode = new HPackCtx(); const buildRequest = (rawH2Request) => {
const hpackEncode = new HPackCtx(); const getField = (name) => {
const peerSettings = new Map(); const fields = rawH2Request.fields.filter((f) => f[0] === name);
const streams = new Map(); if (fields.length === 0) {
return undefined;
socket.write(
encodeFrame({
type: FRAME_TYPE.SETTINGS,
flags: 0,
payload: Buffer.from([]),
})
);
const onData = frameReader((err, frame) => {
if (err) {
console.warn("error from frame layer", err);
socket.destroy();
return;
} }
console.log("received frame", FRAME_TYPE_NAME[frame.type], frame); if (fields.length === 1) {
return fields[0][1];
}
return fields.map((f) => f[1]).join(", ");
};
switch (frame.type) { const method = getField(":method");
case FRAME_TYPE.HEADERS: { if (!method) {
if (!streams.has(frame.streamIdentifier)) { return {
streams.set(frame.streamIdentifier, { ok: false,
headerBuffer: Buffer.from([]), error: "Missing :method",
endHeaders: false, };
}); }
} 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",
};
}
// END_HEADERS return {
if ((frame.flags & 0x04) !== 0) { ok: true,
streams.get(frame.streamIdentifier).endHeaders = true; request: {
} method,
authority,
path,
scheme,
headers: rawH2Request.fields
.filter((f) => !f[0].startsWith(":"))
.map(([name, value]) => [name.toLowerCase(), value]),
peer: rawH2Request.peer,
},
};
};
// PRIORITY /**
const priorityFlag = (frame.flags & 0x20) !== 0; * @param {Response} response
// PADDED */
const paddedFlag = (frame.flags & 0x08) !== 0; const serializeResponseFieldBlock = (fields) => {};
let payload = frame.payload; /**
* @param {EventEmitter} server
*/
const handleConnection =
(server) =>
/**
*
* @param {net.Socket} socket
*/
(socket) => {
const peer = `${socket.remoteAddress}:${socket.remotePort}`;
let paddingLength = 0; console.log(`received connection from ${peer}`);
if (paddedFlag) {
paddingLength = payload[0];
payload = payload.subarray(1);
}
if (priorityFlag) { const hpackDecode = new HPackCtx();
// skip over Exclusive/Stream Dependency, Weight const hpackEncode = new HPackCtx();
payload = payload.subarray(5); const peerSettings = new Map();
} const streams = new Map();
if (paddedFlag) { socket.write(
if (paddingLength > payload.length) { encodeFrame({
console.warn("too much padding"); type: FRAME_TYPE.SETTINGS,
socket.destroy(); flags: 0,
return; streamIdentifier: 0,
} payload: Buffer.from([]),
payload = payload.subarray(0, payload.length - paddingLength); })
} );
if (streams.get(frame.streamIdentifier).endHeaders) { const onData = frameReader((err, frame) => {
const fieldBlockFragement = payload; if (err) {
const fields = hpackDecode.decode(fieldBlockFragement); console.warn("error from frame layer", err);
socket.destroy();
console.log("headers", fields); return;
// we got a request!!!
} else {
throw new Error("expecting CONTINUATION is not yet supported");
}
break;
} }
case FRAME_TYPE.SETTINGS: { console.log("received frame", FRAME_TYPE_NAME[frame.type], frame);
// ACK
if ((frame.flags & 0x1) !== 0) { switch (frame.type) {
if (frame.length !== 0) { case FRAME_TYPE.HEADERS: {
console.warn("received non-empty SETTINGS ack frame"); if (!streams.has(frame.streamIdentifier)) {
socket.destroy(); streams.set(frame.streamIdentifier, {
return; headerBuffer: Buffer.from([]),
endHeaders: false,
});
}
// END_HEADERS
if ((frame.flags & HEADERS_FLAG.END_HEADERS) !== 0) {
streams.get(frame.streamIdentifier).endHeaders = true;
}
// PRIORITY
const priorityFlag = (frame.flags & HEADERS_FLAG.PRIORITY) !== 0;
// PADDED
const paddedFlag = (frame.flags & HEADERS_FLAG.PADDED) !== 0;
let payload = frame.payload;
let paddingLength = 0;
if (paddedFlag) {
paddingLength = payload[0];
payload = payload.subarray(1);
}
if (priorityFlag) {
// skip over Exclusive/Stream Dependency, Weight
payload = payload.subarray(5);
}
if (paddedFlag) {
if (paddingLength > payload.length) {
console.warn("too much padding");
socket.destroy();
return;
}
payload = payload.subarray(0, payload.length - paddingLength);
}
if (streams.get(frame.streamIdentifier).endHeaders) {
const fieldBlockFragement = payload;
const fields = hpackDecode.decode(fieldBlockFragement);
console.log("headers", fields);
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");
} }
break; break;
} }
case FRAME_TYPE.SETTINGS: {
// ACK
if ((frame.flags & 0x1) !== 0) {
if (frame.length !== 0) {
console.warn("received non-empty SETTINGS ack frame");
socket.destroy();
return;
}
if (frame.streamIdentifier !== 0) { break;
console.warn("stream identifier for a SETTINGS"); }
socket.destroy();
return; if (frame.streamIdentifier !== 0) {
console.warn("stream identifier for a SETTINGS");
socket.destroy();
return;
}
if (frame.length % 6 !== 0) {
console.warn("invalid length for SETTINGS frame");
socket.destroy();
return;
}
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
);
peerSettings[SETTING_NAME[identifier]] = value;
}
break;
} }
if (frame.length % 6 !== 0) { case FRAME_TYPE.WINDOW_UPDATE: {
console.warn("invalid length for SETTINGS frame"); // whatever
socket.destroy(); const increment = frame.payload.readUint32BE();
return; console.log("incrementing transfer window by", increment);
break;
} }
default: {
for (let i = 0; i < frame.length; i += 6) { console.warn(
const identifier = frame.payload.readUint16BE(i); `unsupported frame type ${
const value = frame.payload.readUint32BE(i + 2); FRAME_TYPE_NAME[frame.type] ?? frame.type
console.log("SETTINGS setting", SETTING_NAME[identifier], "=", value); }`
);
peerSettings[SETTING_NAME[identifier]] = value;
} }
break;
} }
case FRAME_TYPE.WINDOW_UPDATE: { });
// whatever
const increment = frame.payload.readUint32BE();
console.log("incrementing transfer window by", increment);
break;
}
default: {
console.warn(
`unsupported frame type ${FRAME_TYPE_NAME[frame.type] ?? frame.type}`
);
}
}
});
socket.on("data", onData); socket.on("data", onData);
socket.on("error", (err) => { socket.on("error", (err) => {
console.warn(`error from ${peer}:`, err); server.emit("error", err);
}); });
socket.on("close", () => { socket.on("close", () => {
console.log(`connection closed for ${peer}`); server.emit("close");
}); });
};
/**
* @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 = net.createServer(handleConnection).on("error", (err) => { 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}`); console.error(`error: ${err}`);
}); });
server.listen(8080, () => { tcpServer.listen(8080, () => {
console.log("Listening on", server.address()); console.log("Listening on", tcpServer.address());
}); });