1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-01 09:24:20 -04:00
denoland-deno/std/http/io.ts
Yusuke Sakurai 22f88b9f37
fix: [http] Consume unread body and trailers before reading next request (#3990)
- Added `ServerRequest.finalize()`:  consuming all unread body stream and trailers.
  - This is cleanup method for reading next request from same keep-alive connection.
  - Needed when handler didn't consume all body and trailers even after responding.
- refactor: `ServerRequest._bodyStream()`, `ServerRequestBody` are removed.
  - Now using `bodyReader()` and `chunkedBodyReader()` instead.
- fix: Trailers should only be read `transfer-encoding` is `chunked` and `trailer` header is set and its value is valid.
- fix: use `Headers.append()` on reading trailers.
- fix: delete `trailer` field from headers after reading trailers.
- reorg: Several functions related to IO are moved into `http/io.ts`
2020-02-24 22:49:39 -05:00

213 lines
6.2 KiB
TypeScript

import { BufReader, UnexpectedEOFError, BufWriter } from "../io/bufio.ts";
import { TextProtoReader } from "../textproto/mod.ts";
import { assert } from "../testing/asserts.ts";
import { encoder } from "../strings/mod.ts";
export function emptyReader(): Deno.Reader {
return {
async read(_: Uint8Array): Promise<number | Deno.EOF> {
return Deno.EOF;
}
};
}
export function bodyReader(contentLength: number, r: BufReader): Deno.Reader {
let totalRead = 0;
let finished = false;
async function read(buf: Uint8Array): Promise<number | Deno.EOF> {
if (finished) return Deno.EOF;
let result: number | Deno.EOF;
const remaining = contentLength - totalRead;
if (remaining >= buf.byteLength) {
result = await r.read(buf);
} else {
const readBuf = buf.subarray(0, remaining);
result = await r.read(readBuf);
}
if (result !== Deno.EOF) {
totalRead += result;
}
finished = totalRead === contentLength;
return result;
}
return { read };
}
export function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
// Based on https://tools.ietf.org/html/rfc2616#section-19.4.6
const tp = new TextProtoReader(r);
let finished = false;
const chunks: Array<{
offset: number;
data: Uint8Array;
}> = [];
async function read(buf: Uint8Array): Promise<number | Deno.EOF> {
if (finished) return Deno.EOF;
const [chunk] = chunks;
if (chunk) {
const chunkRemaining = chunk.data.byteLength - chunk.offset;
const readLength = Math.min(chunkRemaining, buf.byteLength);
for (let i = 0; i < readLength; i++) {
buf[i] = chunk.data[chunk.offset + i];
}
chunk.offset += readLength;
if (chunk.offset === chunk.data.byteLength) {
chunks.shift();
// Consume \r\n;
if ((await tp.readLine()) === Deno.EOF) {
throw new UnexpectedEOFError();
}
}
return readLength;
}
const line = await tp.readLine();
if (line === Deno.EOF) throw new UnexpectedEOFError();
// TODO: handle chunk extension
const [chunkSizeString] = line.split(";");
const chunkSize = parseInt(chunkSizeString, 16);
if (Number.isNaN(chunkSize) || chunkSize < 0) {
throw new Error("Invalid chunk size");
}
if (chunkSize > 0) {
if (chunkSize > buf.byteLength) {
let eof = await r.readFull(buf);
if (eof === Deno.EOF) {
throw new UnexpectedEOFError();
}
const restChunk = new Uint8Array(chunkSize - buf.byteLength);
eof = await r.readFull(restChunk);
if (eof === Deno.EOF) {
throw new UnexpectedEOFError();
} else {
chunks.push({
offset: 0,
data: restChunk
});
}
return buf.byteLength;
} else {
const bufToFill = buf.subarray(0, chunkSize);
const eof = await r.readFull(bufToFill);
if (eof === Deno.EOF) {
throw new UnexpectedEOFError();
}
// Consume \r\n
if ((await tp.readLine()) === Deno.EOF) {
throw new UnexpectedEOFError();
}
return chunkSize;
}
} else {
assert(chunkSize === 0);
// Consume \r\n
if ((await r.readLine()) === Deno.EOF) {
throw new UnexpectedEOFError();
}
await readTrailers(h, r);
finished = true;
return Deno.EOF;
}
}
return { read };
}
const kProhibitedTrailerHeaders = [
"transfer-encoding",
"content-length",
"trailer"
];
/**
* Read trailer headers from reader and append values to headers.
* "trailer" field will be deleted.
* */
export async function readTrailers(
headers: Headers,
r: BufReader
): Promise<void> {
const keys = parseTrailer(headers.get("trailer"));
if (!keys) return;
const tp = new TextProtoReader(r);
const result = await tp.readMIMEHeader();
assert(result != Deno.EOF, "trailer must be set");
for (const [k, v] of result) {
if (!keys.has(k)) {
throw new Error("Undeclared trailer field");
}
keys.delete(k);
headers.append(k, v);
}
assert(keys.size === 0, "Missing trailers");
headers.delete("trailer");
}
function parseTrailer(field: string | null): Set<string> | undefined {
if (field == null) {
return undefined;
}
const keys = field.split(",").map(v => v.trim());
if (keys.length === 0) {
throw new Error("Empty trailer");
}
for (const invalid of kProhibitedTrailerHeaders) {
if (keys.includes(invalid)) {
throw new Error(`Prohibited field for trailer`);
}
}
return new Set(keys);
}
export async function writeChunkedBody(
w: Deno.Writer,
r: Deno.Reader
): Promise<void> {
const writer = BufWriter.create(w);
for await (const chunk of Deno.toAsyncIterator(r)) {
if (chunk.byteLength <= 0) continue;
const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`);
const end = encoder.encode("\r\n");
await writer.write(start);
await writer.write(chunk);
await writer.write(end);
}
const endChunk = encoder.encode("0\r\n\r\n");
await writer.write(endChunk);
}
/** write trailer headers to writer. it mostly should be called after writeResponse */
export async function writeTrailers(
w: Deno.Writer,
headers: Headers,
trailers: Headers
): Promise<void> {
const trailer = headers.get("trailer");
if (trailer === null) {
throw new Error('response headers must have "trailer" header field');
}
const transferEncoding = headers.get("transfer-encoding");
if (transferEncoding === null || !transferEncoding.match(/^chunked/)) {
throw new Error(
`trailer headers is only allowed for "transfer-encoding: chunked": got "${transferEncoding}"`
);
}
const writer = BufWriter.create(w);
const trailerHeaderFields = trailer
.split(",")
.map(s => s.trim().toLowerCase());
for (const f of trailerHeaderFields) {
assert(
!kProhibitedTrailerHeaders.includes(f),
`"${f}" is prohibited for trailer header`
);
}
for (const [key, value] of trailers) {
assert(
trailerHeaderFields.includes(key),
`Not trailer header field: ${key}`
);
await writer.write(encoder.encode(`${key}: ${value}\r\n`));
}
await writer.write(encoder.encode("\r\n"));
await writer.flush();
}