├── src ├── node │ ├── huffman │ │ ├── index.ts │ │ ├── decode.ts │ │ ├── __tests__ │ │ │ ├── encode.ts │ │ │ └── decode.ts │ │ ├── encode.ts │ │ └── static.ts │ ├── UnorderedMap.ts │ ├── SHA256Context.ts │ └── utils.ts ├── Platform.ts ├── HTTP2 │ ├── hpack │ │ ├── index.ts │ │ └── internals │ │ │ ├── __Disabled_tests__ │ │ │ └── header-table.ts │ │ │ ├── static-header-list.ts │ │ │ └── header-table.ts │ ├── consts.ts │ ├── frame │ │ ├── connecting-frame.ts │ │ ├── types.ts │ │ ├── __tests__ │ │ │ ├── settings-frame.ts │ │ │ └── frame-writer.ts │ │ ├── header.ts │ │ ├── settings-frame.ts │ │ ├── utils.ts │ │ ├── frame-writer.ts │ │ └── frame-constructor.ts │ ├── README.md │ ├── __Disabled_tests__ │ │ └── integration.ts │ ├── index.ts │ └── stream │ │ ├── index.ts │ │ └── stream-manager.ts ├── DataBuffer.ts ├── UnorderedMap.ts ├── utils │ ├── assert.macro │ │ ├── index.d.ts │ │ └── index.js │ └── index.ts ├── IOnHeadersData.ts ├── INetworkError.ts ├── IConnectionDataSSL.ts ├── nrdp │ ├── UnorderedMap.ts │ └── DataBuffer.ts ├── ICreateSSLNetworkPipeOptions.ts ├── IPipeResult.ts ├── IHTTP.ts ├── ISHA256Context.ts ├── IPendingConnection.ts ├── ws │ ├── mask.ts │ ├── framer │ │ ├── state.ts │ │ ├── types.ts │ │ └── header.ts │ ├── __tests__ │ │ └── mask.ts │ ├── types.ts │ ├── upgrade.ts │ └── buffer.ts ├── ICreateTCPNetworkPipeOptions.ts ├── IHTTPRequest.ts ├── IRequestTimeouts.ts ├── IMilo.ts ├── NetworkError.ts ├── IConnectionOptions.ts ├── IDnsResult.ts ├── ICompressionStream.ts ├── IHTTPHeadersEvent.ts ├── IConnectionData.ts ├── IUnorderedMap.ts ├── Pool.ts ├── IRequestData.ts ├── TODO.md ├── RequestResponse.ts ├── NetworkPipe.ts ├── milo.ts ├── IPlatform.ts ├── types.ts ├── CookieJar.ts ├── EventEmitter.ts └── HTTP1 │ └── ChunkyParser.ts ├── .env ├── autobahn ├── runner │ ├── nrdp │ │ ├── agent.ts │ │ ├── create-artifact.ts │ │ ├── sys-requirements.ts │ │ ├── index.ts │ │ ├── run-nrdp.ts │ │ └── entry.ts │ ├── docker │ │ ├── stop.ts │ │ ├── kill.ts │ │ ├── docker-ps.ts │ │ ├── config.ts │ │ ├── index.ts │ │ └── launch.ts │ ├── paths.ts │ ├── index.ts │ ├── get-agent.ts │ ├── ready.ts │ ├── sys-requirements.ts │ └── run-test.ts ├── types.ts ├── context.ts ├── milo.ts ├── merge-custom-config.ts ├── start.ts ├── run-async.ts ├── autobahn-reports.ts ├── README.md └── test-harness.ts ├── .gitmodules ├── .gitignore ├── rollup.nrdp.test.js ├── bin ├── builds │ ├── rollup-target-plugin.js │ ├── tsconfig.nrdp.json │ ├── tsconfig.node.json │ ├── rollup.node.js │ └── rollup.nrdp.js ├── lint.js ├── generate-ssl-functions.js └── build.js ├── jest.config.js ├── .github └── workflows │ └── main.yml ├── tsconfig.test.json ├── examples └── ws-server │ ├── client-real.ts │ ├── client.node.ts │ └── server.ts ├── tsconfig.json ├── tslint.json ├── tests ├── csr.pem ├── cert.pem ├── key.pem ├── test-client.js ├── rmvsmilo.js ├── test-client-nrdp.js └── test-server.js ├── typings ├── nrdp_platform.d.ts └── nrdp.d.ts └── package.json /src/node/huffman/index.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Platform.ts: -------------------------------------------------------------------------------- 1 | import Platform from "./#{target}/Platform"; 2 | export default Platform; 3 | -------------------------------------------------------------------------------- /src/HTTP2/hpack/index.ts: -------------------------------------------------------------------------------- 1 | // Notes 2 | // 1. Order matters for headers 3 | // 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | UPDATE_REPORT=true 2 | MILO=true 3 | SELF_MANAGED_AUTOBAHN=false 4 | CASES=1.*,2.*,3.*,4.*,5.* 5 | -------------------------------------------------------------------------------- /src/DataBuffer.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from "./#{target}/DataBuffer"; 2 | export default DataBuffer; 3 | 4 | -------------------------------------------------------------------------------- /autobahn/runner/nrdp/agent.ts: -------------------------------------------------------------------------------- 1 | export default function getAgent() { 2 | return 'NRDP_Autobahn'; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /src/UnorderedMap.ts: -------------------------------------------------------------------------------- 1 | import UnorderedMap from "./#{target}/UnorderedMap"; 2 | export default UnorderedMap; 3 | 4 | -------------------------------------------------------------------------------- /src/HTTP2/consts.ts: -------------------------------------------------------------------------------- 1 | export const VersionIdentification = { 2 | Secure: "h2", 3 | NonSecure: "h2c", 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /src/utils/assert.macro/index.d.ts: -------------------------------------------------------------------------------- 1 | declare function assert(condition: any, msg: string): asserts condition; 2 | export = assert; -------------------------------------------------------------------------------- /src/IOnHeadersData.ts: -------------------------------------------------------------------------------- 1 | export default interface IOnHeadersData { 2 | statusCode: number; 3 | headers: string[]; 4 | }; 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "autobahn-testsuite"] 2 | path = autobahn-testsuite 3 | url = https://github.com/crossbario/autobahn-testsuite.git 4 | -------------------------------------------------------------------------------- /src/INetworkError.ts: -------------------------------------------------------------------------------- 1 | import { NetworkErrorCode } from "./types"; 2 | 3 | export default interface INetworkError extends Error { 4 | code: NetworkErrorCode; 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .zshrc.bak 3 | node_modules 4 | build 5 | dist 6 | .milo-test-env 7 | 8 | /src/nrdp/NrdpBoundSSLFunctions.ts 9 | /tests/test-server.ports 10 | -------------------------------------------------------------------------------- /autobahn/types.ts: -------------------------------------------------------------------------------- 1 | export interface IPlatform { 2 | error(...args: any[]): void; 3 | log(...args: any[]): void; 4 | trace(...args: any[]): void; 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/IConnectionDataSSL.ts: -------------------------------------------------------------------------------- 1 | export default interface IConnectionDataSSL { 2 | sslVersion?: string; 3 | sslSessionResumed?: boolean; 4 | sslHandshakeTime?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /src/nrdp/UnorderedMap.ts: -------------------------------------------------------------------------------- 1 | import IUnorderedMap from "../IUnorderedMap"; 2 | 3 | declare const UnorderedMap: new () => IUnorderedMap; 4 | export default UnorderedMap; 5 | -------------------------------------------------------------------------------- /src/ICreateSSLNetworkPipeOptions.ts: -------------------------------------------------------------------------------- 1 | import IPipeResult from "./IPipeResult"; 2 | 3 | export default interface ICreateSSLNetworkPipeOptions extends IPipeResult { 4 | tlsv13?: boolean; 5 | }; 6 | -------------------------------------------------------------------------------- /autobahn/runner/docker/stop.ts: -------------------------------------------------------------------------------- 1 | import { killDocker } from './kill'; 2 | 3 | export async function stop() { 4 | // TODO: Anything else? 5 | await killDocker(); 6 | } 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /rollup.nrdp.test.js: -------------------------------------------------------------------------------- 1 | import config from './rollup.nrdp.js'; 2 | config.input = "build/nrdp/autobahn/runner/nrdp/entry.js"; 3 | config.output.name = 'entry'; 4 | config.output.file = 'dist/nrdp.autobahn.js'; 5 | 6 | export default config; 7 | 8 | -------------------------------------------------------------------------------- /autobahn/runner/paths.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export const root = path.join(__dirname, "..", ".."); 4 | export const autobahnTS = path.join(root, "autobahn-testsuite"); 5 | export const autobahnDocker = path.join(autobahnTS, "docker"); 6 | 7 | -------------------------------------------------------------------------------- /src/IPipeResult.ts: -------------------------------------------------------------------------------- 1 | import IConnectionData from "./IConnectionData"; 2 | import NetworkPipe from "./NetworkPipe"; 3 | import { DnsType } from "./types"; 4 | 5 | export default interface IPipeResult extends IConnectionData { 6 | pipe: NetworkPipe; 7 | }; 8 | -------------------------------------------------------------------------------- /src/IHTTP.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "./EventEmitter"; 2 | import IHTTPRequest from "./IHTTPRequest"; 3 | import NetworkPipe from "./NetworkPipe"; 4 | 5 | export default interface IHTTP extends EventEmitter { 6 | send(pipe: NetworkPipe, request: IHTTPRequest): boolean; 7 | upgrade: boolean; 8 | }; 9 | -------------------------------------------------------------------------------- /src/ISHA256Context.ts: -------------------------------------------------------------------------------- 1 | import IDataBuffer from "./IDataBuffer"; 2 | 3 | export default interface ISHA256Context { 4 | add(buf: Uint8Array | ArrayBuffer | IDataBuffer | string): void; 5 | 6 | final(): ArrayBuffer; 7 | final(md: ArrayBuffer | Uint8Array | IDataBuffer, offset?: number): number; 8 | reset(): void; 9 | }; 10 | -------------------------------------------------------------------------------- /src/IPendingConnection.ts: -------------------------------------------------------------------------------- 1 | import NetworkPipe from "./NetworkPipe"; 2 | import { DnsType } from "./types"; 3 | import IConnectionData from "./IConnectionData"; 4 | 5 | export default interface IPendingConnection extends IConnectionData { 6 | readonly id: number; 7 | 8 | abort(): void; 9 | onNetworkPipe(): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /autobahn/runner/index.ts: -------------------------------------------------------------------------------- 1 | import { systemReq } from './sys-requirements'; 2 | import { runAutobahnTests, AutobahnOpts } from './run-test'; 3 | 4 | export default async function autobahn(WebSocketClass: any, opts: AutobahnOpts): Promise { 5 | await systemReq(); 6 | return await runAutobahnTests(WebSocketClass, opts); 7 | }; 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/ws/mask.ts: -------------------------------------------------------------------------------- 1 | import IDataBuffer from "../IDataBuffer"; 2 | 3 | function mask(buf: IDataBuffer, offset: number, length: number, theMask: IDataBuffer) { 4 | for (let i = offset, j = 0; j < length; ++j, ++i) { 5 | buf.setUInt8(i, buf.getUInt8(i) ^ (theMask.getUInt8(j % 4) & 0xFF)); 6 | } 7 | } 8 | 9 | export default mask; 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/HTTP2/frame/connecting-frame.ts: -------------------------------------------------------------------------------- 1 | import SettingsFrame from "./settings-frame"; 2 | 3 | /** 4 | * create connection frame. This will have a predetermined string then the 5 | * settings frame. 6 | */ 7 | export default function createConnectionFrame(settings: SettingsFrame) { 8 | 9 | const settingsFrame = settings.toFrameWriter(1); 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /src/ICreateTCPNetworkPipeOptions.ts: -------------------------------------------------------------------------------- 1 | import { IpVersion } from "./types"; 2 | 3 | export default interface ICreateTCPNetworkPipeOptions { 4 | hostname: string; // could be an ip literal 5 | port: number; 6 | connectTimeout: number; 7 | dnsTimeout: number; 8 | ipVersion: IpVersion; 9 | ipAddresses?: string[]; 10 | dnsName?: string; 11 | }; 12 | -------------------------------------------------------------------------------- /src/IHTTPRequest.ts: -------------------------------------------------------------------------------- 1 | import IDataBuffer from "./IDataBuffer"; 2 | import { HTTPRequestHeaders, HTTPMethod } from "./types"; 3 | 4 | export default interface IHTTPRequest { 5 | networkStartTime: number, 6 | url: import("url-parse"); 7 | method: HTTPMethod; 8 | requestHeaders: HTTPRequestHeaders; 9 | body?: string | { [key: string]: any } | Uint8Array | ArrayBuffer | IDataBuffer; 10 | }; 11 | -------------------------------------------------------------------------------- /src/IRequestTimeouts.ts: -------------------------------------------------------------------------------- 1 | export default interface IRequestTimeouts { 2 | timeout?: number; 3 | connectTimeout?: number; 4 | dnsTimeout?: number; 5 | dnsFallbackTimeoutWaitFor4?: number; 6 | dnsFallbackTimeoutWaitFor6?: number; 7 | happyEyeballsHeadStart?: number; 8 | lowSpeedLimit?: number; 9 | lowSpeedTime?: number; // ### this is in seconds in curl 10 | delay?: number; 11 | }; 12 | -------------------------------------------------------------------------------- /src/IMilo.ts: -------------------------------------------------------------------------------- 1 | import RequestResponse from "./RequestResponse"; 2 | import IRequestData from "./IRequestData"; 3 | import IPlatform from "./IPlatform"; 4 | import WS, { WSState } from "./ws"; 5 | 6 | export default interface IMilo { 7 | load(data: IRequestData | string, callback?: (response: RequestResponse) => void): number; 8 | ws(url: string, milo: boolean): Promise; 9 | 10 | readonly platform: IPlatform; 11 | }; 12 | -------------------------------------------------------------------------------- /src/NetworkError.ts: -------------------------------------------------------------------------------- 1 | import { NetworkErrorCode, networkErrorCodeToString } from "./types"; 2 | import INetworkError from "./INetworkError"; 3 | 4 | export default class NetworkError extends Error implements INetworkError { 5 | constructor(code: NetworkErrorCode, message?: string) { 6 | super(message || networkErrorCodeToString(code)); 7 | this.code = code; 8 | } 9 | public readonly code: NetworkErrorCode; 10 | }; 11 | -------------------------------------------------------------------------------- /autobahn/runner/nrdp/create-artifact.ts: -------------------------------------------------------------------------------- 1 | import rollup from 'rollup'; 2 | import shell from 'shelljs'; 3 | import path from 'path'; 4 | 5 | import { root } from '../paths'; 6 | import { IPlatform } from '../../types'; 7 | 8 | export default async function createArtifact(Platform: IPlatform) { 9 | Platform.log("Building the NRDP Artifact"); 10 | shell.pushd(root); 11 | shell.exec("npm run build:nrdp:test"); 12 | shell.popd(); 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /autobahn/runner/nrdp/sys-requirements.ts: -------------------------------------------------------------------------------- 1 | import { IPlatform } from '../../types'; 2 | 3 | const nrdpError = `Unable to find Nrdp environment variable 4 | 5 | Either update the .env file with NRDP path run the test with NRDP=... npx ts-node ... 6 | 7 | `; 8 | 9 | export default async function runNrdp(Platform: IPlatform) { 10 | const nrdpExec = process.env.NRDP; 11 | if (!nrdpExec) { 12 | throw new Error(nrdpError); 13 | } 14 | }; 15 | 16 | 17 | -------------------------------------------------------------------------------- /bin/builds/rollup-target-plugin.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | export default function target({ target }) { 4 | return { 5 | name: 'target', 6 | resolveId (source, importer) { 7 | if (source.includes('#{target}')) { 8 | const targetPath = source.replace('#{target}', target) + '.js'; 9 | return path.resolve(path.dirname(importer), targetPath); 10 | } 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/IConnectionOptions.ts: -------------------------------------------------------------------------------- 1 | import Url from "url-parse"; 2 | import ICreateSSLNetworkPipeOptions from "./ICreateSSLNetworkPipeOptions"; 3 | import { DnsType } from "./types"; 4 | 5 | export default interface IConnectionOptions extends ICreateSSLNetworkPipeOptions { 6 | url: Url; 7 | ipAddresses?: string[]; 8 | dnsName?: string; 9 | freshConnect?: boolean; 10 | forbidReuse?: boolean; 11 | connectTimeout: number; 12 | dnsTimeout: number; 13 | }; 14 | -------------------------------------------------------------------------------- /autobahn/runner/docker/kill.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'shelljs'; 2 | import { getDockers } from './docker-ps'; 3 | 4 | 5 | export async function killDocker() { 6 | const dockers = await getDockers(); 7 | dockers.forEach(d => { 8 | if (~d[1].indexOf('crossbario/autobahn-testsuite')) { 9 | exec(`docker kill ${d[0]}`); 10 | } 11 | }); 12 | 13 | // we also have to kill any other server 14 | exec(`docker rm fuzzingserver`); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/IDnsResult.ts: -------------------------------------------------------------------------------- 1 | import { IpVersion, DnsType } from "./types"; 2 | 3 | export default interface IDnsResult { 4 | errorCode: number; 5 | host: string; 6 | error?: string; 7 | aresCode: number; 8 | time: number; 9 | age: number; 10 | name: string; 11 | aliases?: [string]; 12 | channel: string; 13 | ipVersion: IpVersion; 14 | addresses: [string]; 15 | ttl: number; 16 | lastTouched?: number; 17 | state?: string; 18 | type: DnsType; 19 | }; 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | tsConfig: 'tsconfig.test.json' 5 | } 6 | }, 7 | roots: [ 8 | "/src" 9 | ], 10 | testPathIgnorePatterns: [ 11 | "/src/__tests__/utils/get.ts" 12 | ], 13 | testMatch: [ 14 | "**/__tests__/**/*.+(ts|tsx|js)", 15 | ], 16 | "transform": { 17 | "^.+\\.(ts|tsx)$": "ts-jest" 18 | }, 19 | moduleNameMapper: { 20 | "#{target}\/(.*)": "/src/node/$1" 21 | }, 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/ICompressionStream.ts: -------------------------------------------------------------------------------- 1 | import IDataBuffer from "./IDataBuffer"; 2 | 3 | export default interface ICompressionStream { 4 | process(output: Uint8Array | ArrayBuffer | IDataBuffer, 5 | outputOffset: number, 6 | outputLength: number | undefined, 7 | input: Uint8Array | ArrayBuffer | IDataBuffer | string, 8 | inputOffset?: number, 9 | inputLength?: number): number; 10 | 11 | readonly inputUsed: number; 12 | readonly method: string; 13 | readonly type: string; 14 | }; 15 | -------------------------------------------------------------------------------- /autobahn/runner/nrdp/index.ts: -------------------------------------------------------------------------------- 1 | import shell from "shelljs"; 2 | 3 | import { IPlatform } from "../../types"; 4 | import { root } from "../paths"; 5 | 6 | import sysRequirements from "./sys-requirements"; 7 | import createArtifact from "./create-artifact"; 8 | import runNRDP from "./run-nrdp"; 9 | 10 | export default async function testNrdp(Platform: IPlatform): Promise { 11 | // Setup the system. 12 | await sysRequirements(Platform); 13 | await createArtifact(Platform); 14 | 15 | return await runNRDP(Platform); 16 | }; 17 | -------------------------------------------------------------------------------- /autobahn/context.ts: -------------------------------------------------------------------------------- 1 | export interface Killable { 2 | kill: () => void; 3 | } 4 | 5 | // This will help kill programs that are started by shelljs 6 | export interface LocalContext { 7 | runners: Killable[] 8 | }; 9 | 10 | export function killContext(context: LocalContext) { 11 | context.runners.forEach(k => { 12 | try { 13 | k.kill() 14 | } catch (e) { 15 | console.error("killContext Error", e); 16 | } 17 | }); 18 | } 19 | 20 | export const GlobalContext = { 21 | runners: [] 22 | } as LocalContext; 23 | -------------------------------------------------------------------------------- /src/IHTTPHeadersEvent.ts: -------------------------------------------------------------------------------- 1 | import { HTTPMethod, HTTPEncoding } from "./types"; 2 | 3 | export default interface IHTTPHeadersEvent { 4 | contentLength?: number; 5 | headers: string[]; 6 | headersSize: number; 7 | httpVersion: string; 8 | method: HTTPMethod; 9 | requestSize: number; 10 | statusCode: number; 11 | timeToFirstByteRead: number; 12 | timeToFirstByteWritten: number; 13 | redirectUrl?: string; 14 | transferEncoding?: HTTPEncoding[]; 15 | contentEncoding?: HTTPEncoding[]; 16 | setCookie?: string[] | string; 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /bin/builds/tsconfig.nrdp.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "exclude": [ 4 | "**/node_modules", 5 | "**/.*/", 6 | "../../examples", 7 | "../../autobahn", 8 | "../../src/node", 9 | "../../src/**/__tests__", 10 | "../../src/**/__Disabled_tests__", 11 | ], 12 | "compilerOptions": { 13 | "incremental": true, 14 | "sourceMap": true, 15 | "outDir": "../../build/nrdp", 16 | "module": "es6", 17 | "target": "esnext", // target esnext to keep async/await 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bin/builds/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "exclude": [ 4 | "**/node_modules", 5 | "**/.*/", 6 | "../../examples", 7 | "../../autobahn", 8 | "../../src/nrdp", 9 | "../../src/**/__tests__", 10 | "../../src/**/__Disabled_tests__", 11 | ], 12 | "compilerOptions": { 13 | "incremental": true, 14 | "outDir": "../../build/node", 15 | "lib": ["es6"], 16 | "module": "es6", // we keep es6 beacuse rollup will take care of creating cjs 17 | "target": "es6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/IConnectionData.ts: -------------------------------------------------------------------------------- 1 | import IConnectionDataSSL from "./IConnectionDataSSL"; 2 | import { DnsType } from "./types"; 3 | 4 | export default interface IConnectionData extends IConnectionDataSSL { 5 | cname: string; 6 | connectTime: number; 7 | dnsChannel: string; 8 | // this is how long it took us to get a dns result, regardless of 9 | // happy eyeballs, caches etc 10 | dnsTime: number; 11 | // this is the time it took the platform to get this particular 12 | // dns result 13 | dnsWireTime: number; 14 | dnsType: DnsType; 15 | socketReused: boolean; 16 | }; 17 | -------------------------------------------------------------------------------- /src/IUnorderedMap.ts: -------------------------------------------------------------------------------- 1 | export default interface IUnorderedMap { 2 | clear(): void; 3 | clone(): IUnorderedMap; 4 | delete(key: Key): boolean; 5 | entries(): [Key, Value][]; 6 | forEach(func: (key: Key, value: Value, that: IUnorderedMap) => boolean | void): void; 7 | get(key: Key): Value | undefined; 8 | has(key: Key): boolean; 9 | keys(): Key[]; 10 | readonly length: number; 11 | readonly size: number; 12 | set(key: Key, value: Value): IUnorderedMap; 13 | take(key: Key): Value | undefined; 14 | values(): Value[]; 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Milo CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: npm run build --if-present 25 | - run: npm test 26 | env: 27 | CI: true 28 | 29 | 30 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ "**/flycheck_*.ts" ], 4 | "compilerOptions": { 5 | "outDir": "./build/test", 6 | "sourceMap": true, 7 | "noImplicitAny": true, 8 | "lib": ["es6"], 9 | "module": "es6", 10 | "target": "es6", 11 | "esModuleInterop": true, 12 | "allowJs": false, 13 | "strictNullChecks": true, 14 | "strictPropertyInitialization": true, 15 | "strict": true, 16 | "rootDirs": [ 17 | "src/node", 18 | "src/#{target}" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /autobahn/milo.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | import WebSocket from 'ws'; 5 | 6 | // @ts-ignore 7 | import {Platform, WS} from '../dist/milo.node'; 8 | 9 | import mergeCustomConfig from './merge-custom-config'; 10 | mergeCustomConfig(Platform); 11 | 12 | import autobahn from './runner'; 13 | import getAgent from './runner/get-agent'; 14 | 15 | const updateReport = process.env.UPDATE_REPORT === 'true'; 16 | const Class = process.env.MILO === 'true' ? WS : WebSocket 17 | 18 | autobahn(Class, { 19 | updateReport, 20 | port: 9001, 21 | agent: getAgent(), 22 | Platform, 23 | }); 24 | 25 | -------------------------------------------------------------------------------- /examples/ws-server/client-real.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from "ws"; 2 | import Platform from "../../src/Platform"; 3 | 4 | const ws = new WebSocket("ws://mpaulson.netflix.com:8080"); 5 | 6 | ws.on('message', (data) => { 7 | const json = JSON.parse(data.toString()); 8 | 9 | json.count++; 10 | ws.send(JSON.stringify(json)); 11 | }); 12 | 13 | ws.on('close', () => { 14 | Platform.log("Close"); 15 | }); 16 | 17 | ws.on('error', (e) => { 18 | Platform.error("Look its an erro, suck it hiredguns", e); 19 | }); 20 | 21 | ws.send({ count: 0 }, (err) => { 22 | Platform.log("send#ERRR", err); 23 | }); 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/HTTP2/README.md: -------------------------------------------------------------------------------- 1 | ### Goals 2 | * initialize a stream from the client and send over the data to the server. 3 | The should reply with the data + 1. thinking {count: 1} -> server -> {count: 4 | 2} 5 | 6 | * Stream and connection state. 7 | * How to make a stream / connection work with our current system. 8 | * TBD 9 | 10 | ### TODOS 11 | 12 | #### HPACK 13 | * Testing. 14 | * Encode / Decode formats 15 | * Header table format 16 | * go through [Appendix C](https://tools.ietf.org/html/rfc7541#appendix-C) and 17 | create those as tests. They are as reliable as it gets. 18 | * Header return format. Should this be an object? 19 | 20 | -------------------------------------------------------------------------------- /autobahn/runner/docker/docker-ps.ts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | 3 | // Assumes that we are in the proper location when dealing with docker. 4 | export async function getDockers() { 5 | const ps = shell.exec("docker ps"); 6 | 7 | if (ps.stderr) { 8 | throw new Error(ps.stderr); 9 | } 10 | 11 | if (!ps.stdout) { 12 | throw new Error("command: docker ps has no output. We have to be able to run ps"); 13 | } 14 | 15 | const lines = ps.stdout.split('\n').filter(x => x !== '').map(l => { 16 | return l.split(' ').filter(x => x !== ''); 17 | }); 18 | 19 | return lines.slice(1); 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /autobahn/runner/docker/config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import { autobahnDocker } from '../paths'; 5 | 6 | const fuzzyConfigPath = path.join(autobahnDocker, 'config', 'fuzzingserver.json'); 7 | const defaultCases = ["*"]; 8 | 9 | export function readyConfig() { 10 | const fuzzyConfig = JSON.parse(fs.readFileSync(fuzzyConfigPath).toString()); 11 | if (process.env.CASES) { 12 | fuzzyConfig.cases = process.env.CASES.split(",").filter(x => x !== ''); 13 | } else { 14 | fuzzyConfig.cases = defaultCases; 15 | } 16 | fs.writeFileSync(fuzzyConfigPath, JSON.stringify(fuzzyConfig, null, 4)); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /autobahn/runner/get-agent.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import { root } from './paths'; 5 | 6 | export function getVersion(): string { 7 | const packageJson = JSON.parse(fs. 8 | readFileSync(path.join(root, "package.json")).toString()); 9 | 10 | const version = packageJson.version as string; 11 | return version.replace('.', '_').replace('.', '_'); 12 | } 13 | 14 | function defaultAgent(): string { 15 | return `Milo_${getVersion()}`; 16 | } 17 | 18 | let agent = defaultAgent(); 19 | export default function getAgent() { 20 | return agent; 21 | } 22 | 23 | export function setAgent(str: string) { 24 | agent = str; 25 | } 26 | -------------------------------------------------------------------------------- /src/nrdp/DataBuffer.ts: -------------------------------------------------------------------------------- 1 | import IDataBuffer from "../IDataBuffer"; 2 | type ConcatTypes = ArrayBuffer | Uint8Array | IDataBuffer | string | number[] | number; 3 | type DataBufferConstructor = { 4 | new(bytes?: number): IDataBuffer; 5 | new(data: string, encoding?: string): IDataBuffer; 6 | new(data: ArrayBuffer | IDataBuffer | Uint8Array, offset?: number, length?: number): IDataBuffer; 7 | compare(lhs: ConcatTypes, rhs: ConcatTypes): -1 | 0 | 1; 8 | concat(args: ConcatTypes[]): IDataBuffer 9 | of(...args: ConcatTypes[]): IDataBuffer; 10 | random(size: number): IDataBuffer; 11 | } 12 | 13 | declare const DataBuffer: DataBufferConstructor; 14 | export default DataBuffer; 15 | 16 | -------------------------------------------------------------------------------- /src/ws/framer/state.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from '../../DataBuffer'; 2 | 3 | import { 4 | FramerState, 5 | WSState, 6 | WSCallback, 7 | MASK_SIZE, 8 | MAX_HEADER_SIZE, 9 | } from './types'; 10 | 11 | export function createDefaultState(isControlFrame = false): WSState { 12 | // @ts-ignore 13 | return { 14 | isFinished: false, 15 | rsv1: 0, 16 | rsv2: 0, 17 | rsv3: 0, 18 | opcode: 0, 19 | 20 | isMasked: false, 21 | currentMask: new DataBuffer(MASK_SIZE), 22 | 23 | isControlFrame, 24 | state: FramerState.ParsingHeader, 25 | 26 | // 27 | payloadLength: 0, 28 | payloadPtr: 0, 29 | payloads: [], 30 | }; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /autobahn/merge-custom-config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import { root } from './runner/paths'; 5 | import { IPlatform } from './types'; 6 | 7 | export default function mergeCustomConfig(Platform: IPlatform) { 8 | try { 9 | const contents = fs. 10 | readFileSync(path.join(root, ".milo-test-env")). 11 | toString(). 12 | split('\n'). 13 | forEach(line => { 14 | const parts = line.split('='); 15 | process.env[parts[0]] = parts[1]; 16 | }); 17 | } catch (e) { 18 | Platform.log("Unable to read a custom config, if you wish to have your own config for test runs, please create /path/to/milo/.milo-test-env"); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "src/utils/assert.macro/index.js"], 3 | "exclude": [ "**/flycheck_*.ts" ], 4 | "compilerOptions": { 5 | "moduleResolution": "node", 6 | "noImplicitAny": true, 7 | "esModuleInterop": true, 8 | "allowJs": true, 9 | "strictNullChecks": true, 10 | "strictPropertyInitialization": true, 11 | "downlevelIteration": true, 12 | "strict": true, 13 | "target": "es6", 14 | "outDir": "build", 15 | "typeRoots": [ 16 | "./node_modules/@types", 17 | "./typings" 18 | ], 19 | "rootDirs": [ 20 | "src/nrdp", 21 | "src/node", 22 | "src/#{target}" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ws/framer/types.ts: -------------------------------------------------------------------------------- 1 | import IDataBuffer from "../../IDataBuffer"; 2 | 3 | export const enum FramerState { 4 | ParsingHeader = 1, 5 | ParsingBody, 6 | }; 7 | 8 | export type WSState = { 9 | isFinished: boolean; 10 | rsv1: number; 11 | rsv2: number; 12 | rsv3: number; 13 | opcode: number; 14 | currentOpcode: number; 15 | 16 | isMasked: boolean; 17 | currentMask: IDataBuffer; 18 | 19 | isControlFrame: boolean; 20 | state: FramerState; 21 | 22 | payload: IDataBuffer; 23 | payloadLength: number; 24 | payloadPtr: number; 25 | payloads: IDataBuffer[]; 26 | }; 27 | 28 | export const MASK_SIZE = 4; 29 | export const MAX_HEADER_SIZE = 14; 30 | export type WSCallback = (buffer: IDataBuffer, state: WSState) => void; 31 | export type WSErrorCallback = (e: Error) => void; 32 | 33 | -------------------------------------------------------------------------------- /autobahn/runner/docker/index.ts: -------------------------------------------------------------------------------- 1 | import shell, { exec } from 'shelljs'; 2 | 3 | import { GlobalContext, killContext } from '../../context'; 4 | import { IPlatform } from '../../types'; 5 | import { autobahnDocker } from '../paths'; 6 | import { readyConfig } from './config'; 7 | import { killDocker } from './kill'; 8 | import { launch } from './launch'; 9 | import { stop } from './stop'; 10 | 11 | export { 12 | stop 13 | }; 14 | 15 | export async function start(Platform: IPlatform) { 16 | 17 | return new Promise(async (res, rej) => { 18 | 19 | // Kills any currently running dockers 20 | await killDocker(); 21 | 22 | // going full async.... 23 | shell.pushd(autobahnDocker); 24 | 25 | readyConfig(); 26 | await launch(Platform); 27 | 28 | shell.popd(); 29 | 30 | // ... 31 | res(); 32 | }); 33 | } 34 | 35 | -------------------------------------------------------------------------------- /autobahn/runner/docker/launch.ts: -------------------------------------------------------------------------------- 1 | import shell, {exec} from "shelljs"; 2 | 3 | import { autobahnDocker } from "../paths"; 4 | import { IPlatform } from "../../types"; 5 | import runAsync from "../../run-async"; 6 | 7 | // docker run -it --rm \ 8 | // -v "${PWD}/config:/config" \ 9 | // -v "${PWD}/reports:/reports" \ 10 | // -p 9001:9001 -p 8080:8080 \ 11 | // --name fuzzingserver \ 12 | // crossbario/autobahn-testsuite 13 | // 14 | const cmds = [ 15 | "-t", 16 | "--rm", 17 | `-v "${autobahnDocker}/config:/config"`, 18 | `-v "${autobahnDocker}/reports:/reports"`, 19 | "-p 9001:9001 -p 8080:8080", 20 | "--name fuzzingserver", 21 | "crossbario/autobahn-testsuite", 22 | ]; 23 | const dockerCmd = `docker run ${cmds.join(" ")}`; 24 | 25 | export async function launch(Platform: IPlatform) { 26 | await runAsync({ 27 | cmd: dockerCmd, 28 | Platform, 29 | doneString: "Ok, will run" 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import Platform from "../Platform"; 2 | import IDataBuffer from "../IDataBuffer"; 3 | 4 | export function headerValue(headers: string[], header: string): string { 5 | const lower = header.toLowerCase() + ": "; 6 | for (const h of headers) { 7 | if (h.toLowerCase().lastIndexOf(lower, 0) === 0) { 8 | return h.substring(lower.length); 9 | } 10 | } 11 | return ""; 12 | } 13 | 14 | export function escapeData(data: Uint8Array | ArrayBuffer | IDataBuffer | string, 15 | offset?: number, length?: number): string { 16 | if (typeof data !== "string") { 17 | data = Platform.utf8toa(data, offset || 0, length); 18 | } else if (offset && !length) { 19 | data = data.substr(offset); 20 | } else if (length) { 21 | data = data.substr(offset || 0, length); 22 | } 23 | return data.replace(/\r/g, "\\r").replace(/\n/g, "\\n\n"); 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [], 4 | "linterOptions": { 5 | "exclude": [ 6 | "**/flycheck*.ts", 7 | "**/runner.ts", 8 | "**/dist/**", 9 | "**/build/**" 10 | ] 11 | }, 12 | "rules": { 13 | "max-line-length": { 14 | "options": [120] 15 | }, 16 | "new-parens": true, 17 | "no-arg": true, 18 | "no-bitwise": false, 19 | "no-conditional-assignment": true, 20 | "no-consecutive-blank-lines": false, 21 | "no-console": { 22 | "severity": "warning", 23 | "options": ["debug", "info", "log", "time", "timeEnd", "trace"] 24 | }, 25 | "max-classes-per-file": true, 26 | "no-namespace": false 27 | }, 28 | "jsRules": { 29 | "max-line-length": { 30 | "options": [120] 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /autobahn/runner/ready.ts: -------------------------------------------------------------------------------- 1 | import { 2 | root, 3 | autobahnTS, 4 | autobahnDocker, 5 | } from "./paths"; 6 | 7 | import shell from "shelljs"; 8 | 9 | const gitSubmoduleError = `Attempted to cd to autobahn-testsuite directory and was unable to. 10 | This is likely due to not initializing git submodules. Please execute: 11 | 12 | git submodule update --init --recursive 13 | 14 | `; 15 | 16 | const dockerError = `Failed executing: 17 | 18 | which docker 19 | 20 | Docker does not appear to be in your $PATH or not installed on your system. 21 | Please install docker and retry.`; 22 | 23 | export async function systemReq() { 24 | 25 | const res = shell.cd(autobahnDocker); 26 | 27 | if (res.stderr.length) { 28 | throw new Error(gitSubmoduleError); 29 | } 30 | 31 | const whichRes = shell.which("docker"); 32 | if (whichRes.stderr.length) { 33 | throw new Error(dockerError); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/HTTP2/frame/types.ts: -------------------------------------------------------------------------------- 1 | // Don't know what this is? 2 | export const enum FrameType { 3 | HEADERS = 0x1, 4 | PRIORITY = 0x2, 5 | RST_STREAM = 0x3, 6 | SETTINGS = 0x4, 7 | PUSH_PROMISE = 0x5, 8 | PING = 0x6, 9 | GOAWAY = 0x7, 10 | WINDOW_UPDATE = 0x8, 11 | CONTINUATION = 0x9, 12 | }; 13 | 14 | export const enum Settings { 15 | HEADER_TABLE_SIZE = 0x1, 16 | ENABLE_PUSH = 0x2, 17 | MAX_CONCURRENT_STREAMS = 0x3, 18 | INITIAL_WINDOW_SIZE = 0x4, 19 | MAX_FRAME_SIZE = 0x5, 20 | MAX_HEADER_LIST_SIZE = 0x6, 21 | } 22 | 23 | export const enum Flag { 24 | ACK = 0x1, 25 | END_STREAM = 0x1, 26 | PRIORITY = 0x2, 27 | PADDED = 0x8, 28 | }; 29 | 30 | export const SettingsDefault = [ 31 | 0, 32 | 4096, // HEADER_TABLE_SIZE 33 | 1, // PUSH 34 | 2147483647, // MAX_CONCURRENT 35 | 65535, // WINDOW_SIZE 36 | 16384, // FRAME_SIZE 37 | 2147483647 // MAX_HEADER_LIST 38 | ]; 39 | 40 | -------------------------------------------------------------------------------- /autobahn/runner/sys-requirements.ts: -------------------------------------------------------------------------------- 1 | import { 2 | root, 3 | autobahnTS, 4 | autobahnDocker, 5 | } from "./paths"; 6 | 7 | import shell from "shelljs"; 8 | 9 | const gitSubmoduleError = `Attempted to cd to autobahn-testsuite directory and was unable to. 10 | This is likely due to not initializing git submodules. Please execute: 11 | 12 | git submodule update --init --recursive 13 | 14 | `; 15 | 16 | const dockerError = `Failed executing: 17 | 18 | which docker 19 | 20 | Docker does not appear to be in your $PATH or not installed on your system. 21 | Please install docker and retry.`; 22 | 23 | export async function systemReq() { 24 | 25 | const res = shell.cd(autobahnDocker); 26 | 27 | if (res.stderr && res.stderr.length) { 28 | throw new Error(gitSubmoduleError); 29 | } 30 | 31 | const whichRes = shell.which("docker"); 32 | if (whichRes.stderr && whichRes.stderr.length) { 33 | throw new Error(dockerError); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /autobahn/runner/nrdp/run-nrdp.ts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | 3 | // @ts-ignore 4 | import { IPlatform } from '../types'; 5 | import { root } from '../paths'; 6 | import runAsync from '../../run-async'; 7 | 8 | export default async function runNRDP(Platform: IPlatform): Promise { 9 | const cmd = [ 10 | process.env.NRDP, 11 | `-U "file://${root}/dist/nrdp.autobahn.js"`, 12 | ].join(' '); 13 | 14 | shell.pushd(root); 15 | let casesRan = -1; 16 | await runAsync({ 17 | Platform, 18 | onData: (lines: string[]) => { 19 | lines.forEach(l => { 20 | if (~l.indexOf("Will run")) { 21 | const cases = +l.split(' ')[2]; 22 | 23 | if (!isNaN(cases)) { 24 | casesRan = cases; 25 | } 26 | } 27 | }); 28 | }, 29 | cmd, 30 | sync: true, 31 | ignoreOnErr: true, 32 | doneString: "NRDP Test Finished", 33 | }); 34 | shell.popd(root); 35 | 36 | return casesRan; 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /autobahn/start.ts: -------------------------------------------------------------------------------- 1 | import death from 'death'; 2 | 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | // Does not seem to be able to get platform from the dist. 7 | // @ts-ignore 8 | import { Platform } from '../dist/milo.node.js'; 9 | 10 | import mergeCustomConfig from './merge-custom-config'; 11 | mergeCustomConfig(Platform); 12 | 13 | import { systemReq } from './runner/sys-requirements'; 14 | import { killDocker } from './runner/docker/kill'; 15 | import { readyConfig } from './runner/docker/config'; 16 | import { launch } from './runner/docker/launch'; 17 | import { killContext, GlobalContext } from './context'; 18 | 19 | death(async () => { 20 | await killDocker(); 21 | killContext(GlobalContext); 22 | process.exit(); 23 | }); 24 | 25 | export default async function run() { 26 | await systemReq(); 27 | await killDocker(); 28 | await readyConfig() 29 | await launch(Platform); 30 | }; 31 | 32 | async function runner() { 33 | await run(); 34 | killContext(GlobalContext); 35 | } 36 | 37 | if (require.main === module) { 38 | runner(); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /autobahn/runner/nrdp/entry.ts: -------------------------------------------------------------------------------- 1 | import IDataBuffer from "../../../src/IDataBuffer"; 2 | import IRequestData from "../../../src/IRequestData"; 3 | import Request from "../../../src/Request"; 4 | import RequestResponse from "../../../src/RequestResponse"; 5 | import Platform from "../../../src/Platform"; 6 | import WS from "../../../src/ws"; 7 | import { runAutobahnTests, AutobahnOpts } from '../run-test'; 8 | import getAgent from './agent'; 9 | 10 | async function run() { 11 | Platform.log("NRDP Test Started"); 12 | const context = {}; 13 | 14 | let errCode = 0; 15 | try { 16 | Platform.error("NRDP ABout to run tests."); 17 | await runAutobahnTests(WS, { 18 | Platform, 19 | agent: getAgent(), 20 | }); 21 | Platform.error("NRDP Finish Tests"); 22 | } catch (e) { 23 | Platform.error("NRDPs autobahn tests have failed:"); 24 | Platform.error(e); 25 | errCode = 1; 26 | } 27 | 28 | Platform.log("NRDP Test Finished"); 29 | nrdp.exit(errCode); 30 | } 31 | 32 | // @ts-ignore 33 | nrdp.gibbon.init(run); 34 | 35 | -------------------------------------------------------------------------------- /tests/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIC8zCCAdsCAQAwgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh 3 | MRIwEAYDVQQHDAlMb3MgR2F0b3MxEDAOBgNVBAoMB05ldGZsaXgxDDAKBgNVBAsM 4 | A1BQRDEZMBcGA1UEAwwQbWlsby5uZXRmbGl4LmNvbTEiMCAGCSqGSIb3DQEJARYT 5 | YWJha2tlbkBuZXRmbGl4LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 6 | ggEBAMWtv8miuIe3vPNEOLsYRQqMy7hMgj71PINnCuCSkA+hRqeVh3JFzGAOoHUc 7 | UP7abIdcRHbe0Kos+849lrstEAErA8TNV7sJJw6aEnPNHknWFCC1qItgfRq4KZAp 8 | pWnjJjBjQMpDXS6k+8CPHwCZwiV5QoEIbXPSPqYd547Qe0RkKp3tlyBRR/eFUREp 9 | pXxk4EuDzTSQYd7L/3I72sResKco87y1r2D2WZ4jhHszYDZagA8VO8ueczR2ytyf 10 | IHjIsUsUSWxIIJjuasgcadcEPe7+MMMZQj2Ab1/nCueeQ2nEgz3bg5XhUd6VU54A 11 | Fiv7/0oOseTCXVacbKTh2j482oUCAwEAAaAYMBYGCSqGSIb3DQEJAjEJDAdOZXRm 12 | bGl4MA0GCSqGSIb3DQEBCwUAA4IBAQAgVzj0coWSOO41Rwb3ph6kaXsAVXzR76YQ 13 | 9YMej/j2vDd8dzoqxvru1JO0VvA+1k5qwS1FqRhM6bTW1BZnGRl549LsaPR6vU3m 14 | qMrdtIItSSAWURQCyblEuJEXRkINjJ2k5fDi/eWqa75KkphQIg5Fmbaf4gBMfVUV 15 | HsOMYwuMTzkeNeTgKHpW5K8sknpTYHDB+sajvCoYCM20Z2FZQk4rsWZ/NUO6DphE 16 | ZrCmD+7LWVzkVIRwxGM8DxyDaGT0daFFXwFNT2rcoOrWrfmNS6fIguG/oLDBAM8l 17 | ONPXySVLIcJ2upVYym7Pw7YzxPTMkxzsopZF18pnB4WQcGUB/oQK 18 | -----END CERTIFICATE REQUEST----- 19 | -------------------------------------------------------------------------------- /bin/builds/rollup.node.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import target from "./rollup-target-plugin"; 4 | import babel from "rollup-plugin-babel"; 5 | 6 | const SOURCE_DIR = 'build/node'; 7 | const OUTPUT_DIR = 'dist'; 8 | 9 | export default { 10 | input: `${SOURCE_DIR}/milo.js`, 11 | output: { 12 | file: `${OUTPUT_DIR}/milo.node.js`, 13 | format: 'cjs', 14 | exports: "named" 15 | }, plugins: [ 16 | target({ 17 | target: 'node' 18 | }), 19 | babel({ 20 | exclude: "node_modules/**", 21 | babelrc: false, 22 | presets: [ 23 | [ 24 | '@babel/preset-env', 25 | { 26 | targets: { 27 | node: 'current' 28 | } 29 | } 30 | ] 31 | ], 32 | plugins: [ 33 | 'babel-plugin-macros', 34 | ] 35 | }), 36 | resolve({ 37 | preferBuiltins: true 38 | }), 39 | commonjs() 40 | ], 41 | external: [ 'fs', 'net', 'dns' ] 42 | }; 43 | -------------------------------------------------------------------------------- /examples/ws-server/client.node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WS, 3 | WSState, 4 | _wsUpgrade, 5 | // @ts-ignore 6 | } from '../../dist/milo.node.js'; 7 | 8 | import Platform from "../../src/Platform"; 9 | 10 | async function run() { 11 | let dataCount = 0; 12 | const then = Date.now(); 13 | let bytesReceived = 0; 14 | 15 | const byteLength = 4096 * 16; 16 | const dataFetchCount = 10000; 17 | 18 | const buf = new ArrayBuffer(byteLength); 19 | const networkPipe = await _wsUpgrade({ 20 | url: "ws://localhost:1337/", 21 | }); 22 | const ws = new WS(networkPipe); 23 | 24 | ws.onmessage = (buffer: Uint8Array) => { 25 | const bytesRead = buffer.byteLength; 26 | 27 | bytesReceived += bytesRead; 28 | 29 | if (++dataCount === dataFetchCount) { 30 | const now = Date.now(); 31 | Platform.log("Total Bytes Received:", bytesReceived); 32 | Platform.log("Time Spent:", now - then); 33 | Platform.log("Mbps:", (bytesReceived / (now - then)) * 1000); 34 | return; 35 | } 36 | else if (dataCount < dataFetchCount) { 37 | ws.send("send"); 38 | } 39 | }; 40 | 41 | ws.onClose(() => { 42 | Platform.log("close"); 43 | }); 44 | 45 | ws.send("send"); 46 | } 47 | 48 | run(); 49 | -------------------------------------------------------------------------------- /src/HTTP2/frame/__tests__/settings-frame.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FrameType, 3 | Settings, 4 | } from '../types'; 5 | 6 | import SettingsWriter from "../settings-frame"; 7 | import { FRAME_HEADER_SIZE } from "../utils"; 8 | 9 | function toArray(...args: number[]) { 10 | return args; 11 | } 12 | 13 | /* 14 | +-------------------------------+ 15 | | Identifier (16) | 16 | +-------------------------------+-------------------------------+ 17 | | Value (32) | 18 | +---------------------------------------------------------------+ 19 | */ 20 | describe('Settings', () => { 21 | it('should set all the settings.', () => { 22 | const settings = new SettingsWriter(); 23 | 24 | settings.addSetting(Settings.MAX_CONCURRENT_STREAMS, 4); 25 | settings.addSetting(Settings.MAX_FRAME_SIZE, 2 ** 10 * 16); 26 | 27 | const frame = settings.toFrameWriter(5); 28 | const contents = frame.buffer.slice(FRAME_HEADER_SIZE); 29 | const view = new DataView(contents.buffer, contents.byteOffset, contents.byteLength); 30 | 31 | expect(view.getUint16(0)).toEqual(Settings.MAX_CONCURRENT_STREAMS); 32 | expect(view.getUint32(2)).toEqual(4); 33 | expect(view.getUint16(6)).toEqual(Settings.MAX_FRAME_SIZE); 34 | expect(view.getUint32(8)).toEqual(2 ** 10 * 16); 35 | }); 36 | }); 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Pool.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from './DataBuffer'; 2 | import IDataBuffer from "./IDataBuffer"; 3 | 4 | export type PoolItem = { free: () => void, item: T }; 5 | export type PoolFreeFunction = (item: PoolItem) => void; 6 | export type PoolFactory = (freeFn: PoolFreeFunction) => PoolItem; 7 | 8 | export class Pool { 9 | private factory: PoolFactory; 10 | private pool: PoolItem[]; 11 | private boundFree: PoolFreeFunction; 12 | 13 | constructor(factory: PoolFactory) { 14 | this.factory = factory; 15 | this.pool = []; 16 | this.boundFree = this.free.bind(this); 17 | } 18 | 19 | get(): PoolItem { 20 | if (this.pool.length === 0) { 21 | this.pool.push(this.factory(this.boundFree)); 22 | } 23 | 24 | return this.pool.pop() as PoolItem; 25 | } 26 | 27 | private free(item: PoolItem) { 28 | this.pool.push(item); 29 | } 30 | }; 31 | 32 | export function createDataBufferPool(size: number) { 33 | function factory(freeFn: PoolFreeFunction): PoolItem { 34 | const buf = new DataBuffer(size); 35 | const poolItem = { 36 | item: buf, 37 | free: () => { 38 | freeFn(poolItem); 39 | } 40 | }; 41 | 42 | return poolItem; 43 | } 44 | return new Pool(factory); 45 | } 46 | -------------------------------------------------------------------------------- /tests/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDszCCApsCFAC1/iozpprODlFvt6TcOzUnuPh4MA0GCSqGSIb3DQEBCwUAMIGV 3 | MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJTG9z 4 | IEdhdG9zMRAwDgYDVQQKDAdOZXRmbGl4MQwwCgYDVQQLDANQUEQxGTAXBgNVBAMM 5 | EG1pbG8ubmV0ZmxpeC5jb20xIjAgBgkqhkiG9w0BCQEWE2FiYWtrZW5AbmV0Zmxp 6 | eC5jb20wHhcNMjAwNDI3MTgzNDA5WhcNNDcwOTEyMTgzNDA5WjCBlTELMAkGA1UE 7 | BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCUxvcyBHYXRvczEQ 8 | MA4GA1UECgwHTmV0ZmxpeDEMMAoGA1UECwwDUFBEMRkwFwYDVQQDDBBtaWxvLm5l 9 | dGZsaXguY29tMSIwIAYJKoZIhvcNAQkBFhNhYmFra2VuQG5ldGZsaXguY29tMIIB 10 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxa2/yaK4h7e880Q4uxhFCozL 11 | uEyCPvU8g2cK4JKQD6FGp5WHckXMYA6gdRxQ/tpsh1xEdt7Qqiz7zj2Wuy0QASsD 12 | xM1XuwknDpoSc80eSdYUILWoi2B9GrgpkCmlaeMmMGNAykNdLqT7wI8fAJnCJXlC 13 | gQhtc9I+ph3njtB7RGQqne2XIFFH94VRESmlfGTgS4PNNJBh3sv/cjvaxF6wpyjz 14 | vLWvYPZZniOEezNgNlqADxU7y55zNHbK3J8geMixSxRJbEggmO5qyBxp1wQ97v4w 15 | wxlCPYBvX+cK555DacSDPduDleFR3pVTngAWK/v/Sg6x5MJdVpxspOHaPjzahQID 16 | AQABMA0GCSqGSIb3DQEBCwUAA4IBAQAZ8gACulI+PSVLAoYuPDFpl84Pe4pDFsWr 17 | OOPA0BfPziXeXxerG5pZp2o05Zg0OMM7wiGBenfpzMNN9hXalvItL3lZfyhsZr/u 18 | x+L81HbFhVzuSlniDHBWLLktZK8E4dRhzC1Bohh2DbU06r0LRqtJlWijJ5vrxQRG 19 | FvpKsZrUcjXT1BumIQ6Y+YFZmcLzSvNf7YXhoAM4CA5zWmtOA7sBVPKcDXzTeMeI 20 | k0AoDwdrOd9k1oDtjMsSNyA1r3MP1CZGtCsf4qLNG3L8KGAYe6UfabtVVOMYD7nP 21 | kaQV2id1IZC67J98L/K0KU9uvRRE1R1SdMQee7v57BTzdzkOtX8u 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /src/IRequestData.ts: -------------------------------------------------------------------------------- 1 | import { DnsType, HTTPMethod, IpConnectivityMode } from "./types"; 2 | import IOnHeadersData from "./IOnHeadersData"; 3 | import IRequestTimeouts from "./IRequestTimeouts"; 4 | 5 | export default interface IRequestData { 6 | async?: boolean; 7 | baseUrl?: string; 8 | body?: string | ArrayBuffer | Uint8Array; 9 | cache?: string; 10 | debugThroughput?: boolean; 11 | dependsOn?: string | ArrayBuffer | Uint8Array | number; 12 | dnsChannel?: string; 13 | dnsName?: string; 14 | dnsTime?: number; 15 | dnsType?: DnsType; 16 | exclusiveDepends?: boolean; 17 | forbidReuse?: boolean; 18 | format?: "xml" | "json" | "jsonstream" | "arraybuffer" | "uint8array" | "databuffer" | "none"; 19 | freshConnect?: boolean; 20 | headers?: { [key: string]: string }; 21 | http2?: boolean; 22 | ipAddresses?: string[]; 23 | ipConnectivityMode?: IpConnectivityMode; 24 | maxRecvSpeed?: number; 25 | maxSendSpeed?: number; 26 | method?: HTTPMethod; 27 | milo?: boolean; 28 | networkMetricsPrecision?: "us" | "ms" | "none"; 29 | noProxy?: boolean; 30 | onChunk?: (chunk: ArrayBuffer) => void; 31 | onData?: (data: ArrayBuffer) => void; 32 | onHeaders?: (data: IOnHeadersData) => void; 33 | pipeWait?: boolean; 34 | receiveBufferSize?: number; 35 | secure?: boolean; 36 | tcpNoDelay?: boolean; 37 | timeouts?: IRequestTimeouts; 38 | tlsv13?: boolean; 39 | url: string; 40 | weight?: number; 41 | }; 42 | -------------------------------------------------------------------------------- /src/node/huffman/decode.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from "../DataBuffer"; 2 | import IDataBuffer from "../../IDataBuffer"; 3 | import { StaticTreeNode, staticTree, pluckBit } from "./static"; 4 | 5 | const scratchBuffer = Buffer.alloc(50000); 6 | 7 | export default function decode(buf: DataBuffer, offset: number = 0, length?: number): IDataBuffer { 8 | const buffer: Buffer = DataBuffer.toBuffer(buf.subarray(offset, length)); 9 | 10 | let ptr = 0; 11 | let curr: StaticTreeNode = staticTree; 12 | let onlyOnes = 0x1; 13 | 14 | for (let i = 0; i < buffer.byteLength; ++i) { 15 | const value = buffer[i]; 16 | let bitPtr = 8; 17 | 18 | do { 19 | const idx = pluckBit(value, --bitPtr); 20 | const nextCurr = curr[idx]; 21 | 22 | if (typeof nextCurr === 'number') { 23 | scratchBuffer[ptr++] = nextCurr; 24 | curr = staticTree; 25 | onlyOnes = 0x1; 26 | } 27 | else if (nextCurr === undefined) { 28 | throw new Error("When decoding, you hit undefined, you hit connection error."); 29 | } 30 | else { 31 | onlyOnes = 0x1 & idx; 32 | curr = nextCurr; 33 | } 34 | 35 | } while (bitPtr); 36 | } 37 | 38 | if (onlyOnes === 0) { 39 | throw new Error("Decoding Problem. The line ended with a 0 in it instead of a 1"); 40 | } 41 | 42 | const outBuf = Buffer.alloc(ptr); 43 | scratchBuffer.copy(outBuf, 0, 0, ptr); 44 | 45 | return DataBuffer.fromBuffer(outBuf); 46 | }; 47 | 48 | -------------------------------------------------------------------------------- /src/ws/__tests__/mask.ts: -------------------------------------------------------------------------------- 1 | import maskFn from "../mask"; 2 | import DataBuffer from "../../DataBuffer"; 3 | import IDataBuffer from "../../IDataBuffer"; 4 | 5 | // FOR YOU JELMEGA 6 | // LittleEndian is going to be BBAABBAA 7 | // therefore, we write it in BigE. 8 | // 171IQ 9 | const mask = 0xAA_BB_AA_BB; 10 | const maskBuf = new DataBuffer(4); 11 | 12 | maskBuf.setUInt32BE(0, mask); 13 | 14 | // 0b1011 15 | // 0b0100 16 | // == 4 17 | const unmaskedArr = [0xFF, 0x00, 0x00, 0xFF, 0x0F, 0xF0, 0x0F, 0xF0]; 18 | // 0xAA 0xBB 0xAA 0xBB 0xAA 0xBB 0xAA 0xBB 19 | const maskedArr = [0x55, 0xBB, 0xAA, 0x44, 0xA5, 0x4B, 0xA5, 0x4B]; 20 | 21 | const arr = Uint8Array.from(unmaskedArr); 22 | 23 | function isLittleEndian() { 24 | const arrayBuffer = new ArrayBuffer(2); 25 | const uint8Array = new Uint8Array(arrayBuffer); 26 | const uint16array = new Uint16Array(arrayBuffer); 27 | 28 | uint8Array[0] = 0xAA; // set first byte 29 | uint8Array[1] = 0xBB; // set second byte 30 | 31 | return uint16array[0] === 0xBBAA; 32 | } 33 | 34 | function checkBuf(buf: IDataBuffer, a: number[], offset: number = 0) { 35 | for (let i = 0; i < a.length; ++i) { 36 | expect(buf.getUInt8(offset + i)).toEqual(a[i]); 37 | } 38 | } 39 | 40 | describe("WS", () => { 41 | it("should mask properly", () => { 42 | const b = new DataBuffer(1000); 43 | b.set(0, arr); 44 | 45 | maskFn(b, 0, arr.byteLength, maskBuf); 46 | checkBuf(b, maskedArr); 47 | 48 | maskFn(b, 0, arr.byteLength, maskBuf); 49 | checkBuf(b, unmaskedArr); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/node/huffman/__tests__/encode.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from "../../DataBuffer"; 2 | import encode from "../encode"; 3 | 4 | describe.only("encode", () => { 5 | it("encode simple 1", () => { 6 | const one = "1"; 7 | const expected = Buffer.from([ 8 | 0b0000_1111, 9 | ]); 10 | 11 | expect(encode(one)).toEqual(expected); 12 | }); 13 | 14 | it("encode 1337", () => { 15 | /* 16 | '1' ( 49) |00001 1 [ 5] 17 | '3' ( 51) |011001 19 [ 6] 18 | '7' ( 55) |011101 1d [ 6] 19 | */ 20 | const one337 = "1337"; 21 | const expected = Buffer.from([ 22 | 0b0000_1011, 23 | 0b0010_1100, 24 | 0b1011_1011, 25 | ]); 26 | 27 | expect(encode(one337)).toEqual(expected); 28 | }); 29 | 30 | it("encode https://www.example.com, rfc ", () => { 31 | // https://www.example.com 32 | // 9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3 33 | const d = Buffer.from([ 34 | 0x9d, 35 | 0x29, 36 | 0xad, 37 | 0x17, 38 | 0x18, 39 | 0x63, 40 | 0xc7, 41 | 0x8f, 42 | 0x0b, 43 | 0x97, 44 | 0xc8, 45 | 0xe9, 46 | 0xae, 47 | 0x82, 48 | 0xae, 49 | 0x43, 50 | 0xd3, 51 | ]); 52 | 53 | expect(encode("https://www.example.com")).toEqual(d); 54 | }); 55 | }); 56 | 57 | -------------------------------------------------------------------------------- /src/HTTP2/__Disabled_tests__/integration.ts: -------------------------------------------------------------------------------- 1 | import Request from "../../Request"; 2 | import StreamManager, { SMState } from "../stream/stream-manager"; 3 | import http2 from "http2"; 4 | import { createRawConnection } from "../index"; 5 | 6 | describe("HTTP2 integration test", () => { 7 | 8 | it('should setup a http2 server and create a connection to it.', async (done) => { 9 | const server = http2.createServer(); 10 | 11 | server.on('error', (err) => console.error(err)); 12 | server.on('stream', (stream, headers) => { 13 | // stream is a Duplex 14 | stream.respond({ 15 | 'content-type': 'text/html', 16 | ':status': 200 17 | }); 18 | stream.end('

