From f626b04ebe320a96a220af29595c6ca84cf9a10a Mon Sep 17 00:00:00 2001 From: Andy Hayden Date: Sat, 12 Jan 2019 13:50:04 -0800 Subject: Reorgnanize repos, examples and tests (denoland/deno_std#105) Original: https://github.com/denoland/deno_std/commit/c5e6e015b5be19027f60c19ca86283d12f9258f3 --- examples/gist.ts | 2 +- examples/ws.ts | 4 +- flags/example.ts | 2 +- flags/index.ts | 271 -------------------------- flags/mod.ts | 271 ++++++++++++++++++++++++++ flags/tests/all_bool.ts | 2 +- flags/tests/bool.ts | 2 +- flags/tests/dash.ts | 2 +- flags/tests/default_bool.ts | 2 +- flags/tests/dotted.ts | 2 +- flags/tests/kv_short.ts | 2 +- flags/tests/long.ts | 2 +- flags/tests/num.ts | 2 +- flags/tests/parse.ts | 2 +- flags/tests/short.ts | 2 +- flags/tests/stop_early.ts | 2 +- flags/tests/unknown.ts | 2 +- flags/tests/whitespace.ts | 2 +- http/README.md | 26 +++ http/file_server.ts | 241 +++++++++++++++++++++++ http/file_server_test.ts | 51 +++++ http/http.ts | 325 +++++++++++++++++++++++++++++++ http/http_bench.ts | 15 ++ http/http_status.ts | 134 +++++++++++++ http/http_test.ts | 223 +++++++++++++++++++++ http/mod.ts | 8 + io/bufio.ts | 464 ++++++++++++++++++++++++++++++++++++++++++++ io/bufio_test.ts | 341 ++++++++++++++++++++++++++++++++ io/iotest.ts | 61 ++++++ io/ioutil.ts | 36 ++++ io/ioutil_test.ts | 62 ++++++ io/util.ts | 29 +++ log/README.md | 38 ++++ log/handlers.ts | 62 ++++++ log/levels.ts | 34 ++++ log/logger.ts | 62 ++++++ log/mod.ts | 119 ++++++++++++ log/test.ts | 28 +++ logging/README.md | 38 ---- logging/handlers.ts | 62 ------ logging/index.ts | 119 ------------ logging/levels.ts | 34 ---- logging/logger.ts | 62 ------ logging/test.ts | 28 --- net/README.md | 26 --- net/bufio.ts | 464 -------------------------------------------- net/bufio_test.ts | 341 -------------------------------- net/file_server.ts | 241 ----------------------- net/file_server_test.ts | 51 ----- net/http.ts | 325 ------------------------------- net/http_bench.ts | 15 -- net/http_status.ts | 134 ------------- net/http_test.ts | 223 --------------------- net/iotest.ts | 61 ------ net/ioutil.ts | 36 ---- net/ioutil_test.ts | 62 ------ net/sha1.ts | 382 ------------------------------------ net/sha1_test.ts | 8 - net/textproto.ts | 150 -------------- net/textproto_test.ts | 98 ---------- net/util.ts | 29 --- net/ws.ts | 350 --------------------------------- net/ws_test.ts | 138 ------------- test.ts | 15 +- textproto/mod.ts | 150 ++++++++++++++ textproto/test.ts | 98 ++++++++++ ws/mod.ts | 350 +++++++++++++++++++++++++++++++++ ws/sha1.ts | 382 ++++++++++++++++++++++++++++++++++++ ws/sha1_test.ts | 8 + ws/test.ts | 138 +++++++++++++ 70 files changed, 3781 insertions(+), 3772 deletions(-) delete mode 100644 flags/index.ts create mode 100644 flags/mod.ts create mode 100644 http/README.md create mode 100755 http/file_server.ts create mode 100644 http/file_server_test.ts create mode 100644 http/http.ts create mode 100644 http/http_bench.ts create mode 100644 http/http_status.ts create mode 100644 http/http_test.ts create mode 100644 http/mod.ts create mode 100644 io/bufio.ts create mode 100644 io/bufio_test.ts create mode 100644 io/iotest.ts create mode 100644 io/ioutil.ts create mode 100644 io/ioutil_test.ts create mode 100644 io/util.ts create mode 100644 log/README.md create mode 100644 log/handlers.ts create mode 100644 log/levels.ts create mode 100644 log/logger.ts create mode 100644 log/mod.ts create mode 100644 log/test.ts delete mode 100644 logging/README.md delete mode 100644 logging/handlers.ts delete mode 100644 logging/index.ts delete mode 100644 logging/levels.ts delete mode 100644 logging/logger.ts delete mode 100644 logging/test.ts delete mode 100644 net/README.md delete mode 100644 net/bufio.ts delete mode 100644 net/bufio_test.ts delete mode 100755 net/file_server.ts delete mode 100644 net/file_server_test.ts delete mode 100644 net/http.ts delete mode 100644 net/http_bench.ts delete mode 100644 net/http_status.ts delete mode 100644 net/http_test.ts delete mode 100644 net/iotest.ts delete mode 100644 net/ioutil.ts delete mode 100644 net/ioutil_test.ts delete mode 100644 net/sha1.ts delete mode 100644 net/sha1_test.ts delete mode 100644 net/textproto.ts delete mode 100644 net/textproto_test.ts delete mode 100644 net/util.ts delete mode 100644 net/ws.ts delete mode 100644 net/ws_test.ts create mode 100644 textproto/mod.ts create mode 100644 textproto/test.ts create mode 100644 ws/mod.ts create mode 100644 ws/sha1.ts create mode 100644 ws/sha1_test.ts create mode 100644 ws/test.ts diff --git a/examples/gist.ts b/examples/gist.ts index 7b3d59f87..1baff874a 100755 --- a/examples/gist.ts +++ b/examples/gist.ts @@ -1,7 +1,7 @@ #!/usr/bin/env deno --allow-net --allow-env import { args, env, exit, readFile } from "deno"; -import { parse } from "https://deno.land/x/flags/index.ts"; +import { parse } from "https://deno.land/x/flags/mod.ts"; function pathBase(p: string): string { const parts = p.split("/"); diff --git a/examples/ws.ts b/examples/ws.ts index f8e711c49..bc2a7bd0b 100644 --- a/examples/ws.ts +++ b/examples/ws.ts @@ -1,10 +1,10 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -import { serve } from "https://deno.land/x/net/http.ts"; +import { serve } from "https://deno.land/x/http/mod.ts"; import { acceptWebSocket, isWebSocketCloseEvent, isWebSocketPingEvent -} from "https://deno.land/x/net/ws.ts"; +} from "https://deno.land/x/ws/mod.ts"; async function main() { console.log("websocket server is running on 0.0.0.0:8080"); diff --git a/flags/example.ts b/flags/example.ts index 811aacd69..5aa0a5034 100644 --- a/flags/example.ts +++ b/flags/example.ts @@ -1,4 +1,4 @@ import { args } from "deno"; -import { parse } from "./index.ts"; +import { parse } from "./mod.ts"; console.dir(parse(args)); diff --git a/flags/index.ts b/flags/index.ts deleted file mode 100644 index 28a5c8eac..000000000 --- a/flags/index.ts +++ /dev/null @@ -1,271 +0,0 @@ -export interface ArgParsingOptions { - unknown?: Function; - boolean?: Boolean | string | string[]; - alias?: { [key: string]: string | string[] }; - string?: string | string[]; - default?: { [key: string]: any }; - "--"?: Boolean; - stopEarly?: Boolean; -} - -const DEFAULT_OPTIONS = { - unknown: i => i, - boolean: false, - alias: {}, - string: [], - default: {}, - "--": false, - stopEarly: false -}; - -export function parse( - args, - initialOptions?: ArgParsingOptions -): { [key: string]: any } { - const options: ArgParsingOptions = { - ...DEFAULT_OPTIONS, - ...(initialOptions || {}) - }; - - const flags = { - bools: {}, - strings: {}, - unknownFn: options.unknown!, - allBools: false - }; - - // TODO: get rid of this, providing two different options - if (typeof options["boolean"] === "boolean" && options["boolean"]) { - flags.allBools = true; - } else { - [] - .concat(options["boolean"]) - .filter(Boolean) - .forEach(function(key) { - flags.bools[key] = true; - }); - } - - const aliases = {}; - Object.keys(options.alias).forEach(function(key) { - aliases[key] = [].concat(options.alias[key]); - aliases[key].forEach(function(x) { - aliases[x] = [key].concat( - aliases[key].filter(function(y) { - return x !== y; - }) - ); - }); - }); - - [] - .concat(options.string) - .filter(Boolean) - .forEach(function(key) { - flags.strings[key] = true; - if (aliases[key]) { - flags.strings[aliases[key]] = true; - } - }); - - const defaults = options.default!; - - const argv = { _: [] }; - Object.keys(flags.bools).forEach(function(key) { - setArg(key, defaults[key] === undefined ? false : defaults[key]); - }); - - let notFlags = []; - - if (args.indexOf("--") !== -1) { - notFlags = args.slice(args.indexOf("--") + 1); - args = args.slice(0, args.indexOf("--")); - } - - function argDefined(key, arg) { - return ( - (flags.allBools && /^--[^=]+$/.test(arg)) || - flags.strings[key] || - flags.bools[key] || - aliases[key] - ); - } - - function setArg(key, val, arg = null): void { - if (arg && flags.unknownFn && !argDefined(key, arg)) { - if (flags.unknownFn(arg) === false) return; - } - - const value = !flags.strings[key] && isNumber(val) ? Number(val) : val; - setKey(argv, key.split("."), value); - - (aliases[key] || []).forEach(function(x) { - setKey(argv, x.split("."), value); - }); - } - - function setKey(obj, keys, value): void { - let o = obj; - keys.slice(0, -1).forEach(function(key) { - if (o[key] === undefined) o[key] = {}; - o = o[key]; - }); - - const key = keys[keys.length - 1]; - if ( - o[key] === undefined || - flags.bools[key] || - typeof o[key] === "boolean" - ) { - o[key] = value; - } else if (Array.isArray(o[key])) { - o[key].push(value); - } else { - o[key] = [o[key], value]; - } - } - - function aliasIsBoolean(key): boolean { - return aliases[key].some(function(x) { - return flags.bools[x]; - }); - } - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - if (/^--.+=/.test(arg)) { - // Using [\s\S] instead of . because js doesn't support the - // 'dotall' regex modifier. See: - // http://stackoverflow.com/a/1068308/13216 - const m = arg.match(/^--([^=]+)=([\s\S]*)$/); - const key = m[1]; - let value = m[2]; - if (flags.bools[key]) { - value = value !== "false"; - } - setArg(key, value, arg); - } else if (/^--no-.+/.test(arg)) { - const key = arg.match(/^--no-(.+)/)[1]; - setArg(key, false, arg); - } else if (/^--.+/.test(arg)) { - const key = arg.match(/^--(.+)/)[1]; - const next = args[i + 1]; - if ( - next !== undefined && - !/^-/.test(next) && - !flags.bools[key] && - !flags.allBools && - (aliases[key] ? !aliasIsBoolean(key) : true) - ) { - setArg(key, next, arg); - i++; - } else if (/^(true|false)$/.test(next)) { - setArg(key, next === "true", arg); - i++; - } else { - setArg(key, flags.strings[key] ? "" : true, arg); - } - } else if (/^-[^-]+/.test(arg)) { - const letters = arg.slice(1, -1).split(""); - - let broken = false; - for (let j = 0; j < letters.length; j++) { - const next = arg.slice(j + 2); - - if (next === "-") { - setArg(letters[j], next, arg); - continue; - } - - if (/[A-Za-z]/.test(letters[j]) && /=/.test(next)) { - setArg(letters[j], next.split("=")[1], arg); - broken = true; - break; - } - - if ( - /[A-Za-z]/.test(letters[j]) && - /-?\d+(\.\d*)?(e-?\d+)?$/.test(next) - ) { - setArg(letters[j], next, arg); - broken = true; - break; - } - - if (letters[j + 1] && letters[j + 1].match(/\W/)) { - setArg(letters[j], arg.slice(j + 2), arg); - broken = true; - break; - } else { - setArg(letters[j], flags.strings[letters[j]] ? "" : true, arg); - } - } - - const key = arg.slice(-1)[0]; - if (!broken && key !== "-") { - if ( - args[i + 1] && - !/^(-|--)[^-]/.test(args[i + 1]) && - !flags.bools[key] && - (aliases[key] ? !aliasIsBoolean(key) : true) - ) { - setArg(key, args[i + 1], arg); - i++; - } else if (args[i + 1] && /true|false/.test(args[i + 1])) { - setArg(key, args[i + 1] === "true", arg); - i++; - } else { - setArg(key, flags.strings[key] ? "" : true, arg); - } - } - } else { - if (!flags.unknownFn || flags.unknownFn(arg) !== false) { - argv._.push(flags.strings["_"] || !isNumber(arg) ? arg : Number(arg)); - } - if (options.stopEarly) { - argv._.push.apply(argv._, args.slice(i + 1)); - break; - } - } - } - - Object.keys(defaults).forEach(function(key) { - if (!hasKey(argv, key.split("."))) { - setKey(argv, key.split("."), defaults[key]); - - (aliases[key] || []).forEach(function(x) { - setKey(argv, x.split("."), defaults[key]); - }); - } - }); - - if (options["--"]) { - argv["--"] = new Array(); - notFlags.forEach(function(key) { - argv["--"].push(key); - }); - } else { - notFlags.forEach(function(key) { - argv._.push(key); - }); - } - - return argv; -} - -function hasKey(obj, keys) { - let o = obj; - keys.slice(0, -1).forEach(function(key) { - o = o[key] || {}; - }); - - const key = keys[keys.length - 1]; - return key in o; -} - -function isNumber(x: any): boolean { - if (typeof x === "number") return true; - if (/^0x[0-9a-f]+$/i.test(x)) return true; - return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x); -} diff --git a/flags/mod.ts b/flags/mod.ts new file mode 100644 index 000000000..28a5c8eac --- /dev/null +++ b/flags/mod.ts @@ -0,0 +1,271 @@ +export interface ArgParsingOptions { + unknown?: Function; + boolean?: Boolean | string | string[]; + alias?: { [key: string]: string | string[] }; + string?: string | string[]; + default?: { [key: string]: any }; + "--"?: Boolean; + stopEarly?: Boolean; +} + +const DEFAULT_OPTIONS = { + unknown: i => i, + boolean: false, + alias: {}, + string: [], + default: {}, + "--": false, + stopEarly: false +}; + +export function parse( + args, + initialOptions?: ArgParsingOptions +): { [key: string]: any } { + const options: ArgParsingOptions = { + ...DEFAULT_OPTIONS, + ...(initialOptions || {}) + }; + + const flags = { + bools: {}, + strings: {}, + unknownFn: options.unknown!, + allBools: false + }; + + // TODO: get rid of this, providing two different options + if (typeof options["boolean"] === "boolean" && options["boolean"]) { + flags.allBools = true; + } else { + [] + .concat(options["boolean"]) + .filter(Boolean) + .forEach(function(key) { + flags.bools[key] = true; + }); + } + + const aliases = {}; + Object.keys(options.alias).forEach(function(key) { + aliases[key] = [].concat(options.alias[key]); + aliases[key].forEach(function(x) { + aliases[x] = [key].concat( + aliases[key].filter(function(y) { + return x !== y; + }) + ); + }); + }); + + [] + .concat(options.string) + .filter(Boolean) + .forEach(function(key) { + flags.strings[key] = true; + if (aliases[key]) { + flags.strings[aliases[key]] = true; + } + }); + + const defaults = options.default!; + + const argv = { _: [] }; + Object.keys(flags.bools).forEach(function(key) { + setArg(key, defaults[key] === undefined ? false : defaults[key]); + }); + + let notFlags = []; + + if (args.indexOf("--") !== -1) { + notFlags = args.slice(args.indexOf("--") + 1); + args = args.slice(0, args.indexOf("--")); + } + + function argDefined(key, arg) { + return ( + (flags.allBools && /^--[^=]+$/.test(arg)) || + flags.strings[key] || + flags.bools[key] || + aliases[key] + ); + } + + function setArg(key, val, arg = null): void { + if (arg && flags.unknownFn && !argDefined(key, arg)) { + if (flags.unknownFn(arg) === false) return; + } + + const value = !flags.strings[key] && isNumber(val) ? Number(val) : val; + setKey(argv, key.split("."), value); + + (aliases[key] || []).forEach(function(x) { + setKey(argv, x.split("."), value); + }); + } + + function setKey(obj, keys, value): void { + let o = obj; + keys.slice(0, -1).forEach(function(key) { + if (o[key] === undefined) o[key] = {}; + o = o[key]; + }); + + const key = keys[keys.length - 1]; + if ( + o[key] === undefined || + flags.bools[key] || + typeof o[key] === "boolean" + ) { + o[key] = value; + } else if (Array.isArray(o[key])) { + o[key].push(value); + } else { + o[key] = [o[key], value]; + } + } + + function aliasIsBoolean(key): boolean { + return aliases[key].some(function(x) { + return flags.bools[x]; + }); + } + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (/^--.+=/.test(arg)) { + // Using [\s\S] instead of . because js doesn't support the + // 'dotall' regex modifier. See: + // http://stackoverflow.com/a/1068308/13216 + const m = arg.match(/^--([^=]+)=([\s\S]*)$/); + const key = m[1]; + let value = m[2]; + if (flags.bools[key]) { + value = value !== "false"; + } + setArg(key, value, arg); + } else if (/^--no-.+/.test(arg)) { + const key = arg.match(/^--no-(.+)/)[1]; + setArg(key, false, arg); + } else if (/^--.+/.test(arg)) { + const key = arg.match(/^--(.+)/)[1]; + const next = args[i + 1]; + if ( + next !== undefined && + !/^-/.test(next) && + !flags.bools[key] && + !flags.allBools && + (aliases[key] ? !aliasIsBoolean(key) : true) + ) { + setArg(key, next, arg); + i++; + } else if (/^(true|false)$/.test(next)) { + setArg(key, next === "true", arg); + i++; + } else { + setArg(key, flags.strings[key] ? "" : true, arg); + } + } else if (/^-[^-]+/.test(arg)) { + const letters = arg.slice(1, -1).split(""); + + let broken = false; + for (let j = 0; j < letters.length; j++) { + const next = arg.slice(j + 2); + + if (next === "-") { + setArg(letters[j], next, arg); + continue; + } + + if (/[A-Za-z]/.test(letters[j]) && /=/.test(next)) { + setArg(letters[j], next.split("=")[1], arg); + broken = true; + break; + } + + if ( + /[A-Za-z]/.test(letters[j]) && + /-?\d+(\.\d*)?(e-?\d+)?$/.test(next) + ) { + setArg(letters[j], next, arg); + broken = true; + break; + } + + if (letters[j + 1] && letters[j + 1].match(/\W/)) { + setArg(letters[j], arg.slice(j + 2), arg); + broken = true; + break; + } else { + setArg(letters[j], flags.strings[letters[j]] ? "" : true, arg); + } + } + + const key = arg.slice(-1)[0]; + if (!broken && key !== "-") { + if ( + args[i + 1] && + !/^(-|--)[^-]/.test(args[i + 1]) && + !flags.bools[key] && + (aliases[key] ? !aliasIsBoolean(key) : true) + ) { + setArg(key, args[i + 1], arg); + i++; + } else if (args[i + 1] && /true|false/.test(args[i + 1])) { + setArg(key, args[i + 1] === "true", arg); + i++; + } else { + setArg(key, flags.strings[key] ? "" : true, arg); + } + } + } else { + if (!flags.unknownFn || flags.unknownFn(arg) !== false) { + argv._.push(flags.strings["_"] || !isNumber(arg) ? arg : Number(arg)); + } + if (options.stopEarly) { + argv._.push.apply(argv._, args.slice(i + 1)); + break; + } + } + } + + Object.keys(defaults).forEach(function(key) { + if (!hasKey(argv, key.split("."))) { + setKey(argv, key.split("."), defaults[key]); + + (aliases[key] || []).forEach(function(x) { + setKey(argv, x.split("."), defaults[key]); + }); + } + }); + + if (options["--"]) { + argv["--"] = new Array(); + notFlags.forEach(function(key) { + argv["--"].push(key); + }); + } else { + notFlags.forEach(function(key) { + argv._.push(key); + }); + } + + return argv; +} + +function hasKey(obj, keys) { + let o = obj; + keys.slice(0, -1).forEach(function(key) { + o = o[key] || {}; + }); + + const key = keys[keys.length - 1]; + return key in o; +} + +function isNumber(x: any): boolean { + if (typeof x === "number") return true; + if (/^0x[0-9a-f]+$/i.test(x)) return true; + return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x); +} diff --git a/flags/tests/all_bool.ts b/flags/tests/all_bool.ts index 2e0bba4ce..07b12c292 100755 --- a/flags/tests/all_bool.ts +++ b/flags/tests/all_bool.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; // flag boolean true (default all --args to boolean) test(function flagBooleanTrue() { diff --git a/flags/tests/bool.ts b/flags/tests/bool.ts index 5d135028e..6fa014d8d 100755 --- a/flags/tests/bool.ts +++ b/flags/tests/bool.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function flagBooleanDefaultFalse() { const argv = parse(["moo"], { diff --git a/flags/tests/dash.ts b/flags/tests/dash.ts index f8cec6ef7..6ab1a7d75 100755 --- a/flags/tests/dash.ts +++ b/flags/tests/dash.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function hyphen() { assertEqual(parse(["-n", "-"]), { n: "-", _: [] }); diff --git a/flags/tests/default_bool.ts b/flags/tests/default_bool.ts index 8cf6a720b..82dab5538 100755 --- a/flags/tests/default_bool.ts +++ b/flags/tests/default_bool.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function booleanDefaultTrue() { const argv = parse([], { diff --git a/flags/tests/dotted.ts b/flags/tests/dotted.ts index 94867abb2..6b64e3b5e 100755 --- a/flags/tests/dotted.ts +++ b/flags/tests/dotted.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function dottedAlias() { const argv = parse(["--a.b", "22"], { diff --git a/flags/tests/kv_short.ts b/flags/tests/kv_short.ts index 1050d7734..41853c1de 100755 --- a/flags/tests/kv_short.ts +++ b/flags/tests/kv_short.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function short() { const argv = parse(["-b=123"]); diff --git a/flags/tests/long.ts b/flags/tests/long.ts index 41c3e7743..d75ece3b2 100755 --- a/flags/tests/long.ts +++ b/flags/tests/long.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function longOpts() { assertEqual(parse(["--bool"]), { bool: true, _: [] }); diff --git a/flags/tests/num.ts b/flags/tests/num.ts index 0588b51f6..cc5b1b4e3 100755 --- a/flags/tests/num.ts +++ b/flags/tests/num.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function nums() { const argv = parse([ diff --git a/flags/tests/parse.ts b/flags/tests/parse.ts index 30551f875..263335761 100644 --- a/flags/tests/parse.ts +++ b/flags/tests/parse.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function _arseArgs() { assertEqual(parse(["--no-moo"]), { moo: false, _: [] }); diff --git a/flags/tests/short.ts b/flags/tests/short.ts index dee981351..fe8994394 100755 --- a/flags/tests/short.ts +++ b/flags/tests/short.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function numbericShortArgs() { assertEqual(parse(["-n123"]), { n: 123, _: [] }); diff --git a/flags/tests/stop_early.ts b/flags/tests/stop_early.ts index ca64bf97e..aef4d5dc5 100755 --- a/flags/tests/stop_early.ts +++ b/flags/tests/stop_early.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; // stops parsing on the first non-option when stopEarly is set test(function stopParsing() { diff --git a/flags/tests/unknown.ts b/flags/tests/unknown.ts index 2c87b18fe..e86f92796 100755 --- a/flags/tests/unknown.ts +++ b/flags/tests/unknown.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function booleanAndAliasIsNotUnknown() { const unknown = []; diff --git a/flags/tests/whitespace.ts b/flags/tests/whitespace.ts index 8373cd19e..46ad09426 100755 --- a/flags/tests/whitespace.ts +++ b/flags/tests/whitespace.ts @@ -1,5 +1,5 @@ import { test, assertEqual } from "../../testing/mod.ts"; -import { parse } from "../index.ts"; +import { parse } from "../mod.ts"; test(function whitespaceShouldBeWhitespace() { assertEqual(parse(["-x", "\t"]).x, "\t"); diff --git a/http/README.md b/http/README.md new file mode 100644 index 000000000..e81e42a41 --- /dev/null +++ b/http/README.md @@ -0,0 +1,26 @@ +# net + +Usage: + +```typescript +import { serve } from "https://deno.land/x/http/mod.ts"; +const s = serve("0.0.0.0:8000"); + +async function main() { + for await (const req of s) { + req.respond({ body: new TextEncoder().encode("Hello World\n") }); + } +} + +main(); +``` + +### File Server + +A small program for serving local files over HTTP. + +Add the following to your `.bash_profile` + +``` +alias file_server="deno https://deno.land/x/http/file_server.ts --allow-net" +``` diff --git a/http/file_server.ts b/http/file_server.ts new file mode 100755 index 000000000..4437a44e4 --- /dev/null +++ b/http/file_server.ts @@ -0,0 +1,241 @@ +#!/usr/bin/env deno --allow-net + +// This program serves files in the current directory over HTTP. +// TODO Stream responses instead of reading them into memory. +// TODO Add tests like these: +// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js + +import { + listenAndServe, + ServerRequest, + setContentLength, + Response +} from "./mod.ts"; +import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno"; +import { extname } from "../fs/path.ts"; +import { contentType } from "../media_types/mod.ts"; + +const dirViewerTemplate = ` + + + + + + + Deno File Server + + + +

Index of <%DIRNAME%>

+ + + <%CONTENTS%> +
ModeSizeName
+ + +`; + +const serverArgs = args.slice(); +let CORSEnabled = false; +// TODO: switch to flags if we later want to add more options +for (let i = 0; i < serverArgs.length; i++) { + if (serverArgs[i] === "--cors") { + CORSEnabled = true; + serverArgs.splice(i, 1); + break; + } +} +let currentDir = cwd(); +const target = serverArgs[1]; +if (target) { + currentDir = `${currentDir}/${target}`; +} +const addr = `0.0.0.0:${serverArgs[2] || 4500}`; +const encoder = new TextEncoder(); + +function modeToString(isDir: boolean, maybeMode: number | null) { + const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; + + if (maybeMode === null) { + return "(unknown mode)"; + } + const mode = maybeMode!.toString(8); + if (mode.length < 3) { + return "(unknown mode)"; + } + let output = ""; + mode + .split("") + .reverse() + .slice(0, 3) + .forEach(v => { + output = modeMap[+v] + output; + }); + output = `(${isDir ? "d" : "-"}${output})`; + return output; +} + +function fileLenToString(len: number) { + const multipler = 1024; + let base = 1; + const suffix = ["B", "K", "M", "G", "T"]; + let suffixIndex = 0; + + while (base * multipler < len) { + if (suffixIndex >= suffix.length - 1) { + break; + } + base *= multipler; + suffixIndex++; + } + + return `${(len / base).toFixed(2)}${suffix[suffixIndex]}`; +} + +function createDirEntryDisplay( + name: string, + path: string, + size: number | null, + mode: number | null, + isDir: boolean +) { + const sizeStr = size === null ? "" : "" + fileLenToString(size!); + return ` + ${modeToString( + isDir, + mode + )}${sizeStr}${name}${ + isDir ? "/" : "" + } + + `; +} + +// TODO: simplify this after deno.stat and deno.readDir are fixed +async function serveDir(req: ServerRequest, dirPath: string, dirName: string) { + // dirname has no prefix + const listEntry: string[] = []; + const fileInfos = await readDir(dirPath); + for (const info of fileInfos) { + if (info.name === "index.html" && info.isFile()) { + // in case index.html as dir... + return await serveFile(req, info.path); + } + // Yuck! + let mode = null; + try { + mode = (await stat(info.path)).mode; + } catch (e) {} + listEntry.push( + createDirEntryDisplay( + info.name, + dirName + "/" + info.name, + info.isFile() ? info.len : null, + mode, + info.isDirectory() + ) + ); + } + + const page = new TextEncoder().encode( + dirViewerTemplate + .replace("<%DIRNAME%>", dirName + "/") + .replace("<%CONTENTS%>", listEntry.join("")) + ); + + const headers = new Headers(); + headers.set("content-type", "text/html"); + + const res = { + status: 200, + body: page, + headers + }; + setContentLength(res); + return res; +} + +async function serveFile(req: ServerRequest, filename: string) { + const file = await open(filename); + const fileInfo = await stat(filename); + const headers = new Headers(); + headers.set("content-length", fileInfo.len.toString()); + headers.set("content-type", contentType(extname(filename)) || "text/plain"); + + const res = { + status: 200, + body: file, + headers + }; + return res; +} + +async function serveFallback(req: ServerRequest, e: Error) { + if ( + e instanceof DenoError && + (e as DenoError).kind === ErrorKind.NotFound + ) { + return { + status: 404, + body: encoder.encode("Not found") + }; + } else { + return { + status: 500, + body: encoder.encode("Internal server error") + }; + } +} + +function serverLog(req: ServerRequest, res: Response) { + const d = new Date().toISOString(); + const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`; + const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`; + console.log(s); +} + +function setCORS(res: Response) { + if (!res.headers) { + res.headers = new Headers(); + } + res.headers!.append("access-control-allow-origin", "*"); + res.headers!.append( + "access-control-allow-headers", + "Origin, X-Requested-With, Content-Type, Accept, Range" + ); +} + +listenAndServe(addr, async req => { + const fileName = req.url.replace(/\/$/, ""); + const filePath = currentDir + fileName; + + let response: Response; + + try { + const fileInfo = await stat(filePath); + if (fileInfo.isDirectory()) { + // Bug with deno.stat: name and path not populated + // Yuck! + response = await serveDir(req, filePath, fileName); + } else { + response = await serveFile(req, filePath); + } + } catch (e) { + response = await serveFallback(req, e); + } finally { + if (CORSEnabled) { + setCORS(response); + } + serverLog(req, response); + req.respond(response); + } +}); + +console.log(`HTTP server listening on http://${addr}/`); diff --git a/http/file_server_test.ts b/http/file_server_test.ts new file mode 100644 index 000000000..bd00d749b --- /dev/null +++ b/http/file_server_test.ts @@ -0,0 +1,51 @@ +import { readFile } from "deno"; + +import { test, assert, assertEqual } from "../testing/mod.ts"; + +// Promise to completeResolve when all tests completes +let completeResolve; +export const completePromise = new Promise(res => (completeResolve = res)); +let completedTestCount = 0; + +function maybeCompleteTests() { + completedTestCount++; + // Change this when adding more tests + if (completedTestCount === 3) { + completeResolve(); + } +} + +export function runTests(serverReadyPromise: Promise) { + test(async function serveFile() { + await serverReadyPromise; + const res = await fetch("http://localhost:4500/azure-pipelines.yml"); + assert(res.headers.has("access-control-allow-origin")); + assert(res.headers.has("access-control-allow-headers")); + assertEqual(res.headers.get("content-type"), "text/yaml; charset=utf-8"); + const downloadedFile = await res.text(); + const localFile = new TextDecoder().decode( + await readFile("./azure-pipelines.yml") + ); + assertEqual(downloadedFile, localFile); + maybeCompleteTests(); + }); + + test(async function serveDirectory() { + await serverReadyPromise; + const res = await fetch("http://localhost:4500/"); + assert(res.headers.has("access-control-allow-origin")); + assert(res.headers.has("access-control-allow-headers")); + const page = await res.text(); + assert(page.includes("azure-pipelines.yml")); + maybeCompleteTests(); + }); + + test(async function serveFallback() { + await serverReadyPromise; + const res = await fetch("http://localhost:4500/badfile.txt"); + assert(res.headers.has("access-control-allow-origin")); + assert(res.headers.has("access-control-allow-headers")); + assertEqual(res.status, 404); + maybeCompleteTests(); + }); +} diff --git a/http/http.ts b/http/http.ts new file mode 100644 index 000000000..da7bc0169 --- /dev/null +++ b/http/http.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 "../io/util.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_bench.ts b/http/http_bench.ts new file mode 100644 index 000000000..5aca12f55 --- /dev/null +++ b/http/http_bench.ts @@ -0,0 +1,15 @@ +import * as deno from "deno"; +import { serve } from "./mod.ts"; + +const addr = deno.args[1] || "127.0.0.1:4500"; +const server = serve(addr); + +const body = new TextEncoder().encode("Hello World"); + +async function main(): Promise { + for await (const request of server) { + await request.respond({ status: 200, body }); + } +} + +main(); diff --git a/http/http_status.ts b/http/http_status.ts new file mode 100644 index 000000000..a3006d319 --- /dev/null +++ b/http/http_status.ts @@ -0,0 +1,134 @@ +export enum Status { + Continue = 100, // RFC 7231, 6.2.1 + SwitchingProtocols = 101, // RFC 7231, 6.2.2 + Processing = 102, // RFC 2518, 10.1 + + OK = 200, // RFC 7231, 6.3.1 + Created = 201, // RFC 7231, 6.3.2 + Accepted = 202, // RFC 7231, 6.3.3 + NonAuthoritativeInfo = 203, // RFC 7231, 6.3.4 + NoContent = 204, // RFC 7231, 6.3.5 + ResetContent = 205, // RFC 7231, 6.3.6 + PartialContent = 206, // RFC 7233, 4.1 + MultiStatus = 207, // RFC 4918, 11.1 + AlreadyReported = 208, // RFC 5842, 7.1 + IMUsed = 226, // RFC 3229, 10.4.1 + + MultipleChoices = 300, // RFC 7231, 6.4.1 + MovedPermanently = 301, // RFC 7231, 6.4.2 + Found = 302, // RFC 7231, 6.4.3 + SeeOther = 303, // RFC 7231, 6.4.4 + NotModified = 304, // RFC 7232, 4.1 + UseProxy = 305, // RFC 7231, 6.4.5 + // _ = 306, // RFC 7231, 6.4.6 (Unused) + TemporaryRedirect = 307, // RFC 7231, 6.4.7 + PermanentRedirect = 308, // RFC 7538, 3 + + BadRequest = 400, // RFC 7231, 6.5.1 + Unauthorized = 401, // RFC 7235, 3.1 + PaymentRequired = 402, // RFC 7231, 6.5.2 + Forbidden = 403, // RFC 7231, 6.5.3 + NotFound = 404, // RFC 7231, 6.5.4 + MethodNotAllowed = 405, // RFC 7231, 6.5.5 + NotAcceptable = 406, // RFC 7231, 6.5.6 + ProxyAuthRequired = 407, // RFC 7235, 3.2 + RequestTimeout = 408, // RFC 7231, 6.5.7 + Conflict = 409, // RFC 7231, 6.5.8 + Gone = 410, // RFC 7231, 6.5.9 + LengthRequired = 411, // RFC 7231, 6.5.10 + PreconditionFailed = 412, // RFC 7232, 4.2 + RequestEntityTooLarge = 413, // RFC 7231, 6.5.11 + RequestURITooLong = 414, // RFC 7231, 6.5.12 + UnsupportedMediaType = 415, // RFC 7231, 6.5.13 + RequestedRangeNotSatisfiable = 416, // RFC 7233, 4.4 + ExpectationFailed = 417, // RFC 7231, 6.5.14 + Teapot = 418, // RFC 7168, 2.3.3 + MisdirectedRequest = 421, // RFC 7540, 9.1.2 + UnprocessableEntity = 422, // RFC 4918, 11.2 + Locked = 423, // RFC 4918, 11.3 + FailedDependency = 424, // RFC 4918, 11.4 + UpgradeRequired = 426, // RFC 7231, 6.5.15 + PreconditionRequired = 428, // RFC 6585, 3 + TooManyRequests = 429, // RFC 6585, 4 + RequestHeaderFieldsTooLarge = 431, // RFC 6585, 5 + UnavailableForLegalReasons = 451, // RFC 7725, 3 + + InternalServerError = 500, // RFC 7231, 6.6.1 + NotImplemented = 501, // RFC 7231, 6.6.2 + BadGateway = 502, // RFC 7231, 6.6.3 + ServiceUnavailable = 503, // RFC 7231, 6.6.4 + GatewayTimeout = 504, // RFC 7231, 6.6.5 + HTTPVersionNotSupported = 505, // RFC 7231, 6.6.6 + VariantAlsoNegotiates = 506, // RFC 2295, 8.1 + InsufficientStorage = 507, // RFC 4918, 11.5 + LoopDetected = 508, // RFC 5842, 7.2 + NotExtended = 510, // RFC 2774, 7 + NetworkAuthenticationRequired = 511 // RFC 6585, 6 +} + +export const STATUS_TEXT = new Map([ + [Status.Continue, "Continue"], + [Status.SwitchingProtocols, "Switching Protocols"], + [Status.Processing, "Processing"], + + [Status.OK, "OK"], + [Status.Created, "Created"], + [Status.Accepted, "Accepted"], + [Status.NonAuthoritativeInfo, "Non-Authoritative Information"], + [Status.NoContent, "No Content"], + [Status.ResetContent, "Reset Content"], + [Status.PartialContent, "Partial Content"], + [Status.MultiStatus, "Multi-Status"], + [Status.AlreadyReported, "Already Reported"], + [Status.IMUsed, "IM Used"], + + [Status.MultipleChoices, "Multiple Choices"], + [Status.MovedPermanently, "Moved Permanently"], + [Status.Found, "Found"], + [Status.SeeOther, "See Other"], + [Status.NotModified, "Not Modified"], + [Status.UseProxy, "Use Proxy"], + [Status.TemporaryRedirect, "Temporary Redirect"], + [Status.PermanentRedirect, "Permanent Redirect"], + + [Status.BadRequest, "Bad Request"], + [Status.Unauthorized, "Unauthorized"], + [Status.PaymentRequired, "Payment Required"], + [Status.Forbidden, "Forbidden"], + [Status.NotFound, "Not Found"], + [Status.MethodNotAllowed, "Method Not Allowed"], + [Status.NotAcceptable, "Not Acceptable"], + [Status.ProxyAuthRequired, "Proxy Authentication Required"], + [Status.RequestTimeout, "Request Timeout"], + [Status.Conflict, "Conflict"], + [Status.Gone, "Gone"], + [Status.LengthRequired, "Length Required"], + [Status.PreconditionFailed, "Precondition Failed"], + [Status.RequestEntityTooLarge, "Request Entity Too Large"], + [Status.RequestURITooLong, "Request URI Too Long"], + [Status.UnsupportedMediaType, "Unsupported Media Type"], + [Status.RequestedRangeNotSatisfiable, "Requested Range Not Satisfiable"], + [Status.ExpectationFailed, "Expectation Failed"], + [Status.Teapot, "I'm a teapot"], + [Status.MisdirectedRequest, "Misdirected Request"], + [Status.UnprocessableEntity, "Unprocessable Entity"], + [Status.Locked, "Locked"], + [Status.FailedDependency, "Failed Dependency"], + [Status.UpgradeRequired, "Upgrade Required"], + [Status.PreconditionRequired, "Precondition Required"], + [Status.TooManyRequests, "Too Many Requests"], + [Status.RequestHeaderFieldsTooLarge, "Request Header Fields Too Large"], + [Status.UnavailableForLegalReasons, "Unavailable For Legal Reasons"], + + [Status.InternalServerError, "Internal Server Error"], + [Status.NotImplemented, "Not Implemented"], + [Status.BadGateway, "Bad Gateway"], + [Status.ServiceUnavailable, "Service Unavailable"], + [Status.GatewayTimeout, "Gateway Timeout"], + [Status.HTTPVersionNotSupported, "HTTP Version Not Supported"], + [Status.VariantAlsoNegotiates, "Variant Also Negotiates"], + [Status.InsufficientStorage, "Insufficient Storage"], + [Status.LoopDetected, "Loop Detected"], + [Status.NotExtended, "Not Extended"], + [Status.NetworkAuthenticationRequired, "Network Authentication Required"] +]); diff --git a/http/http_test.ts b/http/http_test.ts new file mode 100644 index 000000000..ba0cec3e3 --- /dev/null +++ b/http/http_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 "./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 new file mode 100644 index 000000000..217bd68b3 --- /dev/null +++ b/http/mod.ts @@ -0,0 +1,8 @@ +import { + serve, + listenAndServe, + Response, + setContentLength, + ServerRequest +} from "./http.ts"; +export { serve, listenAndServe, Response, setContentLength, ServerRequest }; diff --git a/io/bufio.ts b/io/bufio.ts new file mode 100644 index 000000000..0dd2b94b4 --- /dev/null +++ b/io/bufio.ts @@ -0,0 +1,464 @@ +// Based on https://github.com/golang/go/blob/891682/src/bufio/bufio.go +// Copyright 2009 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. + +import { Reader, ReadResult, Writer } from "deno"; +import { assert, charCode, copyBytes } from "./util.ts"; + +const DEFAULT_BUF_SIZE = 4096; +const MIN_BUF_SIZE = 16; +const MAX_CONSECUTIVE_EMPTY_READS = 100; +const CR = charCode("\r"); +const LF = charCode("\n"); + +export type BufState = + | null + | "EOF" + | "BufferFull" + | "ShortWrite" + | "NoProgress" + | Error; + +/** BufReader implements buffering for a Reader object. */ +export class BufReader implements Reader { + private buf: Uint8Array; + private rd: Reader; // Reader provided by caller. + private r = 0; // buf read position. + private w = 0; // buf write position. + private lastByte: number; + private lastCharSize: number; + private err: BufState; + + constructor(rd: Reader, size = DEFAULT_BUF_SIZE) { + if (size < MIN_BUF_SIZE) { + size = MIN_BUF_SIZE; + } + this._reset(new Uint8Array(size), rd); + } + + /** Returns the size of the underlying buffer in bytes. */ + size(): number { + return this.buf.byteLength; + } + + buffered(): number { + return this.w - this.r; + } + + private _readErr(): BufState { + const err = this.err; + this.err = null; + return err; + } + + // Reads a new chunk into the buffer. + private async _fill(): Promise { + // Slide existing data to beginning. + if (this.r > 0) { + this.buf.copyWithin(0, this.r, this.w); + this.w -= this.r; + this.r = 0; + } + + if (this.w >= this.buf.byteLength) { + throw Error("bufio: tried to fill full buffer"); + } + + // Read new data: try a limited number of times. + for (let i = MAX_CONSECUTIVE_EMPTY_READS; i > 0; i--) { + let rr: ReadResult; + try { + rr = await this.rd.read(this.buf.subarray(this.w)); + } catch (e) { + this.err = e; + return; + } + assert(rr.nread >= 0, "negative read"); + this.w += rr.nread; + if (rr.eof) { + this.err = "EOF"; + return; + } + if (rr.nread > 0) { + return; + } + } + this.err = "NoProgress"; + } + + /** Discards any buffered data, resets all state, and switches + * the buffered reader to read from r. + */ + reset(r: Reader): void { + this._reset(this.buf, r); + } + + private _reset(buf: Uint8Array, rd: Reader): void { + this.buf = buf; + this.rd = rd; + this.lastByte = -1; + // this.lastRuneSize = -1; + } + + /** reads data into p. + * It returns the number of bytes read into p. + * The bytes are taken from at most one Read on the underlying Reader, + * hence n may be less than len(p). + * At EOF, the count will be zero and err will be io.EOF. + * To read exactly len(p) bytes, use io.ReadFull(b, p). + */ + async read(p: Uint8Array): Promise { + let rr: ReadResult = { nread: p.byteLength, eof: false }; + if (rr.nread === 0) { + if (this.err) { + throw this._readErr(); + } + return rr; + } + + if (this.r === this.w) { + if (this.err) { + throw this._readErr(); + } + if (p.byteLength >= this.buf.byteLength) { + // Large read, empty buffer. + // Read directly into p to avoid copy. + rr = await this.rd.read(p); + assert(rr.nread >= 0, "negative read"); + if (rr.nread > 0) { + this.lastByte = p[rr.nread - 1]; + // this.lastRuneSize = -1; + } + if (this.err) { + throw this._readErr(); + } + return rr; + } + // One read. + // Do not use this.fill, which will loop. + this.r = 0; + this.w = 0; + try { + rr = await this.rd.read(this.buf); + } catch (e) { + this.err = e; + } + assert(rr.nread >= 0, "negative read"); + if (rr.nread === 0) { + if (this.err) { + throw this._readErr(); + } + return rr; + } + this.w += rr.nread; + } + + // copy as much as we can + rr.nread = copyBytes(p as Uint8Array, this.buf.subarray(this.r, this.w), 0); + this.r += rr.nread; + this.lastByte = this.buf[this.r - 1]; + // this.lastRuneSize = -1; + return rr; + } + + /** reads exactly len(p) bytes into p. + * Ported from https://golang.org/pkg/io/#ReadFull + * It returns the number of bytes copied and an error if fewer bytes were read. + * The error is EOF only if no bytes were read. + * If an EOF happens after reading some but not all the bytes, + * readFull returns ErrUnexpectedEOF. ("EOF" for current impl) + * On return, n == len(p) if and only if err == nil. + * If r returns an error having read at least len(buf) bytes, + * the error is dropped. + */ + async readFull(p: Uint8Array): Promise<[number, BufState]> { + let rr = await this.read(p); + let nread = rr.nread; + if (rr.eof) { + return [nread, nread < p.length ? "EOF" : null]; + } + while (!rr.eof && nread < p.length) { + rr = await this.read(p.subarray(nread)); + nread += rr.nread; + } + return [nread, nread < p.length ? "EOF" : null]; + } + + /** Returns the next byte [0, 255] or -1 if EOF. */ + async readByte(): Promise { + while (this.r === this.w) { + await this._fill(); // buffer is empty. + if (this.err == "EOF") { + return -1; + } + if (this.err != null) { + throw this._readErr(); + } + } + const c = this.buf[this.r]; + this.r++; + this.lastByte = c; + return c; + } + + /** readString() reads until the first occurrence of delim in the input, + * returning a string containing the data up to and including the delimiter. + * If ReadString encounters an error before finding a delimiter, + * it returns the data read before the error and the error itself (often io.EOF). + * ReadString returns err != nil if and only if the returned data does not end in + * delim. + * For simple uses, a Scanner may be more convenient. + */ + async readString(delim: string): Promise { + throw new Error("Not implemented"); + } + + /** readLine() is a low-level line-reading primitive. Most callers should use + * readBytes('\n') or readString('\n') instead or use a Scanner. + * + * readLine tries to return a single line, not including the end-of-line bytes. + * If the line was too long for the buffer then isPrefix is set and the + * beginning of the line is returned. The rest of the line will be returned + * from future calls. isPrefix will be false when returning the last fragment + * of the line. The returned buffer is only valid until the next call to + * ReadLine. ReadLine either returns a non-nil line or it returns an error, + * never both. + * + * The text returned from ReadLine does not include the line end ("\r\n" or "\n"). + * No indication or error is given if the input ends without a final line end. + * Calling UnreadByte after ReadLine will always unread the last byte read + * (possibly a character belonging to the line end) even if that byte is not + * part of the line returned by ReadLine. + */ + async readLine(): Promise<[Uint8Array, boolean, BufState]> { + let [line, err] = await this.readSlice(LF); + + if (err === "BufferFull") { + // Handle the case where "\r\n" straddles the buffer. + if (line.byteLength > 0 && line[line.byteLength - 1] === CR) { + // Put the '\r' back on buf and drop it from line. + // Let the next call to ReadLine check for "\r\n". + assert(this.r > 0, "bufio: tried to rewind past start of buffer"); + this.r--; + line = line.subarray(0, line.byteLength - 1); + } + return [line, true, null]; + } + + if (line.byteLength === 0) { + return [line, false, err]; + } + err = null; + + if (line[line.byteLength - 1] == LF) { + let drop = 1; + if (line.byteLength > 1 && line[line.byteLength - 2] === CR) { + drop = 2; + } + line = line.subarray(0, line.byteLength - drop); + } + return [line, false, err]; + } + + /** readSlice() reads until the first occurrence of delim in the input, + * returning a slice pointing at the bytes in the buffer. The bytes stop + * being valid at the next read. If readSlice() encounters an error before + * finding a delimiter, it returns all the data in the buffer and the error + * itself (often io.EOF). readSlice() fails with error ErrBufferFull if the + * buffer fills without a delim. Because the data returned from readSlice() + * will be overwritten by the next I/O operation, most clients should use + * readBytes() or readString() instead. readSlice() returns err != nil if and + * only if line does not end in delim. + */ + async readSlice(delim: number): Promise<[Uint8Array, BufState]> { + let s = 0; // search start index + let line: Uint8Array; + let err: BufState; + while (true) { + // Search buffer. + let i = this.buf.subarray(this.r + s, this.w).indexOf(delim); + if (i >= 0) { + i += s; + line = this.buf.subarray(this.r, this.r + i + 1); + this.r += i + 1; + break; + } + + // Pending error? + if (this.err) { + line = this.buf.subarray(this.r, this.w); + this.r = this.w; + err = this._readErr(); + break; + } + + // Buffer full? + if (this.buffered() >= this.buf.byteLength) { + this.r = this.w; + line = this.buf; + err = "BufferFull"; + break; + } + + s = this.w - this.r; // do not rescan area we scanned before + + await this._fill(); // buffer is not full + } + + // Handle last byte, if any. + let i = line.byteLength - 1; + if (i >= 0) { + this.lastByte = line[i]; + // this.lastRuneSize = -1 + } + + return [line, err]; + } + + /** Peek returns the next n bytes without advancing the reader. The bytes stop + * being valid at the next read call. If Peek returns fewer than n bytes, it + * also returns an error explaining why the read is short. The error is + * ErrBufferFull if n is larger than b's buffer size. + */ + async peek(n: number): Promise<[Uint8Array, BufState]> { + if (n < 0) { + throw Error("negative count"); + } + + while ( + this.w - this.r < n && + this.w - this.r < this.buf.byteLength && + this.err == null + ) { + await this._fill(); // this.w - this.r < len(this.buf) => buffer is not full + } + + if (n > this.buf.byteLength) { + return [this.buf.subarray(this.r, this.w), "BufferFull"]; + } + + // 0 <= n <= len(this.buf) + let err: BufState; + let avail = this.w - this.r; + if (avail < n) { + // not enough data in buffer + n = avail; + err = this._readErr(); + if (!err) { + err = "BufferFull"; + } + } + return [this.buf.subarray(this.r, this.r + n), err]; + } +} + +/** BufWriter implements buffering for an deno.Writer object. + * If an error occurs writing to a Writer, no more data will be + * accepted and all subsequent writes, and flush(), will return the error. + * After all data has been written, the client should call the + * flush() method to guarantee all data has been forwarded to + * the underlying deno.Writer. + */ +export class BufWriter implements Writer { + buf: Uint8Array; + n: number = 0; + err: null | BufState = null; + + constructor(private wr: Writer, size = DEFAULT_BUF_SIZE) { + if (size <= 0) { + size = DEFAULT_BUF_SIZE; + } + this.buf = new Uint8Array(size); + } + + /** Size returns the size of the underlying buffer in bytes. */ + size(): number { + return this.buf.byteLength; + } + + /** Discards any unflushed buffered data, clears any error, and + * resets b to write its output to w. + */ + reset(w: Writer): void { + this.err = null; + this.n = 0; + this.wr = w; + } + + /** Flush writes any buffered data to the underlying io.Writer. */ + async flush(): Promise { + if (this.err != null) { + return this.err; + } + if (this.n == 0) { + return null; + } + + let n: number; + let err: BufState = null; + try { + n = await this.wr.write(this.buf.subarray(0, this.n)); + } catch (e) { + err = e; + } + + if (n < this.n && err == null) { + err = "ShortWrite"; + } + + if (err != null) { + if (n > 0 && n < this.n) { + this.buf.copyWithin(0, n, this.n); + } + this.n -= n; + this.err = err; + return err; + } + this.n = 0; + } + + /** Returns how many bytes are unused in the buffer. */ + available(): number { + return this.buf.byteLength - this.n; + } + + /** buffered returns the number of bytes that have been written into the + * current buffer. + */ + buffered(): number { + return this.n; + } + + /** Writes the contents of p into the buffer. + * Returns the number of bytes written. + */ + async write(p: Uint8Array): Promise { + let nn = 0; + let n: number; + while (p.byteLength > this.available() && !this.err) { + if (this.buffered() == 0) { + // Large write, empty buffer. + // Write directly from p to avoid copy. + try { + n = await this.wr.write(p); + } catch (e) { + this.err = e; + } + } else { + n = copyBytes(this.buf, p, this.n); + this.n += n; + await this.flush(); + } + nn += n; + p = p.subarray(n); + } + if (this.err) { + throw this.err; + } + n = copyBytes(this.buf, p, this.n); + this.n += n; + nn += n; + return nn; + } +} diff --git a/io/bufio_test.ts b/io/bufio_test.ts new file mode 100644 index 000000000..fa8f4b73b --- /dev/null +++ b/io/bufio_test.ts @@ -0,0 +1,341 @@ +// Based on https://github.com/golang/go/blob/891682/src/bufio/bufio_test.go +// Copyright 2009 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. + +import { Buffer, Reader, ReadResult } from "deno"; +import { test, assert, assertEqual } from "../testing/mod.ts"; +import { BufReader, BufState, BufWriter } from "./bufio.ts"; +import * as iotest from "./iotest.ts"; +import { charCode, copyBytes, stringsReader } from "./util.ts"; + +const encoder = new TextEncoder(); + +async function readBytes(buf: BufReader): Promise { + const b = new Uint8Array(1000); + let nb = 0; + while (true) { + let c = await buf.readByte(); + if (c < 0) { + break; // EOF + } + b[nb] = c; + nb++; + } + const decoder = new TextDecoder(); + return decoder.decode(b.subarray(0, nb)); +} + +test(async function bufioReaderSimple() { + const data = "hello world"; + const b = new BufReader(stringsReader(data)); + const s = await readBytes(b); + assertEqual(s, data); +}); + +type ReadMaker = { name: string; fn: (r: Reader) => Reader }; + +const readMakers: ReadMaker[] = [ + { name: "full", fn: r => r }, + { name: "byte", fn: r => new iotest.OneByteReader(r) }, + { name: "half", fn: r => new iotest.HalfReader(r) } + // TODO { name: "data+err", r => new iotest.DataErrReader(r) }, + // { name: "timeout", fn: r => new iotest.TimeoutReader(r) }, +]; + +function readLines(b: BufReader): string { + let s = ""; + while (true) { + let s1 = b.readString("\n"); + if (s1 == null) { + break; // EOF + } + s += s1; + } + return s; +} + +// Call read to accumulate the text of a file +async function reads(buf: BufReader, m: number): Promise { + const b = new Uint8Array(1000); + let nb = 0; + while (true) { + const { nread, eof } = await buf.read(b.subarray(nb, nb + m)); + nb += nread; + if (eof) { + break; + } + } + const decoder = new TextDecoder(); + return decoder.decode(b.subarray(0, nb)); +} + +type NamedBufReader = { name: string; fn: (r: BufReader) => Promise }; + +const bufreaders: NamedBufReader[] = [ + { name: "1", fn: (b: BufReader) => reads(b, 1) }, + { name: "2", fn: (b: BufReader) => reads(b, 2) }, + { name: "3", fn: (b: BufReader) => reads(b, 3) }, + { name: "4", fn: (b: BufReader) => reads(b, 4) }, + { name: "5", fn: (b: BufReader) => reads(b, 5) }, + { name: "7", fn: (b: BufReader) => reads(b, 7) }, + { name: "bytes", fn: readBytes } + // { name: "lines", fn: readLines }, +]; + +const MIN_READ_BUFFER_SIZE = 16; +const bufsizes: number[] = [ + 0, + MIN_READ_BUFFER_SIZE, + 23, + 32, + 46, + 64, + 93, + 128, + 1024, + 4096 +]; + +test(async function bufioBufReader() { + const texts = new Array(31); + let str = ""; + let all = ""; + for (let i = 0; i < texts.length - 1; i++) { + texts[i] = str + "\n"; + all += texts[i]; + str += String.fromCharCode((i % 26) + 97); + } + texts[texts.length - 1] = all; + + for (let text of texts) { + for (let readmaker of readMakers) { + for (let bufreader of bufreaders) { + for (let bufsize of bufsizes) { + const read = readmaker.fn(stringsReader(text)); + const buf = new BufReader(read, bufsize); + const s = await bufreader.fn(buf); + const debugStr = + `reader=${readmaker.name} ` + + `fn=${bufreader.name} bufsize=${bufsize} want=${text} got=${s}`; + assertEqual(s, text, debugStr); + } + } + } + } +}); + +test(async function bufioBufferFull() { + const longString = + "And now, hello, world! It is the time for all good men to come to the aid of their party"; + const buf = new BufReader(stringsReader(longString), MIN_READ_BUFFER_SIZE); + let [line, err] = await buf.readSlice(charCode("!")); + + const decoder = new TextDecoder(); + let actual = decoder.decode(line); + assertEqual(err, "BufferFull"); + assertEqual(actual, "And now, hello, "); + + [line, err] = await buf.readSlice(charCode("!")); + actual = decoder.decode(line); + assertEqual(actual, "world!"); + assert(err == null); +}); + +const testInput = encoder.encode( + "012\n345\n678\n9ab\ncde\nfgh\nijk\nlmn\nopq\nrst\nuvw\nxy" +); +const testInputrn = encoder.encode( + "012\r\n345\r\n678\r\n9ab\r\ncde\r\nfgh\r\nijk\r\nlmn\r\nopq\r\nrst\r\nuvw\r\nxy\r\n\n\r\n" +); +const testOutput = encoder.encode("0123456789abcdefghijklmnopqrstuvwxy"); + +// TestReader wraps a Uint8Array and returns reads of a specific length. +class TestReader implements Reader { + constructor(private data: Uint8Array, private stride: number) {} + + async read(buf: Uint8Array): Promise { + let nread = this.stride; + if (nread > this.data.byteLength) { + nread = this.data.byteLength; + } + if (nread > buf.byteLength) { + nread = buf.byteLength; + } + copyBytes(buf as Uint8Array, this.data); + this.data = this.data.subarray(nread); + let eof = false; + if (this.data.byteLength == 0) { + eof = true; + } + return { nread, eof }; + } +} + +async function testReadLine(input: Uint8Array): Promise { + for (let stride = 1; stride < 2; stride++) { + let done = 0; + let reader = new TestReader(input, stride); + let l = new BufReader(reader, input.byteLength + 1); + while (true) { + let [line, isPrefix, err] = await l.readLine(); + if (line.byteLength > 0 && err != null) { + throw Error("readLine returned both data and error"); + } + assertEqual(isPrefix, false); + if (err == "EOF") { + break; + } + let want = testOutput.subarray(done, done + line.byteLength); + assertEqual( + line, + want, + `Bad line at stride ${stride}: want: ${want} got: ${line}` + ); + done += line.byteLength; + } + assertEqual( + done, + testOutput.byteLength, + `readLine didn't return everything: got: ${done}, ` + + `want: ${testOutput} (stride: ${stride})` + ); + } +} + +test(async function bufioReadLine() { + await testReadLine(testInput); + await testReadLine(testInputrn); +}); + +test(async function bufioPeek() { + const decoder = new TextDecoder(); + let p = new Uint8Array(10); + // string is 16 (minReadBufferSize) long. + let buf = new BufReader( + stringsReader("abcdefghijklmnop"), + MIN_READ_BUFFER_SIZE + ); + + let [actual, err] = await buf.peek(1); + assertEqual(decoder.decode(actual), "a"); + assert(err == null); + + [actual, err] = await buf.peek(4); + assertEqual(decoder.decode(actual), "abcd"); + assert(err == null); + + [actual, err] = await buf.peek(32); + assertEqual(decoder.decode(actual), "abcdefghijklmnop"); + assertEqual(err, "BufferFull"); + + await buf.read(p.subarray(0, 3)); + assertEqual(decoder.decode(p.subarray(0, 3)), "abc"); + + [actual, err] = await buf.peek(1); + assertEqual(decoder.decode(actual), "d"); + assert(err == null); + + [actual, err] = await buf.peek(1); + assertEqual(decoder.decode(actual), "d"); + assert(err == null); + + [actual, err] = await buf.peek(1); + assertEqual(decoder.decode(actual), "d"); + assert(err == null); + + [actual, err] = await buf.peek(2); + assertEqual(decoder.decode(actual), "de"); + assert(err == null); + + let { eof } = await buf.read(p.subarray(0, 3)); + assertEqual(decoder.decode(p.subarray(0, 3)), "def"); + assert(!eof); + assert(err == null); + + [actual, err] = await buf.peek(4); + assertEqual(decoder.decode(actual), "ghij"); + assert(err == null); + + await buf.read(p); + assertEqual(decoder.decode(p), "ghijklmnop"); + + [actual, err] = await buf.peek(0); + assertEqual(decoder.decode(actual), ""); + assert(err == null); + + [actual, err] = await buf.peek(1); + assertEqual(decoder.decode(actual), ""); + assert(err == "EOF"); + /* TODO + // Test for issue 3022, not exposing a reader's error on a successful Peek. + buf = NewReaderSize(dataAndEOFReader("abcd"), 32) + if s, err := buf.Peek(2); string(s) != "ab" || err != nil { + t.Errorf(`Peek(2) on "abcd", EOF = %q, %v; want "ab", nil`, string(s), err) + } + if s, err := buf.Peek(4); string(s) != "abcd" || err != nil { + t.Errorf(`Peek(4) on "abcd", EOF = %q, %v; want "abcd", nil`, string(s), err) + } + if n, err := buf.Read(p[0:5]); string(p[0:n]) != "abcd" || err != nil { + t.Fatalf("Read after peek = %q, %v; want abcd, EOF", p[0:n], err) + } + if n, err := buf.Read(p[0:1]); string(p[0:n]) != "" || err != io.EOF { + t.Fatalf(`second Read after peek = %q, %v; want "", EOF`, p[0:n], err) + } + */ +}); + +test(async function bufioWriter() { + const data = new Uint8Array(8192); + + for (let i = 0; i < data.byteLength; i++) { + data[i] = charCode(" ") + (i % (charCode("~") - charCode(" "))); + } + + const w = new Buffer(); + for (let nwrite of bufsizes) { + for (let bs of bufsizes) { + // Write nwrite bytes using buffer size bs. + // Check that the right amount makes it out + // and that the data is correct. + + w.reset(); + const buf = new BufWriter(w, bs); + + const context = `nwrite=${nwrite} bufsize=${bs}`; + const n = await buf.write(data.subarray(0, nwrite)); + assertEqual(n, nwrite, context); + + await buf.flush(); + + const written = w.bytes(); + assertEqual(written.byteLength, nwrite); + + for (let l = 0; l < written.byteLength; l++) { + assertEqual(written[l], data[l]); + } + } + } +}); + +test(async function bufReaderReadFull() { + const enc = new TextEncoder(); + const dec = new TextDecoder(); + const text = "Hello World"; + const data = new Buffer(enc.encode(text)); + const bufr = new BufReader(data, 3); + { + const buf = new Uint8Array(6); + const [nread, err] = await bufr.readFull(buf); + assertEqual(nread, 6); + assert(!err); + assertEqual(dec.decode(buf), "Hello "); + } + { + const buf = new Uint8Array(6); + const [nread, err] = await bufr.readFull(buf); + assertEqual(nread, 5); + assertEqual(err, "EOF"); + assertEqual(dec.decode(buf.subarray(0, 5)), "World"); + } +}); diff --git a/io/iotest.ts b/io/iotest.ts new file mode 100644 index 000000000..e3a42f58a --- /dev/null +++ b/io/iotest.ts @@ -0,0 +1,61 @@ +// Ported to Deno from +// Copyright 2009 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. + +import { Reader, ReadResult } from "deno"; + +/** OneByteReader returns a Reader that implements + * each non-empty Read by reading one byte from r. + */ +export class OneByteReader implements Reader { + constructor(readonly r: Reader) {} + + async read(p: Uint8Array): Promise { + if (p.byteLength === 0) { + return { nread: 0, eof: false }; + } + if (!(p instanceof Uint8Array)) { + throw Error("expected Uint8Array"); + } + return this.r.read(p.subarray(0, 1)); + } +} + +/** HalfReader returns a Reader that implements Read + * by reading half as many requested bytes from r. + */ +export class HalfReader implements Reader { + constructor(readonly r: Reader) {} + + async read(p: Uint8Array): Promise { + if (!(p instanceof Uint8Array)) { + throw Error("expected Uint8Array"); + } + const half = Math.floor((p.byteLength + 1) / 2); + return this.r.read(p.subarray(0, half)); + } +} + +export class ErrTimeout extends Error { + constructor() { + super("timeout"); + this.name = "ErrTimeout"; + } +} + +/** TimeoutReader returns ErrTimeout on the second read + * with no data. Subsequent calls to read succeed. + */ +export class TimeoutReader implements Reader { + count = 0; + constructor(readonly r: Reader) {} + + async read(p: Uint8Array): Promise { + this.count++; + if (this.count === 2) { + throw new ErrTimeout(); + } + return this.r.read(p); + } +} diff --git a/io/ioutil.ts b/io/ioutil.ts new file mode 100644 index 000000000..68d6e5190 --- /dev/null +++ b/io/ioutil.ts @@ -0,0 +1,36 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { BufReader } from "./bufio.ts"; + +/* Read big endian 16bit short from BufReader */ +export async function readShort(buf: BufReader): Promise { + const [high, low] = [await buf.readByte(), await buf.readByte()]; + return (high << 8) | low; +} + +/* Read big endian 32bit integer from BufReader */ +export async function readInt(buf: BufReader): Promise { + const [high, low] = [await readShort(buf), await readShort(buf)]; + return (high << 16) | low; +} + +const BIT32 = 0xffffffff; +/* Read big endian 64bit long from BufReader */ +export async function readLong(buf: BufReader): Promise { + const [high, low] = [await readInt(buf), await readInt(buf)]; + // ECMAScript doesn't support 64bit bit ops. + return high ? high * (BIT32 + 1) + low : low; +} + +/* Slice number into 64bit big endian byte array */ +export function sliceLongToBytes(d: number, dest = new Array(8)): number[] { + let mask = 0xff; + let low = (d << 32) >>> 32; + let high = (d - low) / (BIT32 + 1); + let shift = 24; + for (let i = 0; i < 4; i++) { + dest[i] = (high >>> shift) & mask; + dest[i + 4] = (low >>> shift) & mask; + shift -= 8; + } + return dest; +} diff --git a/io/ioutil_test.ts b/io/ioutil_test.ts new file mode 100644 index 000000000..422901e4a --- /dev/null +++ b/io/ioutil_test.ts @@ -0,0 +1,62 @@ +import { Reader, ReadResult } from "deno"; +import { assertEqual, test } from "../testing/mod.ts"; +import { readInt, readLong, readShort, sliceLongToBytes } from "./ioutil.ts"; +import { BufReader } from "./bufio.ts"; + +class BinaryReader implements Reader { + index = 0; + + constructor(private bytes: Uint8Array = new Uint8Array(0)) {} + + async read(p: Uint8Array): Promise { + p.set(this.bytes.subarray(this.index, p.byteLength)); + this.index += p.byteLength; + return { nread: p.byteLength, eof: false }; + } +} + +test(async function testReadShort() { + const r = new BinaryReader(new Uint8Array([0x12, 0x34])); + const short = await readShort(new BufReader(r)); + assertEqual(short, 0x1234); +}); + +test(async function testReadInt() { + const r = new BinaryReader(new Uint8Array([0x12, 0x34, 0x56, 0x78])); + const int = await readInt(new BufReader(r)); + assertEqual(int, 0x12345678); +}); + +test(async function testReadLong() { + const r = new BinaryReader( + new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]) + ); + const long = await readLong(new BufReader(r)); + assertEqual(long, 0x1234567812345678); +}); + +test(async function testReadLong2() { + const r = new BinaryReader( + new Uint8Array([0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]) + ); + const long = await readLong(new BufReader(r)); + assertEqual(long, 0x12345678); +}); + +test(async function testSliceLongToBytes() { + const arr = sliceLongToBytes(0x1234567890abcdef); + const actual = readLong(new BufReader(new BinaryReader(new Uint8Array(arr)))); + const expected = readLong( + new BufReader( + new BinaryReader( + new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef]) + ) + ) + ); + assertEqual(actual, expected); +}); + +test(async function testSliceLongToBytes2() { + const arr = sliceLongToBytes(0x12345678); + assertEqual(arr, [0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]); +}); diff --git a/io/util.ts b/io/util.ts new file mode 100644 index 000000000..811940b4d --- /dev/null +++ b/io/util.ts @@ -0,0 +1,29 @@ +import { Buffer, Reader } from "deno"; + +export function assert(cond: boolean, msg = "assert") { + if (!cond) { + throw Error(msg); + } +} + +// `off` is the offset into `dst` where it will at which to begin writing values +// from `src`. +// Returns the number of bytes copied. +export function copyBytes(dst: Uint8Array, src: Uint8Array, off = 0): number { + const r = dst.byteLength - off; + if (src.byteLength > r) { + src = src.subarray(0, r); + } + dst.set(src, off); + return src.byteLength; +} + +export function charCode(s: string): number { + return s.charCodeAt(0); +} + +const encoder = new TextEncoder(); +export function stringsReader(s: string): Reader { + const ui8 = encoder.encode(s); + return new Buffer(ui8.buffer as ArrayBuffer); +} diff --git a/log/README.md b/log/README.md new file mode 100644 index 000000000..1d88cb070 --- /dev/null +++ b/log/README.md @@ -0,0 +1,38 @@ +# Basic usage + +```ts +import * as log from "https://deno.land/x/std/logging/index.ts"; + +// simple console logger +log.debug("Hello world"); +log.info("Hello world"); +log.warning("Hello world"); +log.error("Hello world"); +log.critical("500 Internal server error"); + +// configure as needed +await log.setup({ + handlers: { + console: new log.handlers.ConsoleHandler("DEBUG"), + file: new log.handlers.FileHandler("WARNING", "./log.txt") + }, + + loggers: { + default: { + level: "DEBUG", + handlers: ["console", "file"] + } + } +}); + +// get configured logger +const logger = log.getLogger("default"); +logger.debug("fizz"); // <- logs to `console`, because `file` handler requires 'WARNING' level +logger.warning("buzz"); // <- logs to both `console` and `file` handlers + +// if you try to use a logger that hasn't been configured +// you're good to go, it gets created automatically with level set to 0 +// so no message is logged +const unknownLogger = log.getLogger("mystery"); +unknownLogger.info("foobar"); // no-op +``` diff --git a/log/handlers.ts b/log/handlers.ts new file mode 100644 index 000000000..a0163e6cd --- /dev/null +++ b/log/handlers.ts @@ -0,0 +1,62 @@ +import { open, File, Writer } from "deno"; +import { getLevelByName } from "./levels.ts"; +import { LogRecord } from "./logger.ts"; + +export class BaseHandler { + level: number; + levelName: string; + + constructor(levelName: string) { + this.level = getLevelByName(levelName); + this.levelName = levelName; + } + + handle(logRecord: LogRecord) { + if (this.level > logRecord.level) return; + + // TODO: implement formatter + const msg = `${logRecord.levelName} ${logRecord.msg}`; + + return this.log(msg); + } + + log(msg: string) {} + async setup() {} + async destroy() {} +} + +export class ConsoleHandler extends BaseHandler { + log(msg: string) { + console.log(msg); + } +} + +export abstract class WriterHandler extends BaseHandler { + protected _writer: Writer; + + log(msg: string) { + const encoder = new TextEncoder(); + // promise is intentionally not awaited + this._writer.write(encoder.encode(msg + "\n")); + } +} + +export class FileHandler extends WriterHandler { + private _file: File; + private _filename: string; + + constructor(levelName: string, filename: string) { + super(levelName); + this._filename = filename; + } + + async setup() { + // open file in append mode - write only + this._file = await open(this._filename, "a"); + this._writer = this._file; + } + + async destroy() { + await this._file.close(); + } +} diff --git a/log/levels.ts b/log/levels.ts new file mode 100644 index 000000000..52d28aea5 --- /dev/null +++ b/log/levels.ts @@ -0,0 +1,34 @@ +export const LogLevel = { + NOTSET: 0, + DEBUG: 10, + INFO: 20, + WARNING: 30, + ERROR: 40, + CRITICAL: 50 +}; + +const byName = { + NOTSET: LogLevel.NOTSET, + DEBUG: LogLevel.DEBUG, + INFO: LogLevel.INFO, + WARNING: LogLevel.WARNING, + ERROR: LogLevel.ERROR, + CRITICAL: LogLevel.CRITICAL +}; + +const byLevel = { + [LogLevel.NOTSET]: "NOTSET", + [LogLevel.DEBUG]: "DEBUG", + [LogLevel.INFO]: "INFO", + [LogLevel.WARNING]: "WARNING", + [LogLevel.ERROR]: "ERROR", + [LogLevel.CRITICAL]: "CRITICAL" +}; + +export function getLevelByName(name: string): number { + return byName[name]; +} + +export function getLevelName(level: number): string { + return byLevel[level]; +} diff --git a/log/logger.ts b/log/logger.ts new file mode 100644 index 000000000..9f34f9c32 --- /dev/null +++ b/log/logger.ts @@ -0,0 +1,62 @@ +import { LogLevel, getLevelByName, getLevelName } from "./levels.ts"; +import { BaseHandler } from "./handlers.ts"; + +export interface LogRecord { + msg: string; + args: any[]; + datetime: Date; + level: number; + levelName: string; +} + +export class Logger { + level: number; + levelName: string; + handlers: any[]; + + constructor(levelName: string, handlers?: BaseHandler[]) { + this.level = getLevelByName(levelName); + this.levelName = levelName; + + this.handlers = handlers || []; + } + + _log(level: number, msg: string, ...args: any[]) { + if (this.level > level) return; + + // TODO: it'd be a good idea to make it immutable, so + // no handler mangles it by mistake + // TODO: iterpolate msg with values + const record: LogRecord = { + msg: msg, + args: args, + datetime: new Date(), + level: level, + levelName: getLevelName(level) + }; + + this.handlers.forEach(handler => { + handler.handle(record); + }); + } + + debug(msg: string, ...args: any[]) { + return this._log(LogLevel.DEBUG, msg, ...args); + } + + info(msg: string, ...args: any[]) { + return this._log(LogLevel.INFO, msg, ...args); + } + + warning(msg: string, ...args: any[]) { + return this._log(LogLevel.WARNING, msg, ...args); + } + + error(msg: string, ...args: any[]) { + return this._log(LogLevel.ERROR, msg, ...args); + } + + critical(msg: string, ...args: any[]) { + return this._log(LogLevel.CRITICAL, msg, ...args); + } +} diff --git a/log/mod.ts b/log/mod.ts new file mode 100644 index 000000000..e8c762ac6 --- /dev/null +++ b/log/mod.ts @@ -0,0 +1,119 @@ +import { Logger } from "./logger.ts"; +import { + BaseHandler, + ConsoleHandler, + WriterHandler, + FileHandler +} from "./handlers.ts"; + +export class LoggerConfig { + level?: string; + handlers?: string[]; +} + +export interface LogConfig { + handlers?: { + [name: string]: BaseHandler; + }; + loggers?: { + [name: string]: LoggerConfig; + }; +} + +const DEFAULT_LEVEL = "INFO"; +const DEFAULT_NAME = ""; +const DEFAULT_CONFIG: LogConfig = { + handlers: {}, + + loggers: { + "": { + level: "INFO", + handlers: [""] + } + } +}; + +const defaultHandler = new ConsoleHandler("INFO"); +const defaultLogger = new Logger("INFO", [defaultHandler]); + +const state = { + defaultHandler, + defaultLogger, + handlers: new Map(), + loggers: new Map(), + config: DEFAULT_CONFIG +}; + +export const handlers = { + BaseHandler, + ConsoleHandler, + WriterHandler, + FileHandler +}; + +export const debug = (msg: string, ...args: any[]) => + defaultLogger.debug(msg, ...args); +export const info = (msg: string, ...args: any[]) => + defaultLogger.info(msg, ...args); +export const warning = (msg: string, ...args: any[]) => + defaultLogger.warning(msg, ...args); +export const error = (msg: string, ...args: any[]) => + defaultLogger.error(msg, ...args); +export const critical = (msg: string, ...args: any[]) => + defaultLogger.critical(msg, ...args); + +export function getLogger(name?: string) { + if (!name) { + return defaultLogger; + } + + if (!state.loggers.has(name)) { + const logger = new Logger("NOTSET", []); + state.loggers.set(name, logger); + return logger; + } + + return state.loggers.get(name); +} + +export async function setup(config: LogConfig) { + state.config = config; + + // tear down existing handlers + state.handlers.forEach(handler => { + handler.destroy(); + }); + state.handlers.clear(); + + // setup handlers + const handlers = state.config.handlers || {}; + + for (const handlerName in handlers) { + const handler = handlers[handlerName]; + await handler.setup(); + state.handlers.set(handlerName, handler); + } + + // remove existing loggers + state.loggers.clear(); + + // setup loggers + const loggers = state.config.loggers || {}; + for (const loggerName in loggers) { + const loggerConfig = loggers[loggerName]; + const handlerNames = loggerConfig.handlers || []; + const handlers = []; + + handlerNames.forEach(handlerName => { + if (state.handlers.has(handlerName)) { + handlers.push(state.handlers.get(handlerName)); + } + }); + + const levelName = loggerConfig.level || DEFAULT_LEVEL; + const logger = new Logger(levelName, handlers); + state.loggers.set(loggerName, logger); + } +} + +setup(DEFAULT_CONFIG); diff --git a/log/test.ts b/log/test.ts new file mode 100644 index 000000000..fdc994eb7 --- /dev/null +++ b/log/test.ts @@ -0,0 +1,28 @@ +import { remove, open, readAll } from "deno"; +import { assertEqual, test } from "../testing/mod.ts"; +import * as log from "./mod.ts"; +import { FileHandler } from "./handlers.ts"; + +// TODO: establish something more sophisticated +let testOutput = ""; + +class TestHandler extends log.handlers.BaseHandler { + constructor(levelName: string) { + super(levelName); + } + + log(msg: string) { + testOutput += `${msg}\n`; + } +} + +test(function testDefaultlogMethods() { + log.debug("Foobar"); + log.info("Foobar"); + log.warning("Foobar"); + log.error("Foobar"); + log.critical("Foobar"); + + const logger = log.getLogger(""); + console.log(logger); +}); diff --git a/logging/README.md b/logging/README.md deleted file mode 100644 index 1d88cb070..000000000 --- a/logging/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Basic usage - -```ts -import * as log from "https://deno.land/x/std/logging/index.ts"; - -// simple console logger -log.debug("Hello world"); -log.info("Hello world"); -log.warning("Hello world"); -log.error("Hello world"); -log.critical("500 Internal server error"); - -// configure as needed -await log.setup({ - handlers: { - console: new log.handlers.ConsoleHandler("DEBUG"), - file: new log.handlers.FileHandler("WARNING", "./log.txt") - }, - - loggers: { - default: { - level: "DEBUG", - handlers: ["console", "file"] - } - } -}); - -// get configured logger -const logger = log.getLogger("default"); -logger.debug("fizz"); // <- logs to `console`, because `file` handler requires 'WARNING' level -logger.warning("buzz"); // <- logs to both `console` and `file` handlers - -// if you try to use a logger that hasn't been configured -// you're good to go, it gets created automatically with level set to 0 -// so no message is logged -const unknownLogger = log.getLogger("mystery"); -unknownLogger.info("foobar"); // no-op -``` diff --git a/logging/handlers.ts b/logging/handlers.ts deleted file mode 100644 index a0163e6cd..000000000 --- a/logging/handlers.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { open, File, Writer } from "deno"; -import { getLevelByName } from "./levels.ts"; -import { LogRecord } from "./logger.ts"; - -export class BaseHandler { - level: number; - levelName: string; - - constructor(levelName: string) { - this.level = getLevelByName(levelName); - this.levelName = levelName; - } - - handle(logRecord: LogRecord) { - if (this.level > logRecord.level) return; - - // TODO: implement formatter - const msg = `${logRecord.levelName} ${logRecord.msg}`; - - return this.log(msg); - } - - log(msg: string) {} - async setup() {} - async destroy() {} -} - -export class ConsoleHandler extends BaseHandler { - log(msg: string) { - console.log(msg); - } -} - -export abstract class WriterHandler extends BaseHandler { - protected _writer: Writer; - - log(msg: string) { - const encoder = new TextEncoder(); - // promise is intentionally not awaited - this._writer.write(encoder.encode(msg + "\n")); - } -} - -export class FileHandler extends WriterHandler { - private _file: File; - private _filename: string; - - constructor(levelName: string, filename: string) { - super(levelName); - this._filename = filename; - } - - async setup() { - // open file in append mode - write only - this._file = await open(this._filename, "a"); - this._writer = this._file; - } - - async destroy() { - await this._file.close(); - } -} diff --git a/logging/index.ts b/logging/index.ts deleted file mode 100644 index e8c762ac6..000000000 --- a/logging/index.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Logger } from "./logger.ts"; -import { - BaseHandler, - ConsoleHandler, - WriterHandler, - FileHandler -} from "./handlers.ts"; - -export class LoggerConfig { - level?: string; - handlers?: string[]; -} - -export interface LogConfig { - handlers?: { - [name: string]: BaseHandler; - }; - loggers?: { - [name: string]: LoggerConfig; - }; -} - -const DEFAULT_LEVEL = "INFO"; -const DEFAULT_NAME = ""; -const DEFAULT_CONFIG: LogConfig = { - handlers: {}, - - loggers: { - "": { - level: "INFO", - handlers: [""] - } - } -}; - -const defaultHandler = new ConsoleHandler("INFO"); -const defaultLogger = new Logger("INFO", [defaultHandler]); - -const state = { - defaultHandler, - defaultLogger, - handlers: new Map(), - loggers: new Map(), - config: DEFAULT_CONFIG -}; - -export const handlers = { - BaseHandler, - ConsoleHandler, - WriterHandler, - FileHandler -}; - -export const debug = (msg: string, ...args: any[]) => - defaultLogger.debug(msg, ...args); -export const info = (msg: string, ...args: any[]) => - defaultLogger.info(msg, ...args); -export const warning = (msg: string, ...args: any[]) => - defaultLogger.warning(msg, ...args); -export const error = (msg: string, ...args: any[]) => - defaultLogger.error(msg, ...args); -export const critical = (msg: string, ...args: any[]) => - defaultLogger.critical(msg, ...args); - -export function getLogger(name?: string) { - if (!name) { - return defaultLogger; - } - - if (!state.loggers.has(name)) { - const logger = new Logger("NOTSET", []); - state.loggers.set(name, logger); - return logger; - } - - return state.loggers.get(name); -} - -export async function setup(config: LogConfig) { - state.config = config; - - // tear down existing handlers - state.handlers.forEach(handler => { - handler.destroy(); - }); - state.handlers.clear(); - - // setup handlers - const handlers = state.config.handlers || {}; - - for (const handlerName in handlers) { - const handler = handlers[handlerName]; - await handler.setup(); - state.handlers.set(handlerName, handler); - } - - // remove existing loggers - state.loggers.clear(); - - // setup loggers - const loggers = state.config.loggers || {}; - for (const loggerName in loggers) { - const loggerConfig = loggers[loggerName]; - const handlerNames = loggerConfig.handlers || []; - const handlers = []; - - handlerNames.forEach(handlerName => { - if (state.handlers.has(handlerName)) { - handlers.push(state.handlers.get(handlerName)); - } - }); - - const levelName = loggerConfig.level || DEFAULT_LEVEL; - const logger = new Logger(levelName, handlers); - state.loggers.set(loggerName, logger); - } -} - -setup(DEFAULT_CONFIG); diff --git a/logging/levels.ts b/logging/levels.ts deleted file mode 100644 index 52d28aea5..000000000 --- a/logging/levels.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const LogLevel = { - NOTSET: 0, - DEBUG: 10, - INFO: 20, - WARNING: 30, - ERROR: 40, - CRITICAL: 50 -}; - -const byName = { - NOTSET: LogLevel.NOTSET, - DEBUG: LogLevel.DEBUG, - INFO: LogLevel.INFO, - WARNING: LogLevel.WARNING, - ERROR: LogLevel.ERROR, - CRITICAL: LogLevel.CRITICAL -}; - -const byLevel = { - [LogLevel.NOTSET]: "NOTSET", - [LogLevel.DEBUG]: "DEBUG", - [LogLevel.INFO]: "INFO", - [LogLevel.WARNING]: "WARNING", - [LogLevel.ERROR]: "ERROR", - [LogLevel.CRITICAL]: "CRITICAL" -}; - -export function getLevelByName(name: string): number { - return byName[name]; -} - -export function getLevelName(level: number): string { - return byLevel[level]; -} diff --git a/logging/logger.ts b/logging/logger.ts deleted file mode 100644 index 9f34f9c32..000000000 --- a/logging/logger.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { LogLevel, getLevelByName, getLevelName } from "./levels.ts"; -import { BaseHandler } from "./handlers.ts"; - -export interface LogRecord { - msg: string; - args: any[]; - datetime: Date; - level: number; - levelName: string; -} - -export class Logger { - level: number; - levelName: string; - handlers: any[]; - - constructor(levelName: string, handlers?: BaseHandler[]) { - this.level = getLevelByName(levelName); - this.levelName = levelName; - - this.handlers = handlers || []; - } - - _log(level: number, msg: string, ...args: any[]) { - if (this.level > level) return; - - // TODO: it'd be a good idea to make it immutable, so - // no handler mangles it by mistake - // TODO: iterpolate msg with values - const record: LogRecord = { - msg: msg, - args: args, - datetime: new Date(), - level: level, - levelName: getLevelName(level) - }; - - this.handlers.forEach(handler => { - handler.handle(record); - }); - } - - debug(msg: string, ...args: any[]) { - return this._log(LogLevel.DEBUG, msg, ...args); - } - - info(msg: string, ...args: any[]) { - return this._log(LogLevel.INFO, msg, ...args); - } - - warning(msg: string, ...args: any[]) { - return this._log(LogLevel.WARNING, msg, ...args); - } - - error(msg: string, ...args: any[]) { - return this._log(LogLevel.ERROR, msg, ...args); - } - - critical(msg: string, ...args: any[]) { - return this._log(LogLevel.CRITICAL, msg, ...args); - } -} diff --git a/logging/test.ts b/logging/test.ts deleted file mode 100644 index 17117ae8b..000000000 --- a/logging/test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { remove, open, readAll } from "deno"; -import { assertEqual, test } from "../testing/mod.ts"; -import * as log from "index.ts"; -import { FileHandler } from "./handlers.ts"; - -// TODO: establish something more sophisticated -let testOutput = ""; - -class TestHandler extends log.handlers.BaseHandler { - constructor(levelName: string) { - super(levelName); - } - - log(msg: string) { - testOutput += `${msg}\n`; - } -} - -test(function testDefaultlogMethods() { - log.debug("Foobar"); - log.info("Foobar"); - log.warning("Foobar"); - log.error("Foobar"); - log.critical("Foobar"); - - const logger = log.getLogger(""); - console.log(logger); -}); diff --git a/net/README.md b/net/README.md deleted file mode 100644 index 65f2b9ad7..000000000 --- a/net/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# net - -Usage: - -```typescript -import { serve } from "https://deno.land/x/net/http.ts"; -const s = serve("0.0.0.0:8000"); - -async function main() { - for await (const req of s) { - req.respond({ body: new TextEncoder().encode("Hello World\n") }); - } -} - -main(); -``` - -### File Server - -A small program for serving local files over HTTP. - -Add the following to your `.bash_profile` - -``` -alias file_server="deno https://deno.land/x/net/file_server.ts --allow-net" -``` diff --git a/net/bufio.ts b/net/bufio.ts deleted file mode 100644 index 0dd2b94b4..000000000 --- a/net/bufio.ts +++ /dev/null @@ -1,464 +0,0 @@ -// Based on https://github.com/golang/go/blob/891682/src/bufio/bufio.go -// Copyright 2009 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. - -import { Reader, ReadResult, Writer } from "deno"; -import { assert, charCode, copyBytes } from "./util.ts"; - -const DEFAULT_BUF_SIZE = 4096; -const MIN_BUF_SIZE = 16; -const MAX_CONSECUTIVE_EMPTY_READS = 100; -const CR = charCode("\r"); -const LF = charCode("\n"); - -export type BufState = - | null - | "EOF" - | "BufferFull" - | "ShortWrite" - | "NoProgress" - | Error; - -/** BufReader implements buffering for a Reader object. */ -export class BufReader implements Reader { - private buf: Uint8Array; - private rd: Reader; // Reader provided by caller. - private r = 0; // buf read position. - private w = 0; // buf write position. - private lastByte: number; - private lastCharSize: number; - private err: BufState; - - constructor(rd: Reader, size = DEFAULT_BUF_SIZE) { - if (size < MIN_BUF_SIZE) { - size = MIN_BUF_SIZE; - } - this._reset(new Uint8Array(size), rd); - } - - /** Returns the size of the underlying buffer in bytes. */ - size(): number { - return this.buf.byteLength; - } - - buffered(): number { - return this.w - this.r; - } - - private _readErr(): BufState { - const err = this.err; - this.err = null; - return err; - } - - // Reads a new chunk into the buffer. - private async _fill(): Promise { - // Slide existing data to beginning. - if (this.r > 0) { - this.buf.copyWithin(0, this.r, this.w); - this.w -= this.r; - this.r = 0; - } - - if (this.w >= this.buf.byteLength) { - throw Error("bufio: tried to fill full buffer"); - } - - // Read new data: try a limited number of times. - for (let i = MAX_CONSECUTIVE_EMPTY_READS; i > 0; i--) { - let rr: ReadResult; - try { - rr = await this.rd.read(this.buf.subarray(this.w)); - } catch (e) { - this.err = e; - return; - } - assert(rr.nread >= 0, "negative read"); - this.w += rr.nread; - if (rr.eof) { - this.err = "EOF"; - return; - } - if (rr.nread > 0) { - return; - } - } - this.err = "NoProgress"; - } - - /** Discards any buffered data, resets all state, and switches - * the buffered reader to read from r. - */ - reset(r: Reader): void { - this._reset(this.buf, r); - } - - private _reset(buf: Uint8Array, rd: Reader): void { - this.buf = buf; - this.rd = rd; - this.lastByte = -1; - // this.lastRuneSize = -1; - } - - /** reads data into p. - * It returns the number of bytes read into p. - * The bytes are taken from at most one Read on the underlying Reader, - * hence n may be less than len(p). - * At EOF, the count will be zero and err will be io.EOF. - * To read exactly len(p) bytes, use io.ReadFull(b, p). - */ - async read(p: Uint8Array): Promise { - let rr: ReadResult = { nread: p.byteLength, eof: false }; - if (rr.nread === 0) { - if (this.err) { - throw this._readErr(); - } - return rr; - } - - if (this.r === this.w) { - if (this.err) { - throw this._readErr(); - } - if (p.byteLength >= this.buf.byteLength) { - // Large read, empty buffer. - // Read directly into p to avoid copy. - rr = await this.rd.read(p); - assert(rr.nread >= 0, "negative read"); - if (rr.nread > 0) { - this.lastByte = p[rr.nread - 1]; - // this.lastRuneSize = -1; - } - if (this.err) { - throw this._readErr(); - } - return rr; - } - // One read. - // Do not use this.fill, which will loop. - this.r = 0; - this.w = 0; - try { - rr = await this.rd.read(this.buf); - } catch (e) { - this.err = e; - } - assert(rr.nread >= 0, "negative read"); - if (rr.nread === 0) { - if (this.err) { - throw this._readErr(); - } - return rr; - } - this.w += rr.nread; - } - - // copy as much as we can - rr.nread = copyBytes(p as Uint8Array, this.buf.subarray(this.r, this.w), 0); - this.r += rr.nread; - this.lastByte = this.buf[this.r - 1]; - // this.lastRuneSize = -1; - return rr; - } - - /** reads exactly len(p) bytes into p. - * Ported from https://golang.org/pkg/io/#ReadFull - * It returns the number of bytes copied and an error if fewer bytes were read. - * The error is EOF only if no bytes were read. - * If an EOF happens after reading some but not all the bytes, - * readFull returns ErrUnexpectedEOF. ("EOF" for current impl) - * On return, n == len(p) if and only if err == nil. - * If r returns an error having read at least len(buf) bytes, - * the error is dropped. - */ - async readFull(p: Uint8Array): Promise<[number, BufState]> { - let rr = await this.read(p); - let nread = rr.nread; - if (rr.eof) { - return [nread, nread < p.length ? "EOF" : null]; - } - while (!rr.eof && nread < p.length) { - rr = await this.read(p.subarray(nread)); - nread += rr.nread; - } - return [nread, nread < p.length ? "EOF" : null]; - } - - /** Returns the next byte [0, 255] or -1 if EOF. */ - async readByte(): Promise { - while (this.r === this.w) { - await this._fill(); // buffer is empty. - if (this.err == "EOF") { - return -1; - } - if (this.err != null) { - throw this._readErr(); - } - } - const c = this.buf[this.r]; - this.r++; - this.lastByte = c; - return c; - } - - /** readString() reads until the first occurrence of delim in the input, - * returning a string containing the data up to and including the delimiter. - * If ReadString encounters an error before finding a delimiter, - * it returns the data read before the error and the error itself (often io.EOF). - * ReadString returns err != nil if and only if the returned data does not end in - * delim. - * For simple uses, a Scanner may be more convenient. - */ - async readString(delim: string): Promise { - throw new Error("Not implemented"); - } - - /** readLine() is a low-level line-reading primitive. Most callers should use - * readBytes('\n') or readString('\n') instead or use a Scanner. - * - * readLine tries to return a single line, not including the end-of-line bytes. - * If the line was too long for the buffer then isPrefix is set and the - * beginning of the line is returned. The rest of the line will be returned - * from future calls. isPrefix will be false when returning the last fragment - * of the line. The returned buffer is only valid until the next call to - * ReadLine. ReadLine either returns a non-nil line or it returns an error, - * never both. - * - * The text returned from ReadLine does not include the line end ("\r\n" or "\n"). - * No indication or error is given if the input ends without a final line end. - * Calling UnreadByte after ReadLine will always unread the last byte read - * (possibly a character belonging to the line end) even if that byte is not - * part of the line returned by ReadLine. - */ - async readLine(): Promise<[Uint8Array, boolean, BufState]> { - let [line, err] = await this.readSlice(LF); - - if (err === "BufferFull") { - // Handle the case where "\r\n" straddles the buffer. - if (line.byteLength > 0 && line[line.byteLength - 1] === CR) { - // Put the '\r' back on buf and drop it from line. - // Let the next call to ReadLine check for "\r\n". - assert(this.r > 0, "bufio: tried to rewind past start of buffer"); - this.r--; - line = line.subarray(0, line.byteLength - 1); - } - return [line, true, null]; - } - - if (line.byteLength === 0) { - return [line, false, err]; - } - err = null; - - if (line[line.byteLength - 1] == LF) { - let drop = 1; - if (line.byteLength > 1 && line[line.byteLength - 2] === CR) { - drop = 2; - } - line = line.subarray(0, line.byteLength - drop); - } - return [line, false, err]; - } - - /** readSlice() reads until the first occurrence of delim in the input, - * returning a slice pointing at the bytes in the buffer. The bytes stop - * being valid at the next read. If readSlice() encounters an error before - * finding a delimiter, it returns all the data in the buffer and the error - * itself (often io.EOF). readSlice() fails with error ErrBufferFull if the - * buffer fills without a delim. Because the data returned from readSlice() - * will be overwritten by the next I/O operation, most clients should use - * readBytes() or readString() instead. readSlice() returns err != nil if and - * only if line does not end in delim. - */ - async readSlice(delim: number): Promise<[Uint8Array, BufState]> { - let s = 0; // search start index - let line: Uint8Array; - let err: BufState; - while (true) { - // Search buffer. - let i = this.buf.subarray(this.r + s, this.w).indexOf(delim); - if (i >= 0) { - i += s; - line = this.buf.subarray(this.r, this.r + i + 1); - this.r += i + 1; - break; - } - - // Pending error? - if (this.err) { - line = this.buf.subarray(this.r, this.w); - this.r = this.w; - err = this._readErr(); - break; - } - - // Buffer full? - if (this.buffered() >= this.buf.byteLength) { - this.r = this.w; - line = this.buf; - err = "BufferFull"; - break; - } - - s = this.w - this.r; // do not rescan area we scanned before - - await this._fill(); // buffer is not full - } - - // Handle last byte, if any. - let i = line.byteLength - 1; - if (i >= 0) { - this.lastByte = line[i]; - // this.lastRuneSize = -1 - } - - return [line, err]; - } - - /** Peek returns the next n bytes without advancing the reader. The bytes stop - * being valid at the next read call. If Peek returns fewer than n bytes, it - * also returns an error explaining why the read is short. The error is - * ErrBufferFull if n is larger than b's buffer size. - */ - async peek(n: number): Promise<[Uint8Array, BufState]> { - if (n < 0) { - throw Error("negative count"); - } - - while ( - this.w - this.r < n && - this.w - this.r < this.buf.byteLength && - this.err == null - ) { - await this._fill(); // this.w - this.r < len(this.buf) => buffer is not full - } - - if (n > this.buf.byteLength) { - return [this.buf.subarray(this.r, this.w), "BufferFull"]; - } - - // 0 <= n <= len(this.buf) - let err: BufState; - let avail = this.w - this.r; - if (avail < n) { - // not enough data in buffer - n = avail; - err = this._readErr(); - if (!err) { - err = "BufferFull"; - } - } - return [this.buf.subarray(this.r, this.r + n), err]; - } -} - -/** BufWriter implements buffering for an deno.Writer object. - * If an error occurs writing to a Writer, no more data will be - * accepted and all subsequent writes, and flush(), will return the error. - * After all data has been written, the client should call the - * flush() method to guarantee all data has been forwarded to - * the underlying deno.Writer. - */ -export class BufWriter implements Writer { - buf: Uint8Array; - n: number = 0; - err: null | BufState = null; - - constructor(private wr: Writer, size = DEFAULT_BUF_SIZE) { - if (size <= 0) { - size = DEFAULT_BUF_SIZE; - } - this.buf = new Uint8Array(size); - } - - /** Size returns the size of the underlying buffer in bytes. */ - size(): number { - return this.buf.byteLength; - } - - /** Discards any unflushed buffered data, clears any error, and - * resets b to write its output to w. - */ - reset(w: Writer): void { - this.err = null; - this.n = 0; - this.wr = w; - } - - /** Flush writes any buffered data to the underlying io.Writer. */ - async flush(): Promise { - if (this.err != null) { - return this.err; - } - if (this.n == 0) { - return null; - } - - let n: number; - let err: BufState = null; - try { - n = await this.wr.write(this.buf.subarray(0, this.n)); - } catch (e) { - err = e; - } - - if (n < this.n && err == null) { - err = "ShortWrite"; - } - - if (err != null) { - if (n > 0 && n < this.n) { - this.buf.copyWithin(0, n, this.n); - } - this.n -= n; - this.err = err; - return err; - } - this.n = 0; - } - - /** Returns how many bytes are unused in the buffer. */ - available(): number { - return this.buf.byteLength - this.n; - } - - /** buffered returns the number of bytes that have been written into the - * current buffer. - */ - buffered(): number { - return this.n; - } - - /** Writes the contents of p into the buffer. - * Returns the number of bytes written. - */ - async write(p: Uint8Array): Promise { - let nn = 0; - let n: number; - while (p.byteLength > this.available() && !this.err) { - if (this.buffered() == 0) { - // Large write, empty buffer. - // Write directly from p to avoid copy. - try { - n = await this.wr.write(p); - } catch (e) { - this.err = e; - } - } else { - n = copyBytes(this.buf, p, this.n); - this.n += n; - await this.flush(); - } - nn += n; - p = p.subarray(n); - } - if (this.err) { - throw this.err; - } - n = copyBytes(this.buf, p, this.n); - this.n += n; - nn += n; - return nn; - } -} diff --git a/net/bufio_test.ts b/net/bufio_test.ts deleted file mode 100644 index fa8f4b73b..000000000 --- a/net/bufio_test.ts +++ /dev/null @@ -1,341 +0,0 @@ -// Based on https://github.com/golang/go/blob/891682/src/bufio/bufio_test.go -// Copyright 2009 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. - -import { Buffer, Reader, ReadResult } from "deno"; -import { test, assert, assertEqual } from "../testing/mod.ts"; -import { BufReader, BufState, BufWriter } from "./bufio.ts"; -import * as iotest from "./iotest.ts"; -import { charCode, copyBytes, stringsReader } from "./util.ts"; - -const encoder = new TextEncoder(); - -async function readBytes(buf: BufReader): Promise { - const b = new Uint8Array(1000); - let nb = 0; - while (true) { - let c = await buf.readByte(); - if (c < 0) { - break; // EOF - } - b[nb] = c; - nb++; - } - const decoder = new TextDecoder(); - return decoder.decode(b.subarray(0, nb)); -} - -test(async function bufioReaderSimple() { - const data = "hello world"; - const b = new BufReader(stringsReader(data)); - const s = await readBytes(b); - assertEqual(s, data); -}); - -type ReadMaker = { name: string; fn: (r: Reader) => Reader }; - -const readMakers: ReadMaker[] = [ - { name: "full", fn: r => r }, - { name: "byte", fn: r => new iotest.OneByteReader(r) }, - { name: "half", fn: r => new iotest.HalfReader(r) } - // TODO { name: "data+err", r => new iotest.DataErrReader(r) }, - // { name: "timeout", fn: r => new iotest.TimeoutReader(r) }, -]; - -function readLines(b: BufReader): string { - let s = ""; - while (true) { - let s1 = b.readString("\n"); - if (s1 == null) { - break; // EOF - } - s += s1; - } - return s; -} - -// Call read to accumulate the text of a file -async function reads(buf: BufReader, m: number): Promise { - const b = new Uint8Array(1000); - let nb = 0; - while (true) { - const { nread, eof } = await buf.read(b.subarray(nb, nb + m)); - nb += nread; - if (eof) { - break; - } - } - const decoder = new TextDecoder(); - return decoder.decode(b.subarray(0, nb)); -} - -type NamedBufReader = { name: string; fn: (r: BufReader) => Promise }; - -const bufreaders: NamedBufReader[] = [ - { name: "1", fn: (b: BufReader) => reads(b, 1) }, - { name: "2", fn: (b: BufReader) => reads(b, 2) }, - { name: "3", fn: (b: BufReader) => reads(b, 3) }, - { name: "4", fn: (b: BufReader) => reads(b, 4) }, - { name: "5", fn: (b: BufReader) => reads(b, 5) }, - { name: "7", fn: (b: BufReader) => reads(b, 7) }, - { name: "bytes", fn: readBytes } - // { name: "lines", fn: readLines }, -]; - -const MIN_READ_BUFFER_SIZE = 16; -const bufsizes: number[] = [ - 0, - MIN_READ_BUFFER_SIZE, - 23, - 32, - 46, - 64, - 93, - 128, - 1024, - 4096 -]; - -test(async function bufioBufReader() { - const texts = new Array(31); - let str = ""; - let all = ""; - for (let i = 0; i < texts.length - 1; i++) { - texts[i] = str + "\n"; - all += texts[i]; - str += String.fromCharCode((i % 26) + 97); - } - texts[texts.length - 1] = all; - - for (let text of texts) { - for (let readmaker of readMakers) { - for (let bufreader of bufreaders) { - for (let bufsize of bufsizes) { - const read = readmaker.fn(stringsReader(text)); - const buf = new BufReader(read, bufsize); - const s = await bufreader.fn(buf); - const debugStr = - `reader=${readmaker.name} ` + - `fn=${bufreader.name} bufsize=${bufsize} want=${text} got=${s}`; - assertEqual(s, text, debugStr); - } - } - } - } -}); - -test(async function bufioBufferFull() { - const longString = - "And now, hello, world! It is the time for all good men to come to the aid of their party"; - const buf = new BufReader(stringsReader(longString), MIN_READ_BUFFER_SIZE); - let [line, err] = await buf.readSlice(charCode("!")); - - const decoder = new TextDecoder(); - let actual = decoder.decode(line); - assertEqual(err, "BufferFull"); - assertEqual(actual, "And now, hello, "); - - [line, err] = await buf.readSlice(charCode("!")); - actual = decoder.decode(line); - assertEqual(actual, "world!"); - assert(err == null); -}); - -const testInput = encoder.encode( - "012\n345\n678\n9ab\ncde\nfgh\nijk\nlmn\nopq\nrst\nuvw\nxy" -); -const testInputrn = encoder.encode( - "012\r\n345\r\n678\r\n9ab\r\ncde\r\nfgh\r\nijk\r\nlmn\r\nopq\r\nrst\r\nuvw\r\nxy\r\n\n\r\n" -); -const testOutput = encoder.encode("0123456789abcdefghijklmnopqrstuvwxy"); - -// TestReader wraps a Uint8Array and returns reads of a specific length. -class TestReader implements Reader { - constructor(private data: Uint8Array, private stride: number) {} - - async read(buf: Uint8Array): Promise { - let nread = this.stride; - if (nread > this.data.byteLength) { - nread = this.data.byteLength; - } - if (nread > buf.byteLength) { - nread = buf.byteLength; - } - copyBytes(buf as Uint8Array, this.data); - this.data = this.data.subarray(nread); - let eof = false; - if (this.data.byteLength == 0) { - eof = true; - } - return { nread, eof }; - } -} - -async function testReadLine(input: Uint8Array): Promise { - for (let stride = 1; stride < 2; stride++) { - let done = 0; - let reader = new TestReader(input, stride); - let l = new BufReader(reader, input.byteLength + 1); - while (true) { - let [line, isPrefix, err] = await l.readLine(); - if (line.byteLength > 0 && err != null) { - throw Error("readLine returned both data and error"); - } - assertEqual(isPrefix, false); - if (err == "EOF") { - break; - } - let want = testOutput.subarray(done, done + line.byteLength); - assertEqual( - line, - want, - `Bad line at stride ${stride}: want: ${want} got: ${line}` - ); - done += line.byteLength; - } - assertEqual( - done, - testOutput.byteLength, - `readLine didn't return everything: got: ${done}, ` + - `want: ${testOutput} (stride: ${stride})` - ); - } -} - -test(async function bufioReadLine() { - await testReadLine(testInput); - await testReadLine(testInputrn); -}); - -test(async function bufioPeek() { - const decoder = new TextDecoder(); - let p = new Uint8Array(10); - // string is 16 (minReadBufferSize) long. - let buf = new BufReader( - stringsReader("abcdefghijklmnop"), - MIN_READ_BUFFER_SIZE - ); - - let [actual, err] = await buf.peek(1); - assertEqual(decoder.decode(actual), "a"); - assert(err == null); - - [actual, err] = await buf.peek(4); - assertEqual(decoder.decode(actual), "abcd"); - assert(err == null); - - [actual, err] = await buf.peek(32); - assertEqual(decoder.decode(actual), "abcdefghijklmnop"); - assertEqual(err, "BufferFull"); - - await buf.read(p.subarray(0, 3)); - assertEqual(decoder.decode(p.subarray(0, 3)), "abc"); - - [actual, err] = await buf.peek(1); - assertEqual(decoder.decode(actual), "d"); - assert(err == null); - - [actual, err] = await buf.peek(1); - assertEqual(decoder.decode(actual), "d"); - assert(err == null); - - [actual, err] = await buf.peek(1); - assertEqual(decoder.decode(actual), "d"); - assert(err == null); - - [actual, err] = await buf.peek(2); - assertEqual(decoder.decode(actual), "de"); - assert(err == null); - - let { eof } = await buf.read(p.subarray(0, 3)); - assertEqual(decoder.decode(p.subarray(0, 3)), "def"); - assert(!eof); - assert(err == null); - - [actual, err] = await buf.peek(4); - assertEqual(decoder.decode(actual), "ghij"); - assert(err == null); - - await buf.read(p); - assertEqual(decoder.decode(p), "ghijklmnop"); - - [actual, err] = await buf.peek(0); - assertEqual(decoder.decode(actual), ""); - assert(err == null); - - [actual, err] = await buf.peek(1); - assertEqual(decoder.decode(actual), ""); - assert(err == "EOF"); - /* TODO - // Test for issue 3022, not exposing a reader's error on a successful Peek. - buf = NewReaderSize(dataAndEOFReader("abcd"), 32) - if s, err := buf.Peek(2); string(s) != "ab" || err != nil { - t.Errorf(`Peek(2) on "abcd", EOF = %q, %v; want "ab", nil`, string(s), err) - } - if s, err := buf.Peek(4); string(s) != "abcd" || err != nil { - t.Errorf(`Peek(4) on "abcd", EOF = %q, %v; want "abcd", nil`, string(s), err) - } - if n, err := buf.Read(p[0:5]); string(p[0:n]) != "abcd" || err != nil { - t.Fatalf("Read after peek = %q, %v; want abcd, EOF", p[0:n], err) - } - if n, err := buf.Read(p[0:1]); string(p[0:n]) != "" || err != io.EOF { - t.Fatalf(`second Read after peek = %q, %v; want "", EOF`, p[0:n], err) - } - */ -}); - -test(async function bufioWriter() { - const data = new Uint8Array(8192); - - for (let i = 0; i < data.byteLength; i++) { - data[i] = charCode(" ") + (i % (charCode("~") - charCode(" "))); - } - - const w = new Buffer(); - for (let nwrite of bufsizes) { - for (let bs of bufsizes) { - // Write nwrite bytes using buffer size bs. - // Check that the right amount makes it out - // and that the data is correct. - - w.reset(); - const buf = new BufWriter(w, bs); - - const context = `nwrite=${nwrite} bufsize=${bs}`; - const n = await buf.write(data.subarray(0, nwrite)); - assertEqual(n, nwrite, context); - - await buf.flush(); - - const written = w.bytes(); - assertEqual(written.byteLength, nwrite); - - for (let l = 0; l < written.byteLength; l++) { - assertEqual(written[l], data[l]); - } - } - } -}); - -test(async function bufReaderReadFull() { - const enc = new TextEncoder(); - const dec = new TextDecoder(); - const text = "Hello World"; - const data = new Buffer(enc.encode(text)); - const bufr = new BufReader(data, 3); - { - const buf = new Uint8Array(6); - const [nread, err] = await bufr.readFull(buf); - assertEqual(nread, 6); - assert(!err); - assertEqual(dec.decode(buf), "Hello "); - } - { - const buf = new Uint8Array(6); - const [nread, err] = await bufr.readFull(buf); - assertEqual(nread, 5); - assertEqual(err, "EOF"); - assertEqual(dec.decode(buf.subarray(0, 5)), "World"); - } -}); diff --git a/net/file_server.ts b/net/file_server.ts deleted file mode 100755 index 72432abdd..000000000 --- a/net/file_server.ts +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env deno --allow-net - -// This program serves files in the current directory over HTTP. -// TODO Stream responses instead of reading them into memory. -// TODO Add tests like these: -// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js - -import { - listenAndServe, - ServerRequest, - setContentLength, - Response -} from "./http.ts"; -import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno"; -import { extname } from "../fs/path.ts"; -import { contentType } from "../media_types/mod.ts"; - -const dirViewerTemplate = ` - - - - - - - Deno File Server - - - -

Index of <%DIRNAME%>

- - - <%CONTENTS%> -
ModeSizeName
- - -`; - -const serverArgs = args.slice(); -let CORSEnabled = false; -// TODO: switch to flags if we later want to add more options -for (let i = 0; i < serverArgs.length; i++) { - if (serverArgs[i] === "--cors") { - CORSEnabled = true; - serverArgs.splice(i, 1); - break; - } -} -let currentDir = cwd(); -const target = serverArgs[1]; -if (target) { - currentDir = `${currentDir}/${target}`; -} -const addr = `0.0.0.0:${serverArgs[2] || 4500}`; -const encoder = new TextEncoder(); - -function modeToString(isDir: boolean, maybeMode: number | null) { - const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; - - if (maybeMode === null) { - return "(unknown mode)"; - } - const mode = maybeMode!.toString(8); - if (mode.length < 3) { - return "(unknown mode)"; - } - let output = ""; - mode - .split("") - .reverse() - .slice(0, 3) - .forEach(v => { - output = modeMap[+v] + output; - }); - output = `(${isDir ? "d" : "-"}${output})`; - return output; -} - -function fileLenToString(len: number) { - const multipler = 1024; - let base = 1; - const suffix = ["B", "K", "M", "G", "T"]; - let suffixIndex = 0; - - while (base * multipler < len) { - if (suffixIndex >= suffix.length - 1) { - break; - } - base *= multipler; - suffixIndex++; - } - - return `${(len / base).toFixed(2)}${suffix[suffixIndex]}`; -} - -function createDirEntryDisplay( - name: string, - path: string, - size: number | null, - mode: number | null, - isDir: boolean -) { - const sizeStr = size === null ? "" : "" + fileLenToString(size!); - return ` - ${modeToString( - isDir, - mode - )}${sizeStr}${name}${ - isDir ? "/" : "" - } - - `; -} - -// TODO: simplify this after deno.stat and deno.readDir are fixed -async function serveDir(req: ServerRequest, dirPath: string, dirName: string) { - // dirname has no prefix - const listEntry: string[] = []; - const fileInfos = await readDir(dirPath); - for (const info of fileInfos) { - if (info.name === "index.html" && info.isFile()) { - // in case index.html as dir... - return await serveFile(req, info.path); - } - // Yuck! - let mode = null; - try { - mode = (await stat(info.path)).mode; - } catch (e) {} - listEntry.push( - createDirEntryDisplay( - info.name, - dirName + "/" + info.name, - info.isFile() ? info.len : null, - mode, - info.isDirectory() - ) - ); - } - - const page = new TextEncoder().encode( - dirViewerTemplate - .replace("<%DIRNAME%>", dirName + "/") - .replace("<%CONTENTS%>", listEntry.join("")) - ); - - const headers = new Headers(); - headers.set("content-type", "text/html"); - - const res = { - status: 200, - body: page, - headers - }; - setContentLength(res); - return res; -} - -async function serveFile(req: ServerRequest, filename: string) { - const file = await open(filename); - const fileInfo = await stat(filename); - const headers = new Headers(); - headers.set("content-length", fileInfo.len.toString()); - headers.set("content-type", contentType(extname(filename)) || "text/plain"); - - const res = { - status: 200, - body: file, - headers - }; - return res; -} - -async function serveFallback(req: ServerRequest, e: Error) { - if ( - e instanceof DenoError && - (e as DenoError).kind === ErrorKind.NotFound - ) { - return { - status: 404, - body: encoder.encode("Not found") - }; - } else { - return { - status: 500, - body: encoder.encode("Internal server error") - }; - } -} - -function serverLog(req: ServerRequest, res: Response) { - const d = new Date().toISOString(); - const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`; - const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`; - console.log(s); -} - -function setCORS(res: Response) { - if (!res.headers) { - res.headers = new Headers(); - } - res.headers!.append("access-control-allow-origin", "*"); - res.headers!.append( - "access-control-allow-headers", - "Origin, X-Requested-With, Content-Type, Accept, Range" - ); -} - -listenAndServe(addr, async req => { - const fileName = req.url.replace(/\/$/, ""); - const filePath = currentDir + fileName; - - let response: Response; - - try { - const fileInfo = await stat(filePath); - if (fileInfo.isDirectory()) { - // Bug with deno.stat: name and path not populated - // Yuck! - response = await serveDir(req, filePath, fileName); - } else { - response = await serveFile(req, filePath); - } - } catch (e) { - response = await serveFallback(req, e); - } finally { - if (CORSEnabled) { - setCORS(response); - } - serverLog(req, response); - req.respond(response); - } -}); - -console.log(`HTTP server listening on http://${addr}/`); diff --git a/net/file_server_test.ts b/net/file_server_test.ts deleted file mode 100644 index bd00d749b..000000000 --- a/net/file_server_test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { readFile } from "deno"; - -import { test, assert, assertEqual } from "../testing/mod.ts"; - -// Promise to completeResolve when all tests completes -let completeResolve; -export const completePromise = new Promise(res => (completeResolve = res)); -let completedTestCount = 0; - -function maybeCompleteTests() { - completedTestCount++; - // Change this when adding more tests - if (completedTestCount === 3) { - completeResolve(); - } -} - -export function runTests(serverReadyPromise: Promise) { - test(async function serveFile() { - await serverReadyPromise; - const res = await fetch("http://localhost:4500/azure-pipelines.yml"); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - assertEqual(res.headers.get("content-type"), "text/yaml; charset=utf-8"); - const downloadedFile = await res.text(); - const localFile = new TextDecoder().decode( - await readFile("./azure-pipelines.yml") - ); - assertEqual(downloadedFile, localFile); - maybeCompleteTests(); - }); - - test(async function serveDirectory() { - await serverReadyPromise; - const res = await fetch("http://localhost:4500/"); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - const page = await res.text(); - assert(page.includes("azure-pipelines.yml")); - maybeCompleteTests(); - }); - - test(async function serveFallback() { - await serverReadyPromise; - const res = await fetch("http://localhost:4500/badfile.txt"); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - assertEqual(res.status, 404); - maybeCompleteTests(); - }); -} diff --git a/net/http.ts b/net/http.ts deleted file mode 100644 index e360e0d86..000000000 --- a/net/http.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { listen, Conn, toAsyncIterator, Reader, copy } from "deno"; -import { BufReader, BufState, BufWriter } from "./bufio.ts"; -import { TextProtoReader } from "./textproto.ts"; -import { STATUS_TEXT } from "./http_status.ts"; -import { assert } from "./util.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/net/http_bench.ts b/net/http_bench.ts deleted file mode 100644 index 8e1e24ad6..000000000 --- a/net/http_bench.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as deno from "deno"; -import { serve } from "./http.ts"; - -const addr = deno.args[1] || "127.0.0.1:4500"; -const server = serve(addr); - -const body = new TextEncoder().encode("Hello World"); - -async function main(): Promise { - for await (const request of server) { - await request.respond({ status: 200, body }); - } -} - -main(); diff --git a/net/http_status.ts b/net/http_status.ts deleted file mode 100644 index a3006d319..000000000 --- a/net/http_status.ts +++ /dev/null @@ -1,134 +0,0 @@ -export enum Status { - Continue = 100, // RFC 7231, 6.2.1 - SwitchingProtocols = 101, // RFC 7231, 6.2.2 - Processing = 102, // RFC 2518, 10.1 - - OK = 200, // RFC 7231, 6.3.1 - Created = 201, // RFC 7231, 6.3.2 - Accepted = 202, // RFC 7231, 6.3.3 - NonAuthoritativeInfo = 203, // RFC 7231, 6.3.4 - NoContent = 204, // RFC 7231, 6.3.5 - ResetContent = 205, // RFC 7231, 6.3.6 - PartialContent = 206, // RFC 7233, 4.1 - MultiStatus = 207, // RFC 4918, 11.1 - AlreadyReported = 208, // RFC 5842, 7.1 - IMUsed = 226, // RFC 3229, 10.4.1 - - MultipleChoices = 300, // RFC 7231, 6.4.1 - MovedPermanently = 301, // RFC 7231, 6.4.2 - Found = 302, // RFC 7231, 6.4.3 - SeeOther = 303, // RFC 7231, 6.4.4 - NotModified = 304, // RFC 7232, 4.1 - UseProxy = 305, // RFC 7231, 6.4.5 - // _ = 306, // RFC 7231, 6.4.6 (Unused) - TemporaryRedirect = 307, // RFC 7231, 6.4.7 - PermanentRedirect = 308, // RFC 7538, 3 - - BadRequest = 400, // RFC 7231, 6.5.1 - Unauthorized = 401, // RFC 7235, 3.1 - PaymentRequired = 402, // RFC 7231, 6.5.2 - Forbidden = 403, // RFC 7231, 6.5.3 - NotFound = 404, // RFC 7231, 6.5.4 - MethodNotAllowed = 405, // RFC 7231, 6.5.5 - NotAcceptable = 406, // RFC 7231, 6.5.6 - ProxyAuthRequired = 407, // RFC 7235, 3.2 - RequestTimeout = 408, // RFC 7231, 6.5.7 - Conflict = 409, // RFC 7231, 6.5.8 - Gone = 410, // RFC 7231, 6.5.9 - LengthRequired = 411, // RFC 7231, 6.5.10 - PreconditionFailed = 412, // RFC 7232, 4.2 - RequestEntityTooLarge = 413, // RFC 7231, 6.5.11 - RequestURITooLong = 414, // RFC 7231, 6.5.12 - UnsupportedMediaType = 415, // RFC 7231, 6.5.13 - RequestedRangeNotSatisfiable = 416, // RFC 7233, 4.4 - ExpectationFailed = 417, // RFC 7231, 6.5.14 - Teapot = 418, // RFC 7168, 2.3.3 - MisdirectedRequest = 421, // RFC 7540, 9.1.2 - UnprocessableEntity = 422, // RFC 4918, 11.2 - Locked = 423, // RFC 4918, 11.3 - FailedDependency = 424, // RFC 4918, 11.4 - UpgradeRequired = 426, // RFC 7231, 6.5.15 - PreconditionRequired = 428, // RFC 6585, 3 - TooManyRequests = 429, // RFC 6585, 4 - RequestHeaderFieldsTooLarge = 431, // RFC 6585, 5 - UnavailableForLegalReasons = 451, // RFC 7725, 3 - - InternalServerError = 500, // RFC 7231, 6.6.1 - NotImplemented = 501, // RFC 7231, 6.6.2 - BadGateway = 502, // RFC 7231, 6.6.3 - ServiceUnavailable = 503, // RFC 7231, 6.6.4 - GatewayTimeout = 504, // RFC 7231, 6.6.5 - HTTPVersionNotSupported = 505, // RFC 7231, 6.6.6 - VariantAlsoNegotiates = 506, // RFC 2295, 8.1 - InsufficientStorage = 507, // RFC 4918, 11.5 - LoopDetected = 508, // RFC 5842, 7.2 - NotExtended = 510, // RFC 2774, 7 - NetworkAuthenticationRequired = 511 // RFC 6585, 6 -} - -export const STATUS_TEXT = new Map([ - [Status.Continue, "Continue"], - [Status.SwitchingProtocols, "Switching Protocols"], - [Status.Processing, "Processing"], - - [Status.OK, "OK"], - [Status.Created, "Created"], - [Status.Accepted, "Accepted"], - [Status.NonAuthoritativeInfo, "Non-Authoritative Information"], - [Status.NoContent, "No Content"], - [Status.ResetContent, "Reset Content"], - [Status.PartialContent, "Partial Content"], - [Status.MultiStatus, "Multi-Status"], - [Status.AlreadyReported, "Already Reported"], - [Status.IMUsed, "IM Used"], - - [Status.MultipleChoices, "Multiple Choices"], - [Status.MovedPermanently, "Moved Permanently"], - [Status.Found, "Found"], - [Status.SeeOther, "See Other"], - [Status.NotModified, "Not Modified"], - [Status.UseProxy, "Use Proxy"], - [Status.TemporaryRedirect, "Temporary Redirect"], - [Status.PermanentRedirect, "Permanent Redirect"], - - [Status.BadRequest, "Bad Request"], - [Status.Unauthorized, "Unauthorized"], - [Status.PaymentRequired, "Payment Required"], - [Status.Forbidden, "Forbidden"], - [Status.NotFound, "Not Found"], - [Status.MethodNotAllowed, "Method Not Allowed"], - [Status.NotAcceptable, "Not Acceptable"], - [Status.ProxyAuthRequired, "Proxy Authentication Required"], - [Status.RequestTimeout, "Request Timeout"], - [Status.Conflict, "Conflict"], - [Status.Gone, "Gone"], - [Status.LengthRequired, "Length Required"], - [Status.PreconditionFailed, "Precondition Failed"], - [Status.RequestEntityTooLarge, "Request Entity Too Large"], - [Status.RequestURITooLong, "Request URI Too Long"], - [Status.UnsupportedMediaType, "Unsupported Media Type"], - [Status.RequestedRangeNotSatisfiable, "Requested Range Not Satisfiable"], - [Status.ExpectationFailed, "Expectation Failed"], - [Status.Teapot, "I'm a teapot"], - [Status.MisdirectedRequest, "Misdirected Request"], - [Status.UnprocessableEntity, "Unprocessable Entity"], - [Status.Locked, "Locked"], - [Status.FailedDependency, "Failed Dependency"], - [Status.UpgradeRequired, "Upgrade Required"], - [Status.PreconditionRequired, "Precondition Required"], - [Status.TooManyRequests, "Too Many Requests"], - [Status.RequestHeaderFieldsTooLarge, "Request Header Fields Too Large"], - [Status.UnavailableForLegalReasons, "Unavailable For Legal Reasons"], - - [Status.InternalServerError, "Internal Server Error"], - [Status.NotImplemented, "Not Implemented"], - [Status.BadGateway, "Bad Gateway"], - [Status.ServiceUnavailable, "Service Unavailable"], - [Status.GatewayTimeout, "Gateway Timeout"], - [Status.HTTPVersionNotSupported, "HTTP Version Not Supported"], - [Status.VariantAlsoNegotiates, "Variant Also Negotiates"], - [Status.InsufficientStorage, "Insufficient Storage"], - [Status.LoopDetected, "Loop Detected"], - [Status.NotExtended, "Not Extended"], - [Status.NetworkAuthenticationRequired, "Network Authentication Required"] -]); diff --git a/net/http_test.ts b/net/http_test.ts deleted file mode 100644 index 9235feb02..000000000 --- a/net/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 "./http.ts"; -import { BufWriter, BufReader } from "./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/net/iotest.ts b/net/iotest.ts deleted file mode 100644 index e3a42f58a..000000000 --- a/net/iotest.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Ported to Deno from -// Copyright 2009 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. - -import { Reader, ReadResult } from "deno"; - -/** OneByteReader returns a Reader that implements - * each non-empty Read by reading one byte from r. - */ -export class OneByteReader implements Reader { - constructor(readonly r: Reader) {} - - async read(p: Uint8Array): Promise { - if (p.byteLength === 0) { - return { nread: 0, eof: false }; - } - if (!(p instanceof Uint8Array)) { - throw Error("expected Uint8Array"); - } - return this.r.read(p.subarray(0, 1)); - } -} - -/** HalfReader returns a Reader that implements Read - * by reading half as many requested bytes from r. - */ -export class HalfReader implements Reader { - constructor(readonly r: Reader) {} - - async read(p: Uint8Array): Promise { - if (!(p instanceof Uint8Array)) { - throw Error("expected Uint8Array"); - } - const half = Math.floor((p.byteLength + 1) / 2); - return this.r.read(p.subarray(0, half)); - } -} - -export class ErrTimeout extends Error { - constructor() { - super("timeout"); - this.name = "ErrTimeout"; - } -} - -/** TimeoutReader returns ErrTimeout on the second read - * with no data. Subsequent calls to read succeed. - */ -export class TimeoutReader implements Reader { - count = 0; - constructor(readonly r: Reader) {} - - async read(p: Uint8Array): Promise { - this.count++; - if (this.count === 2) { - throw new ErrTimeout(); - } - return this.r.read(p); - } -} diff --git a/net/ioutil.ts b/net/ioutil.ts deleted file mode 100644 index 68d6e5190..000000000 --- a/net/ioutil.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -import { BufReader } from "./bufio.ts"; - -/* Read big endian 16bit short from BufReader */ -export async function readShort(buf: BufReader): Promise { - const [high, low] = [await buf.readByte(), await buf.readByte()]; - return (high << 8) | low; -} - -/* Read big endian 32bit integer from BufReader */ -export async function readInt(buf: BufReader): Promise { - const [high, low] = [await readShort(buf), await readShort(buf)]; - return (high << 16) | low; -} - -const BIT32 = 0xffffffff; -/* Read big endian 64bit long from BufReader */ -export async function readLong(buf: BufReader): Promise { - const [high, low] = [await readInt(buf), await readInt(buf)]; - // ECMAScript doesn't support 64bit bit ops. - return high ? high * (BIT32 + 1) + low : low; -} - -/* Slice number into 64bit big endian byte array */ -export function sliceLongToBytes(d: number, dest = new Array(8)): number[] { - let mask = 0xff; - let low = (d << 32) >>> 32; - let high = (d - low) / (BIT32 + 1); - let shift = 24; - for (let i = 0; i < 4; i++) { - dest[i] = (high >>> shift) & mask; - dest[i + 4] = (low >>> shift) & mask; - shift -= 8; - } - return dest; -} diff --git a/net/ioutil_test.ts b/net/ioutil_test.ts deleted file mode 100644 index 422901e4a..000000000 --- a/net/ioutil_test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Reader, ReadResult } from "deno"; -import { assertEqual, test } from "../testing/mod.ts"; -import { readInt, readLong, readShort, sliceLongToBytes } from "./ioutil.ts"; -import { BufReader } from "./bufio.ts"; - -class BinaryReader implements Reader { - index = 0; - - constructor(private bytes: Uint8Array = new Uint8Array(0)) {} - - async read(p: Uint8Array): Promise { - p.set(this.bytes.subarray(this.index, p.byteLength)); - this.index += p.byteLength; - return { nread: p.byteLength, eof: false }; - } -} - -test(async function testReadShort() { - const r = new BinaryReader(new Uint8Array([0x12, 0x34])); - const short = await readShort(new BufReader(r)); - assertEqual(short, 0x1234); -}); - -test(async function testReadInt() { - const r = new BinaryReader(new Uint8Array([0x12, 0x34, 0x56, 0x78])); - const int = await readInt(new BufReader(r)); - assertEqual(int, 0x12345678); -}); - -test(async function testReadLong() { - const r = new BinaryReader( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]) - ); - const long = await readLong(new BufReader(r)); - assertEqual(long, 0x1234567812345678); -}); - -test(async function testReadLong2() { - const r = new BinaryReader( - new Uint8Array([0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]) - ); - const long = await readLong(new BufReader(r)); - assertEqual(long, 0x12345678); -}); - -test(async function testSliceLongToBytes() { - const arr = sliceLongToBytes(0x1234567890abcdef); - const actual = readLong(new BufReader(new BinaryReader(new Uint8Array(arr)))); - const expected = readLong( - new BufReader( - new BinaryReader( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef]) - ) - ) - ); - assertEqual(actual, expected); -}); - -test(async function testSliceLongToBytes2() { - const arr = sliceLongToBytes(0x12345678); - assertEqual(arr, [0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]); -}); diff --git a/net/sha1.ts b/net/sha1.ts deleted file mode 100644 index 036c3c552..000000000 --- a/net/sha1.ts +++ /dev/null @@ -1,382 +0,0 @@ -/* - * [js-sha1]{@link https://github.com/emn178/js-sha1} - * - * @version 0.6.0 - * @author Chen, Yi-Cyuan [emn178@gmail.com] - * @copyright Chen, Yi-Cyuan 2014-2017 - * @license MIT - */ -/*jslint bitwise: true */ - -const HEX_CHARS = "0123456789abcdef".split(""); -const EXTRA = [-2147483648, 8388608, 32768, 128]; -const SHIFT = [24, 16, 8, 0]; - -const blocks = []; - -export class Sha1 { - blocks; - block; - start; - bytes; - hBytes; - finalized; - hashed; - first; - - h0 = 0x67452301; - h1 = 0xefcdab89; - h2 = 0x98badcfe; - h3 = 0x10325476; - h4 = 0xc3d2e1f0; - lastByteIndex = 0; - - constructor(sharedMemory: boolean = false) { - if (sharedMemory) { - blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; - this.blocks = blocks; - } else { - this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - } - - this.h0 = 0x67452301; - this.h1 = 0xefcdab89; - this.h2 = 0x98badcfe; - this.h3 = 0x10325476; - this.h4 = 0xc3d2e1f0; - - this.block = this.start = this.bytes = this.hBytes = 0; - this.finalized = this.hashed = false; - this.first = true; - } - - update(data: string | ArrayBuffer) { - if (this.finalized) { - return; - } - let message; - let notString = typeof data !== "string"; - if (notString && data instanceof ArrayBuffer) { - message = new Uint8Array(data); - } else { - message = data; - } - let code, - index = 0, - i, - length = message.length || 0, - blocks = this.blocks; - - while (index < length) { - if (this.hashed) { - this.hashed = false; - blocks[0] = this.block; - blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; - } - - if (notString) { - for (i = this.start; index < length && i < 64; ++index) { - blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; - } - } else { - for (i = this.start; index < length && i < 64; ++index) { - code = message.charCodeAt(index); - if (code < 0x80) { - blocks[i >> 2] |= code << SHIFT[i++ & 3]; - } else if (code < 0x800) { - blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; - } else if (code < 0xd800 || code >= 0xe000) { - blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; - } else { - code = - 0x10000 + - (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); - blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; - blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; - } - } - } - - this.lastByteIndex = i; - this.bytes += i - this.start; - if (i >= 64) { - this.block = blocks[16]; - this.start = i - 64; - this.hash(); - this.hashed = true; - } else { - this.start = i; - } - } - if (this.bytes > 4294967295) { - this.hBytes += (this.bytes / 4294967296) << 0; - this.bytes = this.bytes % 4294967296; - } - return this; - } - - finalize() { - if (this.finalized) { - return; - } - this.finalized = true; - let blocks = this.blocks, - i = this.lastByteIndex; - blocks[16] = this.block; - blocks[i >> 2] |= EXTRA[i & 3]; - this.block = blocks[16]; - if (i >= 56) { - if (!this.hashed) { - this.hash(); - } - blocks[0] = this.block; - blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; - } - blocks[14] = (this.hBytes << 3) | (this.bytes >>> 29); - blocks[15] = this.bytes << 3; - this.hash(); - } - - hash() { - let a = this.h0, - b = this.h1, - c = this.h2, - d = this.h3, - e = this.h4; - let f, - j, - t, - blocks = this.blocks; - - for (j = 16; j < 80; ++j) { - t = blocks[j - 3] ^ blocks[j - 8] ^ blocks[j - 14] ^ blocks[j - 16]; - blocks[j] = (t << 1) | (t >>> 31); - } - - for (j = 0; j < 20; j += 5) { - f = (b & c) | (~b & d); - t = (a << 5) | (a >>> 27); - e = (t + f + e + 1518500249 + blocks[j]) << 0; - b = (b << 30) | (b >>> 2); - - f = (a & b) | (~a & c); - t = (e << 5) | (e >>> 27); - d = (t + f + d + 1518500249 + blocks[j + 1]) << 0; - a = (a << 30) | (a >>> 2); - - f = (e & a) | (~e & b); - t = (d << 5) | (d >>> 27); - c = (t + f + c + 1518500249 + blocks[j + 2]) << 0; - e = (e << 30) | (e >>> 2); - - f = (d & e) | (~d & a); - t = (c << 5) | (c >>> 27); - b = (t + f + b + 1518500249 + blocks[j + 3]) << 0; - d = (d << 30) | (d >>> 2); - - f = (c & d) | (~c & e); - t = (b << 5) | (b >>> 27); - a = (t + f + a + 1518500249 + blocks[j + 4]) << 0; - c = (c << 30) | (c >>> 2); - } - - for (; j < 40; j += 5) { - f = b ^ c ^ d; - t = (a << 5) | (a >>> 27); - e = (t + f + e + 1859775393 + blocks[j]) << 0; - b = (b << 30) | (b >>> 2); - - f = a ^ b ^ c; - t = (e << 5) | (e >>> 27); - d = (t + f + d + 1859775393 + blocks[j + 1]) << 0; - a = (a << 30) | (a >>> 2); - - f = e ^ a ^ b; - t = (d << 5) | (d >>> 27); - c = (t + f + c + 1859775393 + blocks[j + 2]) << 0; - e = (e << 30) | (e >>> 2); - - f = d ^ e ^ a; - t = (c << 5) | (c >>> 27); - b = (t + f + b + 1859775393 + blocks[j + 3]) << 0; - d = (d << 30) | (d >>> 2); - - f = c ^ d ^ e; - t = (b << 5) | (b >>> 27); - a = (t + f + a + 1859775393 + blocks[j + 4]) << 0; - c = (c << 30) | (c >>> 2); - } - - for (; j < 60; j += 5) { - f = (b & c) | (b & d) | (c & d); - t = (a << 5) | (a >>> 27); - e = (t + f + e - 1894007588 + blocks[j]) << 0; - b = (b << 30) | (b >>> 2); - - f = (a & b) | (a & c) | (b & c); - t = (e << 5) | (e >>> 27); - d = (t + f + d - 1894007588 + blocks[j + 1]) << 0; - a = (a << 30) | (a >>> 2); - - f = (e & a) | (e & b) | (a & b); - t = (d << 5) | (d >>> 27); - c = (t + f + c - 1894007588 + blocks[j + 2]) << 0; - e = (e << 30) | (e >>> 2); - - f = (d & e) | (d & a) | (e & a); - t = (c << 5) | (c >>> 27); - b = (t + f + b - 1894007588 + blocks[j + 3]) << 0; - d = (d << 30) | (d >>> 2); - - f = (c & d) | (c & e) | (d & e); - t = (b << 5) | (b >>> 27); - a = (t + f + a - 1894007588 + blocks[j + 4]) << 0; - c = (c << 30) | (c >>> 2); - } - - for (; j < 80; j += 5) { - f = b ^ c ^ d; - t = (a << 5) | (a >>> 27); - e = (t + f + e - 899497514 + blocks[j]) << 0; - b = (b << 30) | (b >>> 2); - - f = a ^ b ^ c; - t = (e << 5) | (e >>> 27); - d = (t + f + d - 899497514 + blocks[j + 1]) << 0; - a = (a << 30) | (a >>> 2); - - f = e ^ a ^ b; - t = (d << 5) | (d >>> 27); - c = (t + f + c - 899497514 + blocks[j + 2]) << 0; - e = (e << 30) | (e >>> 2); - - f = d ^ e ^ a; - t = (c << 5) | (c >>> 27); - b = (t + f + b - 899497514 + blocks[j + 3]) << 0; - d = (d << 30) | (d >>> 2); - - f = c ^ d ^ e; - t = (b << 5) | (b >>> 27); - a = (t + f + a - 899497514 + blocks[j + 4]) << 0; - c = (c << 30) | (c >>> 2); - } - - this.h0 = (this.h0 + a) << 0; - this.h1 = (this.h1 + b) << 0; - this.h2 = (this.h2 + c) << 0; - this.h3 = (this.h3 + d) << 0; - this.h4 = (this.h4 + e) << 0; - } - - hex() { - this.finalize(); - - let h0 = this.h0, - h1 = this.h1, - h2 = this.h2, - h3 = this.h3, - h4 = this.h4; - - return ( - HEX_CHARS[(h0 >> 28) & 0x0f] + - HEX_CHARS[(h0 >> 24) & 0x0f] + - HEX_CHARS[(h0 >> 20) & 0x0f] + - HEX_CHARS[(h0 >> 16) & 0x0f] + - HEX_CHARS[(h0 >> 12) & 0x0f] + - HEX_CHARS[(h0 >> 8) & 0x0f] + - HEX_CHARS[(h0 >> 4) & 0x0f] + - HEX_CHARS[h0 & 0x0f] + - HEX_CHARS[(h1 >> 28) & 0x0f] + - HEX_CHARS[(h1 >> 24) & 0x0f] + - HEX_CHARS[(h1 >> 20) & 0x0f] + - HEX_CHARS[(h1 >> 16) & 0x0f] + - HEX_CHARS[(h1 >> 12) & 0x0f] + - HEX_CHARS[(h1 >> 8) & 0x0f] + - HEX_CHARS[(h1 >> 4) & 0x0f] + - HEX_CHARS[h1 & 0x0f] + - HEX_CHARS[(h2 >> 28) & 0x0f] + - HEX_CHARS[(h2 >> 24) & 0x0f] + - HEX_CHARS[(h2 >> 20) & 0x0f] + - HEX_CHARS[(h2 >> 16) & 0x0f] + - HEX_CHARS[(h2 >> 12) & 0x0f] + - HEX_CHARS[(h2 >> 8) & 0x0f] + - HEX_CHARS[(h2 >> 4) & 0x0f] + - HEX_CHARS[h2 & 0x0f] + - HEX_CHARS[(h3 >> 28) & 0x0f] + - HEX_CHARS[(h3 >> 24) & 0x0f] + - HEX_CHARS[(h3 >> 20) & 0x0f] + - HEX_CHARS[(h3 >> 16) & 0x0f] + - HEX_CHARS[(h3 >> 12) & 0x0f] + - HEX_CHARS[(h3 >> 8) & 0x0f] + - HEX_CHARS[(h3 >> 4) & 0x0f] + - HEX_CHARS[h3 & 0x0f] + - HEX_CHARS[(h4 >> 28) & 0x0f] + - HEX_CHARS[(h4 >> 24) & 0x0f] + - HEX_CHARS[(h4 >> 20) & 0x0f] + - HEX_CHARS[(h4 >> 16) & 0x0f] + - HEX_CHARS[(h4 >> 12) & 0x0f] + - HEX_CHARS[(h4 >> 8) & 0x0f] + - HEX_CHARS[(h4 >> 4) & 0x0f] + - HEX_CHARS[h4 & 0x0f] - ); - } - - toString() { - return this.hex(); - } - - digest() { - this.finalize(); - - let h0 = this.h0, - h1 = this.h1, - h2 = this.h2, - h3 = this.h3, - h4 = this.h4; - - return [ - (h0 >> 24) & 0xff, - (h0 >> 16) & 0xff, - (h0 >> 8) & 0xff, - h0 & 0xff, - (h1 >> 24) & 0xff, - (h1 >> 16) & 0xff, - (h1 >> 8) & 0xff, - h1 & 0xff, - (h2 >> 24) & 0xff, - (h2 >> 16) & 0xff, - (h2 >> 8) & 0xff, - h2 & 0xff, - (h3 >> 24) & 0xff, - (h3 >> 16) & 0xff, - (h3 >> 8) & 0xff, - h3 & 0xff, - (h4 >> 24) & 0xff, - (h4 >> 16) & 0xff, - (h4 >> 8) & 0xff, - h4 & 0xff - ]; - } - - array() { - return this.digest(); - } - - arrayBuffer() { - this.finalize(); - - let buffer = new ArrayBuffer(20); - let dataView = new DataView(buffer); - dataView.setUint32(0, this.h0); - dataView.setUint32(4, this.h1); - dataView.setUint32(8, this.h2); - dataView.setUint32(12, this.h3); - dataView.setUint32(16, this.h4); - return buffer; - } -} diff --git a/net/sha1_test.ts b/net/sha1_test.ts deleted file mode 100644 index b385f18da..000000000 --- a/net/sha1_test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { assertEqual, test } from "../testing/mod.ts"; -import { Sha1 } from "./sha1.ts"; - -test(function testSha1() { - const sha1 = new Sha1(); - sha1.update("abcde"); - assertEqual(sha1.toString(), "03de6c570bfe24bfc328ccd7ca46b76eadaf4334"); -}); diff --git a/net/textproto.ts b/net/textproto.ts deleted file mode 100644 index 832299e1c..000000000 --- a/net/textproto.ts +++ /dev/null @@ -1,150 +0,0 @@ -// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ -// Copyright 2009 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. - -import { BufReader, BufState } from "./bufio.ts"; -import { charCode } from "./util.ts"; - -const asciiDecoder = new TextDecoder(); -function str(buf: Uint8Array): string { - if (buf == null) { - return ""; - } else { - return asciiDecoder.decode(buf); - } -} - -export class ProtocolError extends Error { - constructor(msg: string) { - super(msg); - this.name = "ProtocolError"; - } -} - -export class TextProtoReader { - constructor(readonly r: BufReader) {} - - /** readLine() reads a single line from the TextProtoReader, - * eliding the final \n or \r\n from the returned string. - */ - async readLine(): Promise<[string, BufState]> { - let [line, err] = await this.readLineSlice(); - return [str(line), err]; - } - - /** ReadMIMEHeader reads a MIME-style header from r. - * The header is a sequence of possibly continued Key: Value lines - * ending in a blank line. - * The returned map m maps CanonicalMIMEHeaderKey(key) to a - * sequence of values in the same order encountered in the input. - * - * For example, consider this input: - * - * My-Key: Value 1 - * Long-Key: Even - * Longer Value - * My-Key: Value 2 - * - * Given that input, ReadMIMEHeader returns the map: - * - * map[string][]string{ - * "My-Key": {"Value 1", "Value 2"}, - * "Long-Key": {"Even Longer Value"}, - * } - */ - async readMIMEHeader(): Promise<[Headers, BufState]> { - let m = new Headers(); - let line: Uint8Array; - - // The first line cannot start with a leading space. - let [buf, err] = await this.r.peek(1); - if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) { - [line, err] = await this.readLineSlice(); - } - - [buf, err] = await this.r.peek(1); - if (err == null && (buf[0] == charCode(" ") || buf[0] == charCode("\t"))) { - throw new ProtocolError( - `malformed MIME header initial line: ${str(line)}` - ); - } - - while (true) { - let [kv, err] = await this.readLineSlice(); // readContinuedLineSlice - if (kv.byteLength == 0) { - return [m, err]; - } - - // Key ends at first colon; should not have trailing spaces - // but they appear in the wild, violating specs, so we remove - // them if present. - let i = kv.indexOf(charCode(":")); - if (i < 0) { - throw new ProtocolError(`malformed MIME header line: ${str(kv)}`); - } - let endKey = i; - while (endKey > 0 && kv[endKey - 1] == charCode(" ")) { - endKey--; - } - - //let key = canonicalMIMEHeaderKey(kv.subarray(0, endKey)); - let key = str(kv.subarray(0, endKey)); - - // As per RFC 7230 field-name is a token, tokens consist of one or more chars. - // We could return a ProtocolError here, but better to be liberal in what we - // accept, so if we get an empty key, skip it. - if (key == "") { - continue; - } - - // Skip initial spaces in value. - i++; // skip colon - while ( - i < kv.byteLength && - (kv[i] == charCode(" ") || kv[i] == charCode("\t")) - ) { - i++; - } - let value = str(kv.subarray(i)); - - m.append(key, value); - - if (err != null) { - throw err; - } - } - } - - async readLineSlice(): Promise<[Uint8Array, BufState]> { - // this.closeDot(); - let line: Uint8Array; - while (true) { - let [l, more, err] = await this.r.readLine(); - if (err != null) { - // Go's len(typed nil) works fine, but not in JS - return [new Uint8Array(0), err]; - } - // Avoid the copy if the first call produced a full line. - if (line == null && !more) { - return [l, null]; - } - line = append(line, l); - if (!more) { - break; - } - } - return [line, null]; - } -} - -export function append(a: Uint8Array, b: Uint8Array): Uint8Array { - if (a == null) { - return b; - } else { - const output = new Uint8Array(a.length + b.length); - output.set(a, 0); - output.set(b, a.length); - return output; - } -} diff --git a/net/textproto_test.ts b/net/textproto_test.ts deleted file mode 100644 index e0ae0749c..000000000 --- a/net/textproto_test.ts +++ /dev/null @@ -1,98 +0,0 @@ -// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ -// Copyright 2009 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. - -import { BufReader } from "./bufio.ts"; -import { TextProtoReader, append } from "./textproto.ts"; -import { stringsReader } from "./util.ts"; -import { test, assert, assertEqual } from "../testing/mod.ts"; - -function reader(s: string): TextProtoReader { - return new TextProtoReader(new BufReader(stringsReader(s))); -} - -test(async function textprotoReader() { - let r = reader("line1\nline2\n"); - let [s, err] = await r.readLine(); - assertEqual(s, "line1"); - assert(err == null); - - [s, err] = await r.readLine(); - assertEqual(s, "line2"); - assert(err == null); - - [s, err] = await r.readLine(); - assertEqual(s, ""); - assert(err == "EOF"); -}); - -/* -test(async function textprotoReadMIMEHeader() { - let r = reader("my-key: Value 1 \r\nLong-key: Even \n Longer Value\r\nmy-Key: Value 2\r\n\n"); - let [m, err] = await r.readMIMEHeader(); - - console.log("Got headers", m.toString()); - want := MIMEHeader{ - "My-Key": {"Value 1", "Value 2"}, - "Long-Key": {"Even Longer Value"}, - } - if !reflect.DeepEqual(m, want) || err != nil { - t.Fatalf("ReadMIMEHeader: %v, %v; want %v", m, err, want) - } -}); -*/ - -test(async function textprotoReadMIMEHeaderSingle() { - let r = reader("Foo: bar\n\n"); - let [m, err] = await r.readMIMEHeader(); - assertEqual(m.get("Foo"), "bar"); - assert(!err); -}); - -// Test that we read slightly-bogus MIME headers seen in the wild, -// with spaces before colons, and spaces in keys. -test(async function textprotoReadMIMEHeaderNonCompliant() { - // Invalid HTTP response header as sent by an Axis security - // camera: (this is handled by IE, Firefox, Chrome, curl, etc.) - let r = reader( - "Foo: bar\r\n" + - "Content-Language: en\r\n" + - "SID : 0\r\n" + - // TODO Re-enable Currently fails with: - // "TypeError: audio mode is not a legal HTTP header name" - // "Audio Mode : None\r\n" + - "Privilege : 127\r\n\r\n" - ); - let [m, err] = await r.readMIMEHeader(); - console.log(m.toString()); - assert(!err); - /* - let want = MIMEHeader{ - "Foo": {"bar"}, - "Content-Language": {"en"}, - "Sid": {"0"}, - "Audio Mode": {"None"}, - "Privilege": {"127"}, - } - if !reflect.DeepEqual(m, want) || err != nil { - t.Fatalf("ReadMIMEHeader =\n%v, %v; want:\n%v", m, err, want) - } - */ -}); - -test(async function textprotoAppend() { - const enc = new TextEncoder(); - const dec = new TextDecoder(); - const u1 = enc.encode("Hello "); - const u2 = enc.encode("World"); - const joined = append(u1, u2); - assertEqual(dec.decode(joined), "Hello World"); -}); - -test(async function textprotoReadEmpty() { - let r = reader(""); - let [m, err] = await r.readMIMEHeader(); - // Should not crash! - assertEqual(err, "EOF"); -}); diff --git a/net/util.ts b/net/util.ts deleted file mode 100644 index 811940b4d..000000000 --- a/net/util.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Buffer, Reader } from "deno"; - -export function assert(cond: boolean, msg = "assert") { - if (!cond) { - throw Error(msg); - } -} - -// `off` is the offset into `dst` where it will at which to begin writing values -// from `src`. -// Returns the number of bytes copied. -export function copyBytes(dst: Uint8Array, src: Uint8Array, off = 0): number { - const r = dst.byteLength - off; - if (src.byteLength > r) { - src = src.subarray(0, r); - } - dst.set(src, off); - return src.byteLength; -} - -export function charCode(s: string): number { - return s.charCodeAt(0); -} - -const encoder = new TextEncoder(); -export function stringsReader(s: string): Reader { - const ui8 = encoder.encode(s); - return new Buffer(ui8.buffer as ArrayBuffer); -} diff --git a/net/ws.ts b/net/ws.ts deleted file mode 100644 index 5ce96b3ca..000000000 --- a/net/ws.ts +++ /dev/null @@ -1,350 +0,0 @@ -// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -import { Buffer, Writer, Conn } from "deno"; -import { ServerRequest } from "./http.ts"; -import { BufReader, BufWriter } from "./bufio.ts"; -import { readLong, readShort, sliceLongToBytes } from "./ioutil.ts"; -import { Sha1 } from "./sha1.ts"; - -export const OpCodeContinue = 0x0; -export const OpCodeTextFrame = 0x1; -export const OpCodeBinaryFrame = 0x2; -export const OpCodeClose = 0x8; -export const OpcodePing = 0x9; -export const OpcodePong = 0xa; - -export type WebSocketEvent = - | string - | Uint8Array - | WebSocketCloseEvent - | WebSocketPingEvent - | WebSocketPongEvent; - -export type WebSocketCloseEvent = { - code: number; - reason?: string; -}; - -export function isWebSocketCloseEvent(a): a is WebSocketCloseEvent { - return a && typeof a["code"] === "number"; -} - -export type WebSocketPingEvent = ["ping", Uint8Array]; - -export function isWebSocketPingEvent(a): a is WebSocketPingEvent { - return Array.isArray(a) && a[0] === "ping" && a[1] instanceof Uint8Array; -} - -export type WebSocketPongEvent = ["pong", Uint8Array]; - -export function isWebSocketPongEvent(a): a is WebSocketPongEvent { - return Array.isArray(a) && a[0] === "pong" && a[1] instanceof Uint8Array; -} - -export class SocketClosedError extends Error {} - -export type WebSocketFrame = { - isLastFrame: boolean; - opcode: number; - mask?: Uint8Array; - payload: Uint8Array; -}; - -export type WebSocket = { - readonly isClosed: boolean; - receive(): AsyncIterableIterator; - send(data: string | Uint8Array): Promise; - ping(data?: string | Uint8Array): Promise; - close(code: number, reason?: string): Promise; -}; - -class WebSocketImpl implements WebSocket { - encoder = new TextEncoder(); - constructor(private conn: Conn, private mask?: Uint8Array) {} - - async *receive(): AsyncIterableIterator { - let frames: WebSocketFrame[] = []; - let payloadsLength = 0; - for await (const frame of receiveFrame(this.conn)) { - unmask(frame.payload, frame.mask); - switch (frame.opcode) { - case OpCodeTextFrame: - case OpCodeBinaryFrame: - case OpCodeContinue: - frames.push(frame); - payloadsLength += frame.payload.length; - if (frame.isLastFrame) { - const concat = new Uint8Array(payloadsLength); - let offs = 0; - for (const frame of frames) { - concat.set(frame.payload, offs); - offs += frame.payload.length; - } - if (frames[0].opcode === OpCodeTextFrame) { - // text - yield new Buffer(concat).toString(); - } else { - // binary - yield concat; - } - frames = []; - payloadsLength = 0; - } - break; - case OpCodeClose: - const code = (frame.payload[0] << 16) | frame.payload[1]; - const reason = new Buffer( - frame.payload.subarray(2, frame.payload.length) - ).toString(); - this._isClosed = true; - yield { code, reason }; - return; - case OpcodePing: - yield ["ping", frame.payload] as WebSocketPingEvent; - break; - case OpcodePong: - yield ["pong", frame.payload] as WebSocketPongEvent; - break; - } - } - } - - async send(data: string | Uint8Array): Promise { - if (this.isClosed) { - throw new SocketClosedError("socket has been closed"); - } - const opcode = - typeof data === "string" ? OpCodeTextFrame : OpCodeBinaryFrame; - const payload = typeof data === "string" ? this.encoder.encode(data) : data; - const isLastFrame = true; - await writeFrame( - { - isLastFrame, - opcode, - payload, - mask: this.mask - }, - this.conn - ); - } - - async ping(data: string | Uint8Array): Promise { - const payload = typeof data === "string" ? this.encoder.encode(data) : data; - await writeFrame( - { - isLastFrame: true, - opcode: OpCodeClose, - mask: this.mask, - payload - }, - this.conn - ); - } - - private _isClosed = false; - get isClosed() { - return this._isClosed; - } - - async close(code: number, reason?: string): Promise { - try { - const header = [code >>> 8, code & 0x00ff]; - let payload: Uint8Array; - if (reason) { - const reasonBytes = this.encoder.encode(reason); - payload = new Uint8Array(2 + reasonBytes.byteLength); - payload.set(header); - payload.set(reasonBytes, 2); - } else { - payload = new Uint8Array(header); - } - await writeFrame( - { - isLastFrame: true, - opcode: OpCodeClose, - mask: this.mask, - payload - }, - this.conn - ); - } catch (e) { - throw e; - } finally { - this.ensureSocketClosed(); - } - } - - private ensureSocketClosed(): Error { - if (this.isClosed) return; - try { - this.conn.close(); - } catch (e) { - console.error(e); - } finally { - this._isClosed = true; - } - } -} - -export async function* receiveFrame( - conn: Conn -): AsyncIterableIterator { - let receiving = true; - const reader = new BufReader(conn); - while (receiving) { - const frame = await readFrame(reader); - switch (frame.opcode) { - case OpCodeTextFrame: - case OpCodeBinaryFrame: - case OpCodeContinue: - yield frame; - break; - case OpCodeClose: - await writeFrame( - { - isLastFrame: true, - opcode: OpCodeClose, - payload: frame.payload - }, - conn - ); - conn.close(); - yield frame; - receiving = false; - break; - case OpcodePing: - await writeFrame( - { - isLastFrame: true, - opcode: OpcodePong, - payload: frame.payload - }, - conn - ); - yield frame; - break; - case OpcodePong: - yield frame; - break; - } - } -} - -export async function writeFrame(frame: WebSocketFrame, writer: Writer) { - let payloadLength = frame.payload.byteLength; - let header: Uint8Array; - const hasMask = (frame.mask ? 1 : 0) << 7; - if (payloadLength < 126) { - header = new Uint8Array([ - (0b1000 << 4) | frame.opcode, - hasMask | payloadLength - ]); - } else if (payloadLength < 0xffff) { - header = new Uint8Array([ - (0b1000 << 4) | frame.opcode, - hasMask | 0b01111110, - payloadLength >>> 8, - payloadLength & 0x00ff - ]); - } else { - header = new Uint8Array([ - (0b1000 << 4) | frame.opcode, - hasMask | 0b01111111, - ...sliceLongToBytes(payloadLength) - ]); - } - if (frame.mask) { - unmask(frame.payload, frame.mask); - } - const bytes = new Uint8Array(header.length + payloadLength); - bytes.set(header, 0); - bytes.set(frame.payload, header.length); - const w = new BufWriter(writer); - await w.write(bytes); - await w.flush(); -} - -export function unmask(payload: Uint8Array, mask: Uint8Array) { - if (mask) { - for (let i = 0; i < payload.length; i++) { - payload[i] ^= mask[i % 4]; - } - } -} - -export function acceptable(req: ServerRequest): boolean { - return ( - req.headers.get("upgrade") === "websocket" && - req.headers.has("sec-websocket-key") - ); -} - -export async function acceptWebSocket(req: ServerRequest): Promise { - if (acceptable(req)) { - const sock = new WebSocketImpl(req.conn); - const secKey = req.headers.get("sec-websocket-key"); - const secAccept = createSecAccept(secKey); - await req.respond({ - status: 101, - headers: new Headers({ - Upgrade: "websocket", - Connection: "Upgrade", - "Sec-WebSocket-Accept": secAccept - }) - }); - return sock; - } - throw new Error("request is not acceptable"); -} - -const kGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - -export function createSecAccept(nonce: string) { - const sha1 = new Sha1(); - sha1.update(nonce + kGUID); - const bytes = sha1.digest(); - const hash = bytes.reduce( - (data, byte) => data + String.fromCharCode(byte), - "" - ); - return btoa(hash); -} - -export async function readFrame(buf: BufReader): Promise { - let b = await buf.readByte(); - let isLastFrame = false; - switch (b >>> 4) { - case 0b1000: - isLastFrame = true; - break; - case 0b0000: - isLastFrame = false; - break; - default: - throw new Error("invalid signature"); - } - const opcode = b & 0x0f; - // has_mask & payload - b = await buf.readByte(); - const hasMask = b >>> 7; - let payloadLength = b & 0b01111111; - if (payloadLength === 126) { - payloadLength = await readShort(buf); - } else if (payloadLength === 127) { - payloadLength = await readLong(buf); - } - // mask - let mask; - if (hasMask) { - mask = new Uint8Array(4); - await buf.readFull(mask); - } - // payload - const payload = new Uint8Array(payloadLength); - await buf.readFull(payload); - return { - isLastFrame, - opcode, - mask, - payload - }; -} diff --git a/net/ws_test.ts b/net/ws_test.ts deleted file mode 100644 index 62e5a6089..000000000 --- a/net/ws_test.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -import { Buffer } from "deno"; -import { BufReader } from "./bufio.ts"; -import { test, assert, assertEqual } from "../testing/mod.ts"; -import { - createSecAccept, - OpCodeBinaryFrame, - OpCodeContinue, - OpcodePing, - OpcodePong, - OpCodeTextFrame, - readFrame, - unmask -} from "./ws.ts"; -import { serve } from "./http.ts"; - -test(async function testReadUnmaskedTextFrame() { - // unmasked single text frame with payload "Hello" - const buf = new BufReader( - new Buffer(new Uint8Array([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])) - ); - const frame = await readFrame(buf); - assertEqual(frame.opcode, OpCodeTextFrame); - assertEqual(frame.mask, undefined); - assertEqual(new Buffer(frame.payload).toString(), "Hello"); - assertEqual(frame.isLastFrame, true); -}); - -test(async function testReadMakedTextFrame() { - //a masked single text frame with payload "Hello" - const buf = new BufReader( - new Buffer( - new Uint8Array([ - 0x81, - 0x85, - 0x37, - 0xfa, - 0x21, - 0x3d, - 0x7f, - 0x9f, - 0x4d, - 0x51, - 0x58 - ]) - ) - ); - const frame = await readFrame(buf); - console.dir(frame); - assertEqual(frame.opcode, OpCodeTextFrame); - unmask(frame.payload, frame.mask); - assertEqual(new Buffer(frame.payload).toString(), "Hello"); - assertEqual(frame.isLastFrame, true); -}); - -test(async function testReadUnmaskedSplittedTextFrames() { - const buf1 = new BufReader( - new Buffer(new Uint8Array([0x01, 0x03, 0x48, 0x65, 0x6c])) - ); - const buf2 = new BufReader( - new Buffer(new Uint8Array([0x80, 0x02, 0x6c, 0x6f])) - ); - const [f1, f2] = await Promise.all([readFrame(buf1), readFrame(buf2)]); - assertEqual(f1.isLastFrame, false); - assertEqual(f1.mask, undefined); - assertEqual(f1.opcode, OpCodeTextFrame); - assertEqual(new Buffer(f1.payload).toString(), "Hel"); - - assertEqual(f2.isLastFrame, true); - assertEqual(f2.mask, undefined); - assertEqual(f2.opcode, OpCodeContinue); - assertEqual(new Buffer(f2.payload).toString(), "lo"); -}); - -test(async function testReadUnmaksedPingPongFrame() { - // unmasked ping with payload "Hello" - const buf = new BufReader( - new Buffer(new Uint8Array([0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])) - ); - const ping = await readFrame(buf); - assertEqual(ping.opcode, OpcodePing); - assertEqual(new Buffer(ping.payload).toString(), "Hello"); - - const buf2 = new BufReader( - new Buffer( - new Uint8Array([ - 0x8a, - 0x85, - 0x37, - 0xfa, - 0x21, - 0x3d, - 0x7f, - 0x9f, - 0x4d, - 0x51, - 0x58 - ]) - ) - ); - const pong = await readFrame(buf2); - assertEqual(pong.opcode, OpcodePong); - assert(pong.mask !== undefined); - unmask(pong.payload, pong.mask); - assertEqual(new Buffer(pong.payload).toString(), "Hello"); -}); - -test(async function testReadUnmaksedBigBinaryFrame() { - let a = [0x82, 0x7e, 0x01, 0x00]; - for (let i = 0; i < 256; i++) { - a.push(i); - } - const buf = new BufReader(new Buffer(new Uint8Array(a))); - const bin = await readFrame(buf); - assertEqual(bin.opcode, OpCodeBinaryFrame); - assertEqual(bin.isLastFrame, true); - assertEqual(bin.mask, undefined); - assertEqual(bin.payload.length, 256); -}); - -test(async function testReadUnmaskedBigBigBinaryFrame() { - let a = [0x82, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]; - for (let i = 0; i < 0xffff; i++) { - a.push(i); - } - const buf = new BufReader(new Buffer(new Uint8Array(a))); - const bin = await readFrame(buf); - assertEqual(bin.opcode, OpCodeBinaryFrame); - assertEqual(bin.isLastFrame, true); - assertEqual(bin.mask, undefined); - assertEqual(bin.payload.length, 0xffff + 1); -}); - -test(async function testCreateSecAccept() { - const nonce = "dGhlIHNhbXBsZSBub25jZQ=="; - const d = createSecAccept(nonce); - assertEqual(d, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="); -}); diff --git a/test.ts b/test.ts index 8a3534457..994148178 100755 --- a/test.ts +++ b/test.ts @@ -5,11 +5,6 @@ import "colors/test.ts"; import "datetime/test.ts"; import "examples/test.ts"; import "flags/test.ts"; -import "logging/test.ts"; -import "media_types/test.ts"; -import "net/bufio_test.ts"; -import "net/http_test.ts"; -import "net/textproto_test.ts"; import "fs/mkdirp_test.ts"; import "fs/path/basename_test.ts"; import "fs/path/dirname_test.ts"; @@ -20,12 +15,18 @@ import "fs/path/parse_format_test.ts"; import "fs/path/relative_test.ts"; import "fs/path/resolve_test.ts"; import "fs/path/zero_length_strings_test.ts"; +import "io/bufio_test.ts"; +import "http/http_test.ts"; +import "log/test.ts"; +import "media_types/test.ts"; import "testing/test.ts"; +import "textproto/test.ts"; +import "ws/test.ts"; -import { runTests, completePromise } from "net/file_server_test.ts"; +import { runTests, completePromise } from "http/file_server_test.ts"; const fileServer = run({ - args: ["deno", "--allow-net", "net/file_server.ts", ".", "--cors"] + args: ["deno", "--allow-net", "http/file_server.ts", ".", "--cors"] }); runTests(new Promise(res => setTimeout(res, 5000))); diff --git a/textproto/mod.ts b/textproto/mod.ts new file mode 100644 index 000000000..ee7647296 --- /dev/null +++ b/textproto/mod.ts @@ -0,0 +1,150 @@ +// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ +// Copyright 2009 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. + +import { BufReader, BufState } from "../io/bufio.ts"; +import { charCode } from "../io/util.ts"; + +const asciiDecoder = new TextDecoder(); +function str(buf: Uint8Array): string { + if (buf == null) { + return ""; + } else { + return asciiDecoder.decode(buf); + } +} + +export class ProtocolError extends Error { + constructor(msg: string) { + super(msg); + this.name = "ProtocolError"; + } +} + +export class TextProtoReader { + constructor(readonly r: BufReader) {} + + /** readLine() reads a single line from the TextProtoReader, + * eliding the final \n or \r\n from the returned string. + */ + async readLine(): Promise<[string, BufState]> { + let [line, err] = await this.readLineSlice(); + return [str(line), err]; + } + + /** ReadMIMEHeader reads a MIME-style header from r. + * The header is a sequence of possibly continued Key: Value lines + * ending in a blank line. + * The returned map m maps CanonicalMIMEHeaderKey(key) to a + * sequence of values in the same order encountered in the input. + * + * For example, consider this input: + * + * My-Key: Value 1 + * Long-Key: Even + * Longer Value + * My-Key: Value 2 + * + * Given that input, ReadMIMEHeader returns the map: + * + * map[string][]string{ + * "My-Key": {"Value 1", "Value 2"}, + * "Long-Key": {"Even Longer Value"}, + * } + */ + async readMIMEHeader(): Promise<[Headers, BufState]> { + let m = new Headers(); + let line: Uint8Array; + + // The first line cannot start with a leading space. + let [buf, err] = await this.r.peek(1); + if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) { + [line, err] = await this.readLineSlice(); + } + + [buf, err] = await this.r.peek(1); + if (err == null && (buf[0] == charCode(" ") || buf[0] == charCode("\t"))) { + throw new ProtocolError( + `malformed MIME header initial line: ${str(line)}` + ); + } + + while (true) { + let [kv, err] = await this.readLineSlice(); // readContinuedLineSlice + if (kv.byteLength == 0) { + return [m, err]; + } + + // Key ends at first colon; should not have trailing spaces + // but they appear in the wild, violating specs, so we remove + // them if present. + let i = kv.indexOf(charCode(":")); + if (i < 0) { + throw new ProtocolError(`malformed MIME header line: ${str(kv)}`); + } + let endKey = i; + while (endKey > 0 && kv[endKey - 1] == charCode(" ")) { + endKey--; + } + + //let key = canonicalMIMEHeaderKey(kv.subarray(0, endKey)); + let key = str(kv.subarray(0, endKey)); + + // As per RFC 7230 field-name is a token, tokens consist of one or more chars. + // We could return a ProtocolError here, but better to be liberal in what we + // accept, so if we get an empty key, skip it. + if (key == "") { + continue; + } + + // Skip initial spaces in value. + i++; // skip colon + while ( + i < kv.byteLength && + (kv[i] == charCode(" ") || kv[i] == charCode("\t")) + ) { + i++; + } + let value = str(kv.subarray(i)); + + m.append(key, value); + + if (err != null) { + throw err; + } + } + } + + async readLineSlice(): Promise<[Uint8Array, BufState]> { + // this.closeDot(); + let line: Uint8Array; + while (true) { + let [l, more, err] = await this.r.readLine(); + if (err != null) { + // Go's len(typed nil) works fine, but not in JS + return [new Uint8Array(0), err]; + } + // Avoid the copy if the first call produced a full line. + if (line == null && !more) { + return [l, null]; + } + line = append(line, l); + if (!more) { + break; + } + } + return [line, null]; + } +} + +export function append(a: Uint8Array, b: Uint8Array): Uint8Array { + if (a == null) { + return b; + } else { + const output = new Uint8Array(a.length + b.length); + output.set(a, 0); + output.set(b, a.length); + return output; + } +} diff --git a/textproto/test.ts b/textproto/test.ts new file mode 100644 index 000000000..0f8bee227 --- /dev/null +++ b/textproto/test.ts @@ -0,0 +1,98 @@ +// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ +// Copyright 2009 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. + +import { BufReader } from "../io/bufio.ts"; +import { TextProtoReader, append } from "./mod.ts"; +import { stringsReader } from "../io/util.ts"; +import { test, assert, assertEqual } from "../testing/mod.ts"; + +function reader(s: string): TextProtoReader { + return new TextProtoReader(new BufReader(stringsReader(s))); +} + +test(async function textprotoReader() { + let r = reader("line1\nline2\n"); + let [s, err] = await r.readLine(); + assertEqual(s, "line1"); + assert(err == null); + + [s, err] = await r.readLine(); + assertEqual(s, "line2"); + assert(err == null); + + [s, err] = await r.readLine(); + assertEqual(s, ""); + assert(err == "EOF"); +}); + +/* +test(async function textprotoReadMIMEHeader() { + let r = reader("my-key: Value 1 \r\nLong-key: Even \n Longer Value\r\nmy-Key: Value 2\r\n\n"); + let [m, err] = await r.readMIMEHeader(); + + console.log("Got headers", m.toString()); + want := MIMEHeader{ + "My-Key": {"Value 1", "Value 2"}, + "Long-Key": {"Even Longer Value"}, + } + if !reflect.DeepEqual(m, want) || err != nil { + t.Fatalf("ReadMIMEHeader: %v, %v; want %v", m, err, want) + } +}); +*/ + +test(async function textprotoReadMIMEHeaderSingle() { + let r = reader("Foo: bar\n\n"); + let [m, err] = await r.readMIMEHeader(); + assertEqual(m.get("Foo"), "bar"); + assert(!err); +}); + +// Test that we read slightly-bogus MIME headers seen in the wild, +// with spaces before colons, and spaces in keys. +test(async function textprotoReadMIMEHeaderNonCompliant() { + // Invalid HTTP response header as sent by an Axis security + // camera: (this is handled by IE, Firefox, Chrome, curl, etc.) + let r = reader( + "Foo: bar\r\n" + + "Content-Language: en\r\n" + + "SID : 0\r\n" + + // TODO Re-enable Currently fails with: + // "TypeError: audio mode is not a legal HTTP header name" + // "Audio Mode : None\r\n" + + "Privilege : 127\r\n\r\n" + ); + let [m, err] = await r.readMIMEHeader(); + console.log(m.toString()); + assert(!err); + /* + let want = MIMEHeader{ + "Foo": {"bar"}, + "Content-Language": {"en"}, + "Sid": {"0"}, + "Audio Mode": {"None"}, + "Privilege": {"127"}, + } + if !reflect.DeepEqual(m, want) || err != nil { + t.Fatalf("ReadMIMEHeader =\n%v, %v; want:\n%v", m, err, want) + } + */ +}); + +test(async function textprotoAppend() { + const enc = new TextEncoder(); + const dec = new TextDecoder(); + const u1 = enc.encode("Hello "); + const u2 = enc.encode("World"); + const joined = append(u1, u2); + assertEqual(dec.decode(joined), "Hello World"); +}); + +test(async function textprotoReadEmpty() { + let r = reader(""); + let [m, err] = await r.readMIMEHeader(); + // Should not crash! + assertEqual(err, "EOF"); +}); diff --git a/ws/mod.ts b/ws/mod.ts new file mode 100644 index 000000000..b0b490978 --- /dev/null +++ b/ws/mod.ts @@ -0,0 +1,350 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { Buffer, Writer, Conn } from "deno"; +import { ServerRequest } from "../http/http.ts"; +import { BufReader, BufWriter } from "../io/bufio.ts"; +import { readLong, readShort, sliceLongToBytes } from "../io/ioutil.ts"; +import { Sha1 } from "./sha1.ts"; + +export const OpCodeContinue = 0x0; +export const OpCodeTextFrame = 0x1; +export const OpCodeBinaryFrame = 0x2; +export const OpCodeClose = 0x8; +export const OpcodePing = 0x9; +export const OpcodePong = 0xa; + +export type WebSocketEvent = + | string + | Uint8Array + | WebSocketCloseEvent + | WebSocketPingEvent + | WebSocketPongEvent; + +export type WebSocketCloseEvent = { + code: number; + reason?: string; +}; + +export function isWebSocketCloseEvent(a): a is WebSocketCloseEvent { + return a && typeof a["code"] === "number"; +} + +export type WebSocketPingEvent = ["ping", Uint8Array]; + +export function isWebSocketPingEvent(a): a is WebSocketPingEvent { + return Array.isArray(a) && a[0] === "ping" && a[1] instanceof Uint8Array; +} + +export type WebSocketPongEvent = ["pong", Uint8Array]; + +export function isWebSocketPongEvent(a): a is WebSocketPongEvent { + return Array.isArray(a) && a[0] === "pong" && a[1] instanceof Uint8Array; +} + +export class SocketClosedError extends Error {} + +export type WebSocketFrame = { + isLastFrame: boolean; + opcode: number; + mask?: Uint8Array; + payload: Uint8Array; +}; + +export type WebSocket = { + readonly isClosed: boolean; + receive(): AsyncIterableIterator; + send(data: string | Uint8Array): Promise; + ping(data?: string | Uint8Array): Promise; + close(code: number, reason?: string): Promise; +}; + +class WebSocketImpl implements WebSocket { + encoder = new TextEncoder(); + constructor(private conn: Conn, private mask?: Uint8Array) {} + + async *receive(): AsyncIterableIterator { + let frames: WebSocketFrame[] = []; + let payloadsLength = 0; + for await (const frame of receiveFrame(this.conn)) { + unmask(frame.payload, frame.mask); + switch (frame.opcode) { + case OpCodeTextFrame: + case OpCodeBinaryFrame: + case OpCodeContinue: + frames.push(frame); + payloadsLength += frame.payload.length; + if (frame.isLastFrame) { + const concat = new Uint8Array(payloadsLength); + let offs = 0; + for (const frame of frames) { + concat.set(frame.payload, offs); + offs += frame.payload.length; + } + if (frames[0].opcode === OpCodeTextFrame) { + // text + yield new Buffer(concat).toString(); + } else { + // binary + yield concat; + } + frames = []; + payloadsLength = 0; + } + break; + case OpCodeClose: + const code = (frame.payload[0] << 16) | frame.payload[1]; + const reason = new Buffer( + frame.payload.subarray(2, frame.payload.length) + ).toString(); + this._isClosed = true; + yield { code, reason }; + return; + case OpcodePing: + yield ["ping", frame.payload] as WebSocketPingEvent; + break; + case OpcodePong: + yield ["pong", frame.payload] as WebSocketPongEvent; + break; + } + } + } + + async send(data: string | Uint8Array): Promise { + if (this.isClosed) { + throw new SocketClosedError("socket has been closed"); + } + const opcode = + typeof data === "string" ? OpCodeTextFrame : OpCodeBinaryFrame; + const payload = typeof data === "string" ? this.encoder.encode(data) : data; + const isLastFrame = true; + await writeFrame( + { + isLastFrame, + opcode, + payload, + mask: this.mask + }, + this.conn + ); + } + + async ping(data: string | Uint8Array): Promise { + const payload = typeof data === "string" ? this.encoder.encode(data) : data; + await writeFrame( + { + isLastFrame: true, + opcode: OpCodeClose, + mask: this.mask, + payload + }, + this.conn + ); + } + + private _isClosed = false; + get isClosed() { + return this._isClosed; + } + + async close(code: number, reason?: string): Promise { + try { + const header = [code >>> 8, code & 0x00ff]; + let payload: Uint8Array; + if (reason) { + const reasonBytes = this.encoder.encode(reason); + payload = new Uint8Array(2 + reasonBytes.byteLength); + payload.set(header); + payload.set(reasonBytes, 2); + } else { + payload = new Uint8Array(header); + } + await writeFrame( + { + isLastFrame: true, + opcode: OpCodeClose, + mask: this.mask, + payload + }, + this.conn + ); + } catch (e) { + throw e; + } finally { + this.ensureSocketClosed(); + } + } + + private ensureSocketClosed(): Error { + if (this.isClosed) return; + try { + this.conn.close(); + } catch (e) { + console.error(e); + } finally { + this._isClosed = true; + } + } +} + +export async function* receiveFrame( + conn: Conn +): AsyncIterableIterator { + let receiving = true; + const reader = new BufReader(conn); + while (receiving) { + const frame = await readFrame(reader); + switch (frame.opcode) { + case OpCodeTextFrame: + case OpCodeBinaryFrame: + case OpCodeContinue: + yield frame; + break; + case OpCodeClose: + await writeFrame( + { + isLastFrame: true, + opcode: OpCodeClose, + payload: frame.payload + }, + conn + ); + conn.close(); + yield frame; + receiving = false; + break; + case OpcodePing: + await writeFrame( + { + isLastFrame: true, + opcode: OpcodePong, + payload: frame.payload + }, + conn + ); + yield frame; + break; + case OpcodePong: + yield frame; + break; + } + } +} + +export async function writeFrame(frame: WebSocketFrame, writer: Writer) { + let payloadLength = frame.payload.byteLength; + let header: Uint8Array; + const hasMask = (frame.mask ? 1 : 0) << 7; + if (payloadLength < 126) { + header = new Uint8Array([ + (0b1000 << 4) | frame.opcode, + hasMask | payloadLength + ]); + } else if (payloadLength < 0xffff) { + header = new Uint8Array([ + (0b1000 << 4) | frame.opcode, + hasMask | 0b01111110, + payloadLength >>> 8, + payloadLength & 0x00ff + ]); + } else { + header = new Uint8Array([ + (0b1000 << 4) | frame.opcode, + hasMask | 0b01111111, + ...sliceLongToBytes(payloadLength) + ]); + } + if (frame.mask) { + unmask(frame.payload, frame.mask); + } + const bytes = new Uint8Array(header.length + payloadLength); + bytes.set(header, 0); + bytes.set(frame.payload, header.length); + const w = new BufWriter(writer); + await w.write(bytes); + await w.flush(); +} + +export function unmask(payload: Uint8Array, mask: Uint8Array) { + if (mask) { + for (let i = 0; i < payload.length; i++) { + payload[i] ^= mask[i % 4]; + } + } +} + +export function acceptable(req: ServerRequest): boolean { + return ( + req.headers.get("upgrade") === "websocket" && + req.headers.has("sec-websocket-key") + ); +} + +export async function acceptWebSocket(req: ServerRequest): Promise { + if (acceptable(req)) { + const sock = new WebSocketImpl(req.conn); + const secKey = req.headers.get("sec-websocket-key"); + const secAccept = createSecAccept(secKey); + await req.respond({ + status: 101, + headers: new Headers({ + Upgrade: "websocket", + Connection: "Upgrade", + "Sec-WebSocket-Accept": secAccept + }) + }); + return sock; + } + throw new Error("request is not acceptable"); +} + +const kGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +export function createSecAccept(nonce: string) { + const sha1 = new Sha1(); + sha1.update(nonce + kGUID); + const bytes = sha1.digest(); + const hash = bytes.reduce( + (data, byte) => data + String.fromCharCode(byte), + "" + ); + return btoa(hash); +} + +export async function readFrame(buf: BufReader): Promise { + let b = await buf.readByte(); + let isLastFrame = false; + switch (b >>> 4) { + case 0b1000: + isLastFrame = true; + break; + case 0b0000: + isLastFrame = false; + break; + default: + throw new Error("invalid signature"); + } + const opcode = b & 0x0f; + // has_mask & payload + b = await buf.readByte(); + const hasMask = b >>> 7; + let payloadLength = b & 0b01111111; + if (payloadLength === 126) { + payloadLength = await readShort(buf); + } else if (payloadLength === 127) { + payloadLength = await readLong(buf); + } + // mask + let mask; + if (hasMask) { + mask = new Uint8Array(4); + await buf.readFull(mask); + } + // payload + const payload = new Uint8Array(payloadLength); + await buf.readFull(payload); + return { + isLastFrame, + opcode, + mask, + payload + }; +} diff --git a/ws/sha1.ts b/ws/sha1.ts new file mode 100644 index 000000000..036c3c552 --- /dev/null +++ b/ws/sha1.ts @@ -0,0 +1,382 @@ +/* + * [js-sha1]{@link https://github.com/emn178/js-sha1} + * + * @version 0.6.0 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2017 + * @license MIT + */ +/*jslint bitwise: true */ + +const HEX_CHARS = "0123456789abcdef".split(""); +const EXTRA = [-2147483648, 8388608, 32768, 128]; +const SHIFT = [24, 16, 8, 0]; + +const blocks = []; + +export class Sha1 { + blocks; + block; + start; + bytes; + hBytes; + finalized; + hashed; + first; + + h0 = 0x67452301; + h1 = 0xefcdab89; + h2 = 0x98badcfe; + h3 = 0x10325476; + h4 = 0xc3d2e1f0; + lastByteIndex = 0; + + constructor(sharedMemory: boolean = false) { + if (sharedMemory) { + blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + this.blocks = blocks; + } else { + this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + } + + this.h0 = 0x67452301; + this.h1 = 0xefcdab89; + this.h2 = 0x98badcfe; + this.h3 = 0x10325476; + this.h4 = 0xc3d2e1f0; + + this.block = this.start = this.bytes = this.hBytes = 0; + this.finalized = this.hashed = false; + this.first = true; + } + + update(data: string | ArrayBuffer) { + if (this.finalized) { + return; + } + let message; + let notString = typeof data !== "string"; + if (notString && data instanceof ArrayBuffer) { + message = new Uint8Array(data); + } else { + message = data; + } + let code, + index = 0, + i, + length = message.length || 0, + blocks = this.blocks; + + while (index < length) { + if (this.hashed) { + this.hashed = false; + blocks[0] = this.block; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + + if (notString) { + for (i = this.start; index < length && i < 64; ++index) { + blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = + 0x10000 + + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } + + this.lastByteIndex = i; + this.bytes += i - this.start; + if (i >= 64) { + this.block = blocks[16]; + this.start = i - 64; + this.hash(); + this.hashed = true; + } else { + this.start = i; + } + } + if (this.bytes > 4294967295) { + this.hBytes += (this.bytes / 4294967296) << 0; + this.bytes = this.bytes % 4294967296; + } + return this; + } + + finalize() { + if (this.finalized) { + return; + } + this.finalized = true; + let blocks = this.blocks, + i = this.lastByteIndex; + blocks[16] = this.block; + blocks[i >> 2] |= EXTRA[i & 3]; + this.block = blocks[16]; + if (i >= 56) { + if (!this.hashed) { + this.hash(); + } + blocks[0] = this.block; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + blocks[14] = (this.hBytes << 3) | (this.bytes >>> 29); + blocks[15] = this.bytes << 3; + this.hash(); + } + + hash() { + let a = this.h0, + b = this.h1, + c = this.h2, + d = this.h3, + e = this.h4; + let f, + j, + t, + blocks = this.blocks; + + for (j = 16; j < 80; ++j) { + t = blocks[j - 3] ^ blocks[j - 8] ^ blocks[j - 14] ^ blocks[j - 16]; + blocks[j] = (t << 1) | (t >>> 31); + } + + for (j = 0; j < 20; j += 5) { + f = (b & c) | (~b & d); + t = (a << 5) | (a >>> 27); + e = (t + f + e + 1518500249 + blocks[j]) << 0; + b = (b << 30) | (b >>> 2); + + f = (a & b) | (~a & c); + t = (e << 5) | (e >>> 27); + d = (t + f + d + 1518500249 + blocks[j + 1]) << 0; + a = (a << 30) | (a >>> 2); + + f = (e & a) | (~e & b); + t = (d << 5) | (d >>> 27); + c = (t + f + c + 1518500249 + blocks[j + 2]) << 0; + e = (e << 30) | (e >>> 2); + + f = (d & e) | (~d & a); + t = (c << 5) | (c >>> 27); + b = (t + f + b + 1518500249 + blocks[j + 3]) << 0; + d = (d << 30) | (d >>> 2); + + f = (c & d) | (~c & e); + t = (b << 5) | (b >>> 27); + a = (t + f + a + 1518500249 + blocks[j + 4]) << 0; + c = (c << 30) | (c >>> 2); + } + + for (; j < 40; j += 5) { + f = b ^ c ^ d; + t = (a << 5) | (a >>> 27); + e = (t + f + e + 1859775393 + blocks[j]) << 0; + b = (b << 30) | (b >>> 2); + + f = a ^ b ^ c; + t = (e << 5) | (e >>> 27); + d = (t + f + d + 1859775393 + blocks[j + 1]) << 0; + a = (a << 30) | (a >>> 2); + + f = e ^ a ^ b; + t = (d << 5) | (d >>> 27); + c = (t + f + c + 1859775393 + blocks[j + 2]) << 0; + e = (e << 30) | (e >>> 2); + + f = d ^ e ^ a; + t = (c << 5) | (c >>> 27); + b = (t + f + b + 1859775393 + blocks[j + 3]) << 0; + d = (d << 30) | (d >>> 2); + + f = c ^ d ^ e; + t = (b << 5) | (b >>> 27); + a = (t + f + a + 1859775393 + blocks[j + 4]) << 0; + c = (c << 30) | (c >>> 2); + } + + for (; j < 60; j += 5) { + f = (b & c) | (b & d) | (c & d); + t = (a << 5) | (a >>> 27); + e = (t + f + e - 1894007588 + blocks[j]) << 0; + b = (b << 30) | (b >>> 2); + + f = (a & b) | (a & c) | (b & c); + t = (e << 5) | (e >>> 27); + d = (t + f + d - 1894007588 + blocks[j + 1]) << 0; + a = (a << 30) | (a >>> 2); + + f = (e & a) | (e & b) | (a & b); + t = (d << 5) | (d >>> 27); + c = (t + f + c - 1894007588 + blocks[j + 2]) << 0; + e = (e << 30) | (e >>> 2); + + f = (d & e) | (d & a) | (e & a); + t = (c << 5) | (c >>> 27); + b = (t + f + b - 1894007588 + blocks[j + 3]) << 0; + d = (d << 30) | (d >>> 2); + + f = (c & d) | (c & e) | (d & e); + t = (b << 5) | (b >>> 27); + a = (t + f + a - 1894007588 + blocks[j + 4]) << 0; + c = (c << 30) | (c >>> 2); + } + + for (; j < 80; j += 5) { + f = b ^ c ^ d; + t = (a << 5) | (a >>> 27); + e = (t + f + e - 899497514 + blocks[j]) << 0; + b = (b << 30) | (b >>> 2); + + f = a ^ b ^ c; + t = (e << 5) | (e >>> 27); + d = (t + f + d - 899497514 + blocks[j + 1]) << 0; + a = (a << 30) | (a >>> 2); + + f = e ^ a ^ b; + t = (d << 5) | (d >>> 27); + c = (t + f + c - 899497514 + blocks[j + 2]) << 0; + e = (e << 30) | (e >>> 2); + + f = d ^ e ^ a; + t = (c << 5) | (c >>> 27); + b = (t + f + b - 899497514 + blocks[j + 3]) << 0; + d = (d << 30) | (d >>> 2); + + f = c ^ d ^ e; + t = (b << 5) | (b >>> 27); + a = (t + f + a - 899497514 + blocks[j + 4]) << 0; + c = (c << 30) | (c >>> 2); + } + + this.h0 = (this.h0 + a) << 0; + this.h1 = (this.h1 + b) << 0; + this.h2 = (this.h2 + c) << 0; + this.h3 = (this.h3 + d) << 0; + this.h4 = (this.h4 + e) << 0; + } + + hex() { + this.finalize(); + + let h0 = this.h0, + h1 = this.h1, + h2 = this.h2, + h3 = this.h3, + h4 = this.h4; + + return ( + HEX_CHARS[(h0 >> 28) & 0x0f] + + HEX_CHARS[(h0 >> 24) & 0x0f] + + HEX_CHARS[(h0 >> 20) & 0x0f] + + HEX_CHARS[(h0 >> 16) & 0x0f] + + HEX_CHARS[(h0 >> 12) & 0x0f] + + HEX_CHARS[(h0 >> 8) & 0x0f] + + HEX_CHARS[(h0 >> 4) & 0x0f] + + HEX_CHARS[h0 & 0x0f] + + HEX_CHARS[(h1 >> 28) & 0x0f] + + HEX_CHARS[(h1 >> 24) & 0x0f] + + HEX_CHARS[(h1 >> 20) & 0x0f] + + HEX_CHARS[(h1 >> 16) & 0x0f] + + HEX_CHARS[(h1 >> 12) & 0x0f] + + HEX_CHARS[(h1 >> 8) & 0x0f] + + HEX_CHARS[(h1 >> 4) & 0x0f] + + HEX_CHARS[h1 & 0x0f] + + HEX_CHARS[(h2 >> 28) & 0x0f] + + HEX_CHARS[(h2 >> 24) & 0x0f] + + HEX_CHARS[(h2 >> 20) & 0x0f] + + HEX_CHARS[(h2 >> 16) & 0x0f] + + HEX_CHARS[(h2 >> 12) & 0x0f] + + HEX_CHARS[(h2 >> 8) & 0x0f] + + HEX_CHARS[(h2 >> 4) & 0x0f] + + HEX_CHARS[h2 & 0x0f] + + HEX_CHARS[(h3 >> 28) & 0x0f] + + HEX_CHARS[(h3 >> 24) & 0x0f] + + HEX_CHARS[(h3 >> 20) & 0x0f] + + HEX_CHARS[(h3 >> 16) & 0x0f] + + HEX_CHARS[(h3 >> 12) & 0x0f] + + HEX_CHARS[(h3 >> 8) & 0x0f] + + HEX_CHARS[(h3 >> 4) & 0x0f] + + HEX_CHARS[h3 & 0x0f] + + HEX_CHARS[(h4 >> 28) & 0x0f] + + HEX_CHARS[(h4 >> 24) & 0x0f] + + HEX_CHARS[(h4 >> 20) & 0x0f] + + HEX_CHARS[(h4 >> 16) & 0x0f] + + HEX_CHARS[(h4 >> 12) & 0x0f] + + HEX_CHARS[(h4 >> 8) & 0x0f] + + HEX_CHARS[(h4 >> 4) & 0x0f] + + HEX_CHARS[h4 & 0x0f] + ); + } + + toString() { + return this.hex(); + } + + digest() { + this.finalize(); + + let h0 = this.h0, + h1 = this.h1, + h2 = this.h2, + h3 = this.h3, + h4 = this.h4; + + return [ + (h0 >> 24) & 0xff, + (h0 >> 16) & 0xff, + (h0 >> 8) & 0xff, + h0 & 0xff, + (h1 >> 24) & 0xff, + (h1 >> 16) & 0xff, + (h1 >> 8) & 0xff, + h1 & 0xff, + (h2 >> 24) & 0xff, + (h2 >> 16) & 0xff, + (h2 >> 8) & 0xff, + h2 & 0xff, + (h3 >> 24) & 0xff, + (h3 >> 16) & 0xff, + (h3 >> 8) & 0xff, + h3 & 0xff, + (h4 >> 24) & 0xff, + (h4 >> 16) & 0xff, + (h4 >> 8) & 0xff, + h4 & 0xff + ]; + } + + array() { + return this.digest(); + } + + arrayBuffer() { + this.finalize(); + + let buffer = new ArrayBuffer(20); + let dataView = new DataView(buffer); + dataView.setUint32(0, this.h0); + dataView.setUint32(4, this.h1); + dataView.setUint32(8, this.h2); + dataView.setUint32(12, this.h3); + dataView.setUint32(16, this.h4); + return buffer; + } +} diff --git a/ws/sha1_test.ts b/ws/sha1_test.ts new file mode 100644 index 000000000..b385f18da --- /dev/null +++ b/ws/sha1_test.ts @@ -0,0 +1,8 @@ +import { assertEqual, test } from "../testing/mod.ts"; +import { Sha1 } from "./sha1.ts"; + +test(function testSha1() { + const sha1 = new Sha1(); + sha1.update("abcde"); + assertEqual(sha1.toString(), "03de6c570bfe24bfc328ccd7ca46b76eadaf4334"); +}); diff --git a/ws/test.ts b/ws/test.ts new file mode 100644 index 000000000..bf800f7e3 --- /dev/null +++ b/ws/test.ts @@ -0,0 +1,138 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { Buffer } from "deno"; +import { BufReader } from "../io/bufio.ts"; +import { test, assert, assertEqual } from "../testing/mod.ts"; +import { + createSecAccept, + OpCodeBinaryFrame, + OpCodeContinue, + OpcodePing, + OpcodePong, + OpCodeTextFrame, + readFrame, + unmask +} from "./mod.ts"; +import { serve } from "../http/http.ts"; + +test(async function testReadUnmaskedTextFrame() { + // unmasked single text frame with payload "Hello" + const buf = new BufReader( + new Buffer(new Uint8Array([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])) + ); + const frame = await readFrame(buf); + assertEqual(frame.opcode, OpCodeTextFrame); + assertEqual(frame.mask, undefined); + assertEqual(new Buffer(frame.payload).toString(), "Hello"); + assertEqual(frame.isLastFrame, true); +}); + +test(async function testReadMakedTextFrame() { + //a masked single text frame with payload "Hello" + const buf = new BufReader( + new Buffer( + new Uint8Array([ + 0x81, + 0x85, + 0x37, + 0xfa, + 0x21, + 0x3d, + 0x7f, + 0x9f, + 0x4d, + 0x51, + 0x58 + ]) + ) + ); + const frame = await readFrame(buf); + console.dir(frame); + assertEqual(frame.opcode, OpCodeTextFrame); + unmask(frame.payload, frame.mask); + assertEqual(new Buffer(frame.payload).toString(), "Hello"); + assertEqual(frame.isLastFrame, true); +}); + +test(async function testReadUnmaskedSplittedTextFrames() { + const buf1 = new BufReader( + new Buffer(new Uint8Array([0x01, 0x03, 0x48, 0x65, 0x6c])) + ); + const buf2 = new BufReader( + new Buffer(new Uint8Array([0x80, 0x02, 0x6c, 0x6f])) + ); + const [f1, f2] = await Promise.all([readFrame(buf1), readFrame(buf2)]); + assertEqual(f1.isLastFrame, false); + assertEqual(f1.mask, undefined); + assertEqual(f1.opcode, OpCodeTextFrame); + assertEqual(new Buffer(f1.payload).toString(), "Hel"); + + assertEqual(f2.isLastFrame, true); + assertEqual(f2.mask, undefined); + assertEqual(f2.opcode, OpCodeContinue); + assertEqual(new Buffer(f2.payload).toString(), "lo"); +}); + +test(async function testReadUnmaksedPingPongFrame() { + // unmasked ping with payload "Hello" + const buf = new BufReader( + new Buffer(new Uint8Array([0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])) + ); + const ping = await readFrame(buf); + assertEqual(ping.opcode, OpcodePing); + assertEqual(new Buffer(ping.payload).toString(), "Hello"); + + const buf2 = new BufReader( + new Buffer( + new Uint8Array([ + 0x8a, + 0x85, + 0x37, + 0xfa, + 0x21, + 0x3d, + 0x7f, + 0x9f, + 0x4d, + 0x51, + 0x58 + ]) + ) + ); + const pong = await readFrame(buf2); + assertEqual(pong.opcode, OpcodePong); + assert(pong.mask !== undefined); + unmask(pong.payload, pong.mask); + assertEqual(new Buffer(pong.payload).toString(), "Hello"); +}); + +test(async function testReadUnmaksedBigBinaryFrame() { + let a = [0x82, 0x7e, 0x01, 0x00]; + for (let i = 0; i < 256; i++) { + a.push(i); + } + const buf = new BufReader(new Buffer(new Uint8Array(a))); + const bin = await readFrame(buf); + assertEqual(bin.opcode, OpCodeBinaryFrame); + assertEqual(bin.isLastFrame, true); + assertEqual(bin.mask, undefined); + assertEqual(bin.payload.length, 256); +}); + +test(async function testReadUnmaskedBigBigBinaryFrame() { + let a = [0x82, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]; + for (let i = 0; i < 0xffff; i++) { + a.push(i); + } + const buf = new BufReader(new Buffer(new Uint8Array(a))); + const bin = await readFrame(buf); + assertEqual(bin.opcode, OpCodeBinaryFrame); + assertEqual(bin.isLastFrame, true); + assertEqual(bin.mask, undefined); + assertEqual(bin.payload.length, 0xffff + 1); +}); + +test(async function testCreateSecAccept() { + const nonce = "dGhlIHNhbXBsZSBub25jZQ=="; + const d = createSecAccept(nonce); + assertEqual(d, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="); +}); -- cgit v1.2.3