@hpke/ohttp
Version:
Oblivious HTTP (RFC 9458) client and server using HPKE
119 lines (118 loc) • 3.87 kB
JavaScript
/**
* Binary HTTP (RFC 9292) encoder.
*
* Messages are always encoded in the known-length format without padding.
* Trailers and informational responses cannot be expressed with the Fetch API
* `Request`/`Response` objects and are therefore never emitted.
*
* @module
*/
import { OhttpError } from "../errors.js";
import { varintLength, writeVarint } from "./varint.js";
const FRAMING_KNOWN_LENGTH_REQUEST = 0;
const FRAMING_KNOWN_LENGTH_RESPONSE = 1;
/** Accumulates byte chunks and serializes them into a single buffer. */
class Writer {
constructor() {
Object.defineProperty(this, "_parts", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "_length", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
}
varint(v) {
this._parts.push(v);
this._length += varintLength(v);
return this;
}
/** Writes a varint length prefix followed by `bytes`. */
lengthPrefixed(bytes) {
this.varint(bytes.byteLength);
this._parts.push(bytes);
this._length += bytes.byteLength;
return this;
}
finish() {
const out = new Uint8Array(this._length);
let p = 0;
for (const part of this._parts) {
if (typeof part === "number") {
p = writeVarint(out, p, part);
}
else {
out.set(part, p);
p += part.byteLength;
}
}
return out;
}
}
/**
* Converts a ByteString (as used by Fetch API headers, methods and
* serialized URL components) to bytes. Each code unit maps to one byte.
*/
function byteStringToBytes(s) {
const out = new Uint8Array(s.length);
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c > 0xff) {
throw new OhttpError(`Invalid ByteString: ${JSON.stringify(s)}`);
}
out[i] = c;
}
return out;
}
/** Encodes a field section (headers) as a series of field lines. */
function encodeFieldSection(headers) {
const w = new Writer();
headers.forEach((value, name) => {
w.lengthPrefixed(byteStringToBytes(name));
w.lengthPrefixed(byteStringToBytes(value));
});
return w.finish();
}
/**
* Encodes a `Request` as a known-length Binary HTTP request.
*/
export async function encodeRequest(request) {
const url = new URL(request.url);
const content = new Uint8Array(await request.arrayBuffer());
return new Writer()
.varint(FRAMING_KNOWN_LENGTH_REQUEST)
// Request control data
.lengthPrefixed(byteStringToBytes(request.method))
.lengthPrefixed(byteStringToBytes(url.protocol.slice(0, -1)))
.lengthPrefixed(byteStringToBytes(url.host))
.lengthPrefixed(byteStringToBytes(url.pathname + url.search))
// Known-length field section, content and (empty) trailer section
.lengthPrefixed(encodeFieldSection(request.headers))
.lengthPrefixed(content)
.varint(0)
.finish();
}
/**
* Encodes a `Response` as a known-length Binary HTTP response.
*/
export async function encodeResponse(response) {
const status = response.status;
if (status < 200 || status > 599) {
throw new OhttpError(`Unsupported response status: ${status}`);
}
const content = new Uint8Array(await response.arrayBuffer());
return new Writer()
.varint(FRAMING_KNOWN_LENGTH_RESPONSE)
// Final response control data
.varint(status)
// Known-length field section, content and (empty) trailer section
.lengthPrefixed(encodeFieldSection(response.headers))
.lengthPrefixed(content)
.varint(0)
.finish();
}