mirror of
https://github.com/Noratrieb/h2.js.git
synced 2026-01-14 09:55:03 +01:00
res
This commit is contained in:
parent
2ca0fea9a7
commit
9adad6436c
1 changed files with 322 additions and 126 deletions
448
h2.mjs
448
h2.mjs
|
|
@ -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());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue