From 132650cdf366e676c0ffec0cc68792c45149aacd Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Sat, 2 Feb 2019 18:57:38 -0500 Subject: Rename http/http.ts to http/server.ts (denoland/deno_std#170) Remove http/mod.ts Original: https://github.com/denoland/deno_std/commit/e79cb5a31ab3d7667f2253824bf89cecc23ab231 --- http/README.md | 2 +- http/file_server.ts | 2 +- http/http.ts | 325 ---------------------------------------------------- http/http_test.ts | 223 ----------------------------------- http/mod.ts | 7 -- http/server.ts | 325 ++++++++++++++++++++++++++++++++++++++++++++++++++++ http/server_test.ts | 223 +++++++++++++++++++++++++++++++++++ 7 files changed, 550 insertions(+), 557 deletions(-) delete mode 100644 http/http.ts delete mode 100644 http/http_test.ts delete mode 100644 http/mod.ts create mode 100644 http/server.ts create mode 100644 http/server_test.ts (limited to 'http') diff --git a/http/README.md b/http/README.md index c598cdef4..2c9a90853 100644 --- a/http/README.md +++ b/http/README.md @@ -5,7 +5,7 @@ A framework for creating HTTP/HTTPS server. ## Example ```typescript -import { serve } from "https://deno.land/x/http/mod.ts"; +import { serve } from "https://deno.land/x/http/server.ts"; const s = serve("0.0.0.0:8000"); async function main() { diff --git a/http/file_server.ts b/http/file_server.ts index 4437a44e4..d1a34ab79 100755 --- a/http/file_server.ts +++ b/http/file_server.ts @@ -10,7 +10,7 @@ import { ServerRequest, setContentLength, Response -} from "./mod.ts"; +} from "./server.ts"; import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno"; import { extname } from "../fs/path.ts"; import { contentType } from "../media_types/mod.ts"; diff --git a/http/http.ts b/http/http.ts deleted file mode 100644 index fe25a5f6e..000000000 --- a/http/http.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { listen, Conn, toAsyncIterator, Reader, copy } from "deno"; -import { BufReader, BufState, BufWriter } from "../io/bufio.ts"; -import { TextProtoReader } from "../textproto/mod.ts"; -import { STATUS_TEXT } from "./http_status.ts"; -import { assert } from "../testing/mod.ts"; - -interface Deferred { - promise: Promise<{}>; - resolve: () => void; - reject: () => void; -} - -function deferred(): Deferred { - let resolve, reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { - promise, - resolve, - reject - }; -} - -interface ServeEnv { - reqQueue: ServerRequest[]; - serveDeferred: Deferred; -} - -/** Continuously read more requests from conn until EOF - * Calls maybeHandleReq. - * bufr is empty on a fresh TCP connection. - * Would be passed around and reused for later request on same conn - * TODO: make them async function after this change is done - * https://github.com/tc39/ecma262/pull/1250 - * See https://v8.dev/blog/fast-async - */ -function serveConn(env: ServeEnv, conn: Conn, bufr?: BufReader) { - readRequest(conn, bufr).then(maybeHandleReq.bind(null, env, conn)); -} -function maybeHandleReq(env: ServeEnv, conn: Conn, maybeReq: any) { - const [req, _err] = maybeReq; - if (_err) { - conn.close(); // assume EOF for now... - return; - } - env.reqQueue.push(req); // push req to queue - env.serveDeferred.resolve(); // signal while loop to process it -} - -export async function* serve(addr: string) { - const listener = listen("tcp", addr); - const env: ServeEnv = { - reqQueue: [], // in case multiple promises are ready - serveDeferred: deferred() - }; - - // Routine that keeps calling accept - const acceptRoutine = () => { - const handleConn = (conn: Conn) => { - serveConn(env, conn); // don't block - scheduleAccept(); // schedule next accept - }; - const scheduleAccept = () => { - listener.accept().then(handleConn); - }; - scheduleAccept(); - }; - - acceptRoutine(); - - // Loop hack to allow yield (yield won't work in callbacks) - while (true) { - await env.serveDeferred.promise; - env.serveDeferred = deferred(); // use a new deferred - let queueToProcess = env.reqQueue; - env.reqQueue = []; - for (const result of queueToProcess) { - yield result; - // Continue read more from conn when user is done with the current req - // Moving this here makes it easier to manage - serveConn(env, result.conn, result.r); - } - } - listener.close(); -} - -export async function listenAndServe( - addr: string, - handler: (req: ServerRequest) => void -) { - const server = serve(addr); - - for await (const request of server) { - await handler(request); - } -} - -export interface Response { - status?: number; - headers?: Headers; - body?: Uint8Array | Reader; -} - -export function setContentLength(r: Response): void { - if (!r.headers) { - r.headers = new Headers(); - } - - if (r.body) { - if (!r.headers.has("content-length")) { - if (r.body instanceof Uint8Array) { - const bodyLength = r.body.byteLength; - r.headers.append("Content-Length", bodyLength.toString()); - } else { - r.headers.append("Transfer-Encoding", "chunked"); - } - } - } -} - -export class ServerRequest { - url: string; - method: string; - proto: string; - headers: Headers; - conn: Conn; - r: BufReader; - w: BufWriter; - - public async *bodyStream() { - if (this.headers.has("content-length")) { - const len = +this.headers.get("content-length"); - if (Number.isNaN(len)) { - return new Uint8Array(0); - } - let buf = new Uint8Array(1024); - let rr = await this.r.read(buf); - let nread = rr.nread; - while (!rr.eof && nread < len) { - yield buf.subarray(0, rr.nread); - buf = new Uint8Array(1024); - rr = await this.r.read(buf); - nread += rr.nread; - } - yield buf.subarray(0, rr.nread); - } else { - if (this.headers.has("transfer-encoding")) { - const transferEncodings = this.headers - .get("transfer-encoding") - .split(",") - .map(e => e.trim().toLowerCase()); - if (transferEncodings.includes("chunked")) { - // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 - const tp = new TextProtoReader(this.r); - let [line, _] = await tp.readLine(); - // TODO: handle chunk extension - let [chunkSizeString, optExt] = line.split(";"); - let chunkSize = parseInt(chunkSizeString, 16); - if (Number.isNaN(chunkSize) || chunkSize < 0) { - throw new Error("Invalid chunk size"); - } - while (chunkSize > 0) { - let data = new Uint8Array(chunkSize); - let [nread, err] = await this.r.readFull(data); - if (nread !== chunkSize) { - throw new Error("Chunk data does not match size"); - } - yield data; - await this.r.readLine(); // Consume \r\n - [line, _] = await tp.readLine(); - chunkSize = parseInt(line, 16); - } - const [entityHeaders, err] = await tp.readMIMEHeader(); - if (!err) { - for (let [k, v] of entityHeaders) { - this.headers.set(k, v); - } - } - /* Pseudo code from https://tools.ietf.org/html/rfc2616#section-19.4.6 - length := 0 - read chunk-size, chunk-extension (if any) and CRLF - while (chunk-size > 0) { - read chunk-data and CRLF - append chunk-data to entity-body - length := length + chunk-size - read chunk-size and CRLF - } - read entity-header - while (entity-header not empty) { - append entity-header to existing header fields - read entity-header - } - Content-Length := length - Remove "chunked" from Transfer-Encoding - */ - return; // Must return here to avoid fall through - } - // TODO: handle other transfer-encoding types - } - // Otherwise... - yield new Uint8Array(0); - } - } - - // Read the body of the request into a single Uint8Array - public async body(): Promise { - return readAllIterator(this.bodyStream()); - } - - private async _streamBody(body: Reader, bodyLength: number) { - const n = await copy(this.w, body); - assert(n == bodyLength); - } - - private async _streamChunkedBody(body: Reader) { - const encoder = new TextEncoder(); - - for await (const chunk of toAsyncIterator(body)) { - const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`); - const end = encoder.encode("\r\n"); - await this.w.write(start); - await this.w.write(chunk); - await this.w.write(end); - } - - const endChunk = encoder.encode("0\r\n\r\n"); - await this.w.write(endChunk); - } - - async respond(r: Response): Promise { - const protoMajor = 1; - const protoMinor = 1; - const statusCode = r.status || 200; - const statusText = STATUS_TEXT.get(statusCode); - if (!statusText) { - throw Error("bad status code"); - } - - let out = `HTTP/${protoMajor}.${protoMinor} ${statusCode} ${statusText}\r\n`; - - setContentLength(r); - - if (r.headers) { - for (const [key, value] of r.headers) { - out += `${key}: ${value}\r\n`; - } - } - out += "\r\n"; - - const header = new TextEncoder().encode(out); - let n = await this.w.write(header); - assert(header.byteLength == n); - - if (r.body) { - if (r.body instanceof Uint8Array) { - n = await this.w.write(r.body); - assert(r.body.byteLength == n); - } else { - if (r.headers.has("content-length")) { - await this._streamBody( - r.body, - parseInt(r.headers.get("content-length")) - ); - } else { - await this._streamChunkedBody(r.body); - } - } - } - - await this.w.flush(); - } -} - -async function readRequest( - c: Conn, - bufr?: BufReader -): Promise<[ServerRequest, BufState]> { - if (!bufr) { - bufr = new BufReader(c); - } - const bufw = new BufWriter(c); - const req = new ServerRequest(); - req.conn = c; - req.r = bufr!; - req.w = bufw; - const tp = new TextProtoReader(bufr!); - - let s: string; - let err: BufState; - - // First line: GET /index.html HTTP/1.0 - [s, err] = await tp.readLine(); - if (err) { - return [null, err]; - } - [req.method, req.url, req.proto] = s.split(" ", 3); - - [req.headers, err] = await tp.readMIMEHeader(); - - return [req, err]; -} - -async function readAllIterator( - it: AsyncIterableIterator -): Promise { - const chunks = []; - let len = 0; - for await (const chunk of it) { - chunks.push(chunk); - len += chunk.length; - } - if (chunks.length === 0) { - // No need for copy - return chunks[0]; - } - const collected = new Uint8Array(len); - let offset = 0; - for (let chunk of chunks) { - collected.set(chunk, offset); - offset += chunk.length; - } - return collected; -} diff --git a/http/http_test.ts b/http/http_test.ts deleted file mode 100644 index ba0cec3e3..000000000 --- a/http/http_test.ts +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Ported from -// https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go - -import { Buffer } from "deno"; -import { test, assert, assertEqual } from "../testing/mod.ts"; -import { - listenAndServe, - ServerRequest, - setContentLength, - Response -} from "./mod.ts"; -import { BufWriter, BufReader } from "../io/bufio.ts"; - -interface ResponseTest { - response: Response; - raw: string; -} - -const enc = new TextEncoder(); -const dec = new TextDecoder(); - -const responseTests: ResponseTest[] = [ - // Default response - { - response: {}, - raw: "HTTP/1.1 200 OK\r\n" + "\r\n" - }, - // HTTP/1.1, chunked coding; empty trailer; close - { - response: { - status: 200, - body: new Buffer(new TextEncoder().encode("abcdef")) - }, - - raw: - "HTTP/1.1 200 OK\r\n" + - "transfer-encoding: chunked\r\n\r\n" + - "6\r\nabcdef\r\n0\r\n\r\n" - } -]; - -test(async function responseWrite() { - for (const testCase of responseTests) { - const buf = new Buffer(); - const bufw = new BufWriter(buf); - const request = new ServerRequest(); - request.w = bufw; - - await request.respond(testCase.response); - assertEqual(buf.toString(), testCase.raw); - } -}); - -test(async function requestBodyWithContentLength() { - { - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("content-length", "5"); - const buf = new Buffer(enc.encode("Hello")); - req.r = new BufReader(buf); - const body = dec.decode(await req.body()); - assertEqual(body, "Hello"); - } - - // Larger than internal buf - { - const longText = "1234\n".repeat(1000); - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("Content-Length", "5000"); - const buf = new Buffer(enc.encode(longText)); - req.r = new BufReader(buf); - const body = dec.decode(await req.body()); - assertEqual(body, longText); - } -}); - -test(async function requestBodyWithTransferEncoding() { - { - const shortText = "Hello"; - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("transfer-encoding", "chunked"); - let chunksData = ""; - let chunkOffset = 0; - const maxChunkSize = 70; - while (chunkOffset < shortText.length) { - const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); - chunksData += `${chunkSize.toString(16)}\r\n${shortText.substr( - chunkOffset, - chunkSize - )}\r\n`; - chunkOffset += chunkSize; - } - chunksData += "0\r\n\r\n"; - const buf = new Buffer(enc.encode(chunksData)); - req.r = new BufReader(buf); - const body = dec.decode(await req.body()); - assertEqual(body, shortText); - } - - // Larger than internal buf - { - const longText = "1234\n".repeat(1000); - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("transfer-encoding", "chunked"); - let chunksData = ""; - let chunkOffset = 0; - const maxChunkSize = 70; - while (chunkOffset < longText.length) { - const chunkSize = Math.min(maxChunkSize, longText.length - chunkOffset); - chunksData += `${chunkSize.toString(16)}\r\n${longText.substr( - chunkOffset, - chunkSize - )}\r\n`; - chunkOffset += chunkSize; - } - chunksData += "0\r\n\r\n"; - const buf = new Buffer(enc.encode(chunksData)); - req.r = new BufReader(buf); - const body = dec.decode(await req.body()); - assertEqual(body, longText); - } -}); - -test(async function requestBodyStreamWithContentLength() { - { - const shortText = "Hello"; - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("content-length", "" + shortText.length); - const buf = new Buffer(enc.encode(shortText)); - req.r = new BufReader(buf); - const it = await req.bodyStream(); - let offset = 0; - for await (const chunk of it) { - const s = dec.decode(chunk); - assertEqual(shortText.substr(offset, s.length), s); - offset += s.length; - } - } - - // Larger than internal buf - { - const longText = "1234\n".repeat(1000); - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("Content-Length", "5000"); - const buf = new Buffer(enc.encode(longText)); - req.r = new BufReader(buf); - const it = await req.bodyStream(); - let offset = 0; - for await (const chunk of it) { - const s = dec.decode(chunk); - assertEqual(longText.substr(offset, s.length), s); - offset += s.length; - } - } -}); - -test(async function requestBodyStreamWithTransferEncoding() { - { - const shortText = "Hello"; - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("transfer-encoding", "chunked"); - let chunksData = ""; - let chunkOffset = 0; - const maxChunkSize = 70; - while (chunkOffset < shortText.length) { - const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); - chunksData += `${chunkSize.toString(16)}\r\n${shortText.substr( - chunkOffset, - chunkSize - )}\r\n`; - chunkOffset += chunkSize; - } - chunksData += "0\r\n\r\n"; - const buf = new Buffer(enc.encode(chunksData)); - req.r = new BufReader(buf); - const it = await req.bodyStream(); - let offset = 0; - for await (const chunk of it) { - const s = dec.decode(chunk); - assertEqual(shortText.substr(offset, s.length), s); - offset += s.length; - } - } - - // Larger than internal buf - { - const longText = "1234\n".repeat(1000); - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("transfer-encoding", "chunked"); - let chunksData = ""; - let chunkOffset = 0; - const maxChunkSize = 70; - while (chunkOffset < longText.length) { - const chunkSize = Math.min(maxChunkSize, longText.length - chunkOffset); - chunksData += `${chunkSize.toString(16)}\r\n${longText.substr( - chunkOffset, - chunkSize - )}\r\n`; - chunkOffset += chunkSize; - } - chunksData += "0\r\n\r\n"; - const buf = new Buffer(enc.encode(chunksData)); - req.r = new BufReader(buf); - const it = await req.bodyStream(); - let offset = 0; - for await (const chunk of it) { - const s = dec.decode(chunk); - assertEqual(longText.substr(offset, s.length), s); - offset += s.length; - } - } -}); diff --git a/http/mod.ts b/http/mod.ts deleted file mode 100644 index a89f04417..000000000 --- a/http/mod.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { - serve, - listenAndServe, - Response, - setContentLength, - ServerRequest -} from "./http.ts"; diff --git a/http/server.ts b/http/server.ts new file mode 100644 index 000000000..fe25a5f6e --- /dev/null +++ b/http/server.ts @@ -0,0 +1,325 @@ +import { listen, Conn, toAsyncIterator, Reader, copy } from "deno"; +import { BufReader, BufState, BufWriter } from "../io/bufio.ts"; +import { TextProtoReader } from "../textproto/mod.ts"; +import { STATUS_TEXT } from "./http_status.ts"; +import { assert } from "../testing/mod.ts"; + +interface Deferred { + promise: Promise<{}>; + resolve: () => void; + reject: () => void; +} + +function deferred(): Deferred { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { + promise, + resolve, + reject + }; +} + +interface ServeEnv { + reqQueue: ServerRequest[]; + serveDeferred: Deferred; +} + +/** Continuously read more requests from conn until EOF + * Calls maybeHandleReq. + * bufr is empty on a fresh TCP connection. + * Would be passed around and reused for later request on same conn + * TODO: make them async function after this change is done + * https://github.com/tc39/ecma262/pull/1250 + * See https://v8.dev/blog/fast-async + */ +function serveConn(env: ServeEnv, conn: Conn, bufr?: BufReader) { + readRequest(conn, bufr).then(maybeHandleReq.bind(null, env, conn)); +} +function maybeHandleReq(env: ServeEnv, conn: Conn, maybeReq: any) { + const [req, _err] = maybeReq; + if (_err) { + conn.close(); // assume EOF for now... + return; + } + env.reqQueue.push(req); // push req to queue + env.serveDeferred.resolve(); // signal while loop to process it +} + +export async function* serve(addr: string) { + const listener = listen("tcp", addr); + const env: ServeEnv = { + reqQueue: [], // in case multiple promises are ready + serveDeferred: deferred() + }; + + // Routine that keeps calling accept + const acceptRoutine = () => { + const handleConn = (conn: Conn) => { + serveConn(env, conn); // don't block + scheduleAccept(); // schedule next accept + }; + const scheduleAccept = () => { + listener.accept().then(handleConn); + }; + scheduleAccept(); + }; + + acceptRoutine(); + + // Loop hack to allow yield (yield won't work in callbacks) + while (true) { + await env.serveDeferred.promise; + env.serveDeferred = deferred(); // use a new deferred + let queueToProcess = env.reqQueue; + env.reqQueue = []; + for (const result of queueToProcess) { + yield result; + // Continue read more from conn when user is done with the current req + // Moving this here makes it easier to manage + serveConn(env, result.conn, result.r); + } + } + listener.close(); +} + +export async function listenAndServe( + addr: string, + handler: (req: ServerRequest) => void +) { + const server = serve(addr); + + for await (const request of server) { + await handler(request); + } +} + +export interface Response { + status?: number; + headers?: Headers; + body?: Uint8Array | Reader; +} + +export function setContentLength(r: Response): void { + if (!r.headers) { + r.headers = new Headers(); + } + + if (r.body) { + if (!r.headers.has("content-length")) { + if (r.body instanceof Uint8Array) { + const bodyLength = r.body.byteLength; + r.headers.append("Content-Length", bodyLength.toString()); + } else { + r.headers.append("Transfer-Encoding", "chunked"); + } + } + } +} + +export class ServerRequest { + url: string; + method: string; + proto: string; + headers: Headers; + conn: Conn; + r: BufReader; + w: BufWriter; + + public async *bodyStream() { + if (this.headers.has("content-length")) { + const len = +this.headers.get("content-length"); + if (Number.isNaN(len)) { + return new Uint8Array(0); + } + let buf = new Uint8Array(1024); + let rr = await this.r.read(buf); + let nread = rr.nread; + while (!rr.eof && nread < len) { + yield buf.subarray(0, rr.nread); + buf = new Uint8Array(1024); + rr = await this.r.read(buf); + nread += rr.nread; + } + yield buf.subarray(0, rr.nread); + } else { + if (this.headers.has("transfer-encoding")) { + const transferEncodings = this.headers + .get("transfer-encoding") + .split(",") + .map(e => e.trim().toLowerCase()); + if (transferEncodings.includes("chunked")) { + // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 + const tp = new TextProtoReader(this.r); + let [line, _] = await tp.readLine(); + // TODO: handle chunk extension + let [chunkSizeString, optExt] = line.split(";"); + let chunkSize = parseInt(chunkSizeString, 16); + if (Number.isNaN(chunkSize) || chunkSize < 0) { + throw new Error("Invalid chunk size"); + } + while (chunkSize > 0) { + let data = new Uint8Array(chunkSize); + let [nread, err] = await this.r.readFull(data); + if (nread !== chunkSize) { + throw new Error("Chunk data does not match size"); + } + yield data; + await this.r.readLine(); // Consume \r\n + [line, _] = await tp.readLine(); + chunkSize = parseInt(line, 16); + } + const [entityHeaders, err] = await tp.readMIMEHeader(); + if (!err) { + for (let [k, v] of entityHeaders) { + this.headers.set(k, v); + } + } + /* Pseudo code from https://tools.ietf.org/html/rfc2616#section-19.4.6 + length := 0 + read chunk-size, chunk-extension (if any) and CRLF + while (chunk-size > 0) { + read chunk-data and CRLF + append chunk-data to entity-body + length := length + chunk-size + read chunk-size and CRLF + } + read entity-header + while (entity-header not empty) { + append entity-header to existing header fields + read entity-header + } + Content-Length := length + Remove "chunked" from Transfer-Encoding + */ + return; // Must return here to avoid fall through + } + // TODO: handle other transfer-encoding types + } + // Otherwise... + yield new Uint8Array(0); + } + } + + // Read the body of the request into a single Uint8Array + public async body(): Promise { + return readAllIterator(this.bodyStream()); + } + + private async _streamBody(body: Reader, bodyLength: number) { + const n = await copy(this.w, body); + assert(n == bodyLength); + } + + private async _streamChunkedBody(body: Reader) { + const encoder = new TextEncoder(); + + for await (const chunk of toAsyncIterator(body)) { + const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`); + const end = encoder.encode("\r\n"); + await this.w.write(start); + await this.w.write(chunk); + await this.w.write(end); + } + + const endChunk = encoder.encode("0\r\n\r\n"); + await this.w.write(endChunk); + } + + async respond(r: Response): Promise { + const protoMajor = 1; + const protoMinor = 1; + const statusCode = r.status || 200; + const statusText = STATUS_TEXT.get(statusCode); + if (!statusText) { + throw Error("bad status code"); + } + + let out = `HTTP/${protoMajor}.${protoMinor} ${statusCode} ${statusText}\r\n`; + + setContentLength(r); + + if (r.headers) { + for (const [key, value] of r.headers) { + out += `${key}: ${value}\r\n`; + } + } + out += "\r\n"; + + const header = new TextEncoder().encode(out); + let n = await this.w.write(header); + assert(header.byteLength == n); + + if (r.body) { + if (r.body instanceof Uint8Array) { + n = await this.w.write(r.body); + assert(r.body.byteLength == n); + } else { + if (r.headers.has("content-length")) { + await this._streamBody( + r.body, + parseInt(r.headers.get("content-length")) + ); + } else { + await this._streamChunkedBody(r.body); + } + } + } + + await this.w.flush(); + } +} + +async function readRequest( + c: Conn, + bufr?: BufReader +): Promise<[ServerRequest, BufState]> { + if (!bufr) { + bufr = new BufReader(c); + } + const bufw = new BufWriter(c); + const req = new ServerRequest(); + req.conn = c; + req.r = bufr!; + req.w = bufw; + const tp = new TextProtoReader(bufr!); + + let s: string; + let err: BufState; + + // First line: GET /index.html HTTP/1.0 + [s, err] = await tp.readLine(); + if (err) { + return [null, err]; + } + [req.method, req.url, req.proto] = s.split(" ", 3); + + [req.headers, err] = await tp.readMIMEHeader(); + + return [req, err]; +} + +async function readAllIterator( + it: AsyncIterableIterator +): Promise { + const chunks = []; + let len = 0; + for await (const chunk of it) { + chunks.push(chunk); + len += chunk.length; + } + if (chunks.length === 0) { + // No need for copy + return chunks[0]; + } + const collected = new Uint8Array(len); + let offset = 0; + for (let chunk of chunks) { + collected.set(chunk, offset); + offset += chunk.length; + } + return collected; +} diff --git a/http/server_test.ts b/http/server_test.ts new file mode 100644 index 000000000..5fdb63ceb --- /dev/null +++ b/http/server_test.ts @@ -0,0 +1,223 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Ported from +// https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go + +import { Buffer } from "deno"; +import { test, assert, assertEqual } from "../testing/mod.ts"; +import { + listenAndServe, + ServerRequest, + setContentLength, + Response +} from "./server.ts"; +import { BufWriter, BufReader } from "../io/bufio.ts"; + +interface ResponseTest { + response: Response; + raw: string; +} + +const enc = new TextEncoder(); +const dec = new TextDecoder(); + +const responseTests: ResponseTest[] = [ + // Default response + { + response: {}, + raw: "HTTP/1.1 200 OK\r\n" + "\r\n" + }, + // HTTP/1.1, chunked coding; empty trailer; close + { + response: { + status: 200, + body: new Buffer(new TextEncoder().encode("abcdef")) + }, + + raw: + "HTTP/1.1 200 OK\r\n" + + "transfer-encoding: chunked\r\n\r\n" + + "6\r\nabcdef\r\n0\r\n\r\n" + } +]; + +test(async function responseWrite() { + for (const testCase of responseTests) { + const buf = new Buffer(); + const bufw = new BufWriter(buf); + const request = new ServerRequest(); + request.w = bufw; + + await request.respond(testCase.response); + assertEqual(buf.toString(), testCase.raw); + } +}); + +test(async function requestBodyWithContentLength() { + { + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("content-length", "5"); + const buf = new Buffer(enc.encode("Hello")); + req.r = new BufReader(buf); + const body = dec.decode(await req.body()); + assertEqual(body, "Hello"); + } + + // Larger than internal buf + { + const longText = "1234\n".repeat(1000); + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("Content-Length", "5000"); + const buf = new Buffer(enc.encode(longText)); + req.r = new BufReader(buf); + const body = dec.decode(await req.body()); + assertEqual(body, longText); + } +}); + +test(async function requestBodyWithTransferEncoding() { + { + const shortText = "Hello"; + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("transfer-encoding", "chunked"); + let chunksData = ""; + let chunkOffset = 0; + const maxChunkSize = 70; + while (chunkOffset < shortText.length) { + const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); + chunksData += `${chunkSize.toString(16)}\r\n${shortText.substr( + chunkOffset, + chunkSize + )}\r\n`; + chunkOffset += chunkSize; + } + chunksData += "0\r\n\r\n"; + const buf = new Buffer(enc.encode(chunksData)); + req.r = new BufReader(buf); + const body = dec.decode(await req.body()); + assertEqual(body, shortText); + } + + // Larger than internal buf + { + const longText = "1234\n".repeat(1000); + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("transfer-encoding", "chunked"); + let chunksData = ""; + let chunkOffset = 0; + const maxChunkSize = 70; + while (chunkOffset < longText.length) { + const chunkSize = Math.min(maxChunkSize, longText.length - chunkOffset); + chunksData += `${chunkSize.toString(16)}\r\n${longText.substr( + chunkOffset, + chunkSize + )}\r\n`; + chunkOffset += chunkSize; + } + chunksData += "0\r\n\r\n"; + const buf = new Buffer(enc.encode(chunksData)); + req.r = new BufReader(buf); + const body = dec.decode(await req.body()); + assertEqual(body, longText); + } +}); + +test(async function requestBodyStreamWithContentLength() { + { + const shortText = "Hello"; + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("content-length", "" + shortText.length); + const buf = new Buffer(enc.encode(shortText)); + req.r = new BufReader(buf); + const it = await req.bodyStream(); + let offset = 0; + for await (const chunk of it) { + const s = dec.decode(chunk); + assertEqual(shortText.substr(offset, s.length), s); + offset += s.length; + } + } + + // Larger than internal buf + { + const longText = "1234\n".repeat(1000); + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("Content-Length", "5000"); + const buf = new Buffer(enc.encode(longText)); + req.r = new BufReader(buf); + const it = await req.bodyStream(); + let offset = 0; + for await (const chunk of it) { + const s = dec.decode(chunk); + assertEqual(longText.substr(offset, s.length), s); + offset += s.length; + } + } +}); + +test(async function requestBodyStreamWithTransferEncoding() { + { + const shortText = "Hello"; + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("transfer-encoding", "chunked"); + let chunksData = ""; + let chunkOffset = 0; + const maxChunkSize = 70; + while (chunkOffset < shortText.length) { + const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); + chunksData += `${chunkSize.toString(16)}\r\n${shortText.substr( + chunkOffset, + chunkSize + )}\r\n`; + chunkOffset += chunkSize; + } + chunksData += "0\r\n\r\n"; + const buf = new Buffer(enc.encode(chunksData)); + req.r = new BufReader(buf); + const it = await req.bodyStream(); + let offset = 0; + for await (const chunk of it) { + const s = dec.decode(chunk); + assertEqual(shortText.substr(offset, s.length), s); + offset += s.length; + } + } + + // Larger than internal buf + { + const longText = "1234\n".repeat(1000); + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("transfer-encoding", "chunked"); + let chunksData = ""; + let chunkOffset = 0; + const maxChunkSize = 70; + while (chunkOffset < longText.length) { + const chunkSize = Math.min(maxChunkSize, longText.length - chunkOffset); + chunksData += `${chunkSize.toString(16)}\r\n${longText.substr( + chunkOffset, + chunkSize + )}\r\n`; + chunkOffset += chunkSize; + } + chunksData += "0\r\n\r\n"; + const buf = new Buffer(enc.encode(chunksData)); + req.r = new BufReader(buf); + const it = await req.bodyStream(); + let offset = 0; + for await (const chunk of it) { + const s = dec.decode(chunk); + assertEqual(longText.substr(offset, s.length), s); + offset += s.length; + } + } +}); -- cgit v1.2.3