Hello World

'); 19 | }); 20 | 21 | server.listen(8000); 22 | 23 | // create the http2 upgrade request 24 | const pipe = await createRawConnection('http://localhost:8000'); 25 | const streamManager = new StreamManager(pipe); 26 | 27 | let count = 0; 28 | const expects = [ 29 | SMState.WAITING_ON_SETTINGS_ACK, 30 | SMState.OPEN, 31 | SMState.CLOSED, 32 | ]; 33 | 34 | streamManager.onStateChange(newState => { 35 | expect(newState).toEqual(expects[count++]); 36 | 37 | if (streamManager.isInitialized()) { 38 | streamManager.close(); 39 | } 40 | 41 | if (newState === SMState.CLOSED) { 42 | server.close(() => { 43 | done(); 44 | }); 45 | } 46 | }); 47 | }); 48 | }); 49 | 50 | -------------------------------------------------------------------------------- /autobahn/run-async.ts: -------------------------------------------------------------------------------- 1 | import child_process from "child_process"; 2 | import { GlobalContext } from "./context"; 3 | import { IPlatform } from "./types"; 4 | 5 | export type AsyncRunOpts = { 6 | cmd: string; 7 | sync?: boolean; 8 | doneString: string; 9 | errorString?: string[]; 10 | ignoreOnErr?: boolean; 11 | Platform: IPlatform; 12 | onData?: (line: string[]) => void; 13 | } 14 | 15 | export default async function runAsync(opts: AsyncRunOpts) { 16 | return new Promise((res, rej) => { 17 | const e = child_process.exec(opts.cmd); 18 | 19 | GlobalContext.runners.push(e); 20 | 21 | if (!e.stderr || !e.stdout) { 22 | rej("Unable to execute the async command"); 23 | return; 24 | } 25 | 26 | function onOut(data: string) { 27 | const lines = data.split('\n'); 28 | if (opts.onData) { 29 | opts.onData(lines); 30 | } 31 | 32 | lines.forEach((l: string) => { 33 | opts.Platform.log(l); 34 | if (~l.indexOf(opts.doneString)) { 35 | res(); 36 | } 37 | 38 | if (opts.errorString) { 39 | if (opts.errorString.some(x => ~l.indexOf(x))) { 40 | rej(data); 41 | } 42 | } 43 | }); 44 | } 45 | 46 | function onErr(data: string) { 47 | if (opts.ignoreOnErr) { 48 | return; 49 | } 50 | rej(data); 51 | } 52 | 53 | e.stdout.on("data", onOut); 54 | e.stderr.on("data", onErr); 55 | }); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/HTTP2/frame/header.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FrameType, 3 | Flag 4 | } from './types'; 5 | 6 | import * as FrameUtils from "./utils"; 7 | import FrameWriter from "./frame-writer"; 8 | 9 | /* 10 | +---------------+ 11 | |Pad Length? (8)| 12 | +-+-------------+-----------------------------------------------+ 13 | |E| Stream Dependency? (31) | 14 | +-+-------------+-----------------------------------------------+ 15 | | Weight? (8) | 16 | +-+-------------+-----------------------------------------------+ 17 | | Header Block Fragment (*) ... 18 | +---------------------------------------------------------------+ 19 | | Padding (*) ... 20 | +---------------------------------------------------------------+ 21 | */ 22 | export function writeHeaderData( 23 | frame: FrameWriter, data: Uint8Array, 24 | streamDependency?: number, isExclusive?: boolean, weight?: number) { 25 | 26 | if (streamDependency !== undefined) { 27 | frame.addFlag(Flag.PRIORITY); 28 | 29 | const sD = streamDependency | (isExclusive ? 1 << 32 : 0); 30 | frame.write32(sD); 31 | frame.write8(weight || 0); 32 | } 33 | 34 | frame.write(data); 35 | } 36 | 37 | // TODO: will we know a head of time the buffer size? 38 | export function createHeaderFrame( 39 | bytesToSend: number, flags: number, 40 | streamIdentifier: number, padding: number = 0): FrameWriter { 41 | 42 | const frameWriter = new FrameWriter(bytesToSend, streamIdentifier, flags); 43 | frameWriter.addPadding(padding); 44 | 45 | return frameWriter; 46 | } 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/ws-server/server.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from "ws"; 2 | import Platform from "../../src/Platform"; 3 | 4 | 5 | Platform.log("We are about to server.", 1337); 6 | const wss = new WebSocket.Server({ 7 | port: 1337 8 | }); 9 | 10 | const reallyLargeBuffer = Buffer.alloc(1024 * 1024); 11 | for (let i = 0; i < reallyLargeBuffer.byteLength; ++i) { 12 | reallyLargeBuffer.writeUInt8(i % 256, i); 13 | } 14 | 15 | const smallBuf = Buffer.alloc(1024); 16 | for (let i = 0; i < smallBuf.byteLength; ++i) { 17 | smallBuf.writeUInt8(i % 256, i); 18 | } 19 | 20 | function getBigAssBufferSlice() { 21 | const mid = Math.floor(reallyLargeBuffer.byteLength / 2); 22 | const low = Math.floor(Math.random() * mid); 23 | const high = mid + Math.floor(Math.random() * mid); 24 | 25 | return reallyLargeBuffer.slice(low, high); 26 | } 27 | 28 | Platform.log("We are about to connection."); 29 | wss.on('connection', (websocket: WebSocket) => { 30 | Platform.log("OHHHH MY WE ARE CONNECTED."); 31 | 32 | // let timerId: number = 0; 33 | function sendData() { 34 | // @ts-ignore 35 | const buffer = getBigAssBufferSlice(); 36 | websocket.send(buffer); // smallBuf); 37 | } 38 | 39 | function stopData() { 40 | // @ts-ignore 41 | // clearTimeout(timerId); 42 | } 43 | 44 | 45 | // 50 Mbps 46 | websocket.on('message', (data) => { 47 | const str = data.toString(); 48 | switch (str) { 49 | case "send": 50 | sendData(); 51 | break; 52 | case "stop": 53 | stopData(); 54 | break; 55 | } 56 | }); 57 | }); 58 | 59 | Platform.log(Object.keys(wss)); 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/ws/types.ts: -------------------------------------------------------------------------------- 1 | export const enum Opcodes { 2 | ContinuationFrame = 0x0, // denotes a continuation frame 3 | TextFrame = 0x1, // denotes a text frame 4 | BinaryFrame = 0x2, // denotes a binary frame 5 | CloseConnection = 0x8, // denotes a connection close 6 | Ping = 0x9, // denotes a ping 7 | Pong = 0xA, // denotes a pong 8 | }; 9 | 10 | export function stringifyOpcode(code: Opcodes): string { 11 | switch (code) { 12 | case Opcodes.ContinuationFrame: 13 | return "ContinuationFrame"; 14 | case Opcodes.TextFrame: 15 | return "TextFrame"; 16 | case Opcodes.BinaryFrame: 17 | return "BinaryFrame"; 18 | case Opcodes.CloseConnection: 19 | return "CloseConnection"; 20 | case Opcodes.Ping: 21 | return "Ping"; 22 | case Opcodes.Pong: 23 | return "Pong"; 24 | default: 25 | return "Unknown"; 26 | } 27 | } 28 | 29 | export function isValidOpcode(code: Opcodes): boolean { 30 | let valid = false; 31 | switch (code) { 32 | case Opcodes.ContinuationFrame: 33 | case Opcodes.TextFrame: 34 | case Opcodes.BinaryFrame: 35 | case Opcodes.CloseConnection: 36 | case Opcodes.Ping: 37 | case Opcodes.Pong: 38 | valid = true; 39 | } 40 | 41 | return valid; 42 | } 43 | 44 | // TODO: Fill in values 45 | export const enum CloseValues { 46 | Shutdown = 1000, 47 | GoAway = 1002, 48 | NoStatusCode = 1005, 49 | }; 50 | 51 | export type WSOptions = { 52 | maxFrameSize: number; 53 | eventWrapper: boolean; 54 | }; 55 | 56 | export type UrlObject = { 57 | host: string, 58 | port: string | number 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /tests/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAxa2/yaK4h7e880Q4uxhFCozLuEyCPvU8g2cK4JKQD6FGp5WH 3 | ckXMYA6gdRxQ/tpsh1xEdt7Qqiz7zj2Wuy0QASsDxM1XuwknDpoSc80eSdYUILWo 4 | i2B9GrgpkCmlaeMmMGNAykNdLqT7wI8fAJnCJXlCgQhtc9I+ph3njtB7RGQqne2X 5 | IFFH94VRESmlfGTgS4PNNJBh3sv/cjvaxF6wpyjzvLWvYPZZniOEezNgNlqADxU7 6 | y55zNHbK3J8geMixSxRJbEggmO5qyBxp1wQ97v4wwxlCPYBvX+cK555DacSDPduD 7 | leFR3pVTngAWK/v/Sg6x5MJdVpxspOHaPjzahQIDAQABAoIBAAraRSM+yb65uafp 8 | YOv0oyW3ISAXIzdto0rbh19Y7IvPIN/md68wYc4MP4bubQvw1fCAgkm8ZdxR5+kk 9 | vHe+O5i00domI+Di2cAVeVneMwF+vVAmaQBt5bBn1/BQOQKdM+WILXBtTlpXOHMT 10 | 6HHR/zSreZk36zpmmoXoMmRG0d2pQWexmg7jJiMozfCQjsjxpClFiKZPl5zOsNMI 11 | UWWmxrpyvQVkD+RYbdZd6f0hUf6gOjMHqMZxfgNyQDiG83mUl45GatcMnwVFY71z 12 | 4fk64xTEPzIuSvp1OHX3zqgnwd8qjS1PlwvLb8k20/lE+cDLQvxHN3ZZDN4U7ikS 13 | mvkfvcECgYEA7l9gbgXAwPPDg94aXpvkMRkwY16VBAWmD6WZlTPcRDpAUWpOMsvZ 14 | QJmQFuI6hTQgd4zd0l66BB7yV3Hi/NefZYfmkJd8t+kkzTYOw8revI9sSMISydOF 15 | ldeoKjB1SMooQxHlirn90Xbc90kHZ0OpEOmvzfQY+JwTcnwIz5Fkho0CgYEA1Ev/ 16 | lqQVgZXVXGZAs6Fu0W0eMsa9C0j5tQxyJLHwHF1vks1GhhZxU7tO9LWfseMwcLnd 17 | xqAaDIt3YgA1zn7cqDJRXDslw+jSOVSbIw0U1X9+RXcr9Jbu1MBvgBh29UrpNhP/ 18 | 6GVRPuZ3h3E4ydGgN2jk9OepBbuwF8INQ1SRQdkCgYA6Y2hmashVOyB4C45gAEV7 19 | 9VSR8pgDvTJ5ALJHBsX8fFxG1QhEjwQvO648vLti52rQfdPT9NoRqlboLoBQOKzV 20 | nN1QK5skHVqjXYtuUCIRA/ydMdSSVVqSYpnRg78mzkXgb+DPA1H13eywMdY7aCpg 21 | dy1WAGWhd/GXiGRMG6wI/QKBgQCptmOJYnzKziP1rMyWjTwQ8YFNqDCiH+F3SY7K 22 | JPFFS7CRE8cIFFLeVAPHfpY+V9d2li9jxMNuOePjFTXucN4nG6qM57/GAI5mRGuM 23 | tr1r+3LPR3h9HFeq/ndkWKpoZ01N7OkMOEqd1RHC1dFHDWxeP12hJrypF6SrKXb+ 24 | 11ET6QKBgDTEhBmIkRixlsXx6Dyx6Z5mFPdUbLHyWXRBChUKJgs+eiOvR4G+65Qy 25 | 3Sn0+TMDIO1amO5tncwaaXP1p84babyhMq7lvldAfyzR1vB2ILeiDnoPZR0zcF1q 26 | O09fsk9I1A/UeYdgISkWEHMH1YbBtap8MxNdSOnO/0OwHfjDX4BV 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/TODO.md: -------------------------------------------------------------------------------- 1 | ### HTTP2 2 | * Basic frame validation process and ignores. 3 | * https://tools.ietf.org/html/rfc7540#section-4.1 4 | * ignore flags/types/reserved bit issues. 5 | 6 | * Header compression and decrompression 7 | * https://tools.ietf.org/html/rfc7540#section-4.3 8 | 9 | ### WebSocket 10 | * Figure out why NRDP is so slow 11 | * Set up autobahn test suite and make it pass 12 | * Set up autotest in the cloud to check performance under more realistic circumstances 13 | * Get wss working 14 | 15 | ### Request 16 | 17 | * Don't close TCP socket on SSL_ERROR_ZERO_RETURN 18 | * ssl resumption 19 | * ssl 0-RTT 20 | * ssl false start 21 | * http pipelining 22 | * enforce http connection management 23 | * handle other X-Gibbon-Cache-Control stuff than key= 24 | * handle file:/// and data:/ urls, maybe localcontrol as well. 25 | * Pool more classes, things like HTTP1 and Request should be quite poolable 26 | * network.js? 27 | * connection racing 28 | * happy eyeballs ipv6 29 | * fix location setting, gotta always load it 30 | * fix images, gotta always load it first 31 | * disallow non-const enums somehow (eslint, tsconfig) 32 | * handling of format === "none" is not right with content length and compression stream and so on 33 | * nrdp.gibbon.cookie must be polyfilled 34 | * synchronous network requests, maybe some select enter loop business 35 | * network statistics for milo 36 | * /command for milo 37 | 38 | ### DONE 39 | * Support -X 40 | * Report peer verification errors correctly 41 | * don't make a databuffer when output format is arraybuffer/uint8array and data is in chunks 42 | * call callbacks when I get errors 43 | * transfer-encoding/transport-encoding inflate,gzip etc 44 | * consider sockets surviving scriptengine restart or not?, considered, they will not 45 | -------------------------------------------------------------------------------- /src/node/huffman/encode.ts: -------------------------------------------------------------------------------- 1 | import staticList from "./static"; 2 | 3 | const scratchBuffer = Buffer.alloc(50000); 4 | const bitMasks = new Uint8Array(8).map((x, i) => 2 ** i - 1); 5 | 6 | export default function encode(buffer: string | Buffer, offset: number = 0, length?: number): Buffer { 7 | let buf: Buffer; 8 | 9 | if (typeof buffer === 'string') { 10 | buf = Buffer.from(buffer); 11 | } else { 12 | buf = buffer; 13 | } 14 | 15 | let ptr = 0; 16 | let bitLen = 0; 17 | 18 | if (typeof length === "undefined") 19 | length = buf.byteLength; 20 | 21 | for (let i = offset; i < length; ++i) { 22 | const staticData: [number, number] = staticList[buf[i]]; 23 | 24 | const bits = staticData[0]; 25 | let bitsRemaining = staticData[1]; 26 | 27 | bitLen += bitsRemaining; 28 | 29 | do { 30 | const idx = Math.floor(ptr / 8); 31 | const bitIdx = ptr % 8; 32 | const bitsToEncode = Math.min(8 - bitIdx, bitsRemaining); 33 | const valueToEncode = ((bits >> (bitsRemaining - bitsToEncode)) & 34 | ((2 ** bitsToEncode) - 1)) << ((8 - bitIdx) - bitsToEncode); 35 | 36 | scratchBuffer[idx] |= valueToEncode; 37 | 38 | bitsRemaining -= bitsToEncode; 39 | ptr += bitsToEncode; 40 | 41 | } while (bitsRemaining > 0); 42 | } 43 | 44 | const oLength = Math.ceil(bitLen / 8); 45 | const outBuf = Buffer.alloc(oLength); 46 | scratchBuffer.copy(outBuf, 0, 0, oLength); 47 | 48 | const remainingOutBits = 8 - bitLen % 8; 49 | outBuf[oLength - 1] = outBuf[oLength - 1] | ((2 ** remainingOutBits) - 1); 50 | 51 | scratchBuffer.slice(0, oLength).fill(0); 52 | 53 | return outBuf; 54 | }; 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/utils/assert.macro/index.js: -------------------------------------------------------------------------------- 1 | const { createMacro, MacroError } = require('babel-plugin-macros'); 2 | 3 | module.exports = createMacro(assertMacro, MacroError); 4 | 5 | function assertMacro({ references, state, babel }) { 6 | const { template } = babel; 7 | const { default: defaultImport = [], ...otherReferences } = references; 8 | 9 | const enabled = process.env.NODE_ENV === 'development'; 10 | const implTarget = process.env.TARGET_PLATFORM; 11 | 12 | const invalidImports = Object.keys(otherReferences); 13 | if (invalidImports.length > 0) { 14 | throw new MacroError(`You should only import assert as default. You are also importing ${invalidImports.join(', ')}.`); 15 | } 16 | 17 | if (defaultImport.length < 1) { 18 | return; 19 | } 20 | 21 | let assertTemplate; 22 | if (implTarget === 'nrdp') { 23 | assertTemplate = template(`const assert = nrdp.assert;`); 24 | } 25 | if (implTarget === 'node') { 26 | assertTemplate = template(`const assert = require('assert');`); 27 | } 28 | 29 | if (!assertTemplate) { 30 | throw new MacroError(`assert template not found for target: ${implTarget}`); 31 | } 32 | 33 | if (enabled) { 34 | 35 | state.file.ast.program.body.unshift(assertTemplate()); 36 | } 37 | 38 | defaultImport.forEach((referencePath) => { 39 | if (!enabled) { 40 | referencePath.parentPath.remove(); 41 | return; 42 | } 43 | 44 | if (referencePath.parentPath.type !== 'CallExpression') { 45 | throw new MacroError('you must call the macro'); 46 | } 47 | 48 | const args = referencePath.parentPath.get('arguments'); 49 | if (args.length < 1) { 50 | throw new MacroError(`assert() needs at least 1 argument (the assertion condition).`); 51 | } 52 | }); 53 | } -------------------------------------------------------------------------------- /typings/nrdp_platform.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace nrdp_platform { 2 | type IDataBuffer = import("../src/IDataBuffer").default; 3 | type ArrayBufferConcatType = Uint8Array | IDataBuffer | ArrayBuffer; 4 | 5 | function utf8Length(str: string): number; 6 | function arrayBufferConcat(...buffers: ArrayBufferConcatType[]): ArrayBuffer; 7 | function bufferSet(dest: Uint8Array | ArrayBuffer | IDataBuffer, 8 | destOffset: number, 9 | src: Uint8Array | ArrayBuffer | string | IDataBuffer, 10 | srcOffset?: number, 11 | srcLength?: number): void; 12 | 13 | function random(length: number): Uint8Array; 14 | class Hasher { 15 | constructor(type: "sha1" | "sha256" | "sha512" | "md5"); 16 | add(data: string | Uint8Array | ArrayBuffer | IDataBuffer): void; 17 | final(): ArrayBuffer; 18 | final(md: Uint8Array | ArrayBuffer | IDataBuffer): number; 19 | reset(): void; 20 | } 21 | 22 | namespace JSON { 23 | function parse(data: string | IDataBuffer): any[] | undefined; 24 | } 25 | class CompressionStream { 26 | constructor(method: "zlib" | "gzip", type: boolean | "compress" | "uncompress"); 27 | 28 | process(output: Uint8Array | ArrayBuffer | IDataBuffer, 29 | outputOffset: number, 30 | outputLength: number | undefined, 31 | input: Uint8Array | ArrayBuffer | IDataBuffer | string, 32 | inputOffset?: number, 33 | inputLength?: number): number; 34 | 35 | readonly inputUsed: number; 36 | readonly method: string; 37 | readonly type: string; 38 | } 39 | 40 | function utf8Length(str: string): number; 41 | 42 | function parseXML(data: string | IDataBuffer): any; 43 | 44 | const hasServerTime: boolean; 45 | } 46 | -------------------------------------------------------------------------------- /autobahn/autobahn-reports.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import shelljs from 'shelljs'; 4 | 5 | import { root } from './runner/paths'; 6 | import { IPlatform } from './types'; 7 | 8 | export const reportsDir = path.join(root, 'autobahn-testsuite/docker/reports/clients'); 9 | export function clearReports(platform: IPlatform) { 10 | shelljs.pushd(reportsDir); 11 | try { 12 | shelljs.exec(`rm -rf ${reportsDir}`); 13 | } catch (e) { 14 | platform.error(`Erro executing "rm -rf ${reportsDir}" ${e}`); 15 | } 16 | try { 17 | shelljs.exec(`mkdir ${reportsDir}`); 18 | } catch (e) { 19 | platform.error(`Erro executing "mkdir ${reportsDir}" ${e}`); 20 | } 21 | shelljs.popd(); 22 | } 23 | 24 | export function getReports(agentName: string): Promise { 25 | return new Promise((res, rej) => { 26 | fs.readdir(reportsDir, (err, items) => { 27 | if (err) { 28 | rej(err); 29 | return; 30 | } 31 | 32 | res(items. 33 | filter(file => ~file.indexOf(agentName)). 34 | filter(file => ~file.indexOf('json'))); 35 | }); 36 | }); 37 | }; 38 | 39 | export type Report = { 40 | behavior: string; 41 | behaviorClose: string; 42 | id: string; 43 | }; 44 | 45 | function getFileContents(fileName: string): Report { 46 | const contents = fs.readFileSync(path.join(reportsDir, fileName)).toString(); 47 | return JSON.parse(contents) as Report; 48 | } 49 | 50 | export function testPass(fileName: string): boolean { 51 | const results = getFileContents(fileName); 52 | 53 | return results.behavior === 'OK' && 54 | results.behaviorClose === 'OK'; 55 | } 56 | 57 | export function getId(fileName: string): string { 58 | const results = getFileContents(fileName); 59 | return results.id; 60 | } 61 | -------------------------------------------------------------------------------- /src/node/UnorderedMap.ts: -------------------------------------------------------------------------------- 1 | import IUnorderedMap from "../IUnorderedMap"; 2 | 3 | export default class UnorderedMap implements IUnorderedMap { 4 | private map: Map; 5 | constructor(map?: Map) { 6 | if (map) { 7 | this.map = new Map(map); 8 | } else { 9 | this.map = new Map(); 10 | } 11 | } 12 | clear(): void { 13 | this.map.clear(); 14 | } 15 | clone(): IUnorderedMap { 16 | return new UnorderedMap(this.map); 17 | } 18 | delete(key: Key): boolean { 19 | if (this.map.has(key)) { 20 | this.map.delete(key); 21 | return true; 22 | } 23 | return false; 24 | } 25 | entries(): [Key, Value][] { 26 | return Array.from(this.map.entries()); 27 | } 28 | forEach(func: (key: Key, value: Value, that: UnorderedMap) => boolean | void): void { 29 | for (const [key, value] of this.map) { 30 | const ret = func(key, value, this); 31 | if (typeof ret === "boolean" && !ret) { 32 | break; 33 | } 34 | } 35 | } 36 | get(key: Key): Value | undefined { 37 | return this.map.get(key); 38 | } 39 | has(key: Key): boolean { 40 | return this.map.has(key); 41 | } 42 | keys(): Key[] { 43 | return Array.from(this.map.keys()); 44 | } 45 | get length() { 46 | return this.map.size; 47 | } 48 | get size() { 49 | return this.map.size; 50 | } 51 | set(key: Key, value: Value): UnorderedMap { 52 | this.map.set(key, value); 53 | return this; 54 | } 55 | take(key: Key): Value | undefined { 56 | const ret = this.map.get(key); 57 | this.map.delete(key); 58 | return ret; 59 | } 60 | values(): Value[] { 61 | return Array.from(this.map.values()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/HTTP2/hpack/internals/__Disabled_tests__/header-table.ts: -------------------------------------------------------------------------------- 1 | import HeaderTable from "../header-table"; 2 | import createStaticList from "../static-header-list"; 3 | 4 | const staticList = createStaticList(); 5 | 6 | describe("HeaderTable", () => { 7 | it("should be able to look up or resolve static value.", () => { 8 | const table = new HeaderTable(1024 * 16); 9 | 10 | for (let i = 0; i < staticList.length; ++i) { 11 | 12 | const [ 13 | name, value 14 | ] = staticList[i]; 15 | 16 | let expName; 17 | let expValue; 18 | if (value === null) { 19 | expName = table.getName(i + 1); 20 | expValue = null; 21 | } 22 | else { 23 | const nV = table.getNameAndValue(i + 1); 24 | expName = nV.name; 25 | expValue = nV.value; 26 | } 27 | 28 | expect(expName).toEqual(name); 29 | expect(expValue).toEqual(value); 30 | } 31 | }); 32 | 33 | it("should throw an error when name or name-pair are not found.", () => { 34 | const table = new HeaderTable(1024 * 16); 35 | 36 | // [":authority",null], 37 | // [":method","GET"], 38 | let errCount = 0; 39 | try { 40 | // authorize only 41 | table.getNameAndValue(1); 42 | } catch (e) { 43 | errCount++ 44 | } 45 | 46 | try { 47 | // method : get 48 | table.getName(2); 49 | } catch (e) { 50 | errCount++ 51 | } 52 | 53 | expect(errCount).toEqual(2); 54 | }); 55 | 56 | it("should insert into the dynamic table.", () => { 57 | const table = new HeaderTable(1024 * 16); 58 | 59 | const fooId = table.insert("foo", null); 60 | const barId = table.insert("foo", "bar"); 61 | 62 | expect(table.getName(fooId)).toEqual("foo"); 63 | expect(table.getNameAndValue(barId)).toEqual({ name: "foo", value: "bar" }); 64 | }); 65 | 66 | it("should dynamic size the header table.", () => { 67 | const table = new HeaderTable(200); 68 | }); 69 | }); 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/HTTP2/hpack/internals/static-header-list.ts: -------------------------------------------------------------------------------- 1 | export default function createStaticList(): [string, string | null][] { 2 | return [ 3 | [":authority",null], 4 | [":method","GET"], 5 | [":method","POST"], 6 | [":path","/"], 7 | [":path","/index.html"], 8 | [":scheme","http"], 9 | [":scheme","https"], 10 | [":status","200"], 11 | [":status","204"], 12 | [":status","206"], 13 | [":status","304"], 14 | [":status","400"], 15 | [":status","404"], 16 | [":status","500"], 17 | ["accept-charset",null], 18 | ["accept-encoding","gzip,deflate"], 19 | ["accept-language",null], 20 | ["accept-ranges",null], 21 | ["accept",null], 22 | ["access-control-allow-origin",null], 23 | ["age",null], 24 | ["allow",null], 25 | ["authorization",null], 26 | ["cache-control",null], 27 | ["content-disposition",null], 28 | ["content-encoding",null], 29 | ["content-language",null], 30 | ["content-length",null], 31 | ["content-location",null], 32 | ["content-range",null], 33 | ["content-type",null], 34 | ["cookie",null], 35 | ["date",null], 36 | ["etag",null], 37 | ["expect",null], 38 | ["expires",null], 39 | ["from",null], 40 | ["host",null], 41 | ["if-match",null], 42 | ["if-modified-since",null], 43 | ["if-none-match",null], 44 | ["if-range",null], 45 | ["if-unmodified-since",null], 46 | ["last-modified",null], 47 | ["link",null], 48 | ["location",null], 49 | ["max-forwards",null], 50 | ["proxy-authenticate",null], 51 | ["proxy-authorization",null], 52 | ["range",null], 53 | ["referer",null], 54 | ["refresh",null], 55 | ["retry-after",null], 56 | ["server",null], 57 | ["set-cookie",null], 58 | ["strict-transport-security",null], 59 | ["transfer-encoding",null], 60 | ["user-agent",null], 61 | ["vary",null], 62 | ["via",null], 63 | ["www-authenticate",null], 64 | ]; 65 | }; 66 | 67 | -------------------------------------------------------------------------------- /src/HTTP2/index.ts: -------------------------------------------------------------------------------- 1 | import IRequestData from "../IRequestData"; 2 | import Platform from "../Platform"; 3 | import Request from "../Request"; 4 | import { NetworkPipe } from "../NetworkPipe"; 5 | import { VersionIdentification } from "./consts"; 6 | 7 | // RFC: 3.4 Starting a request with prior knowledge. 8 | // This requires a special connection frame with a octet string of 9 | // "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" 10 | // Then a settings frame directly after. 11 | export async function createRawConnection(data: IRequestData | string): Promise { 12 | // TODO: YOU MUST CHANGE THIS. 13 | // @ts-ignore 14 | return await Request.connect(data); 15 | } 16 | 17 | export async function http2Upgrade(data: IRequestData): Promise { 18 | Platform.log("HTTP2 Got some data headers or something.", data); 19 | 20 | if (!data.headers) { 21 | data.headers = {}; 22 | } 23 | 24 | data.headers.Upgrade = data.secure ? VersionIdentification.Secure : 25 | VersionIdentification.NonSecure; 26 | 27 | data.headers.Connection = "Upgrade, HTTP2-Settings"; 28 | // Note: https://tools.ietf.org/html/rfc7540#section-3.2 29 | // This is a bit complicated and seems oddly worded when I don't really 30 | // try to read the rfc but instead talk to the twitch chat about RHCP. 31 | data.headers["HTTP2-Settings"] = '';/* TODO: base64 encode these bad boys */; 32 | 33 | const req = new Request(data); 34 | let pipe; 35 | try { 36 | const response = await req.send(); 37 | Platform.log("Got response", response); 38 | 39 | if (response.statusCode !== 101) { 40 | throw new Error("status code"); 41 | } 42 | 43 | Platform.log("successfully upgraded (maybe)."); 44 | 45 | pipe = req.networkPipe; 46 | 47 | // Send Preface... (TODO:) 48 | 49 | } catch (e) { 50 | Platform.log("Got e", e); 51 | throw e; 52 | } 53 | 54 | // TODO: This literally can never happen, but due to the structure of the 55 | // typescript, it can. 56 | if (!pipe) { 57 | throw new Error("Somehow your pipe was undefined. I think its Ricky Gervais fault.");; 58 | } 59 | 60 | return pipe; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/RequestResponse.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode } from "./types"; 2 | import IDataBuffer from "./IDataBuffer"; 3 | 4 | export default class RequestResponse { 5 | constructor(id: number, url: string | string[]) { 6 | this.id = id; 7 | if (typeof url === "string") { 8 | this.url = url; 9 | } else { 10 | this._urls = url; 11 | this.url = url[0]; 12 | } 13 | this.headers = []; 14 | } 15 | bytesRead?: number; 16 | cacheKey?: ArrayBuffer; 17 | cname?: string; 18 | connectTime?: number; 19 | data?: string | ArrayBuffer | Uint8Array | IDataBuffer; 20 | dns?: string; 21 | dnsChannel?: string; 22 | dnsTime?: number; 23 | dnsWireTime?: number; 24 | duration?: number; 25 | errorString?: string; 26 | errorcode?: ErrorCode; 27 | errorgroup?: number; 28 | exception?: Error; 29 | headers: string[]; 30 | headersSize?: number; 31 | httpVersion?: string; 32 | id: number; 33 | json?: any; 34 | jsonError?: boolean; 35 | metricsPrecision?: "us" | "ms" | "none"; 36 | nativeErrorCode?: number; 37 | networkStartTime?: number; 38 | reason?: number; 39 | requestSize?: number; 40 | serverIp?: string; 41 | size?: number; 42 | socket?: number; 43 | socketReused?: boolean; 44 | sslHandshakeTime?: number; 45 | sslSessionResumed?: boolean; 46 | sslVersion?: string; 47 | state?: string; 48 | statusCode?: number; 49 | timeToFirstByteRead?: number; 50 | timeToFirstByteWritten?: number; 51 | transactionTime?: number; 52 | url: string; 53 | xml?: any; 54 | 55 | get urls() { 56 | if (this._urls) 57 | return this._urls; 58 | return [this.url]; 59 | } 60 | 61 | get finalURL() { 62 | return this._urls ? this._urls[this._urls.length - 1] : this.url; 63 | } 64 | 65 | get responseDataLength() { 66 | return this.size || 0; 67 | } 68 | 69 | addUrl(url: string): number { 70 | if (!this._urls) { 71 | this._urls = [this.url, url]; 72 | } else { 73 | this._urls.push(url); 74 | } 75 | return this._urls.length; 76 | } 77 | 78 | private _urls?: string[]; 79 | }; 80 | -------------------------------------------------------------------------------- /src/HTTP2/frame/settings-frame.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FrameType, 3 | Flag, 4 | Settings, 5 | SettingsDefault, 6 | } from './types'; 7 | 8 | import * as FrameUtils from "./utils"; 9 | import FrameWriter from "./frame-writer"; 10 | 11 | /* 12 | +-------------------------------+ 13 | | Identifier (16) | 14 | +-------------------------------+-------------------------------+ 15 | | Value (32) | 16 | +---------------------------------------------------------------+ 17 | */ 18 | type DefinedSettings = [number, number]; 19 | 20 | export default class SettingsCreator { 21 | private settings: DefinedSettings[]; 22 | 23 | constructor() { 24 | this.settings = []; 25 | } 26 | 27 | addSetting(setting: Settings, value: number) { 28 | this.settings.push([setting, value]); 29 | } 30 | 31 | getLength() { 32 | return this.settings.length; 33 | } 34 | 35 | /** 36 | * Gets the setting's value or its default value. 37 | */ 38 | get(setting: Settings): number { 39 | for (const s of this.settings) { 40 | if (s[0] === setting) { 41 | return s[1]; 42 | } 43 | } 44 | 45 | return SettingsDefault[setting]; 46 | } 47 | 48 | parse(buffer: Uint8Array, offset: number, length: number) { 49 | const view = new DataView(buffer); 50 | let count = length / 48; 51 | let ptr = offset; 52 | 53 | while (count--) { 54 | this.settings.push([view.getUint16(ptr), view.getUint32(ptr)]); 55 | ptr += 6; 56 | } 57 | } 58 | 59 | toFrameWriter(streamIdentifier: number): FrameWriter { 60 | const frame = new FrameWriter( 61 | this.settings.length * 6, streamIdentifier, FrameType.SETTINGS); 62 | 63 | this.settings.forEach(settings => { 64 | frame.write16(settings[0]); 65 | frame.write32(settings[1]); 66 | }); 67 | 68 | return frame; 69 | } 70 | 71 | // TODO: Should we just have a single object that doesn't create so much 72 | // garbage 73 | public static ackFrame(streamIdentifier: number): FrameWriter { 74 | const writer = new SettingsCreator().toFrameWriter(streamIdentifier); 75 | writer.addFlag(Flag.ACK); 76 | 77 | return writer; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ws/upgrade.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from "../DataBuffer"; 2 | import IRequestData from "../IRequestData"; 3 | import NetworkPipe from "../NetworkPipe"; 4 | import Platform from "../Platform"; 5 | import Request from "../Request"; 6 | import { UrlObject, } from './types'; 7 | import { headerValue } from "../utils"; 8 | 9 | export function upgrade(u: string | UrlObject): Promise { 10 | let url = u; 11 | if (typeof url === "object") { 12 | // TODO: Should I do this? 13 | url = `ws://${url.host}:${url.port}`; 14 | } 15 | 16 | const data: IRequestData = { forbidReuse: true, url, format: "databuffer" }; 17 | 18 | return new Promise((resolve, reject) => { 19 | if (!data.headers) { 20 | data.headers = {}; 21 | } 22 | 23 | // TODO: Ask Jordan, WHY TYPESCRIPT WHY... 24 | const arrayBufferKey = new DataBuffer(16); 25 | 26 | arrayBufferKey.randomize(); 27 | const key = arrayBufferKey.toString("base64"); 28 | 29 | 30 | data.headers.Upgrade = "websocket"; 31 | data.headers.Connection = "Upgrade"; 32 | data.headers["Sec-WebSocket-Key"] = key; 33 | data.headers["Sec-WebSocket-Version"] = "13"; 34 | data.forbidReuse = true; 35 | data.freshConnect = true; 36 | const req = new Request(data); 37 | req.send().then(response => { 38 | if (response.statusCode !== 101) 39 | throw new Error("status code"); 40 | 41 | const upgradeKeyResponse = headerValue(response.headers, "sec-websocket-accept"); 42 | if (!upgradeKeyResponse) { 43 | throw new Error("no Sec-WebSocket-Accept key"); 44 | } 45 | 46 | const WS_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 47 | const shadkey = Platform.btoa(Platform.sha1(key + WS_KEY)); 48 | if (shadkey !== upgradeKeyResponse) { 49 | throw new Error(`Key mismatch expected: ${shadkey} got: ${upgradeKeyResponse}`); 50 | } 51 | 52 | Platform.trace("successfully upgraded"); 53 | 54 | // houstone we have a problem 55 | // TODO: Come back to this... 56 | // @ts-ignore 57 | resolve(req.networkPipe); 58 | }).catch(error => { 59 | Platform.trace("Got error", error); 60 | reject(error); 61 | }); 62 | }); 63 | } 64 | 65 | -------------------------------------------------------------------------------- /bin/builds/rollup.nrdp.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import babel from "rollup-plugin-babel"; 4 | import target from "./rollup-target-plugin"; 5 | import sourcemaps from 'rollup-plugin-sourcemaps'; 6 | import { terser } from "rollup-plugin-terser"; 7 | import replace from '@rollup/plugin-replace'; 8 | 9 | const SOURCE_DIR = 'build/nrdp'; 10 | const OUTPUT_DIR = 'dist'; 11 | 12 | const getPluginsConfig = (prod) => { 13 | const plugins = [ 14 | target({ 15 | target: "nrdp" 16 | }), 17 | babel({ 18 | exclude: "node_modules/**", 19 | babelrc: false, 20 | inputSourceMap: false, 21 | sourceMaps: false, 22 | presets: [ 23 | [ 24 | '@babel/preset-env', 25 | { 26 | loose: true, 27 | targets: { 28 | safari: '6' 29 | }, 30 | modules: false, 31 | useBuiltIns: 'entry', 32 | corejs: 3, 33 | exclude: [ 34 | '@babel/plugin-transform-async-to-generator', 35 | '@babel/plugin-transform-regenerator' 36 | ] 37 | } 38 | ] 39 | ], 40 | plugins: [ 41 | 'babel-plugin-macros', 42 | ['babel-plugin-transform-async-to-promises', { 43 | hoist: true 44 | }] 45 | ] 46 | }), 47 | resolve(), 48 | replace({ 49 | "process.env.NODE_ENV": JSON.stringify(prod ? "production" : "development"), 50 | }), 51 | commonjs(), 52 | sourcemaps() 53 | ]; 54 | 55 | if (prod) { 56 | plugins.push( 57 | terser() 58 | ); 59 | } 60 | return plugins; 61 | } 62 | 63 | export default (CLIArgs) => { 64 | const prod = !!CLIArgs.prod; 65 | const bundle = { 66 | input: `${SOURCE_DIR}/milo.js`, 67 | output: { 68 | file: prod ? `${OUTPUT_DIR}/milo.nrdp.prod.js` : `${OUTPUT_DIR}/milo.nrdp.js`, 69 | format: "iife", 70 | name: "milo", 71 | exports: "named", 72 | sourcemap: true 73 | }, 74 | }; 75 | 76 | // Apply plugins 77 | bundle.plugins = getPluginsConfig(prod); 78 | 79 | return bundle; 80 | } -------------------------------------------------------------------------------- /tests/test-client.js: -------------------------------------------------------------------------------- 1 | function runTests(options) 2 | { 3 | var table = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n"; 4 | 5 | var payloads = []; 6 | for (var s=1; s<16; ++s) { 7 | var size = Math.pow(2, s); 8 | var payload = ""; 9 | for (var i=0; i 51 | // number | ArrayBuffer' is not assignable to type '{ (): ArrayBuffer; (md: 52 | // Uint8Array | ArrayBuffer | IDataBuffer, offset?: number | undefined): 53 | // number; }'. Type 'number | ArrayBuffer' is not assignable to type 54 | // 'ArrayBuffer'. Type 'number' is not assignable to type 55 | // 'ArrayBuffer'. 56 | // @ts-ignore 57 | final(md?: ArrayBuffer | Uint8Array | IDataBuffer, offset?: number): ArrayBuffer | number { 58 | if (md) { 59 | throw new Error("Not Implemented"); 60 | return 0; 61 | } 62 | 63 | const buf = Buffer.from(this.hash.digest('hex')); 64 | return bufferToArrayBufferCopy(buf, 0, buf.byteLength); 65 | } 66 | 67 | reset(): void { 68 | this.salt = 'abcdefghijklmnop!' + Date.now(); 69 | this.hash = crypto.createHmac('sha256', this.salt); 70 | } 71 | } 72 | 73 | 74 | -------------------------------------------------------------------------------- /bin/lint.js: -------------------------------------------------------------------------------- 1 | const child_process = require("child_process"); 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | 5 | child_process.execFile(path.join(__dirname, "../node_modules/.bin/tslint"), 6 | [ "--project", path.join(__dirname, "..") ], 7 | (error, stdout, stderr) => { 8 | // if (error) { 9 | // console.error("Failed to run tslint", error); 10 | // } 11 | if (stdout) { 12 | let file; 13 | console.log(stdout.split("\n").map(line => { 14 | if (!line) 15 | return undefined; 16 | 17 | const match = /^(\/.*):[0-9]+:[0-9]+$/.exec(line); 18 | if (match) { 19 | file = match[1]; 20 | return undefined; 21 | } 22 | 23 | if (!file) { 24 | console.error("FAILED TO GROK LINE, no file", line); 25 | process.exit(1); 26 | return undefined; 27 | } 28 | 29 | const error = /^ERROR: ([0-9]+):([0-9]+) *(.*)$/.exec(line); 30 | if (!error) { 31 | const warning = /^WARNING: ([0-9]+):([0-9]+) *(.*)$/.exec(line); 32 | if (!warning) { 33 | console.error(`FAILED TO GROK LINE, what is this? ${line} ${line.length}`); 34 | process.exit(1); 35 | } 36 | return `${file}:${warning[1]}:${warning[2]}: warning: ${warning[3]}`; 37 | } 38 | return `${file}:${error[1]}:${error[2]}: error: ${error[3]}`; 39 | }).filter(x => x).join("\n")); 40 | // console.log("got stdout", typeof stdout); 41 | } 42 | if (stderr) { 43 | console.error(stderr); 44 | } 45 | process.exit(error || stderr || stdout ? 1 : 0); 46 | }); 47 | -------------------------------------------------------------------------------- /src/node/huffman/__tests__/decode.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from "../../DataBuffer"; 2 | import decode from "../decode"; 3 | 4 | describe("decode", () => { 5 | it("decode simple 1", () => { 6 | const one = new DataBuffer(1); 7 | one.setUInt8(0, 0b0000_1111); 8 | 9 | const expected = new DataBuffer(1); 10 | expected.setUInt8(0, 49); 11 | 12 | expect(decode(one).getUInt8(0)).toEqual(expected.getUInt8(0)); 13 | }); 14 | 15 | it("decode 1337", () => { 16 | /* 17 | '1' ( 49) |00001 1 [ 5] 18 | '3' ( 51) |011001 19 [ 6] 19 | '7' ( 55) |011101 1d [ 6] 20 | */ 21 | const one337 = new DataBuffer(3); 22 | one337.setUInt8(0, 0b0000_1011); 23 | one337.setUInt8(1, 0b0010_1100); 24 | one337.setUInt8(2, 0b1011_1011); 25 | 26 | const expected = new DataBuffer(4); 27 | expected.setUInt8(0, 49); 28 | expected.setUInt8(1, 51); 29 | expected.setUInt8(2, 51); 30 | expected.setUInt8(3, 55); 31 | 32 | expect(decode(one337)).toEqual(expected); 33 | }); 34 | 35 | it("decode https://www.example.com, rfc ", () => { 36 | // https://www.example.com 37 | // 9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3 38 | const d = [ 39 | 0x9d, 40 | 0x29, 41 | 0xad, 42 | 0x17, 43 | 0x18, 44 | 0x63, 45 | 0xc7, 46 | 0x8f, 47 | 0x0b, 48 | 0x97, 49 | 0xc8, 50 | 0xe9, 51 | 0xae, 52 | 0x82, 53 | 0xae, 54 | 0x43, 55 | 0xd3, 56 | ]; 57 | const www = new DataBuffer(17); 58 | d.forEach((x, i) => www.setUInt8(i, x)); 59 | 60 | const expected = new DataBuffer(21); 61 | expect(decode(www).toString()).toEqual("https://www.example.com"); 62 | }); 63 | 64 | it("decode 307, from rfc C.6.2", () => { 65 | // 640e ff 66 | const three07 = new DataBuffer(3); 67 | 68 | // 64 69 | three07.setUInt8(0, 0b0110_0100); 70 | 71 | // 0e 72 | three07.setUInt8(1, 0b0000_1110); 73 | 74 | // FF 75 | three07.setUInt8(2, 0b1111_1111); 76 | 77 | const expected = new DataBuffer(3); 78 | expected.setUInt8(0, 51); 79 | expected.setUInt8(1, 48); 80 | expected.setUInt8(2, 55); 81 | 82 | expect(decode(three07)).toEqual(expected); 83 | }); 84 | }); 85 | 86 | 87 | -------------------------------------------------------------------------------- /autobahn/README.md: -------------------------------------------------------------------------------- 1 | ### Getting started 2 | To get started please make sure you have docker completely installed and 3 | working on your system. 4 | 5 | #### .env 6 | The `.env` drives the basic values for the autobahn server and client. If you 7 | wish to override those values create and reassign the values in a file called 8 | `.milo-test-env`. 9 | 10 | ##### Properties 11 | To control your experience using the autobahn tester, here are the variables 12 | you need to have. 13 | 14 | ###### CASES 15 | Cases determines which tests to run. 16 | 17 | *Running a specific case* 18 | 19 | CASES=1.1.1 20 | 21 | *Running a set of cases* 22 | 23 | CASES=1.* 24 | 25 | *Running Several Cases* 26 | 27 | CASES=1.*,2.* 28 | 29 | *Running all cases* 30 | 31 | CASES=* 32 | 33 | ###### NRDP 34 | NRDP has to be defined (the command used to execute NRDP, `pvm current run --` 35 | is a good example of a possible value of NRDP. 36 | 37 | ###### SELF_MANAGED_AUTOBAHN 38 | If you wish to run your own server, set this value `false` in `.milo-test-env`. 39 | This is a good variable to set false when developing or debugging against 40 | autobahn 41 | 42 | ### Developing/Debuging Against Autobahn 43 | To develop against autobahn, follow these steps. 44 | 45 | 1. Set `SELF_MANAGED_AUTOBAHN` to false in your `.milo-test-env`. 46 | 2. Set `CASES` to the set of tests you wish to run against in you `.milo-test-env`. 47 | 3. Start the server to test against. `npx ts-node autobahn/start.ts` 48 | 4. In a new terminal window start the client. I would suggest building before 49 | running. `npm run node && npx ts-node autobahn/test-harness.ts` 50 | 5. When the tests have ran, the output will be saved and viewable at 51 | [http://localhost:8080](http://localhost:8080). Click the `Client Reports` link. 52 | 53 | #### Debugger 54 | If you wish to launch the node debugger, run the `test-harness` with `node 55 | --inspect ./node_modules/.bin/ts-node autobahn/test-hardness.ts`. This will 56 | still launch the node tester with a node debugger. 57 | 58 | #### NRDP 59 | If you wish to run NRDP as the client, you need to slightly modify the `client` command, step 4. 60 | 61 | *NRDP Version* 62 | 4. In a new terminal window start the client. I would suggest building before 63 | running. `npm run nrdp && npx ts-node autobahn/test-harness.ts nrdp` 64 | 65 | #### Viewing Results 66 | All results are viewable at [http://localhost:8080](http://localhost:8080), but 67 | to view them you must have your server started. 68 | 69 | If you wish to clean your results and start fresh then you need to do the 70 | following. 71 | 72 | 1. Stop your server. 73 | 2. Delete the reports directory, `rm -rf autobahn-testsuite/docker/reports` 74 | 3. Start your server back up. 75 | -------------------------------------------------------------------------------- /autobahn/test-harness.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | import death from 'death'; 5 | 6 | // @ts-ignore 7 | import { WS, Platform } from '../dist/milo.node'; 8 | 9 | import mergeCustomConfig from './merge-custom-config'; 10 | mergeCustomConfig(Platform); 11 | 12 | import autobahn from './runner'; 13 | import { setAgent, getVersion } from './runner/get-agent'; 14 | import autobahnTestSuite from './start'; 15 | import { killContext, GlobalContext } from './context'; 16 | import { killDocker } from './runner/docker/kill'; 17 | import { clearReports, getReports, testPass, getId } from './autobahn-reports'; 18 | import getNrdpAgent from './runner/nrdp/agent'; 19 | import testNrdp from './runner/nrdp'; 20 | 21 | const isNrdpRun = process.argv[2] === 'nrdp'; 22 | 23 | const ON_DEATH = death({ uncaughtException: true }); 24 | 25 | // Attempts to kill all autobahn testsuites 26 | ON_DEATH(() => { 27 | if (process.env.SELF_MANAGED_AUTOBAHN !== 'true') { 28 | killDocker(); 29 | } 30 | 31 | killContext(GlobalContext); 32 | 33 | process.exit(); 34 | }); 35 | 36 | async function wait(ms: number) { 37 | return new Promise(res => { 38 | setTimeout(res, ms); 39 | }); 40 | } 41 | 42 | async function run() { 43 | let agent = `test_harness_${getVersion()}`; 44 | Platform.log("Testing Autobahn with", process.env.CASES); 45 | Platform.log("Agent", agent); 46 | Platform.log("If this is wrong, please edit your .env file."); 47 | 48 | setAgent(agent); 49 | 50 | await wait(1000); 51 | clearReports(Platform); 52 | 53 | if (process.env.SELF_MANAGED_AUTOBAHN !== 'true') { 54 | await autobahnTestSuite(); 55 | } 56 | 57 | let cases = -1; 58 | if (isNrdpRun) { 59 | agent = getNrdpAgent(); 60 | cases = await testNrdp(Platform); 61 | } 62 | else { 63 | cases = await autobahn(WS, { 64 | updateReport: true, 65 | port: 9001, 66 | Platform, 67 | agent, 68 | }); 69 | } 70 | 71 | Platform.log("examining reports"); 72 | const reports = await getReports(agent); 73 | 74 | if (reports.length < cases) { 75 | throw new Error(`Expected ${cases} to be reported, but got ${reports.length}`); 76 | } 77 | 78 | const fails = reports.map(r => { 79 | if (!testPass(r)) { 80 | return getId(r); 81 | } 82 | 83 | return false; 84 | }).filter(x => x) as string[]; 85 | 86 | Platform.error("examining reports finished, failures are ", fails); 87 | if (fails.length) { 88 | Platform.log(fails.length, "Test cases have failed:", fails.join(', ')); 89 | process.exit(1); 90 | } 91 | 92 | Platform.log("Successfully passed", process.env.CASES, "autobahn tests"); 93 | killContext(GlobalContext); 94 | process.exit(0); 95 | } 96 | 97 | run(); 98 | 99 | -------------------------------------------------------------------------------- /src/HTTP2/frame/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FrameType, 3 | Flag, 4 | } from './types'; 5 | 6 | /* FRAME HEADER 7 | +-----------------------------------------------+ 8 | | Length (24) | 9 | +---------------+---------------+---------------+ 10 | | Type (8) | Flags (8) | 11 | +-+-------------+---------------+-------------------------------+ 12 | |R| Stream Identifier (31) | 13 | +=+=============================================================+ 14 | | Frame Payload (0...) ... 15 | +---------------------------------------------------------------+ 16 | */ 17 | 18 | const maxStreamIdentifier = 2147483647; 19 | const tmpBuffer = new Uint8Array(4); 20 | const tmpView = new DataView(tmpBuffer.buffer); 21 | 22 | export const FRAME_HEADER_SIZE = 9; 23 | export const FRAME_HEADER_FLAGS_OFFSET = 4; 24 | export const FRAME_HEADER_TYPE_OFFSET = 3; 25 | 26 | export function getLength(buffer: Uint8Array, offset: number = 0) { 27 | tmpBuffer[0] = 0; 28 | tmpBuffer[1] = buffer[offset]; 29 | tmpBuffer[2] = buffer[offset + 1]; 30 | tmpBuffer[3] = buffer[offset + 2]; 31 | 32 | return tmpView.getUint32(0); 33 | } 34 | 35 | export function getType(buffer: Uint8Array, offset: number = 0) { 36 | return buffer[offset + FRAME_HEADER_TYPE_OFFSET]; 37 | } 38 | 39 | // The length, of course, is a 24bit number. Because... Numbers... 40 | // TODO: SETTINGS_MAX_FRAME_SIZE this should be tested in the http2 class. 41 | // (https://tools.ietf.org/html/rfc7540#section-4.1) 42 | export function setLength(buffer: Uint8Array, length: number) { 43 | tmpView.setUint32(0, length); 44 | 45 | if (tmpBuffer[0] > 0) { 46 | throw new Error(`You cannot take me down, also your length is to much. ${length}`); 47 | } 48 | 49 | buffer[0] = tmpBuffer[1]; 50 | buffer[1] = tmpBuffer[2]; 51 | buffer[2] = tmpBuffer[3]; 52 | } 53 | 54 | export function setType(buffer: Uint8Array, type: FrameType) { 55 | buffer[3] = type; 56 | }; 57 | 58 | export function zeroFlags(buffer: Uint8Array) { 59 | buffer[FRAME_HEADER_FLAGS_OFFSET] = 0; 60 | }; 61 | 62 | export function setFlags(buffer: Uint8Array, flags: Flag) { 63 | buffer[FRAME_HEADER_FLAGS_OFFSET] |= flags; 64 | }; 65 | 66 | export function isAckFrame(buffer: Uint8Array, offset: number = 0) { 67 | return buffer[offset + FRAME_HEADER_FLAGS_OFFSET] & Flag.ACK; 68 | } 69 | 70 | export function setStreamIdentifier(buffer: Uint8Array, streamIdentifier: number) { 71 | if (streamIdentifier > maxStreamIdentifier) { 72 | throw new Error(`setStreamIdentifier received a streamIdentifier that is too large: ${streamIdentifier}`); 73 | } 74 | 75 | tmpView.setUint32(0, streamIdentifier); 76 | buffer[5] = tmpBuffer[0]; 77 | buffer[6] = tmpBuffer[1]; 78 | buffer[7] = tmpBuffer[2]; 79 | buffer[8] = tmpBuffer[3]; 80 | }; 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/HTTP2/frame/__tests__/frame-writer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FrameType, 3 | Flag, 4 | } from '../types'; 5 | 6 | import FrameWriter from "../frame-writer"; 7 | import { FRAME_HEADER_FLAGS_OFFSET, FRAME_HEADER_SIZE } from "../utils"; 8 | 9 | function toArray(...args: number[]) { 10 | return args; 11 | } 12 | 13 | /* FRAME HEADER 14 | +-----------------------------------------------+ 15 | | Length (24) | 16 | +---------------+---------------+---------------+ 17 | | Type (8) | Flags (8) | 18 | +-+-------------+---------------+-------------------------------+ 19 | |R| Stream Identifier (31) | 20 | +=+=============================================================+ 21 | | Frame Payload (0...) ... 22 | +---------------------------------------------------------------+ 23 | */ 24 | /* DATA FRAME PAYLOAD PADDING PADDED 25 | +---------------+ 26 | |Pad Length? (8)| 27 | +---------------+-----------------------------------------------+ 28 | | Data (*) ... 29 | +---------------------------------------------------------------+ 30 | | Padding (*) ... 31 | +---------------------------------------------------------------+ 32 | */ 33 | describe('FrameWriter', () => { 34 | it('should initialize and correctly setup the frame.', () => { 35 | const frame = new FrameWriter(70000, 3, FrameType.HEADERS); 36 | const header = frame.buffer.slice(0, FRAME_HEADER_SIZE); 37 | const headerView = new DataView(header.buffer); 38 | 39 | const length = header.slice(0, 3); 40 | const lengthExpBuf = Buffer.alloc(4); 41 | new DataView(lengthExpBuf.buffer).setUint32(0, 70000); 42 | expect(frame.buffer.byteLength).toEqual(70000 + FRAME_HEADER_SIZE); 43 | 44 | // TODO: How to fix this error? 45 | // @ts-ignore 46 | expect(toArray(...length)).toEqual(toArray(...lengthExpBuf.slice(1))); 47 | expect(header[3]).toEqual(FrameType.HEADERS); 48 | expect(header[FRAME_HEADER_FLAGS_OFFSET]).toEqual(0); 49 | expect(header[5] & (1 << 32)).toEqual(0); 50 | expect(headerView.getUint32(5)).toEqual(3); 51 | }); 52 | 53 | it('should be able to set flags', () => { 54 | const frame = new FrameWriter(70000, 3, FrameType.HEADERS); 55 | const header = frame.buffer.subarray(0, FRAME_HEADER_SIZE); 56 | const headerView = new DataView(header.buffer); 57 | 58 | expect(header[FRAME_HEADER_FLAGS_OFFSET]).toEqual(0); 59 | 60 | frame.addFlag(Flag.END_STREAM); 61 | expect(header[FRAME_HEADER_FLAGS_OFFSET]).toEqual(Flag.END_STREAM); 62 | 63 | frame.addFlag(Flag.PRIORITY); 64 | expect(header[FRAME_HEADER_FLAGS_OFFSET]).toEqual(Flag.END_STREAM | Flag.PRIORITY); 65 | }); 66 | }); 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/HTTP2/stream/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * +--------+ 3 | * send PP | | recv PP 4 | * ,--------| idle |--------. 5 | * / | | \ 6 | * v +--------+ v 7 | * +----------+ | +----------+ 8 | * | | | send H / | | 9 | * ,------| reserved | | recv H | reserved |------. 10 | * | | (local) | | | (remote) | | 11 | * | +----------+ v +----------+ | 12 | * | | +--------+ | | 13 | * | | recv ES | | send ES | | 14 | * | send H | ,-------| open |-------. | recv H | 15 | * | | / | | \ | | 16 | * | v v +--------+ v v | 17 | * | +----------+ | +----------+ | 18 | * | | half | | | half | | 19 | * | | closed | | send R / | closed | | 20 | * | | (remote) | | recv R | (local) | | 21 | * | +----------+ | +----------+ | 22 | * | | | | | 23 | * | | send ES / | recv ES / | | 24 | * | | send R / v send R / | | 25 | * | | recv R +--------+ recv R | | 26 | * | send R / `----------->| |<-----------' send R / | 27 | * | recv R | closed | recv R | 28 | * `----------------------->| |<----------------------' 29 | * +--------+ 30 | */ 31 | 32 | export const enum StreamState { 33 | idle, 34 | reservedLocal, 35 | reservedRemote, 36 | open, 37 | halfClosedLocal, 38 | halfClosedRemote, 39 | closed, 40 | } 41 | 42 | export class Stream { 43 | private state: StreamState; 44 | private streamIdentifier: number; 45 | 46 | constructor(streamIdentifier: number) { 47 | this.streamIdentifier = streamIdentifier; 48 | this.state = StreamState.idle; 49 | } 50 | 51 | private transition(state: StreamState) { 52 | switch (state) { 53 | case StreamState.open: 54 | break; 55 | case StreamState.reservedLocal: 56 | break; 57 | case StreamState.reservedRemote: 58 | break; 59 | case StreamState.halfClosedLocal: 60 | break; 61 | case StreamState.halfClosedRemote: 62 | break; 63 | case StreamState.closed: 64 | break; 65 | default: 66 | throw new Error("You cannot transition to an unknown state: " + state); 67 | break; 68 | } 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "milo", 3 | "version": "2.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "homepage": "https://github.com/ThePrimeagen/milo#readme", 7 | "bugs": { 8 | "url": "https://github.com/ThePrimeagen/milo/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ThePrimeagen/milo.git" 13 | }, 14 | "license": "MIT", 15 | "author": "", 16 | "main": "dist/milo.node.js", 17 | "scripts": { 18 | "all": "node bin/build.js", 19 | "build": "node bin/build.js", 20 | "prod": "node bin/build.js --prod", 21 | "build:nrdp:test": "npm run nrdp-generate-ssl-functions && tsc -p tsconfig.nrdp.json && rollup -c rollup.nrdp.test.js", 22 | "clean": "del-cli build/ dist/", 23 | "lint": "node bin/lint.js", 24 | "node": "node bin/build.js --target node", 25 | "nrdp": "node bin/build.js --target nrdp", 26 | "nrdp:prod": "node bin/build.js --target nrdp --prod", 27 | "nrdp-generate-ssl-functions": "node bin/generate-ssl-functions.js", 28 | "test": "jest", 29 | "test:all": "npm run test && npm run test:autobahn", 30 | "test:autobahn": "ts-node ./autobahn/test-harness.ts", 31 | "test:autobahn:nrdp": "ts-node ./autobahn/test-harness.ts nrdp" 32 | }, 33 | "dependencies": { 34 | "@types/cookiejar": "^2.1.1", 35 | "atob": "^2.1.2", 36 | "btoa": "^1.2.1", 37 | "cookiejar": "^2.1.2", 38 | "network-byte-order": "^0.2.0", 39 | "sha1": "^1.1.1", 40 | "url-parse": "^1.4.7", 41 | "utf-8-validate": "^5.0.2", 42 | "ws": "^7.2.1" 43 | }, 44 | "devDependencies": { 45 | "@babel/preset-env": "^7.8.7", 46 | "@rollup/plugin-commonjs": "^11.0.1", 47 | "@rollup/plugin-node-resolve": "^7.0.0", 48 | "@rollup/plugin-replace": "^2.3.1", 49 | "@types/atob": "^2.1.2", 50 | "@types/btoa": "^1.2.3", 51 | "@types/death": "^1.1.0", 52 | "@types/express": "^4.17.2", 53 | "@types/jest": "^24.0.24", 54 | "@types/node": "^12.12.18", 55 | "@types/sha1": "^1.1.2", 56 | "@types/shelljs": "^0.8.6", 57 | "@types/url-parse": "^1.4.3", 58 | "@types/ws": "^6.0.4", 59 | "babel-plugin-macros": "^2.8.0", 60 | "babel-plugin-transform-async-to-promises": "^0.8.15", 61 | "bindings": "^1.5.0", 62 | "body-parser": "^1.19.0", 63 | "death": "^1.1.0", 64 | "del-cli": "^3.0.0", 65 | "dotenv": "^8.2.0", 66 | "execa": "^4.0.0", 67 | "express": "^4.17.1", 68 | "http2": "^3.3.7", 69 | "jest": "^24.9.0", 70 | "listr": "^0.14.3", 71 | "rollup": "^1.29.0", 72 | "rollup-plugin-babel": "^4.4.0", 73 | "rollup-plugin-sourcemaps": "^0.5.0", 74 | "rollup-plugin-terser": "^5.3.0", 75 | "seedrandom": "^3.0.5", 76 | "shelljs": "^0.8.3", 77 | "symbol-es6": "^0.1.2", 78 | "ts-jest": "^24.2.0", 79 | "ts-node": "^8.5.4", 80 | "tslib": "^1.10.0", 81 | "tslint": "^6.1.0", 82 | "typescript": "^3.7.3", 83 | "yargs": "^15.3.1" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/node/utils.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from "./DataBuffer"; 2 | import IDataBuffer from "../IDataBuffer"; 3 | 4 | export function bufferToUint8Array(buf: Buffer) { 5 | return new Uint8Array( 6 | buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)); 7 | } 8 | 9 | export function normalizeBufferLen(buf: string | Uint8Array | ArrayBuffer, offset: number, length: number): Buffer { 10 | return normalizeBuffer(buf, offset, offset + length); 11 | }; 12 | 13 | export function bufferToArrayBufferCopy(buf: Buffer, offset: number, length: number): ArrayBuffer { 14 | const out = new ArrayBuffer(buf.byteLength); 15 | const view = new Uint8Array(out); 16 | 17 | buf.copy(view, 0); 18 | return out; 19 | } 20 | 21 | export function normalizeBuffer(buf: string | Uint8Array | ArrayBuffer, offset: number, endIdx?: number): Buffer { 22 | /* 23 | let outBuf; 24 | if (typeof buf === 'string') { 25 | outBuf = Buffer.from(buf); 26 | } 27 | else if (buf instanceof Uint8Array) { 28 | outBuf = Buffer.from(buf); 29 | } 30 | else { 31 | outBuf = Buffer.from(buf); 32 | } 33 | * This works, why wont this one below work. 34 | */ 35 | 36 | // @ts-ignore 37 | return Buffer.from(buf).slice(offset, isNaN(endIdx) ? undefined : endIdx); 38 | }; 39 | 40 | export function normalizeUint8Array(buf: string | Uint8Array | ArrayBuffer, 41 | offset: number, endIdx?: number): Uint8Array { 42 | return bufferToUint8Array(normalizeBuffer(buf, offset, endIdx)); 43 | }; 44 | 45 | export function normalizeUint8ArrayLen(buf: string | Uint8Array | ArrayBuffer, 46 | offset: number, length?: number): Uint8Array { 47 | // @ts-ignore 48 | return bufferToUint8Array(normalizeBufferLen(buf, offset, length)); 49 | }; 50 | 51 | function stringToUint8Array(str: string): Uint8Array { 52 | const buf = Buffer.from(str); 53 | return new Uint8Array(buf); 54 | } 55 | 56 | export function toUint8Array(buf: string | Uint8Array | ArrayBuffer | IDataBuffer): Uint8Array { 57 | 58 | if (buf instanceof ArrayBuffer) { 59 | return new Uint8Array(buf); 60 | } 61 | else if (typeof buf === 'string') { 62 | return stringToUint8Array(buf); 63 | } 64 | else if (buf instanceof Uint8Array) { 65 | return buf; 66 | } 67 | 68 | // NOTE: This is only for Node implementation, should really be dropped 69 | // as its horrifying 70 | const out = new Uint8Array(buf.byteLength); 71 | 72 | for (let i = 0; i < buf.byteLength; ++i) { 73 | out[i] = buf.getUInt8(i); 74 | } 75 | 76 | return out; 77 | } 78 | 79 | export function createNonCopyBuffer(buf: ArrayBuffer | IDataBuffer, offset: number, length: number): Buffer { 80 | if (buf instanceof ArrayBuffer) { 81 | return Buffer.from(buf).slice(offset, offset + length); 82 | } 83 | 84 | const adjOffset = buf.byteOffset + offset; 85 | return (buf as DataBuffer).buffer.slice(adjOffset, adjOffset + length); 86 | }; 87 | 88 | -------------------------------------------------------------------------------- /bin/generate-ssl-functions.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* global BigInt */ 4 | 5 | const fs = require("fs"); 6 | const path = require("path"); 7 | 8 | function stat(file) 9 | { 10 | try { 11 | const stat = fs.statSync(file); 12 | // console.log(stat); 13 | return stat.mtimeMs; 14 | } catch (err) { 15 | return 0; 16 | } 17 | } 18 | 19 | const outputFile = path.join(__dirname, "../src/nrdp/NrdpBoundSSLFunctions.ts"); 20 | const statGenerate = stat(path.join(__dirname, "generate-ssl-functions.js")); 21 | const statJson = stat(path.join(__dirname, "../src/nrdp/bound_ssl_functions.json")); 22 | const statOutput = stat(outputFile); 23 | 24 | if (statOutput > statGenerate && statOutput > statJson) { 25 | // console.log(statGenerate, statJson, statOutput); 26 | // nothing to do 27 | process.exit(0); 28 | } 29 | 30 | let ts = `import IDataBuffer from "../IDataBuffer"; 31 | import IPlatform from "../IPlatform"; 32 | import IUnorderedMap from "../IUnorderedMap"; 33 | import N = nrdsocket; 34 | 35 | `; 36 | 37 | function layout(text) { 38 | if (text.length > 120) { 39 | let idx = 0; 40 | while (text.charCodeAt(idx) === 32) { 41 | ++idx; 42 | } 43 | return `${' '.repeat(idx)}/* tslint:disable:max-line-length */ 44 | ${text} 45 | `; 46 | } 47 | return text + "\n"; 48 | } 49 | 50 | const data = JSON.parse(fs.readFileSync(path.join(__dirname, "../src/nrdp/bound_ssl_functions.json"))); 51 | 52 | data.functions.sort((a, b) => { 53 | return a.name.localeCompare(b.name); 54 | }); 55 | 56 | data.constants.sort((a, b) => { 57 | return a.name.localeCompare(b.name); 58 | }); 59 | 60 | data.functions.forEach((func) => { 61 | ts += layout(`type ${func.name}_type = ${func.type};`); 62 | }); 63 | 64 | ts += ` 65 | 66 | export default class NrdpBoundSSLFunctions { 67 | /* tslint:disable:variable-name */ 68 | `; 69 | 70 | data.functions.forEach((func) => { 71 | ts += layout(` public ${func.name}: ${func.name}_type`); 72 | }); 73 | 74 | ts += "\n"; 75 | data.constants.filter(x => !x.disabled).forEach((constant) => { 76 | if (typeof constant.value === "number") { 77 | ts += layout(` public readonly ${constant.name} = ${constant.value};`); 78 | } else { 79 | const value = BigInt(constant.value); 80 | if (value > Number.MAX_SAFE_INTEGER) { 81 | ts += layout(` public readonly ${constant.name} = \"${constant.value}\";`); 82 | } else { 83 | ts += layout(` public readonly ${constant.name} = ${constant.value};`); 84 | } 85 | } 86 | }); 87 | 88 | ts += ` 89 | constructor() { 90 | `; 91 | 92 | data.functions.forEach((func) => { 93 | ts += layout(` this.${func.name} = N.bindFunction<${func.name}_type>(\"${func.signature}\");`); 94 | }); 95 | 96 | ts += ` } 97 | }; 98 | `; 99 | 100 | // this.BIO_ctrl = N.bindFunction("long BIO_ctrl(BIO *bp, int cmd, long larg, void *parg);"); 101 | 102 | try { 103 | fs.writeFileSync(outputFile, ts); 104 | } catch (err) { 105 | console.error("Failed to write file", err); 106 | process.exit(1); 107 | } 108 | -------------------------------------------------------------------------------- /src/HTTP2/frame/frame-writer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FrameType, 3 | Flag 4 | } from './types'; 5 | 6 | import * as FrameUtils from "./utils"; 7 | 8 | const FRAME_HEADER_SIZE = FrameUtils.FRAME_HEADER_SIZE; 9 | const tmpBuffer = new Uint8Array(4); 10 | const tmpView = new DataView(tmpBuffer.buffer); 11 | 12 | /* FRAME HEADER 13 | +-----------------------------------------------+ 14 | | Length (24) | 15 | +---------------+---------------+---------------+ 16 | | Type (8) | Flags (8) | 17 | +-+-------------+---------------+-------------------------------+ 18 | |R| Stream Identifier (31) | 19 | +=+=============================================================+ 20 | | Frame Payload (0...) ... 21 | +---------------------------------------------------------------+ 22 | */ 23 | /* DATA FRAME PAYLOAD PADDING PADDED 24 | +---------------+ 25 | |Pad Length? (8)| 26 | +---------------+-----------------------------------------------+ 27 | | Data (*) ... 28 | +---------------------------------------------------------------+ 29 | | Padding (*) ... 30 | +---------------------------------------------------------------+ 31 | */ 32 | 33 | export default class FrameWriter { 34 | public buffer: Uint8Array; 35 | public view: DataView; 36 | 37 | private ptr: number; 38 | 39 | constructor(length: number, streamIdentifier: number, type: FrameType) { 40 | this.buffer = new Uint8Array(length + FRAME_HEADER_SIZE); 41 | this.view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength); 42 | this.ptr = FRAME_HEADER_SIZE; 43 | 44 | FrameUtils.setLength(this.buffer, length); 45 | FrameUtils.setType(this.buffer, type); 46 | FrameUtils.zeroFlags(this.buffer); 47 | FrameUtils.setStreamIdentifier(this.buffer, streamIdentifier); 48 | } 49 | 50 | write8(item: number) { 51 | this.buffer[this.ptr++] = item; 52 | } 53 | 54 | write16(item: number) { 55 | this.view.setUint16(this.ptr, item); 56 | this.ptr += 2; 57 | } 58 | 59 | write32(item: number) { 60 | this.view.setUint32(this.ptr, item); 61 | this.ptr += 4; 62 | } 63 | 64 | write(item: Uint8Array) { 65 | this.buffer.set(item, this.ptr); 66 | this.ptr += item.byteLength; 67 | } 68 | 69 | addFlag(flag: Flag) { 70 | FrameUtils.setFlags(this.buffer, flag); 71 | } 72 | 73 | hasPriorityFlag(): boolean { 74 | return !!( 75 | this.buffer[FrameUtils.FRAME_HEADER_FLAGS_OFFSET] & Flag.PRIORITY); 76 | } 77 | 78 | addPadding(padding: number) { 79 | if (padding === 0) { 80 | return; 81 | } 82 | 83 | if (this.ptr !== FRAME_HEADER_SIZE) { 84 | throw new Error("You must set the padding flag before setting any data on the payload."); 85 | } 86 | 87 | this.buffer[this.ptr++] = padding; 88 | 89 | FrameUtils.setFlags(this.buffer, Flag.PADDED); 90 | this.buffer.fill(0, this.buffer.byteLength - padding); 91 | } 92 | } 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/NetworkPipe.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "./EventEmitter"; 2 | import DataBuffer from "./DataBuffer"; 3 | import IDataBuffer from "./IDataBuffer"; 4 | import IPlatform from "./IPlatform"; 5 | import assert from './utils/assert.macro'; 6 | 7 | let pipeId = 0; 8 | export abstract class NetworkPipe extends EventEmitter { 9 | protected platform: IPlatform; 10 | private stashBuffer?: IDataBuffer; 11 | 12 | constructor(platform: IPlatform) { 13 | super(); 14 | this.idle = false; 15 | if (++pipeId === 2147483647) { 16 | pipeId = 1; 17 | } 18 | this.id = pipeId; 19 | this.platform = platform; 20 | this.forbidReuse = false; 21 | this.freshConnect = false; 22 | } 23 | 24 | // concrete properties 25 | idle: boolean; 26 | forbidReuse: boolean; 27 | freshConnect: boolean; 28 | 29 | readonly id: number; 30 | 31 | // abstract properties 32 | abstract bytesRead: number; 33 | abstract bytesWritten: number; 34 | abstract hostname: string; 35 | abstract port: number; 36 | abstract readonly ipAddress: string; 37 | abstract socket: number; // socket 38 | abstract firstByteWritten: number; 39 | abstract firstByteRead: number; 40 | 41 | abstract readonly ssl: boolean; 42 | abstract readonly closed: boolean; 43 | 44 | // abstract methods 45 | abstract removeEventHandlers(): void; 46 | abstract write(buf: IDataBuffer | Uint8Array | ArrayBuffer | string, offset: number, length: number): void; 47 | abstract write(buf: string): void; 48 | abstract read(buf: ArrayBuffer | IDataBuffer, offset: number, length: number): number; 49 | 50 | abstract close(): void; 51 | abstract clearStats(): void; 52 | 53 | // concrete methods 54 | stash(buf: ArrayBuffer | Uint8Array | IDataBuffer, offset: number, length: number): void { 55 | if (length === undefined) { 56 | length = buf.byteLength - offset; 57 | } 58 | assert(length > 0, "Must have length"); 59 | if (this.stashBuffer) { 60 | this.stashBuffer.bufferLength = this.stashBuffer.bufferLength + length; 61 | this.stashBuffer.set(this.stashBuffer.bufferLength - length, buf, offset, length); 62 | } else if (buf instanceof DataBuffer) { 63 | this.stashBuffer = buf.subarray(offset, length); 64 | } else { 65 | this.stashBuffer = new DataBuffer(buf, offset, length); 66 | } 67 | } 68 | 69 | unstash(buf: IDataBuffer | ArrayBuffer | Uint8Array, offset: number, length: number): number { 70 | if (this.stashBuffer) { 71 | const byteLength = this.stashBuffer.byteLength; 72 | if (length >= byteLength) { 73 | this.platform.bufferSet(buf, offset, this.stashBuffer, 0, byteLength); 74 | this.stashBuffer = undefined; 75 | return byteLength; 76 | } 77 | 78 | this.platform.bufferSet(buf, offset, this.stashBuffer, 0, length); 79 | this.stashBuffer.setView(length, this.stashBuffer.byteLength - length); 80 | this.platform.log("NetworkPipe#unstash#PARTIAL", offset, length, this.stashBuffer.byteLength); 81 | return length; 82 | } 83 | return -1; 84 | } 85 | 86 | hasStash() { 87 | return !!this.stashBuffer; 88 | } 89 | }; 90 | 91 | export default NetworkPipe; 92 | -------------------------------------------------------------------------------- /tests/rmvsmilo.js: -------------------------------------------------------------------------------- 1 | /*global nrdp*/ 2 | 3 | var concurrentReqs = 10; 4 | var totalReqs = 10; 5 | var url = "https://secure.netflix.com/us/tvui/ql/patch/20200403_13751/2/release/darwinBootstrap.js?dh=720&q=&dw=1280&dar=16_9®=true&getMainUrlFromCodex=true&taskDefaultTimeoutV2=120000&bootloader_trace=apiusernotnull__false&nq=true&nq_control_tag=tvui-main"; 6 | 7 | function doIt(milo) 8 | { 9 | return new Promise(function(resolve, reject) { 10 | var startStart = Date.now(); 11 | var urlidx = 0; 12 | var active = 0; 13 | var results = { 14 | responses: [], 15 | totalTotal: 0, 16 | bytes: 0, 17 | sslSessionResumed: 0, 18 | socketReused: 0 19 | }; 20 | 21 | function send() { 22 | var start = Date.now(); 23 | var opts = { 24 | milo: milo, 25 | cache: "no-cache", 26 | url: url + "&foobar=" + ++urlidx, 27 | tlsv13: true, 28 | // headers: { 29 | // "Accept-Encoding": "identity" 30 | // } 31 | }; 32 | var idx = results.responses.length; 33 | results.responses.push(undefined); 34 | 35 | // nrdp.l("calling it", arg.milo ? "milo" : "rm", totalReqs * concurrentReqs, results.length); 36 | // arg.url = url + "&foobar=" + ++urlidx; 37 | ++active; 38 | nrdp.gibbon.load(opts, function(response) { 39 | var end = Date.now(); 40 | if (response.statusCode !== 200) { 41 | reject(new Error("Bad status " + response.statusCode)); 42 | return; 43 | } 44 | results.responses[idx] = end - start; 45 | results.bytes += response.data.length; 46 | if (response.sslSessionResumed) 47 | ++results.resumptions; 48 | if (response.socketReused) 49 | ++results.socketReused; 50 | --active; 51 | if (results.responses.length < totalReqs * concurrentReqs) { 52 | send(); 53 | } else if (!active) { 54 | var endEnd = Date.now(); 55 | results.totalTotal = endEnd - startStart; 56 | resolve(results); 57 | } 58 | }); 59 | } 60 | for (var i=0; i void): number { 15 | if (typeof data === "string") 16 | data = { url: data }; 17 | const url = data.url; 18 | const req = new Request(data); 19 | req.send().then(response => { 20 | if (!callback) 21 | return; 22 | try { 23 | callback(response); 24 | } catch (err) { 25 | Platform.error("Got error from callback", err); 26 | } 27 | }).catch((error: Error) => { 28 | Platform.error("Got error", error); 29 | if (callback) { 30 | assert(typeof data === "object", "This must be an object"); 31 | const resp = new RequestResponse(req.id, url); 32 | resp.errorString = error.message; 33 | if (error instanceof NetworkError) { 34 | resp.nativeErrorCode = error.code; 35 | } else { 36 | resp.nativeErrorCode = NetworkErrorCode.UnknownError; 37 | assert(error instanceof Error, "This should be an error"); 38 | } 39 | resp.errorString = error.message; 40 | callback(resp); 41 | } 42 | }); 43 | return req.id; 44 | } 45 | 46 | let wsIdx = 0; 47 | function ws(url: string, useMilo: boolean): Promise { 48 | if (useMilo) { 49 | return new Promise((res) => { 50 | // @ts-ignore 51 | const websocket = new WS(url); 52 | const id = ++wsIdx; 53 | const ret = { 54 | send: websocket.send.bind(ws), 55 | onmessage: (event: any) => { Platform.trace("got event", event); } 56 | }; 57 | 58 | websocket.onmessage = (buffer: IDataBuffer) => { 59 | if (ret.onmessage) { 60 | ret.onmessage({ 61 | type: "message", 62 | websocket: id, 63 | opcode: 2, 64 | statusCode: 1000, 65 | buffer, 66 | binary: true 67 | }); 68 | }; 69 | }; 70 | 71 | websocket.onopen = () => { 72 | // who did this? 73 | // @ts-ignore 74 | res(ret); 75 | }; 76 | }); 77 | } else { 78 | return new Promise((resolve, reject) => { 79 | // @ts-ignore 80 | const websocket = new nrdp.WebSocket(url); 81 | websocket.onopen = () => { 82 | resolve(websocket); 83 | }; 84 | websocket.onerror = (err: any) => { 85 | reject(err); 86 | }; 87 | }); 88 | } 89 | } 90 | 91 | const milo: IMilo = { 92 | load, 93 | ws, 94 | platform: Platform, 95 | // @ts-ignore 96 | headerValue 97 | }; 98 | 99 | if (!Platform.loadMilo(milo)) { 100 | return; 101 | } 102 | 103 | })(); 104 | 105 | // Note: If we wish to use this library as a third party, it needs to export 106 | // WS and platform 107 | export { 108 | Platform, 109 | WS, 110 | } 111 | -------------------------------------------------------------------------------- /tests/test-client-nrdp.js: -------------------------------------------------------------------------------- 1 | /* global runTests */ 2 | var root = nrdp.gibbon.location; 3 | var idx = root.lastIndexOf("test-client-nrdp.js"); 4 | var testClient = root.substr(0, idx) + "test-client.js"; 5 | nrdp.gibbon.loadScript({url: testClient, async: false}); 6 | 7 | nrdp.l(nrdp.js_options); 8 | var http = nrdp.js_options.MILO_TEST_HTTP_SERVER || "http://milo.netflix.com:60000"; 9 | var https = nrdp.js_options.MILO_TEST_HTTPS_SERVER || "https://milo.netflix.com:60001"; 10 | if (http === "0") 11 | http = undefined; 12 | if (https === "0") 13 | https = undefined; 14 | 15 | var testNrdp = nrdp.js_options.MILO_TEST_NRDP !== "0"; 16 | var testMilo = nrdp.js_options.MILO_TEST_MILO !== "0"; 17 | 18 | function fixReq(req, milo) { 19 | req.milo = milo; 20 | req.ipAddresses = [ "127.0.0.1" ]; 21 | req.cache = "no-cache"; 22 | req.dnsType = 5; 23 | req.dnsName = "milo.netflix.com"; 24 | return req; 25 | } 26 | 27 | var names = []; 28 | var times = []; 29 | 30 | Promise.resolve(). 31 | then(function() { 32 | if (!testNrdp || !https) 33 | return undefined; 34 | names.push("Resource manager https"); 35 | times.push(nrdp.mono()); 36 | // return undefined; 37 | return runTests({ 38 | name: names[names.length - 1], 39 | server: https, 40 | load: function(req, cb) { 41 | nrdp.gibbon.load(fixReq(req, false), cb); 42 | }, 43 | log: nrdp.l.bind(nrdp) 44 | }); 45 | }). 46 | then(function() { 47 | if (!testNrdp || !http) 48 | return undefined; 49 | 50 | names.push("Resource manager http"); 51 | var now = nrdp.mono(); 52 | if (times.length) 53 | times[times.length - 1] = now - times[times.length - 1]; 54 | times.push(now); 55 | // return undefined; 56 | return runTests({ 57 | name: names[names.length - 1], 58 | server: http, 59 | load: function(req, cb) { 60 | nrdp.gibbon.load(fixReq(req, false), cb); 61 | }, 62 | log: nrdp.l.bind(nrdp) 63 | }); 64 | }). 65 | then(function() { 66 | if (!testMilo || !https) 67 | return undefined; 68 | 69 | names.push("Milo https"); 70 | var now = nrdp.mono(); 71 | if (times.length) 72 | times[times.length - 1] = now - times[times.length - 1]; 73 | times.push(now); 74 | // return undefined; 75 | return runTests({ 76 | name: names[names.length - 1], 77 | server: https, 78 | load: function(req, cb) { 79 | nrdp.gibbon.load(fixReq(req, true), cb); 80 | }, 81 | log: nrdp.l.bind(nrdp) 82 | }); 83 | }). 84 | then(function() { 85 | if (!testMilo || !http) 86 | return undefined; 87 | 88 | names.push("Milo http"); 89 | var now = nrdp.mono(); 90 | if (times.length) 91 | times[times.length - 1] = now - times[times.length - 1]; 92 | times.push(now); 93 | return runTests({ 94 | name: names[names.length - 1], 95 | server: http, 96 | load: function(req, cb) { 97 | nrdp.gibbon.load(fixReq(req, true), cb); 98 | }, 99 | log: nrdp.l.bind(nrdp) 100 | }); 101 | }). 102 | then(function() { 103 | var now = nrdp.mono(); 104 | times[times.length - 1] = now - times[times.length - 1]; 105 | nrdp.l("All tests ran successfully"); 106 | for (var i=0; i void): void; 14 | } 15 | 16 | namespace device { 17 | let UILanguages: string[]; 18 | let ipConnectivityMode: "4" | "6" | "dual" | "invalid"; 19 | let tlsv13SmallAssetsEnabled: boolean; 20 | let tlsv13StreamingEnabled: boolean; 21 | const SDKVersion: { [key: string]: any }; 22 | const ESNPrefix: string; 23 | const certificationVersion: number; 24 | } 25 | 26 | namespace gibbon { 27 | let location: string; 28 | let load: (req: IRequestData | string, callback?: (response: RequestResponse) => void) => number; 29 | let loadScript: (req: IRequestData | string, callback?: (response: RequestResponse) => void) => number; 30 | let stopLoad: (id: number) => void; 31 | const eval: (data: string | Uint8Array | IDataBuffer | ArrayBuffer, url: string) => any; 32 | } 33 | 34 | namespace resourcemanager { 35 | function processCookies(url: string, value: string | string[]): void; 36 | function cookies(url: string, flags?: number): string; 37 | } 38 | 39 | namespace l { 40 | function success(...args: any[]): void; 41 | function error(...args: any[]): void; 42 | function trace(...args: any[]): void; 43 | } 44 | 45 | namespace options { 46 | /* tslint:disable:variable-name */ 47 | const default_network_connect_timeout: number; 48 | const default_network_delay: number; 49 | const default_network_dns_fallback_timeout_wait_for_4: number; 50 | const default_network_dns_fallback_timeout_wait_for_6: number; 51 | const default_network_dns_timeout: number; 52 | const default_network_happy_eyeballs_head_start: number; 53 | const default_network_low_speed_limit: number; 54 | const default_network_low_speed_time: number; 55 | const default_network_max_recv_speed: number; 56 | const default_network_max_send_speed: number; 57 | const default_network_timeout: number; 58 | const ssl_peer_verification: boolean; 59 | const send_secure_cookies: boolean; 60 | } 61 | 62 | function exit(exitCode?: number): void; 63 | function stacktrace(): string; 64 | function now(): number; 65 | const trustStoreHash: string; 66 | let trustStore: ArrayBuffer; 67 | 68 | let cipherList: string; 69 | 70 | function mono(): number; 71 | 72 | function assert(cond: any, message: string): void; 73 | function atoutf8(input: Uint8Array | ArrayBuffer | IDataBuffer | string): Uint8Array; 74 | function utf8toa(input: Uint8Array | ArrayBuffer | IDataBuffer | string, offset?: number, length?: number): string; 75 | function hash(type: string, data: Uint8Array | ArrayBuffer | IDataBuffer | string): Uint8Array; 76 | function btoa(buffer: Uint8Array | ArrayBuffer | IDataBuffer | string, returnUint8Array: true): Uint8Array; 77 | /* tslint:disable:unified-signatures */ 78 | function btoa(buffer: Uint8Array | ArrayBuffer | IDataBuffer | string, returnUint8Array: false | undefined): string; 79 | function btoa(buffer: Uint8Array | ArrayBuffer | IDataBuffer | string): string; 80 | function atob(buffer: Uint8Array | ArrayBuffer | IDataBuffer | string, returnUint8Array: true): Uint8Array; 81 | function atob(buffer: Uint8Array | ArrayBuffer | IDataBuffer | string, returnUint8Array: false | undefined): string; 82 | function atob(buffer: Uint8Array | ArrayBuffer | IDataBuffer | string): string; 83 | 84 | const js_options: any; 85 | 86 | let milo: IMilo | undefined; 87 | } 88 | -------------------------------------------------------------------------------- /src/ws/buffer.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from "../DataBuffer"; 2 | import IDataBuffer from "../IDataBuffer"; 3 | 4 | const r = "\r".charCodeAt(0); 5 | const n = "\n".charCodeAt(0); 6 | const newLine = [r, n]; 7 | const space = " ".charCodeAt(0); 8 | const colon = ":".charCodeAt(0); 9 | const contentLength = "content-length".split('').map(x => x.charCodeAt(0)); 10 | 11 | const NotFound = -1; 12 | 13 | export function parse64BigInt(buffer: Uint8Array, offset: number): BigInt { 14 | throw new Error('Cannot have a 4GB packet rook.'); 15 | // @ts-ignore 16 | // TODO michael fix me 17 | return BigInt(`0x${buffer.subarray(offset, offset + 8).toString('hex')}`); 18 | }; 19 | 20 | export class BufferPool { 21 | private pool: IDataBuffer[]; 22 | private size: number; 23 | 24 | constructor(size: number) { 25 | this.pool = []; 26 | this.size = size; 27 | } 28 | 29 | malloc() { 30 | if (this.pool.length === 0) { 31 | this.pool.push(new DataBuffer(this.size)); 32 | } 33 | 34 | return this.pool.pop(); 35 | } 36 | 37 | free(buffer: IDataBuffer) { 38 | this.pool.push(buffer); 39 | } 40 | 41 | } 42 | 43 | export interface BufferBuilderInterface { 44 | length: () => number; 45 | getBuffer: () => Uint8Array; 46 | addString: (str: string) => void; 47 | addNewLine: () => void; 48 | clear: () => void; 49 | }; 50 | 51 | class BufferBuilder implements BufferBuilderInterface { 52 | private ptr: number; 53 | private buffer: Uint8Array; 54 | 55 | constructor(buf: Uint8Array | number = 4096) { 56 | this.ptr = 0; 57 | if (typeof buf === 'number') { 58 | this.buffer = new Uint8Array(buf); 59 | } 60 | 61 | else { 62 | this.buffer = buf; 63 | } 64 | } 65 | 66 | length() { 67 | return this.ptr; 68 | } 69 | 70 | getBuffer() { 71 | return this.buffer; 72 | } 73 | 74 | addString(str: string) { 75 | for (let i = 0; i < str.length; ++i) { 76 | this.buffer[this.ptr++] = str.charCodeAt(i); 77 | } 78 | } 79 | 80 | addNewLine() { 81 | this.buffer[this.ptr++] = r; 82 | this.buffer[this.ptr++] = n; 83 | } 84 | 85 | clear() { 86 | this.ptr = 0; 87 | } 88 | } 89 | 90 | export function createBufferBuilder(buf: Uint8Array | number = 4096): BufferBuilderInterface { 91 | return new BufferBuilder(buf); 92 | }; 93 | 94 | export function getCharacterIdx(buf: Uint8Array, needle: number, offset: number, maxLength?: number) { 95 | let idx = NotFound; 96 | maxLength = maxLength || buf.length; 97 | for (let i = offset; idx === NotFound && i < maxLength; ++i) { 98 | if (buf[i] === needle) { 99 | idx = i; 100 | } 101 | } 102 | 103 | return idx; 104 | } 105 | 106 | export function getColonIdx(buf: Uint8Array, offset: number, maxLength: number): number { 107 | return getCharacterIdx(buf, colon, offset, maxLength); 108 | } 109 | 110 | export function getSpaceIdx(buf: Uint8Array, offset: number) { 111 | return getCharacterIdx(buf, space, offset); 112 | } 113 | 114 | export { 115 | NotFound, 116 | r, 117 | n, 118 | }; 119 | 120 | export function getEndLineOffset(buf: Uint8Array, offset: number, maxLength: number): number { 121 | let i = offset; 122 | let found = false; 123 | 124 | for (; i < maxLength; ++i) { 125 | if (buf[i] === r && 126 | buf[i + 1] === n) { 127 | 128 | found = true; 129 | break; 130 | } 131 | } 132 | 133 | return found ? i : -1; 134 | } 135 | 136 | export function getHTTPHeaderEndOffset(buf: Uint8Array, offset: number, maxLength: number): number { 137 | let i = offset; 138 | let found = false; 139 | 140 | for (; i < maxLength; ++i) { 141 | if (buf[i] === r && 142 | buf[i + 1] === n && 143 | buf[i + 2] === r && 144 | buf[i + 3] === n) { 145 | 146 | found = true; 147 | break; 148 | } 149 | } 150 | 151 | return found ? i : -1; 152 | }; 153 | 154 | -------------------------------------------------------------------------------- /src/IPlatform.ts: -------------------------------------------------------------------------------- 1 | import ConnectionPool from "./ConnectionPool"; 2 | import ICompressionStream from "./ICompressionStream"; 3 | import ICreateSSLNetworkPipeOptions from "./ICreateSSLNetworkPipeOptions"; 4 | import ICreateTCPNetworkPipeOptions from "./ICreateTCPNetworkPipeOptions"; 5 | import IDataBuffer from "./IDataBuffer"; 6 | import IDnsResult from "./IDnsResult"; 7 | import IMilo from "./IMilo"; 8 | import IPipeResult from "./IPipeResult"; 9 | import IRequestTimeouts from "./IRequestTimeouts"; 10 | import ISHA256Context from "./ISHA256Context"; 11 | import Url from "url-parse"; 12 | // import { CookieAccessInfo, CookieJar } from "cookiejar"; 13 | import { IpConnectivityMode, IpVersion, CompressionStreamType, CompressionStreamMethod } from "./types"; 14 | 15 | type ArrayBufferConcatType = Uint8Array | IDataBuffer | ArrayBuffer; 16 | 17 | export default interface IPlatform { 18 | // return number of octets 19 | utf8Length(str: string): number; 20 | 21 | sha1(input: string): Uint8Array; 22 | // base64 encode 23 | btoa(buffer: Uint8Array | ArrayBuffer | IDataBuffer | string, returnUint8Array: true): Uint8Array; 24 | btoa(buffer: Uint8Array | ArrayBuffer | IDataBuffer | string, returnUint8Array?: false): string; 25 | 26 | // base64 decode 27 | atob(buffer: Uint8Array | ArrayBuffer | IDataBuffer | string, returnUint8Array: true): Uint8Array; 28 | atob(buffer: Uint8Array | ArrayBuffer | IDataBuffer | string, returnUint8Array?: false): string; 29 | 30 | // string to uint8array 31 | atoutf8(input: Uint8Array | ArrayBuffer | IDataBuffer | string): Uint8Array; 32 | 33 | // uint8array to string 34 | utf8toa(input: IDataBuffer | Uint8Array | ArrayBuffer | IDataBuffer | string, 35 | offset?: number, length?: number): string; 36 | 37 | bufferSet(dest: Uint8Array | ArrayBuffer | IDataBuffer, 38 | destOffset: number, 39 | src: Uint8Array | ArrayBuffer | IDataBuffer, 40 | srcOffset?: number, 41 | srcLength?: number): void; 42 | 43 | bufferSet(dest: Uint8Array | ArrayBuffer | IDataBuffer, 44 | destOffset: number, 45 | src: string): void; 46 | 47 | arrayBufferConcat(...buffers: ArrayBufferConcatType[]): ArrayBuffer; 48 | randomBytes(len: number): Uint8Array 49 | 50 | writeFile(fileName: string, 51 | contents: Uint8Array | ArrayBuffer | IDataBuffer | string): boolean; 52 | appendFile(fileName: string, 53 | contents: Uint8Array | ArrayBuffer | IDataBuffer | string): boolean; 54 | stacktrace(): string; 55 | 56 | trace(...args: any[]): void; 57 | log(...args: any[]): void; 58 | error(...args: any[]): void; 59 | 60 | mono(): number; 61 | 62 | serverTime(): number | undefined; // can return undefined if it hasn't been validated yet 63 | 64 | readonly ipConnectivityMode: IpConnectivityMode; 65 | readonly sendSecureCookies: boolean; 66 | 67 | readonly standardHeaders: { [key: string]: string }; 68 | 69 | readonly tlsv13SmallAssetsEnabled: boolean; 70 | readonly tlsv13StreamingEnabled: boolean; 71 | createTCPNetworkPipe(options: ICreateTCPNetworkPipeOptions): Promise; 72 | createSSLNetworkPipe(options: ICreateSSLNetworkPipeOptions): Promise; 73 | createSHA256Context(): ISHA256Context; 74 | createCompressionStream(method: CompressionStreamMethod, 75 | compress: boolean | CompressionStreamType): ICompressionStream; 76 | 77 | lookupDnsHost(host: string, 78 | ipVersion: IpVersion, 79 | timeout: number, 80 | callback: (result: IDnsResult) => void): void; 81 | 82 | readonly UILanguages: string[]; 83 | readonly location: string; 84 | readonly scratch: IDataBuffer; 85 | 86 | readonly connectionPool: ConnectionPool; 87 | // readonly cookieJar: CookieJar; 88 | // readonly cookieAccessInfo: CookieAccessInfo; 89 | cookies(url: Url): string | undefined; 90 | processCookies(url: Url, value: string | string[]): void; 91 | readonly defaultRequestTimeouts: IRequestTimeouts; 92 | 93 | quit(exitCode?: number): void; 94 | 95 | parseXML(data: string | IDataBuffer): any; 96 | parseJSONStream(data: string | IDataBuffer): any[] | undefined; 97 | parseJSON(data: string | IDataBuffer): any | undefined; 98 | 99 | options(key: string): any; 100 | 101 | loadMilo(milo: IMilo): boolean; 102 | }; 103 | -------------------------------------------------------------------------------- /src/HTTP2/hpack/internals/header-table.ts: -------------------------------------------------------------------------------- 1 | import getStaticList from "./static-header-list"; 2 | import Platform from "../../../Platform"; 3 | import UnorderedMap from "../../../UnorderedMap"; 4 | import IUnorderedMap from "../../../IUnorderedMap"; 5 | 6 | export type Name = string; 7 | export type NameValue = { name: string, value: string }; 8 | 9 | export type HeaderField = { 10 | name: string, 11 | value: string | null, 12 | 13 | // TODO: Read section 8 14 | }; 15 | 16 | export interface HeaderTable { 17 | setSize(sizeInBytes: number): void; 18 | insert(key: string, value: string | null): void; 19 | 20 | // TODO: seems like a possible decode error 21 | getName(idx: number): string; 22 | getNameAndValue(idx: number): NameValue; 23 | } 24 | 25 | const STATIC_TABLE_SIZE = 62; 26 | export default class DynamicTable implements HeaderTable { 27 | 28 | // TODO: I assume that this definitely has a type, just what are they. 29 | private byName: IUnorderedMap; 30 | private byNameAndValue: IUnorderedMap>; 31 | private byIdx: IUnorderedMap; 32 | 33 | private initializing: boolean; 34 | private maxSize: number; 35 | private currentSize: number; 36 | 37 | private insertCount: number; 38 | 39 | constructor(maxSize: number) { 40 | this.byName = new UnorderedMap(); 41 | this.byNameAndValue = new UnorderedMap(); 42 | this.byIdx = new UnorderedMap(); 43 | this.initializing = true 44 | this.maxSize = maxSize; 45 | this.insertCount = 0; 46 | this.currentSize = 0; 47 | 48 | const staticList = getStaticList(); 49 | for (const items of staticList) { 50 | this.insert(items[0], items[1]); 51 | } 52 | 53 | this.initializing = false; 54 | } 55 | 56 | setSize(newSize: number): void { 57 | this.maxSize = newSize; 58 | this.resize(0); 59 | } 60 | 61 | getNameAndValue(idx: number): NameValue { 62 | const out = this.byIdx.get(idx); 63 | 64 | if (!out) { 65 | throw new Error(`Decode Error, missing index in dynamic table: idx ${idx}`); 66 | } 67 | 68 | if (typeof out === 'string') { 69 | throw new Error(`Decode Error, name when name-value pair was requested: ${idx}.`); 70 | } 71 | 72 | return out; 73 | } 74 | 75 | getName(idx: number): string { 76 | const out = this.byIdx.get(idx); 77 | 78 | if (!out) { 79 | throw new Error(`Decode Error, missing index in dynamic table: idx ${idx}`); 80 | } 81 | 82 | if (typeof out !== 'string') { 83 | throw new Error(`Decode Error, name-value pair when name was requested: ${idx}.`); 84 | } 85 | 86 | return out; 87 | } 88 | 89 | insert(key: string, value: string | null): number { 90 | 91 | const id = this.insertCount++ + 1; 92 | 93 | // Adjust the table size 94 | const size = Platform.utf8Length(key) + 95 | (value && Platform.utf8Length(value) || 0); 96 | this.resize(size); 97 | 98 | // no insert is made. 99 | // todo: should we log this? 100 | if (this.maxSize < size) { 101 | return -1; 102 | } 103 | 104 | // Name only insert 105 | if (value === null) { 106 | this.byName.set(key, id); 107 | this.byIdx.set(id, key); 108 | } 109 | else { 110 | 111 | // TODO: It already exists, should our library ever run into this 112 | // case? 113 | let valueMap = this.byNameAndValue.get(key); 114 | if (!valueMap) { 115 | valueMap = new UnorderedMap(); 116 | this.byNameAndValue.set(key, valueMap); 117 | } 118 | 119 | valueMap.set(value, id); 120 | this.byIdx.set(id, { name: key, value }); 121 | } 122 | 123 | if (!this.initializing) { 124 | this.currentSize += size; 125 | } 126 | 127 | return id; 128 | } 129 | 130 | private resize(sizeAdded: number) { 131 | throw new Error("This needs to be implemented right meow"); 132 | 133 | // this sucks, can we do better? 134 | const keys = new Array(this.byIdx.keys()).reverse(); 135 | const max = keys.length - 1; 136 | 137 | // for (let i = max; i >= 0 && this.currentSize > this.maxSize; ++i) { 138 | // } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type CompressionMethod = "zlib" | "zlibbase64" | "zlibgzip" | "lzham" | "lz4"; 2 | export type DnsType = "lookup" | "cache" | "literal" | "hostsFile" | "unknown" | "preresolved"; 3 | export type EncodingType = "escaped" | "base32" | "base64" | "base64_urlsafe" | "base85" | "url" | "hex" | "utf8"; 4 | export type HTTPMethod = "POST" | "HEAD" | "PUT" | "DELETE" | "PATCH" | "GET"; 5 | export type HTTPRequestHeaders = { [key: string]: any }; 6 | export type HashType = "sha1" | "sha256" | "sha512" | "md5"; 7 | export type IpConnectivityMode = 4 | 6 | 10 | 0; // 0 is invalid, 10 is dual 8 | export type IpVersion = 4 | 6; 9 | export type CompressionStreamMethod = "zlib" | "gzip"; 10 | export type CompressionStreamType = "compress" | "uncompress"; 11 | 12 | export const enum ErrorCode { 13 | None = 0 14 | }; 15 | 16 | export type EventListenerCallback = (...args: any[]) => void; 17 | 18 | export const enum RequestId { 19 | Min = 16777216, 20 | Max = 9007199254740991 21 | } 22 | 23 | export const enum CookieFlag { 24 | None = 0x0, 25 | HttpOnly = 0x1, 26 | Trusted = 0x2 27 | }; 28 | 29 | export const enum HTTPEncoding { 30 | Chunked = 1, 31 | Compress = 2, 32 | Deflate = 3, 33 | Gzip = 4, 34 | Identity = 5 35 | }; 36 | 37 | export const enum RequestResponseDnsType { 38 | DNS_Unknown = 0, 39 | DNS_Literal = 1, 40 | DNS_HostsFile = 2, 41 | DNS_Lookup = 3, 42 | DNS_CacheHit = 4, 43 | DNS_Preresolved = 5 44 | }; 45 | 46 | export const enum NetError { 47 | // Should be in sync with NetErrorInternal.h 48 | SUCCESS = 0, 49 | NO_IP_ADDRESS = 1, 50 | CONNECTIVITY_ERROR = 2, 51 | NAMERESOLVEERROR = 3, 52 | SSLERROR = 4, 53 | CRLOCSPERROR = 5, 54 | HTTP_ERROR = 6, 55 | DNS_CHECK = 7, 56 | UNKNOWN_ERROR = 8, 57 | NOTSECUREERROR = 1, 58 | FILEACCESSERROR = 2, 59 | DATAURIERROR = 3, 60 | CONNECT_ERROR = 4, 61 | TIMEOUTERROR = 5, 62 | DNS_ERROR = 6, 63 | SSLHANDSHAKEERROR = 7, 64 | SSLCACERTERROR = 8, 65 | SSLCACERTFILEERROR = 9, 66 | CERTSTATUSSSLERROR = 10, 67 | CERTSTATUSREVOKED = 11, 68 | CERTSTATUSPEWREVOKED = 12, 69 | SENDERROR = 13, 70 | RECVERROR = 14, 71 | COMPRESSIONERROR = 15, 72 | NO_DNS_SERVER = 16, 73 | NETWORKERROR = 17, 74 | SECURITYERROR = 18, 75 | INVALIDHASH_ERROR = 20, 76 | ABORTED = 21, 77 | INVALID_PLATFORMHASH_ERROR = 22, 78 | SSLGENERICERROR = 23 79 | } 80 | 81 | export const enum NetworkErrorCode { 82 | BadContentLength, 83 | BadHeader, 84 | BadStatusLine, 85 | ChunkyError, 86 | ConnectFailure, 87 | ConnectTimeout, 88 | ContentLengthTooLong, 89 | DnsError, 90 | InvalidIpAddress, 91 | InvalidIpVersion, 92 | InvalidUrl, 93 | NotImplemented, 94 | SSLConnectFailure, 95 | SocketReadError, 96 | SocketWriteError, 97 | Timeout, 98 | TooManyRedirects, 99 | UnknownError, 100 | ZeroLengthWrite, 101 | }; 102 | 103 | export function networkErrorCodeToString(code: NetworkErrorCode): string { 104 | switch (code) { 105 | case NetworkErrorCode.BadContentLength: return "Bad content length"; 106 | case NetworkErrorCode.BadHeader: return "Bad header"; 107 | case NetworkErrorCode.BadStatusLine: return "Bad HTTP1x status line"; 108 | case NetworkErrorCode.ChunkyError: return "Chunky error"; 109 | case NetworkErrorCode.ConnectFailure: return "Connect failure"; 110 | case NetworkErrorCode.ConnectTimeout: return "Connect timeout"; 111 | case NetworkErrorCode.ContentLengthTooLong: return "Content length too long"; 112 | case NetworkErrorCode.DnsError: return "Dns error"; 113 | case NetworkErrorCode.InvalidIpAddress: return "Invalid ip address"; 114 | case NetworkErrorCode.InvalidIpVersion: return "Invalid ip version"; 115 | case NetworkErrorCode.InvalidUrl: return "Invalid url"; 116 | case NetworkErrorCode.NotImplemented: return "Not implemented"; 117 | case NetworkErrorCode.SSLConnectFailure: return "SSL connect failure"; 118 | case NetworkErrorCode.SocketReadError: return "Socket read error"; 119 | case NetworkErrorCode.SocketWriteError: return "Socket write error"; 120 | case NetworkErrorCode.Timeout: return "Request timeout"; 121 | case NetworkErrorCode.Timeout: return "Timeout"; 122 | case NetworkErrorCode.TooManyRedirects: return "Too many redirects"; 123 | case NetworkErrorCode.UnknownError: return "Unknown error"; 124 | case NetworkErrorCode.ZeroLengthWrite: return "Zero length write"; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const execa = require('execa'); 3 | const Listr = require('listr'); 4 | 5 | const targetPlatforms = [ 6 | 'node', 7 | 'nrdp', 8 | ]; 9 | 10 | const taskRenderer = process.env.EMACS ? 'verbose' : 'default'; 11 | 12 | require('yargs') 13 | .command({ 14 | command: '$0', 15 | desc: 'Milo build command', 16 | builder: (yargs) => { 17 | yargs 18 | .option('target', { 19 | describe: `Specify a target platform to build for: ${targetPlatforms}`, 20 | type: 'string', 21 | choices: targetPlatforms, 22 | }) 23 | .option('prod', { 24 | describe: 'Create a build stripped out of __DEV__ specific code and minified', 25 | type: 'boolean', 26 | default: false 27 | }); 28 | }, 29 | handler: (argv) => { 30 | const { target, prod } = argv; 31 | 32 | let tasks; 33 | if (target) { 34 | tasks = [getTargetTasks(target, {prod})]; 35 | } else { 36 | /* Build for all the target platforms */ 37 | const buildAllTasks = targetPlatforms.map(t => getTargetTasks(t, {prod})); 38 | tasks = [ 39 | { 40 | title: `build platforms: ${targetPlatforms.join(', ')}`, 41 | task: () => { 42 | return new Listr(buildAllTasks, { concurrent: true }); 43 | } 44 | } 45 | ]; 46 | } 47 | 48 | const hrstart = process.hrtime(); 49 | new Listr(tasks, { 50 | renderer: taskRenderer 51 | }).run() 52 | .then(() => { 53 | const hrend = process.hrtime(hrstart); 54 | console.info("\nAll tasks successfully completed in %ds", hrend[0]); 55 | }) 56 | .catch(err => { 57 | console.error(err); 58 | process.exit(1); 59 | }); 60 | } 61 | }) 62 | .help() 63 | .argv; 64 | 65 | function getTargetTasks(target, {prod}) { 66 | return { 67 | title: `build target: ${target}`, 68 | task: () => { 69 | return new Listr([ 70 | target === 'nrdp' && getNRDPSSLGenerationTask(), 71 | { 72 | title: `lint & compile TS (${target})`, 73 | task: () => { 74 | return new Listr([ 75 | getLintTask(target), 76 | getTscTask(target), 77 | ], { concurrent: true }); 78 | } 79 | }, 80 | getRollupTask(target, {prod}) 81 | ].filter(Boolean)); 82 | } 83 | }; 84 | } 85 | 86 | function getNRDPSSLGenerationTask() { 87 | return { 88 | title: `generate ssl functions`, 89 | task: () => execa('node', [path.join(__dirname, 'generate-ssl-functions.js')], { stdout: 'inherit' }) 90 | }; 91 | } 92 | 93 | function getLintTask(target) { 94 | return { 95 | title: `linting (${target})`, 96 | task: () => execa('node', [path.join(__dirname, 'lint.js')], { stdout: 'inherit' }) 97 | }; 98 | } 99 | 100 | function getTscTask(target) { 101 | const tscConfig = path.resolve( 102 | __dirname, 103 | `builds/tsconfig.${target}.json` 104 | ); 105 | return { 106 | title: `compile typescript (${target})`, 107 | task: () => execa('tsc', ['-p', tscConfig], { stdout: 'inherit' }) 108 | }; 109 | } 110 | 111 | function getRollupTask(target, { prod }) { 112 | const rollupConfig = path.resolve( 113 | __dirname, 114 | `builds/rollup.${target}.js` 115 | ); 116 | const rollupOptions = [ 117 | '-c', 118 | rollupConfig, 119 | ]; 120 | 121 | if (prod) { 122 | rollupOptions.push('--prod'); 123 | } 124 | return { 125 | title: `create bundle (${target})`, 126 | task: () => execa( 127 | 'rollup', 128 | rollupOptions, 129 | { 130 | env: { 131 | NODE_ENV: prod ? "production" : "development", 132 | TARGET_PLATFORM: target 133 | }, 134 | stdout: 'inherit' 135 | } 136 | ) 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /tests/test-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nodejs 2 | 3 | const express = require("express"); 4 | const fs = require("fs"); 5 | const path = require("path"); 6 | const https = require("https"); 7 | const http = require("http"); 8 | const zlib = require("zlib"); 9 | const seedrandom = require("seedrandom"); 10 | const bodyParser = require("body-parser"); 11 | 12 | const argv = require("minimist")(process.argv.slice(2)); 13 | const app = express(); 14 | 15 | // app.use(bodyParser.urlencoded({ extended: true })); 16 | // app.use(bodyParser.json()); 17 | app.use(bodyParser.raw()); 18 | 19 | const options = { 20 | key: fs.readFileSync(path.join(__dirname, "key.pem")), 21 | cert: fs.readFileSync(path.join(__dirname, "cert.pem")) 22 | }; 23 | 24 | const file = argv["ports-file"] || path.join(__dirname, "/test-server.ports"); 25 | const host = argv["server"] || "milo.netflix.com"; 26 | 27 | let startPort = 59999; 28 | const maxPort = 65535; 29 | function createServer(opts) 30 | { 31 | return new Promise((resolve, reject) => { 32 | function tryPort() 33 | { 34 | let server; 35 | const port = ++startPort; 36 | // console.log("trying", port); 37 | if (port > maxPort) { 38 | reject(new Error("Couldn't find a port")); 39 | return; 40 | } 41 | 42 | const id = setTimeout(() => { // timed out 43 | tryPort(); 44 | }, 250); 45 | 46 | try { 47 | if (opts) { 48 | server = https.createServer(opts, app); 49 | } else { 50 | server = http.createServer(app); 51 | } 52 | server.listen(port, () => { 53 | const s = `${opts ? "https" : "http"}://${host}:${port}`; 54 | server.address = s; 55 | console.log("listening on", s); 56 | clearTimeout(id); 57 | resolve(server); 58 | }); 59 | } catch (err) { 60 | tryPort(); 61 | return; 62 | } 63 | 64 | server.on("error", err => { 65 | clearTimeout(id); 66 | tryPort(); 67 | }); 68 | } 69 | tryPort(); 70 | }); 71 | } 72 | let httpServer, httpsServer; 73 | createServer().then(server => { 74 | httpServer = server; 75 | return createServer(options); 76 | }).then(server => { 77 | httpsServer = server; 78 | fs.writeFileSync(file, `MILO_TEST_HTTP_SERVER=${httpServer.address}\nMILO_TEST_HTTPS_SERVER=${httpsServer.address}\n`); 79 | }).catch(err => { 80 | console.error(err); 81 | process.exit(1); 82 | }); 83 | 84 | const table = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n"; 85 | 86 | function handler(req, res, body) { 87 | if (body) 88 | console.log(typeof body, body); //, Object.keys(body.prototype)); 89 | const size = parseInt(req.query.size) || 1024; 90 | let payload = Buffer.allocUnsafe(size); 91 | for (let idx=0; idx= c.expires) { 66 | this.cookieArray.splice(idx--, 1); 67 | continue; 68 | } 69 | } else if (expiresType !== "undefined") { 70 | throw new Error("Bad cookie: Bad expires"); 71 | } 72 | } 73 | } 74 | } 75 | 76 | serialize() { 77 | return JSON.stringify(this.cookieArray); 78 | } 79 | 80 | processCookies(url: Url, setCookie: string) { 81 | // const parts = setCookie.split(";"); 82 | } 83 | 84 | cookies(url: Url, flags?: CookieFlag): string | undefined { 85 | const serverTime = Platform.serverTime(); 86 | 87 | let ret = ""; 88 | const domain = url.hostname; 89 | let path: string | undefined = url.pathname; 90 | if (path === "/") 91 | path = undefined; 92 | for (let idx = 0; idx < this.cookies.length; ++idx) { 93 | const cookie = this.cookieArray[idx]; 94 | if (!domainSuffix(domain, cookie.domain)) 95 | continue; 96 | if (!path) { 97 | if (cookie.path !== "/") 98 | continue; 99 | } else if (path.lastIndexOf(cookie.path, 0) !== 0) { 100 | continue; 101 | } 102 | 103 | if (cookie.secure 104 | && (!flags || !(flags & CookieFlag.Trusted)) 105 | && !Platform.sendSecureCookies 106 | && url.protocol !== "https:") { 107 | continue; 108 | } 109 | 110 | if (serverTime && cookie.expires && serverTime >= cookie.expires) { 111 | this.cookieArray.splice(idx--, 1); 112 | continue; 113 | } 114 | 115 | 116 | if (ret) { 117 | ret += "; "; 118 | } 119 | 120 | 121 | ret += this.toRequestHeader(cookie); 122 | } 123 | 124 | return ret; 125 | } 126 | 127 | private toRequestHeader(cookie: Cookie) { 128 | 129 | 130 | return ""; 131 | } 132 | 133 | } 134 | 135 | -------------------------------------------------------------------------------- /src/HTTP2/frame/frame-constructor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FrameType, 3 | Settings, 4 | } from './types'; 5 | 6 | import * as FrameUtils from "./utils"; 7 | 8 | /* FRAME HEADER 9 | +-----------------------------------------------+ 10 | | Length (24) | 11 | +---------------+---------------+---------------+ 12 | | Type (8) | Flags (8) | 13 | +-+-------------+---------------+-------------------------------+ 14 | |R| Stream Identifier (31) | 15 | +=+=============================================================+ 16 | | Frame Payload (0...) ... 17 | +---------------------------------------------------------------+ 18 | */ 19 | 20 | const enum State { 21 | WaitingOnHeadersLength = 0x1, 22 | WaitingOnHeadersType = 0x2, 23 | WaitingOnHeadersFlags = 0x4, 24 | WaitingOnHeadersStreamIden = 0x8, 25 | ParsingData = 0x10, 26 | Finished = 0x20, 27 | }; 28 | 29 | export const WaitingOnHeaders = State.WaitingOnHeadersLength | State.WaitingOnHeadersType | 30 | State.WaitingOnHeadersFlags | State.WaitingOnHeadersStreamIden; 31 | 32 | const EMPTY_BUFFER = new Uint8Array(1); 33 | 34 | /* #region framecontr */ 35 | export default class FrameConstructor { 36 | 37 | public len: number; 38 | public header: Uint8Array; 39 | 40 | private buffer: Uint8Array; 41 | private state: number; 42 | private headerPtr: number; 43 | private ptr: number; 44 | 45 | constructor() { 46 | this.len = this.headerPtr = this.state = this.ptr = 0; 47 | this.header = new Uint8Array(FrameUtils.FRAME_HEADER_SIZE); 48 | this.buffer = EMPTY_BUFFER; 49 | this.reset(); 50 | } 51 | 52 | isFinished(): boolean { 53 | return this.state === State.Finished; 54 | } 55 | 56 | /** 57 | * parses the data and returns the amount of bytes that it parsed. 58 | */ 59 | parse(buffer: Uint8Array, offset: number = 0, length?: number): number { 60 | if (length === undefined) { 61 | length = buffer.byteLength; 62 | } 63 | 64 | let ptr = offset; 65 | if (this.state < State.ParsingData) { 66 | ptr += this.parseHeader(buffer, offset, length); 67 | } 68 | 69 | if (this.state === State.ParsingData) { 70 | if (this.buffer === EMPTY_BUFFER) { 71 | this.buffer = new Uint8Array(this.len); 72 | } 73 | 74 | const byteCount = 75 | Math.min(length - (ptr - offset), this.len - this.ptr); 76 | 77 | this.buffer.set(buffer.subarray(ptr, ptr + byteCount)); 78 | this.ptr += byteCount; 79 | ptr += byteCount; 80 | 81 | if (this.ptr === this.len) { 82 | this.state = State.Finished; 83 | } 84 | } 85 | 86 | return ptr - offset; 87 | } 88 | 89 | /** 90 | * Gets the length of the frame. If the frame hasn't finished parsing the 91 | * header, it will return a length of -1. 92 | */ 93 | public getLength(): number { 94 | if (this.state < State.ParsingData) { 95 | return -1; 96 | } 97 | 98 | return this.len; 99 | } 100 | 101 | public isSettingsFrame(): boolean { 102 | if (this.state < State.ParsingData) { 103 | return false; 104 | } 105 | 106 | return FrameUtils.getType(this.header) === FrameType.SETTINGS; 107 | } 108 | 109 | public copyBuffer(): Uint8Array { 110 | if (this.state !== State.Finished) { 111 | throw new Error("cannot copy data without the state being in Finished."); 112 | } 113 | 114 | return this.buffer.slice(); 115 | } 116 | 117 | public reset() { 118 | this.state = WaitingOnHeaders; 119 | this.buffer = EMPTY_BUFFER; 120 | this.header.fill(0); 121 | this.headerPtr = 0; 122 | this.ptr = 0; 123 | this.len = 0; 124 | } 125 | 126 | private parseHeader(buffer: Uint8Array, offset: number, length: number): number { 127 | const remainingBytes = FrameUtils.FRAME_HEADER_SIZE - this.headerPtr; 128 | const bytesToRead = Math.min(remainingBytes, length); 129 | 130 | this.header. 131 | set(buffer.subarray(offset, offset + bytesToRead), this.headerPtr); 132 | this.headerPtr += bytesToRead; 133 | 134 | if (this.headerPtr === FrameUtils.FRAME_HEADER_SIZE) { 135 | this.len = FrameUtils.getLength(this.header, 0); 136 | this.state = State.ParsingData; 137 | } 138 | 139 | return bytesToRead; 140 | } 141 | 142 | private parseBody(buffer: Uint8Array, offset: number, length: number): number { 143 | return 0; 144 | } 145 | }; 146 | /* #region framecontr */ 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /src/HTTP2/stream/stream-manager.ts: -------------------------------------------------------------------------------- 1 | import Platform from "../../Platform"; 2 | import * as FrameUtils from "../frame/utils"; 3 | import DataBuffer from "../../DataBuffer"; 4 | import FrameConstructor from "../frame/frame-constructor"; 5 | import NetworkPipe from "../../NetworkPipe"; 6 | import SettingsFrame from "../frame/settings-frame"; 7 | import IDataBuffer from "../../IDataBuffer"; 8 | 9 | // 1. Everytime a new connection request is made to the same server, we need 10 | // to start a http2 stream with server and hand back a common 11 | // NetworkPipe to the caller 12 | // 13 | // 2. We need a good startup sequence manager. 14 | // 15 | // TODO: Happy case only implementation, please fixme. 16 | const BUFFER_SIZE = 1024 * 16; 17 | 18 | export const enum SMState { 19 | WAITING_ON_SETTINGS = 0x1, 20 | WAITING_ON_SETTINGS_ACK = 0x2, 21 | OPEN = 0x4, 22 | CLOSED = 0x8, 23 | }; 24 | 25 | type StateChangeCallback = (state: SMState) => void; 26 | 27 | function zeroBit(value: number, bit: number) { 28 | return value & (0xFFFF ^ bit); 29 | } 30 | 31 | export default class StreamManager { 32 | private pipe: NetworkPipe; 33 | // TODO: REMOVE THIS AND USE THE H E DOUBLE HOCKEY STICKS READ BUFF FROM 34 | // THE PLATFORM 35 | private readBuffer: IDataBuffer; 36 | private nextId: number; 37 | private currentFrame: FrameConstructor; 38 | private state: number; 39 | private stateChanges: StateChangeCallback[]; 40 | 41 | // TODO: Settings? 42 | constructor(pipe: NetworkPipe, settings = {}) { 43 | this.readBuffer = new DataBuffer(BUFFER_SIZE); 44 | this.pipe = pipe; 45 | this.nextId = 1; 46 | this.currentFrame = new FrameConstructor(); 47 | this.state = 48 | SMState.WAITING_ON_SETTINGS_ACK | SMState.WAITING_ON_SETTINGS; 49 | 50 | this.stateChanges = []; 51 | 52 | // @ts-ignore 53 | pipe.on("data", () => { 54 | this.read(); 55 | }); 56 | 57 | // pipe.once("close", () => { 58 | // this._close(); 59 | // }); 60 | 61 | // A preamble must be sent as the first thing. (which is the string 62 | // below). 63 | // 64 | // A settings frame must be sent after the preamble. The stream 65 | // manager should only be created after the upgrade, or upon connection 66 | // to a http2 server. 67 | // 68 | // Source: https://tools.ietf.org/html/rfc7540#section-3.5 69 | const preamble = Platform.atoutf8("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"); 70 | const buf = new SettingsFrame().toFrameWriter(0).buffer; 71 | 72 | pipe.write(preamble, 0, preamble.byteLength); 73 | pipe.write(buf, 0, buf.byteLength); 74 | } 75 | 76 | onStateChange(cb: StateChangeCallback) { 77 | this.stateChanges.push(cb); 78 | } 79 | 80 | isInitialized(): boolean { 81 | return this.state === SMState.OPEN; 82 | } 83 | 84 | close() { 85 | this.pipe.close(); 86 | this._close(); 87 | } 88 | 89 | private _close() { 90 | this.state = SMState.CLOSED; 91 | this.notifyStateChange(); 92 | } 93 | 94 | private notifyStateChange() { 95 | this.stateChanges.forEach(cb => cb(this.state)); 96 | } 97 | 98 | private processFrame() { 99 | // Then we should expect a settings frame and an ACK frame for our 100 | // settings. 101 | // 102 | if (this.state !== SMState.OPEN) { 103 | if (!this.currentFrame.isSettingsFrame()) { 104 | // Throw new connection error.... 105 | throw new Error("OHHH NO"); 106 | } 107 | 108 | // 1. A settings frame 109 | // 2. A settings ACK frame. 110 | // 111 | // we got ourselves the settings frame 112 | if (FrameUtils.isAckFrame(this.currentFrame.header)) { 113 | this.state = 114 | zeroBit(this.state, SMState.WAITING_ON_SETTINGS_ACK); 115 | } 116 | else { 117 | this.state = zeroBit(this.state, SMState.WAITING_ON_SETTINGS); 118 | const frame = SettingsFrame.ackFrame(0); 119 | this.pipe.write(frame.buffer, 0, frame.buffer.byteLength); 120 | } 121 | 122 | if (this.state === 0) { 123 | this.state = SMState.OPEN; 124 | } 125 | 126 | this.notifyStateChange(); 127 | } 128 | } 129 | 130 | private read() { 131 | /* 132 | const bytesRead = this.pipe.read(this.readBuffer, 0, BUFFER_SIZE); 133 | let ptr = 0; 134 | 135 | do { 136 | const bytesProcessed = 137 | this.currentFrame.parse(this.readBuffer, 0, bytesRead); 138 | 139 | ptr += bytesProcessed; 140 | if (this.currentFrame.isFinished()) { 141 | this.processFrame(); 142 | this.currentFrame.reset(); 143 | } 144 | } while (ptr < bytesRead); 145 | */ 146 | } 147 | }; 148 | 149 | -------------------------------------------------------------------------------- /src/ws/framer/header.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from "../../DataBuffer"; 2 | import IDataBuffer from "../../IDataBuffer"; 3 | import { Opcodes } from "../types"; 4 | import { WSState, MASK_SIZE, } from "./types"; 5 | 6 | /* 7 | * 8 | * straight out of rfc: 9 | * https://tools.ietf.org/html/rfc6455 10 | * 11 | 0 1 2 3 12 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 13 | +-+-+-+-+-------+-+-------------+-------------------------------+ 14 | |F|R|R|R| opcode|M| Payload len | Extended payload length | 15 | |I|S|S|S| (4) |A| (7) | (16/64) | 16 | |N|V|V|V| |S| | (if payload len==126/127) | 17 | | |1|2|3| |K| | | 18 | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 19 | | Extended payload length continued, if payload len == 127 | 20 | + - - - - - - - - - - - - - - - +-------------------------------+ 21 | | |Masking-key, if MASK set to 1 | 22 | +-------------------------------+-------------------------------+ 23 | | Masking-key (continued) | Payload Data | 24 | +-------------------------------- - - - - - - - - - - - - - - - + 25 | : Payload Data continued ... : 26 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 27 | | Payload Data continued ... | 28 | +---------------------------------------------------------------+ 29 | */ 30 | 31 | const maskBuf = new DataBuffer(MASK_SIZE); 32 | 33 | export function generateMask(): IDataBuffer { 34 | maskBuf.setUInt8(0, Math.floor(Math.random() * 256)); 35 | maskBuf.setUInt8(1, Math.floor(Math.random() * 256)); 36 | maskBuf.setUInt8(2, Math.floor(Math.random() * 256)); 37 | maskBuf.setUInt8(3, Math.floor(Math.random() * 256)); 38 | return maskBuf; 39 | } 40 | 41 | export function constructFrameHeader( 42 | buf: IDataBuffer, 43 | isFinished: boolean, 44 | opCode: number, 45 | payloadLength: number, 46 | mask?: IDataBuffer, 47 | ): number { 48 | let ptr = 0; 49 | 50 | let firstByte = 0x0; 51 | if (isFinished) { 52 | firstByte |= (0x1) << 7; 53 | } 54 | 55 | firstByte |= (opCode & 0xF); 56 | buf.setUInt8(ptr++, firstByte); 57 | 58 | // payload encoding 59 | let secondByte = 0; 60 | if (mask) { 61 | secondByte = 0x1 << 7; 62 | } 63 | 64 | ptr++; 65 | if (payloadLength <= 125) { 66 | secondByte |= (payloadLength & 0x7F); 67 | } 68 | else if (payloadLength < 0xFFFF) { 69 | secondByte |= (126 & 0x7F); 70 | buf.setUInt16BE(ptr, payloadLength); 71 | ptr += 2; 72 | } 73 | else { 74 | // TODO: I could put something here to make the window larger. 75 | // TODO: put an exception in WS Constructor if you attempt to make 76 | // frames larger than 64KB 77 | // 78 | // TODO: Or should we allow it? Maybe? 79 | // 80 | // NOTE: This should just never be an option. It really is 81 | // insanity wolf to make a packet this big that would throttle the 82 | // whole ws pipeline. 83 | throw new Error("Bad implementation, Prime"); 84 | } 85 | 86 | buf.setUInt8(1, secondByte); 87 | 88 | if (mask) { 89 | buf.set(ptr, mask); 90 | ptr += 4; 91 | } 92 | 93 | return ptr; 94 | } 95 | 96 | export function isHeaderParsable(packet: IDataBuffer, len: number): boolean { 97 | if (len < 2) { 98 | return false; 99 | } 100 | 101 | const byte2 = packet.getUInt8(1); 102 | 103 | const isMasked = (byte2 & 0x80) >>> 7 === 1; 104 | const payloadLength = (byte2 & 0x7F); 105 | 106 | let size = 2; 107 | if (payloadLength === 126) { 108 | size += 2; 109 | } 110 | else if (payloadLength === 127) { 111 | size += 8; 112 | } 113 | 114 | if (isMasked) { 115 | size += 4; 116 | } 117 | 118 | return len >= size; 119 | } 120 | 121 | export function parseHeader(header: IDataBuffer, state: WSState): number { 122 | let ptr = 0; 123 | const byte1 = header.getUInt8(ptr++); 124 | state.isFinished = (byte1 & (0x80)) >>> 7 === 1; 125 | 126 | state.rsv1 = (byte1 & 0x40) >> 6; 127 | state.rsv2 = (byte1 & 0x20) >> 5; 128 | state.rsv3 = (byte1 & 0x10) >> 4; 129 | 130 | const opcode = byte1 & 0xF; 131 | 132 | state.currentOpcode = opcode; 133 | if (opcode !== Opcodes.ContinuationFrame) { 134 | state.opcode = opcode; 135 | } 136 | 137 | const byte2 = header.getUInt8(ptr++); 138 | 139 | state.isMasked = (byte2 & 0x80) >>> 7 === 1; 140 | 141 | state.payloadLength = (byte2 & 0x7F); 142 | 143 | if (state.payloadLength === 126) { 144 | state.payloadLength = header.getUInt16BE(ptr); 145 | ptr += 2; 146 | } 147 | 148 | else if (state.payloadLength === 127) { 149 | state.payloadLength = header.getUInt32BE(ptr + 4); 150 | ptr += 8; 151 | } 152 | 153 | if (state.isMasked) { 154 | state.currentMask.set(0, header, ptr, MASK_SIZE); 155 | ptr += 4; 156 | } 157 | 158 | state.payloadPtr = 0; 159 | state.payload = new DataBuffer(state.payloadLength); 160 | 161 | return ptr; 162 | } 163 | 164 | -------------------------------------------------------------------------------- /src/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | import UnorderedMap from "./#{target}/UnorderedMap"; 2 | import IUnorderedMap from "./IUnorderedMap"; 3 | import { EventListenerCallback } from "./types"; 4 | 5 | interface EventConnection { 6 | listener: EventListenerCallback; 7 | once: true; 8 | }; 9 | 10 | type EventConnectionType = EventConnection | EventListenerCallback; 11 | 12 | export default class EventEmitter { 13 | constructor() { 14 | this.listenerMap = new UnorderedMap(); 15 | } 16 | 17 | addListener(event: string, listener: EventListenerCallback): this { 18 | return this.add(event, listener, false); 19 | } 20 | 21 | addOnceListener(event: string, listener: EventListenerCallback): this { 22 | return this.add(event, { listener, once: true }, false); 23 | } 24 | 25 | on(event: string, listener: EventListenerCallback): this { 26 | return this.add(event, listener, false); 27 | } 28 | 29 | once(event: string, listener: EventListenerCallback): this { 30 | return this.add(event, { listener, once: true }, false); 31 | } 32 | 33 | removeListener(event: string, listener: EventListenerCallback): this { 34 | return this.off(event, listener); 35 | } 36 | 37 | off(event: string, listener: EventListenerCallback): this { 38 | const connections = this.listenerMap.get(event); 39 | if (!connections) 40 | return this; 41 | if (Array.isArray(connections)) { 42 | for (let idx = 0; idx < connections.length; ++idx) { 43 | const val = connections[idx]; 44 | let match; 45 | if (typeof val === "object") { 46 | match = val.listener === listener; 47 | } else { 48 | match = val === listener; 49 | } 50 | if (match) { 51 | if (connections.length === 1) { 52 | this.listenerMap.delete(event); 53 | } else { 54 | connections.splice(idx, 1); 55 | } 56 | break; 57 | } 58 | } 59 | } else if (typeof connections === "function") { 60 | if (connections === listener) 61 | this.listenerMap.delete(event); 62 | } else if (connections.listener === listener) { 63 | this.listenerMap.delete(event); 64 | } 65 | return this; 66 | } 67 | 68 | removeAllListeners(event?: string): this { 69 | if (event) { 70 | this.listenerMap.delete(event); 71 | } else { 72 | this.listenerMap.clear(); 73 | } 74 | return this; 75 | } 76 | 77 | listeners(event: string): EventListenerCallback[] { 78 | const connections = this.listenerMap.get(event); 79 | if (!connections) 80 | return []; 81 | if (Array.isArray(connections)) { 82 | return connections.map(l => { 83 | if (typeof l === "function") 84 | return l; 85 | return l.listener; 86 | }); 87 | } else if (typeof connections === "function") { 88 | return [connections]; 89 | } else { 90 | return [connections.listener]; 91 | } 92 | } 93 | emit(event: string, ...args: any[]): boolean { 94 | const connections = this.listenerMap.get(event); 95 | if (!connections) { 96 | return false; 97 | } 98 | 99 | if (Array.isArray(connections)) { 100 | let idx = 0; 101 | while (idx < connections.length) { 102 | let conn = connections[idx]; 103 | if (typeof conn === "function") { 104 | ++idx; 105 | } else { // once 106 | connections.splice(idx, 1); 107 | conn = conn.listener; 108 | } 109 | try { 110 | conn.apply(this, args); 111 | } catch (err) { 112 | this.unarray(event, connections); 113 | throw err; 114 | } 115 | } 116 | this.unarray(event, connections); 117 | } else if (typeof connections === "function") { 118 | connections.apply(this, args); 119 | } else { // once 120 | this.listenerMap.delete(event); 121 | connections.listener.apply(this, args); 122 | } 123 | return true; 124 | } 125 | 126 | hasListener(event: string): boolean { 127 | return this.listenerMap.has(event); 128 | } 129 | 130 | listenerCount(event: string): number { 131 | const connections = this.listenerMap.get(event); 132 | if (!connections) { 133 | return 0; 134 | } 135 | if (Array.isArray(connections)) { 136 | return connections.length; 137 | } 138 | return 1; 139 | } 140 | 141 | prependListener(event: string, listener: EventListenerCallback): this { 142 | return this.add(event, listener, true); 143 | } 144 | 145 | prependOnceListener(event: string, listener: EventListenerCallback): this { 146 | return this.add(event, { listener, once: true }, true); 147 | } 148 | 149 | eventNames(): string[] { 150 | return this.listenerMap.keys(); 151 | } 152 | 153 | private add(event: string, conn: EventConnectionType, prepend: boolean): this { 154 | const connections = this.listenerMap.get(event); 155 | if (connections) { 156 | if (Array.isArray(connections)) { 157 | if (prepend) { 158 | connections.unshift(conn); 159 | } else { 160 | connections.push(conn); 161 | } 162 | } else { 163 | this.listenerMap.set(event, prepend ? [conn, connections] : [connections, conn]); 164 | } 165 | } else { 166 | this.listenerMap.set(event, conn); 167 | } 168 | return this; 169 | } 170 | private unarray(event: string, conns: EventConnectionType[]): void { 171 | switch (conns.length) { 172 | case 0: 173 | this.listenerMap.delete(event); 174 | break; 175 | case 1: 176 | this.listenerMap.set(event, conns[0]); 177 | break; 178 | default: 179 | break; 180 | } 181 | } 182 | 183 | private listenerMap: IUnorderedMap; 184 | } 185 | -------------------------------------------------------------------------------- /autobahn/runner/run-test.ts: -------------------------------------------------------------------------------- 1 | export type IPlatform = { 2 | error(...args: any[]): void; 3 | log(...args: any[]): void; 4 | trace(...args: any[]): void; 5 | } 6 | 7 | export type AutobahnOpts = { 8 | updateReport?: boolean; 9 | port?: number; 10 | Platform: IPlatform; 11 | agent: string; 12 | }; 13 | 14 | export async function runAutobahnTests(WebSocketClass: any, { 15 | updateReport = true, 16 | port = 9001, 17 | agent, 18 | Platform, 19 | }: AutobahnOpts): Promise { 20 | 21 | const wsuri = `ws://localhost:${port}`; 22 | let currentCaseId: number; 23 | let caseCount: number; 24 | 25 | Platform.log("runAutobahnTests", agent, wsuri); 26 | let doneRes: (...args: any[]) => void; 27 | 28 | /* tslint:disable-next-line */ 29 | new Promise(res => { 30 | doneRes = res 31 | }); 32 | 33 | return new Promise((res, rej) => { 34 | let hasError = false; 35 | 36 | function reject(e: Error) { 37 | Platform.log(currentCaseId, "reject the websockets", e); 38 | hasError = true; 39 | rej(e); 40 | doneRes(); 41 | } 42 | 43 | function startTestRun() { 44 | Platform.log(currentCaseId, "startTestRun"); 45 | currentCaseId = 1; 46 | getCaseCount(runNextCase); 47 | } 48 | 49 | function updateStatus(msg: string) { 50 | Platform.log(msg); 51 | } 52 | 53 | function openWebSocket(wsUri: string) { 54 | Platform.log(currentCaseId, "openSocket", wsUri); 55 | // @ts-ignore 56 | return new WebSocketClass(wsUri); 57 | } 58 | 59 | function getCaseCount(cont: () => void) { 60 | const wsUri = wsuri + "/getCaseCount"; 61 | const webSocket = openWebSocket(wsUri); 62 | Platform.log("getCaseCount --------------- START -----------------------"); 63 | 64 | // @ts-ignore 65 | webSocket.onopen = () => { 66 | Platform.log("getCaseCount#onopen"); 67 | } 68 | 69 | webSocket.onerror = (e: any) => { 70 | Platform.log("getCaseCount#onerror", e); 71 | } 72 | 73 | webSocket.onmessage = (e: { data: any }) => { 74 | caseCount = JSON.parse(e.data); 75 | Platform.log("getCaseCount#onmessage", caseCount); 76 | updateStatus("Will run " + caseCount + " cases .."); 77 | } 78 | 79 | // @ts-ignore 80 | webSocket.onclose = () => { 81 | Platform.log(currentCaseId, "getCaseCount#close", caseCount); 82 | Platform.log("getCaseCount --------------- END -----------------------"); 83 | cont(); 84 | } 85 | } 86 | 87 | function updateReports() { 88 | Platform.log(currentCaseId, "updateReport"); 89 | if (!updateReport) { 90 | return; 91 | } 92 | const wsUri = wsuri + "/updateReports?agent=" + agent; 93 | const webSocket = openWebSocket(wsUri); 94 | 95 | // @ts-ignore 96 | webSocket.onopen = () => { 97 | updateStatus("Updating reports .."); 98 | } 99 | 100 | // @ts-ignore 101 | webSocket.onclose = () => { 102 | updateStatus("Reports updated."); 103 | updateStatus("Test suite finished!"); 104 | 105 | Platform.log(currentCaseId, "webSocket#close Is autobahn runner completed?", currentCaseId, caseCount); 106 | // Last socket closed. 107 | if (currentCaseId >= caseCount) { 108 | res(caseCount); 109 | } 110 | } 111 | } 112 | 113 | let hasFinished = false; 114 | function finish() { 115 | Platform.log(currentCaseId, "Finished the websockets"); 116 | if (hasFinished) { 117 | return; 118 | } 119 | hasFinished = true; 120 | updateStatus("All test cases executed."); 121 | updateReports(); 122 | doneRes(); 123 | } 124 | 125 | function readyNextCase() { 126 | Platform.log(`EMPTY SPACE ----------------- START ${currentCaseId} --------------- `); 127 | // setTimeout(() => { 128 | Platform.log(`EMPTY SPACE ----------------- END ${currentCaseId} --------------- `); 129 | currentCaseId = currentCaseId + 1; 130 | if (!hasFinished && !hasError && currentCaseId <= caseCount) { 131 | runNextCase(); 132 | } else { 133 | finish(); 134 | } 135 | // }, 100); 136 | } 137 | 138 | function runNextCase() { 139 | if (isNaN(+caseCount)) { 140 | const e = new Error( 141 | `CaseCount for autobahn is not a number: ${caseCount}`); 142 | Platform.error(e); 143 | reject(e); 144 | return; 145 | } 146 | 147 | Platform.log(`runNextCase ----------------- START ${currentCaseId} --------------- `); 148 | 149 | if (hasError || hasFinished) { 150 | Platform.log(currentCaseId, "hadError or hasFinished", hasError, hasFinished); 151 | finish(); 152 | } 153 | 154 | const wsUri = wsuri + "/runCase?case=" + currentCaseId + "&agent=" + agent; 155 | const webSocket = openWebSocket(wsUri); 156 | 157 | Platform.log(currentCaseId, "runNextCase", wsUri); 158 | 159 | webSocket.binaryType = "arraybuffer"; 160 | webSocket.onopen = () => { 161 | Platform.log(currentCaseId, "runNextCase#webSocket.onopen"); 162 | updateStatus("Executing test case " + currentCaseId + "/" + caseCount); 163 | } 164 | 165 | webSocket.onerror = (e: any) => { 166 | Platform.log(currentCaseId, "runNextCase#webSocket.onerror"); 167 | Platform.log(currentCaseId, "getCaseCount#onerror", e); 168 | reject(e); 169 | } 170 | 171 | webSocket.onclose = () => { 172 | Platform.trace(`runNextCase ----------------- END ${currentCaseId} --------------- `); 173 | readyNextCase(); 174 | } 175 | 176 | webSocket.onmessage = (e: { data: any }) => { 177 | Platform.log(currentCaseId, "Websocket:onmessage (Not including message due to length)"); 178 | webSocket.send(e.data); 179 | } 180 | } 181 | 182 | startTestRun(); 183 | }); 184 | } 185 | -------------------------------------------------------------------------------- /src/HTTP1/ChunkyParser.ts: -------------------------------------------------------------------------------- 1 | import DataBuffer from "../DataBuffer"; 2 | import EventEmitter from "../EventEmitter"; 3 | import IDataBuffer from "../IDataBuffer"; 4 | import NetworkError from "../NetworkError"; 5 | import assert from '../utils/assert.macro'; 6 | import { NetworkErrorCode } from "../types"; 7 | import { escapeData } from "../utils"; 8 | 9 | export default class ChunkyParser extends EventEmitter { 10 | private buffers: IDataBuffer[]; 11 | private offset: number; 12 | private dataNeeded: number; 13 | private available: number; 14 | 15 | constructor() { 16 | super(); 17 | this.buffers = []; 18 | this.offset = 0; 19 | this.dataNeeded = -1; 20 | this.available = 0; 21 | } 22 | 23 | feed(data: IDataBuffer, offset: number, length: number): void { 24 | assert(this.hasListener("chunk"), "Gotta have an onchunk"); 25 | this.buffers.push(data.slice(offset, length)); 26 | this.available += length; 27 | this._process(); 28 | } 29 | 30 | dump(): string { 31 | let str = ""; 32 | for (let bi = 0; bi < this.buffers.length; ++bi) { 33 | const idx = bi ? 0 : this.offset; 34 | str += escapeData(this.buffers[bi], idx); 35 | } 36 | return str; 37 | } 38 | 39 | private _process(): void { 40 | while (true) { 41 | if (this.dataNeeded === -1) { 42 | if (this.available > 2) { 43 | let lastWasBackslashR = false; 44 | let consumed = 0; 45 | let str = ""; 46 | for (let bi = 0; bi < this.buffers.length && this.dataNeeded === -1; ++bi) { 47 | // Platform.trace("shit", bi, this.buffers.length); 48 | const buf = this.buffers[bi]; 49 | // Platform.trace("this is", buf, Platform.utf8toa(buf)); 50 | for (let i = bi ? 0 : this.offset; i < buf.byteLength; ++i) { 51 | // Platform.trace("looking at", i, bi, buf[i], String.fromCharCode(buf[i]), str); 52 | ++consumed; 53 | if (lastWasBackslashR) { 54 | if (buf.get(i) === 10) { 55 | const len = parseInt(str, 16); 56 | if (isNaN(len)) { 57 | const msg = "Failed to chunky parse [" + str + "] " + len; 58 | this.emit("error", new NetworkError(NetworkErrorCode.ChunkyError, msg)); 59 | return; 60 | } 61 | this.dataNeeded = len; 62 | // Platform.trace("got len", len, "for", str, consumed + "\n" + this.dump()); 63 | this._consume(consumed); 64 | break; 65 | } 66 | } else if (buf.get(i) === 13) { 67 | lastWasBackslashR = true; 68 | } else { 69 | lastWasBackslashR = false; 70 | str += String.fromCharCode(buf.get(i)); 71 | } 72 | } 73 | } 74 | } 75 | if (this.dataNeeded === -1) 76 | break; 77 | } else if (!this.dataNeeded && this.available >= 2) { 78 | this._consume(2); 79 | const buffer = this.available ? this._extractChunk(this.available) : undefined; 80 | assert(!this.available, "Nothing should be left"); 81 | assert(!this.buffers.length, "No buffers here"); 82 | this.emit("done", buffer); 83 | } else if (this.dataNeeded + 2 <= this.available) { 84 | const chunk = this._extractChunk(this.dataNeeded); 85 | // Platform.trace("extracted a chunk", Platform.utf8toa(chunk)); 86 | this._consume(2); 87 | this.dataNeeded = -1; 88 | this.emit("chunk", chunk); 89 | } else { 90 | break; 91 | } 92 | } 93 | } 94 | 95 | private _consume(bytes: number): void { 96 | assert(bytes <= this.available, "Not enough bytes to consume"); 97 | // Platform.trace("consuoming", bytes, "from", this.buffers, this.available); 98 | let consumed = 0; 99 | while (consumed < bytes) { 100 | const bufferAvailable = this.buffers[0].byteLength - this.offset; 101 | if (bytes - consumed >= bufferAvailable) { 102 | this.buffers.shift(); 103 | this.offset = 0; 104 | consumed += bufferAvailable; 105 | } else { 106 | const wanted = bytes - consumed; 107 | this.offset += wanted; 108 | consumed += wanted; 109 | assert(consumed === bytes, "consumed should === bytes"); 110 | break; 111 | } 112 | } 113 | assert(consumed === bytes, 114 | `Bytes should be nothing by now bytes: ${bytes} consumed: ${consumed} available: ${this.available}`); 115 | this.available -= consumed; 116 | } 117 | 118 | private _extractChunk(size: number): IDataBuffer { 119 | assert(this.available >= size, "available's gotta be more than size"); 120 | // grab the whole first chunk 121 | if (!this.offset && this.buffers[0].byteLength === size) { 122 | this.available -= size; 123 | const buf = this.buffers.shift(); 124 | assert(buf !== undefined, "Must have buffers"); 125 | return buf; 126 | } 127 | 128 | const ret = new DataBuffer(size); 129 | let idx = 0; 130 | while (idx < size) { 131 | const buf = this.buffers[0]; 132 | const wanted = size - idx; 133 | const bufferAvailable = buf.byteLength - this.offset; 134 | if (bufferAvailable > size - idx) { 135 | ret.set(idx, buf, this.offset, wanted); 136 | idx += wanted; 137 | this.offset += wanted; 138 | break; 139 | } else if (this.offset) { 140 | assert(bufferAvailable <= wanted, "foo"); 141 | ret.set(idx, buf, this.offset, bufferAvailable); 142 | this.offset = 0; 143 | this.buffers.shift(); 144 | idx += bufferAvailable; 145 | } else { 146 | assert(bufferAvailable <= wanted, "bar"); 147 | assert(!this.offset, "zot"); 148 | ret.set(idx, buf); 149 | this.buffers.shift(); 150 | idx += bufferAvailable; 151 | } 152 | } 153 | assert(idx === size, "We should be done now"); 154 | this.available -= size; 155 | return ret; 156 | } 157 | }; 158 | -------------------------------------------------------------------------------- /src/node/huffman/static.ts: -------------------------------------------------------------------------------- 1 | function toBigEndian(num: number): Buffer { 2 | const buf = Buffer.alloc(4); 3 | 4 | let byteCount = 0; 5 | let remaining = num; 6 | do { 7 | remaining = remaining >> 8; 8 | byteCount++; 9 | } while (remaining > 0); 10 | 11 | buf.writeUIntBE(num, 0, byteCount); 12 | return buf; 13 | } 14 | 15 | const staticList: [number, number][] = [ 16 | [0x1ff8, 13], 17 | [0x7fffd8, 23], 18 | [0xfffffe2, 28], 19 | [0xfffffe3, 28], 20 | [0xfffffe4, 28], 21 | [0xfffffe5, 28], 22 | [0xfffffe6, 28], 23 | [0xfffffe7, 28], 24 | [0xfffffe8, 28], 25 | [0xffffea, 24], 26 | [0x3ffffffc, 30], 27 | [0xfffffe9, 28], 28 | [0xfffffea, 28], 29 | [0x3ffffffd, 30], 30 | [0xfffffeb, 28], 31 | [0xfffffec, 28], 32 | [0xfffffed, 28], 33 | [0xfffffee, 28], 34 | [0xfffffef, 28], 35 | [0xffffff0, 28], 36 | [0xffffff1, 28], 37 | [0xffffff2, 28], 38 | [0x3ffffffe, 30], 39 | [0xffffff3, 28], 40 | [0xffffff4, 28], 41 | [0xffffff5, 28], 42 | [0xffffff6, 28], 43 | [0xffffff7, 28], 44 | [0xffffff8, 28], 45 | [0xffffff9, 28], 46 | [0xffffffa, 28], 47 | [0xffffffb, 28], 48 | [0x14, 6], 49 | [0x3f8, 10], 50 | [0x3f9, 10], 51 | [0xffa, 12], 52 | [0x1ff9, 13], 53 | [0x15, 6], 54 | [0xf8, 8], 55 | [0x7fa, 11], 56 | [0x3fa, 10], 57 | [0x3fb, 10], 58 | [0xf9, 8], 59 | [0x7fb, 11], 60 | [0xfa, 8], 61 | [0x16, 6], 62 | [0x17, 6], 63 | [0x18, 6], 64 | [0x0, 5], 65 | [0x1, 5], 66 | [0x2, 5], 67 | [0x19, 6], 68 | [0x1a, 6], 69 | [0x1b, 6], 70 | [0x1c, 6], 71 | [0x1d, 6], 72 | [0x1e, 6], 73 | [0x1f, 6], 74 | [0x5c, 7], 75 | [0xfb, 8], 76 | [0x7ffc, 15], 77 | [0x20, 6], 78 | [0xffb, 12], 79 | [0x3fc, 10], 80 | [0x1ffa, 13], 81 | [0x21, 6], 82 | [0x5d, 7], 83 | [0x5e, 7], 84 | [0x5f, 7], 85 | [0x60, 7], 86 | [0x61, 7], 87 | [0x62, 7], 88 | [0x63, 7], 89 | [0x64, 7], 90 | [0x65, 7], 91 | [0x66, 7], 92 | [0x67, 7], 93 | [0x68, 7], 94 | [0x69, 7], 95 | [0x6a, 7], 96 | [0x6b, 7], 97 | [0x6c, 7], 98 | [0x6d, 7], 99 | [0x6e, 7], 100 | [0x6f, 7], 101 | [0x70, 7], 102 | [0x71, 7], 103 | [0x72, 7], 104 | [0xfc, 8], 105 | [0x73, 7], 106 | [0xfd, 8], 107 | [0x1ffb, 13], 108 | [0x7fff0, 19], 109 | [0x1ffc, 13], 110 | [0x3ffc, 14], 111 | [0x22, 6], 112 | [0x7ffd, 15], 113 | [0x3, 5], 114 | [0x23, 6], 115 | [0x4, 5], 116 | [0x24, 6], 117 | [0x5, 5], 118 | [0x25, 6], 119 | [0x26, 6], 120 | [0x27, 6], 121 | [0x6, 5], 122 | [0x74, 7], 123 | [0x75, 7], 124 | [0x28, 6], 125 | [0x29, 6], 126 | [0x2a, 6], 127 | [0x7, 5], 128 | [0x2b, 6], 129 | [0x76, 7], 130 | [0x2c, 6], 131 | [0x8, 5], 132 | [0x9, 5], 133 | [0x2d, 6], 134 | [0x77, 7], 135 | [0x78, 7], 136 | [0x79, 7], 137 | [0x7a, 7], 138 | [0x7b, 7], 139 | [0x7ffe, 15], 140 | [0x7fc, 11], 141 | [0x3ffd, 14], 142 | [0x1ffd, 13], 143 | [0xffffffc, 28], 144 | [0xfffe6, 20], 145 | [0x3fffd2, 22], 146 | [0xfffe7, 20], 147 | [0xfffe8, 20], 148 | [0x3fffd3, 22], 149 | [0x3fffd4, 22], 150 | [0x3fffd5, 22], 151 | [0x7fffd9, 23], 152 | [0x3fffd6, 22], 153 | [0x7fffda, 23], 154 | [0x7fffdb, 23], 155 | [0x7fffdc, 23], 156 | [0x7fffdd, 23], 157 | [0x7fffde, 23], 158 | [0xffffeb, 24], 159 | [0x7fffdf, 23], 160 | [0xffffec, 24], 161 | [0xffffed, 24], 162 | [0x3fffd7, 22], 163 | [0x7fffe0, 23], 164 | [0xffffee, 24], 165 | [0x7fffe1, 23], 166 | [0x7fffe2, 23], 167 | [0x7fffe3, 23], 168 | [0x7fffe4, 23], 169 | [0x1fffdc, 21], 170 | [0x3fffd8, 22], 171 | [0x7fffe5, 23], 172 | [0x3fffd9, 22], 173 | [0x7fffe6, 23], 174 | [0x7fffe7, 23], 175 | [0xffffef, 24], 176 | [0x3fffda, 22], 177 | [0x1fffdd, 21], 178 | [0xfffe9, 20], 179 | [0x3fffdb, 22], 180 | [0x3fffdc, 22], 181 | [0x7fffe8, 23], 182 | [0x7fffe9, 23], 183 | [0x1fffde, 21], 184 | [0x7fffea, 23], 185 | [0x3fffdd, 22], 186 | [0x3fffde, 22], 187 | [0xfffff0, 24], 188 | [0x1fffdf, 21], 189 | [0x3fffdf, 22], 190 | [0x7fffeb, 23], 191 | [0x7fffec, 23], 192 | [0x1fffe0, 21], 193 | [0x1fffe1, 21], 194 | [0x3fffe0, 22], 195 | [0x1fffe2, 21], 196 | [0x7fffed, 23], 197 | [0x3fffe1, 22], 198 | [0x7fffee, 23], 199 | [0x7fffef, 23], 200 | [0xfffea, 20], 201 | [0x3fffe2, 22], 202 | [0x3fffe3, 22], 203 | [0x3fffe4, 22], 204 | [0x7ffff0, 23], 205 | [0x3fffe5, 22], 206 | [0x3fffe6, 22], 207 | [0x7ffff1, 23], 208 | [0x3ffffe0, 26], 209 | [0x3ffffe1, 26], 210 | [0xfffeb, 20], 211 | [0x7fff1, 19], 212 | [0x3fffe7, 22], 213 | [0x7ffff2, 23], 214 | [0x3fffe8, 22], 215 | [0x1ffffec, 25], 216 | [0x3ffffe2, 26], 217 | [0x3ffffe3, 26], 218 | [0x3ffffe4, 26], 219 | [0x7ffffde, 27], 220 | [0x7ffffdf, 27], 221 | [0x3ffffe5, 26], 222 | [0xfffff1, 24], 223 | [0x1ffffed, 25], 224 | [0x7fff2, 19], 225 | [0x1fffe3, 21], 226 | [0x3ffffe6, 26], 227 | [0x7ffffe0, 27], 228 | [0x7ffffe1, 27], 229 | [0x3ffffe7, 26], 230 | [0x7ffffe2, 27], 231 | [0xfffff2, 24], 232 | [0x1fffe4, 21], 233 | [0x1fffe5, 21], 234 | [0x3ffffe8, 26], 235 | [0x3ffffe9, 26], 236 | [0xffffffd, 28], 237 | [0x7ffffe3, 27], 238 | [0x7ffffe4, 27], 239 | [0x7ffffe5, 27], 240 | [0xfffec, 20], 241 | [0xfffff3, 24], 242 | [0xfffed, 20], 243 | [0x1fffe6, 21], 244 | [0x3fffe9, 22], 245 | [0x1fffe7, 21], 246 | [0x1fffe8, 21], 247 | [0x7ffff3, 23], 248 | [0x3fffea, 22], 249 | [0x3fffeb, 22], 250 | [0x1ffffee, 25], 251 | [0x1ffffef, 25], 252 | [0xfffff4, 24], 253 | [0xfffff5, 24], 254 | [0x3ffffea, 26], 255 | [0x7ffff4, 23], 256 | [0x3ffffeb, 26], 257 | [0x7ffffe6, 27], 258 | [0x3ffffec, 26], 259 | [0x3ffffed, 26], 260 | [0x7ffffe7, 27], 261 | [0x7ffffe8, 27], 262 | [0x7ffffe9, 27], 263 | [0x7ffffea, 27], 264 | [0x7ffffeb, 27], 265 | [0xffffffe, 28], 266 | [0x7ffffec, 27], 267 | [0x7ffffed, 27], 268 | [0x7ffffee, 27], 269 | [0x7ffffef, 27], 270 | [0x7fffff0, 27], 271 | [0x3ffffee, 26], 272 | [0x3fffffff, 30], // EOS 273 | ]; 274 | 275 | export type StaticTreeNode = [StaticTreeNode | undefined | number, StaticTreeNode | undefined | number]; 276 | 277 | function createStaticTreeNode(): StaticTreeNode { 278 | return [undefined, undefined]; 279 | } 280 | 281 | export function pluckBit(value: number, ptr: number): number { 282 | return (value & 0x1 << ptr) ? 1 : 0; 283 | } 284 | 285 | export const staticTree: StaticTreeNode = createStaticTreeNode(); 286 | 287 | staticList.forEach((item, i) => { 288 | const value = item[0]; 289 | const bits = item[1]; 290 | 291 | let curr = staticTree; 292 | let ptr = bits; 293 | 294 | do { 295 | const idx = pluckBit(value, --ptr); 296 | 297 | if (curr[idx] === undefined) { 298 | curr[idx] = createStaticTreeNode(); 299 | } 300 | else if (typeof curr[idx] === 'number') { 301 | throw new Error("You tree building skills are as good as your reading skills."); 302 | } 303 | 304 | // What am I doing wrong 305 | // @ts-ignore 306 | curr = curr[idx]; 307 | 308 | } while (ptr > 1); 309 | 310 | const bit = pluckBit(value, 0); 311 | curr[bit] = i; 312 | }); 313 | 314 | export default staticList; 315 | --------------------------------------------------------------------------------