mirror of
https://github.com/Noratrieb/h2.js.git
synced 2026-01-14 18:05:03 +01:00
res
This commit is contained in:
parent
2ca0fea9a7
commit
9adad6436c
1 changed files with 322 additions and 126 deletions
240
h2.mjs
240
h2.mjs
|
|
@ -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,9 +469,36 @@ 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,
|
||||
HEADERS: 0x1,
|
||||
|
|
@ -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");
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @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}`);
|
||||
});
|
||||
|
||||
server.listen(8080, () => {
|
||||
console.log("Listening on", server.address());
|
||||
tcpServer.listen(8080, () => {
|
||||
console.log("Listening on", tcpServer.address());
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue