├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── backend ├── backend-node.js ├── backend-runtime.js └── backend-test.js ├── benchmark └── server-benchmark.js ├── example ├── run-client-get.js ├── run-server-eshttp-dynamic.js ├── run-server-eshttp.js ├── run-server-http.js └── test.js ├── index-node.js ├── index-runtime.js ├── lib ├── backend.js ├── date.js ├── eshttp.js ├── fetch.js ├── headers.js ├── http-client.js ├── http-codes.js ├── http-connection.js ├── http-parser.js ├── http-request.js ├── http-response.js ├── http-server.js ├── response-header.js └── tokens.js ├── package.json └── test ├── headers.js ├── index.js ├── parser.js ├── request.js ├── server.js └── test-case-parser.js /.eslintrc: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | es6: true 4 | 5 | globals: 6 | debug: true 7 | performance: true 8 | 9 | rules: 10 | indent: [2, 2] 11 | quotes: [2, "single", "avoid-escape"] 12 | brace-style: [2, "1tbs", { "allowSingleLine": true }] 13 | camelcase: [2, { properties: "always" }] 14 | comma-style: [2, "last"] 15 | func-style: [2, "declaration"] 16 | guard-for-in: 2 17 | no-nested-ternary: 2 18 | no-undefined: 2 19 | no-undef: 2 20 | no-labels: 2 21 | no-multi-spaces: 2 22 | semi: [2, "always"] 23 | radix: 2 24 | yoda: 0 25 | new-cap: [2, { "capIsNew": false }] 26 | no-unused-vars: 0 27 | no-fallthrough: 0 28 | no-loop-func: 0 29 | accessor-pairs: 2 30 | no-use-before-define: [2, "nofunc"] 31 | dot-notation: 2 32 | dot-location: [2, "property"] 33 | eqeqeq: 2 34 | no-caller: 2 35 | no-underscore-dangle: 0 36 | no-eq-null: 2 37 | space-before-function-paren: [2, "never"] 38 | space-after-keywords: [2, "always"] 39 | space-before-blocks: [2, "always"] 40 | spaced-comment: [2, "always", { exceptions: ["-"]}] 41 | consistent-return: 2 42 | strict: [2, "global"] 43 | wrap-iife: [2, "inside"] 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swo 2 | *.swp 3 | /node_modules 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 4.0 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Sergii Iefremov 2 | 3 | This software is released under the MIT license: 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## eshttp 2 | 3 | [![Build Status](https://travis-ci.org/iefserge/eshttp.svg?branch=master)](https://travis-ci.org/iefserge/eshttp) 4 | 5 | Portable pure JavaScript ES6 HTTP library. Includes fast streaming regex-free parser for HTTP/1.0 and HTTP/1.1. 6 | 7 | - pure JavaScript (ES6/2015) 8 | - high-performance and low-level, no stream abstractions 9 | - portable, multiple backends support (Node.js/other platforms) 10 | 11 | ## USAGE 12 | 13 | ```bash 14 | npm install eshttp 15 | ``` 16 | 17 | Requires ES6/2015 JS engine (Node.js 4.0). Example web server using eshttp: 18 | 19 | ```js 20 | 'use strict'; 21 | const eshttp = require('eshttp'); 22 | const server = new eshttp.HttpServer(); 23 | const response = new eshttp.HttpResponse(200, { 'x-header': 'value' }, 'hello'); 24 | 25 | server.onrequest = request => { 26 | request.respondWith(response); 27 | }; 28 | 29 | server.listen(8080); 30 | ``` 31 | 32 | ## TODO 33 | 34 | - parser improvements 35 | - chunked responses 36 | - fetch api 37 | 38 | ## BENCHMARK 39 | 40 | ``` 41 | $ node -v 42 | v4.2.1 43 | ``` 44 | 45 | [Node.js builtin http module](https://github.com/iefserge/eshttp/blob/master/example/run-server-http.js): 46 | 47 | ``` 48 | $ wrk -t12 -c400 -d30s http://127.0.0.1:8080/ 49 | Running 30s test @ http://127.0.0.1:8080/ 50 | 12 threads and 400 connections 51 | Thread Stats Avg Stdev Max +/- Stdev 52 | Latency 21.12ms 2.32ms 98.16ms 88.23% 53 | Req/Sec 1.25k 489.35 4.03k 83.52% 54 | 336337 requests in 30.09s, 38.81MB read 55 | Socket errors: connect 155, read 181, write 0, timeout 0 56 | Requests/sec: 11177.42 57 | Transfer/sec: 1.29MB 58 | ``` 59 | 60 | [eshttp](https://github.com/iefserge/eshttp/blob/master/example/run-server-eshttp.js): 61 | 62 | ``` 63 | $ wrk -t12 -c400 -d30s http://127.0.0.1:8080/ 64 | Running 30s test @ http://127.0.0.1:8080/ 65 | 12 threads and 400 connections 66 | Thread Stats Avg Stdev Max +/- Stdev 67 | Latency 11.46ms 1.75ms 76.40ms 94.03% 68 | Req/Sec 1.76k 1.36k 5.97k 62.83% 69 | 630789 requests in 30.10s, 72.79MB read 70 | Socket errors: connect 155, read 130, write 21, timeout 0 71 | Requests/sec: 20959.47 72 | Transfer/sec: 2.42MB 73 | ``` 74 | 75 | ## API 76 | 77 | ```js 78 | const eshttp = require('eshttp'); 79 | ``` 80 | 81 | ### eshttp.HttpResponse 82 | 83 | Represents immutable HTTP response. 84 | 85 | #### HttpResponse.constructor(code, headers, body) 86 | 87 | Construct immutable HTTP response object that can be reused multiple times to serve different clients. 88 | 89 | Argument | Type | Description 90 | --- | --- | --- 91 | code | number | HTTP code 92 | headers | Headers \| object | Response headers object 93 | body | string | Response body string 94 | 95 | ```js 96 | const response = new eshttp.HttpResponse(200, { server: 'eshttp' }, 'OK.'); 97 | ``` 98 | 99 | ### eshttp.HttpRequest 100 | 101 | Represents immutable HTTP request. 102 | 103 | #### HttpRequest.constructor(method, path, headers, body) 104 | 105 | Construct immutable HTTP request object that can be reused multiple times. 106 | 107 | Argument | Type | Description 108 | --- | --- | --- 109 | method | string | HTTP request method (e.g. 'GET') 110 | path | string | Request path 111 | headers | Headers \| object | Request headers object 112 | body | string | Request body string (optional) 113 | 114 | ```js 115 | const request = new eshttp.HttpRequest('GET', '/', { 'User-Agent': 'eshttp' }); 116 | ``` 117 | 118 | ### eshttp.HttpServer 119 | 120 | Represents HTTP server. 121 | 122 | #### HttpServer.constructor() 123 | 124 | Construct HTTP server object. 125 | 126 | ```js 127 | const server = new eshttp.HttpServer(); 128 | ``` 129 | 130 | #### HttpServer.listen(port) 131 | 132 | Start listening to HTTP requests. 133 | 134 | ```js 135 | server.listen(8080); 136 | ``` 137 | 138 | #### HttpServer.close() 139 | 140 | Stop listening. 141 | 142 | ```js 143 | server.close(); 144 | ``` 145 | 146 | #### HttpServer.onrequest = function(request) 147 | 148 | Request handler callback. 149 | 150 | ```js 151 | server.onrequest = request => { 152 | console.log('new incoming request'); 153 | }; 154 | ``` 155 | 156 | ##LICENSE 157 | 158 | MIT 159 | -------------------------------------------------------------------------------- /backend/backend-node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var net = require('net'); 3 | 4 | exports.stringToBuffer = function(str) { 5 | return new Buffer(str); 6 | }; 7 | 8 | exports.stringToSocketData = function(str) { 9 | return str; 10 | }; 11 | 12 | // Server handle 13 | 14 | exports.createServerHandle = function(httpServer) { 15 | return net.createServer(function(socket) { 16 | socket.on('data', function(nodebuf) { 17 | httpServer._dataHandler(socket, nodebuf); 18 | }); 19 | 20 | socket.on('end', function() { 21 | httpServer._endHandler(socket); 22 | }); 23 | 24 | socket.on('close', function() { 25 | httpServer._closeHandler(socket); 26 | }); 27 | 28 | socket.on('error', function() { 29 | }); 30 | 31 | httpServer._connectionHandler(socket); 32 | }); 33 | }; 34 | 35 | exports.listen = function(handle, port) { 36 | handle.listen(port); 37 | }; 38 | 39 | exports.unlisten = function(handle) { 40 | handle.close(); 41 | }; 42 | 43 | exports.sendAndClose = function(socket, nodebuf) { 44 | socket.end(nodebuf); 45 | }; 46 | 47 | exports.close = function(socket) { 48 | socket.end(); 49 | }; 50 | 51 | // Client handle 52 | 53 | exports.createClientHandle = function(httpClient) { 54 | var socket = new net.Socket({ 55 | allowHalfOpen: true 56 | }); 57 | 58 | socket.on('connect', function() { 59 | httpClient._openHandler(); 60 | }); 61 | 62 | socket.on('data', function(nodebuf) { 63 | httpClient._dataHandler(nodebuf); 64 | }); 65 | 66 | socket.on('end', function() { 67 | socket.end(); 68 | httpClient._endHandler(); 69 | }); 70 | 71 | socket.on('close', function() { 72 | httpClient._closeHandler(); 73 | }); 74 | 75 | return socket; 76 | }; 77 | 78 | exports.closeClientHandle = function(socket) { 79 | socket.end(); 80 | }; 81 | 82 | exports.connect = function(handle, ip, port) { 83 | handle.connect(port, ip); 84 | }; 85 | 86 | exports.send = function(socket, nodebuf) { 87 | socket.write(nodebuf); 88 | }; 89 | -------------------------------------------------------------------------------- /backend/backend-runtime.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global runtime */ 4 | var TCPServerSocket = runtime.net.TCPServerSocket; 5 | var TCPSocket = runtime.net.TCPSocket; 6 | var enc = new TextEncoder(); 7 | 8 | exports.stringToBuffer = function(str) { 9 | return enc.encode(str); 10 | }; 11 | 12 | exports.stringToSocketData = function(str) { 13 | return enc.encode(str); 14 | }; 15 | 16 | 17 | exports.createServerHandle = function(httpServer) { 18 | var socket = new TCPServerSocket(); 19 | socket.onconnect = function(connSocket) { 20 | connSocket.ondata = function(u8) { 21 | httpServer._dataHandler(connSocket, u8); 22 | }; 23 | 24 | connSocket.onend = function() { 25 | httpServer._endHandler(connSocket); 26 | }; 27 | 28 | connSocket.onclose = function() { 29 | httpServer._closeHandler(connSocket); 30 | }; 31 | 32 | httpServer._connectionHandler(connSocket); 33 | }; 34 | 35 | return socket; 36 | }; 37 | 38 | exports.listen = function(handle, port) { 39 | handle.listen(port); 40 | }; 41 | 42 | exports.unlisten = function(handle) { 43 | handle.close(); 44 | }; 45 | 46 | exports.sendAndClose = function(socket, u8) { 47 | socket.send(u8); 48 | socket.close(); 49 | }; 50 | 51 | exports.close = function(socket) { 52 | socket.close(); 53 | }; 54 | 55 | exports.send = function(socket, u8) { 56 | socket.send(u8); 57 | }; 58 | 59 | exports.createClientHandle = function(httpClient) { 60 | var socket = new TCPSocket(); 61 | 62 | socket.onopen = function() { 63 | httpClient._openHandler(); 64 | } 65 | 66 | socket.ondata = function(u8) { 67 | httpClient._dataHandler(u8); 68 | } 69 | 70 | socket.onend = function() { 71 | socket.close(); 72 | httpClient._endHandler(); 73 | } 74 | 75 | socket.onclose = function() { 76 | httpClient._closeHandler(); 77 | } 78 | 79 | return socket; 80 | } 81 | 82 | exports.closeClientHandle = function(socket) { 83 | socket.close(); 84 | } 85 | 86 | exports.connect = function(handle, ip, port) { 87 | handle.open(ip, port); 88 | } 89 | -------------------------------------------------------------------------------- /backend/backend-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var testServer = null; 3 | 4 | exports.stringToBuffer = function(str) { 5 | return new Buffer(str); 6 | }; 7 | 8 | exports.stringToSocketData = function(str) { 9 | return str; 10 | }; 11 | 12 | exports.getServer = function() { 13 | return testServer; 14 | }; 15 | 16 | exports.createServerHandle = function(httpServer) { 17 | testServer = { 18 | addConnection: function(onsend, onclose) { 19 | var conn = { 20 | data: function(u8) { 21 | httpServer._dataHandler(conn, u8); 22 | }, 23 | end: function() { 24 | httpServer._endHandler(conn); 25 | }, 26 | onsend: onsend, 27 | onclose: onclose 28 | }; 29 | httpServer._connectionHandler(conn); 30 | return conn; 31 | }, 32 | }; 33 | 34 | return testServer; 35 | }; 36 | 37 | exports.listen = function(handle, port) {}; 38 | exports.unlisten = function(handle) {}; 39 | 40 | exports.sendAndClose = function(socket, u8) { 41 | socket.onsend(u8); 42 | socket.onclose(u8); 43 | }; 44 | -------------------------------------------------------------------------------- /benchmark/server-benchmark.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'test'; 4 | if (!global.performance) { 5 | global.performance = { 6 | now: require('performance-now') 7 | }; 8 | } 9 | 10 | function U8(str) { 11 | var u8 = new Uint8Array(str.length); 12 | for (var i = 0; i < str.length; ++i) { 13 | u8[i] = str.charCodeAt(i); 14 | } 15 | return u8; 16 | } 17 | 18 | var eshttp = require('../index-node'); 19 | var HttpServer = eshttp.HttpServer; 20 | var HttpResponse = eshttp.HttpResponse; 21 | var backend = require('../backend/backend-test'); 22 | 23 | var server = new HttpServer(); 24 | var response = new HttpResponse(200, { 'x-header': 'value' }, 'hello'); 25 | 26 | server.onrequest = function(request) { 27 | request.respondWith(response); 28 | }; 29 | server.listen(8080); 30 | 31 | var tcpServer = backend.getServer(); 32 | 33 | function onsend(u8) { 34 | if (u8[9] !== '2' || u8[10] !== '0' || u8[11] !== '0') { 35 | throw new Error('non 200 response'); 36 | } 37 | }; 38 | function onclose() {}; 39 | 40 | var data = U8([ 41 | 'GET / HTTP/1.1', 42 | 'Connection: close', 43 | 'Host: localhost:8080', 44 | 'Accept: text/html, text/plain', 45 | 'User-Agent: http-test', 46 | '', 47 | '' 48 | ].join('\r\n')); 49 | 50 | console.log('started...'); 51 | var time = performance.now(); 52 | 53 | var COUNT = 100000; 54 | 55 | for (var i = 0; i < COUNT; ++i) { 56 | var conn = tcpServer.addConnection(onsend, onclose); 57 | conn.data(data); 58 | } 59 | 60 | var timeEnd = (performance.now() - time) | 0; 61 | console.log('done ' + COUNT + ' connections in ' + timeEnd + 'ms (' + (timeEnd / COUNT).toFixed(4) + 'ms per connection)'); 62 | server.close(); 63 | process.exit(); 64 | -------------------------------------------------------------------------------- /example/run-client-get.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var eshttp = require('../index-node'); 3 | var HttpClient = eshttp.HttpClient; 4 | var HttpRequest = eshttp.HttpRequest; 5 | 6 | var request = new HttpRequest('GET', '/', { 'x-header': 'value' }); 7 | var client = new HttpClient('127.0.0.1', 8080); 8 | 9 | for (var i = 0; i < 10; ++i) { 10 | client.request(request, function(err, response) { 11 | console.log('response: ' + response.statusCode + ' ' + response.statusMessage); 12 | 13 | response.ondata = function(u8) { 14 | console.log('body chunk', u8); 15 | }; 16 | 17 | response.onend = function() { 18 | console.log('ended'); 19 | }; 20 | }); 21 | } 22 | 23 | client.close(); 24 | -------------------------------------------------------------------------------- /example/run-server-eshttp-dynamic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var eshttp = require('../index-node'); 3 | var server = new eshttp.HttpServer(); 4 | 5 | server.onrequest = request => { 6 | request.respondWith(200, { 'x-header': 'value' }, 'hello'); 7 | }; 8 | 9 | server.listen(8000); 10 | console.log('eshttp: listening to port 8000'); 11 | -------------------------------------------------------------------------------- /example/run-server-eshttp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var eshttp = require('../index-node'); 3 | var server = new eshttp.HttpServer(); 4 | var response = new eshttp.HttpResponse(200, { 'x-header': 'value' }, 'hello'); 5 | 6 | server.onrequest = request => { 7 | request.respondWith(response); 8 | }; 9 | 10 | server.listen(8080); 11 | console.log('eshttp: listening to port 8080'); 12 | -------------------------------------------------------------------------------- /example/run-server-http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var http = require('http'); 3 | 4 | var headers = { 'x-header': 'value', 'content-length': 5 }; 5 | 6 | var server = http.createServer((req, res) => { 7 | res.writeHead(200, headers); 8 | res.end('hello'); 9 | }); 10 | 11 | server.listen(8080); 12 | console.log('http: listening to port 8080'); 13 | -------------------------------------------------------------------------------- /example/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var eshttp = require('../index-node'); 3 | 4 | var request = new eshttp.HttpRequest('GET', '/ip', { 'host': 'httpbin.org' }); 5 | var client = new eshttp.HttpClient('54.175.222.246', 80); 6 | 7 | console.log(request.toString()); 8 | 9 | for (var i = 0; i < 10; ++i) { 10 | client.request(request, function(err, response) { 11 | console.log('response: ' + response.statusCode + ' ' + response.statusMessage); 12 | 13 | response.ondata = function(u8) { 14 | console.log('body chunk', Buffer(u8).toString()); 15 | }; 16 | 17 | response.onend = function() { 18 | console.log('ended'); 19 | }; 20 | }); 21 | } 22 | 23 | client.close(); 24 | -------------------------------------------------------------------------------- /index-node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (process.env.NODE_ENV === 'test') { 3 | require('./lib/backend').setBackend(require('./backend/backend-test')); 4 | } else { 5 | require('./lib/backend').setBackend(require('./backend/backend-node')); 6 | } 7 | module.exports = require('./lib/eshttp'); 8 | -------------------------------------------------------------------------------- /index-runtime.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./lib/backend').setBackend(require('./backend/backend-runtime')); 3 | module.exports = require('./lib/eshttp'); 4 | -------------------------------------------------------------------------------- /lib/backend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var backend = null; 4 | 5 | module.exports = function() { 6 | if (!backend) { 7 | throw new Error('backend has not been configured'); 8 | } 9 | return backend; 10 | }; 11 | 12 | module.exports.setBackend = function(b) { 13 | backend = b; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/date.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var dateCached = ''; 3 | var dateValue = 0; 4 | var refcount = 0; 5 | var interval = null; 6 | var servers = []; 7 | 8 | function updateDate() { 9 | var date = new Date(); 10 | dateCached = date.toUTCString(); 11 | dateValue = Math.round(date.getTime() / 1000); 12 | 13 | for (var i = 0, l = servers.length; i < l; ++i) { 14 | servers[i]._timeoutTick(); 15 | } 16 | } 17 | 18 | // set initial date 19 | updateDate(); 20 | 21 | // get date string to use in http header 22 | exports.getDateHeaderString = function() { 23 | return dateCached; 24 | }; 25 | 26 | // get date value for header caching 27 | exports.getDateValue = function() { 28 | return dateValue; 29 | }; 30 | 31 | exports.ref = function(server) { 32 | servers.push(server); 33 | if (++refcount === 1) { 34 | // refresh date every 5 seconds to improve performance 35 | interval = setInterval(updateDate, 5000); 36 | } 37 | }; 38 | 39 | exports.unref = function(server) { 40 | servers.splice(servers.indexOf(server), 1); 41 | if (--refcount === 0) { 42 | clearInterval(interval); 43 | interval = null; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /lib/eshttp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | exports.HttpServer = require('./http-server'); 3 | exports.HttpResponse = require('./http-response'); 4 | exports.HttpRequest = require('./http-request'); 5 | exports.HttpClient = require('./http-client'); 6 | -------------------------------------------------------------------------------- /lib/fetch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Request { 4 | } 5 | 6 | class Response { 7 | } 8 | 9 | function fetch(request) { 10 | return new Promise(function(resolve, reject) { 11 | }); 12 | } 13 | 14 | module.exports = fetch; 15 | 16 | global.Request = Request; 17 | global.Response = Response; 18 | global.fetch = fetch; 19 | -------------------------------------------------------------------------------- /lib/headers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // trying to be compatible with the spec 4 | // (except browser security features) 5 | // https://fetch.spec.whatwg.org/#headers-class 6 | class Headers { 7 | constructor(data) { 8 | this._names = []; 9 | this._values = []; 10 | 11 | if (data) { 12 | var keys = Object.keys(data); 13 | for (var i = 0; i < keys.length; ++i) { 14 | var key = keys[i]; 15 | var value = data[key]; 16 | 17 | if (key && value) { 18 | this._names.push(key.toLowerCase()); 19 | this._values.push(value); 20 | } 21 | } 22 | } 23 | } 24 | 25 | has(name) { 26 | return this._names.indexOf(name.toLowerCase()) >= 0; 27 | } 28 | 29 | get(name) { 30 | var index = this._names.indexOf(name.toLowerCase()); 31 | return index >= 0 ? this._values[index] : null; 32 | } 33 | 34 | getAll(name) { 35 | var result = []; 36 | name = name.toLowerCase(); 37 | for (var i = 0, l = this._names.length; i < l; ++i) { 38 | if (this._names[i] === name) { 39 | result.push(this._values[i]); 40 | } 41 | } 42 | return result; 43 | } 44 | 45 | set(name, value) { 46 | name = name.toLowerCase(); 47 | var index = this._names.indexOf(name); 48 | if (index >= 0) { 49 | this._values[index] = value; 50 | return; 51 | } 52 | 53 | this._names.push(name); 54 | this._values.push(value); 55 | } 56 | 57 | append(name, value) { 58 | this._names.push(name.toLowerCase()); 59 | this._values.push(value); 60 | } 61 | 62 | delete(name) { 63 | name = name.toLowerCase(); 64 | var index = this._names.indexOf(name); 65 | if (index === -1) { 66 | return; 67 | } 68 | 69 | var newNames = []; 70 | var newValues = []; 71 | for (var i = 0, l = this._names.length; i < l; ++i) { 72 | if (this._names[i] !== name) { 73 | newNames.push(this._names[i]); 74 | newValues.push(this._values[i]); 75 | } 76 | } 77 | 78 | this._names = newNames; 79 | this._values = newValues; 80 | } 81 | 82 | keys() { 83 | return this._names; 84 | } 85 | 86 | values() { 87 | return this._values; 88 | } 89 | } 90 | 91 | // TODO: use ES6 computed property in class body 92 | // (when enabled in Node) 93 | Headers.prototype[Symbol.iterator] = function*() { 94 | for (var i = 0, l = this._names.length; i < l; ++i) { 95 | yield [this._names[i], this._values[i]]; 96 | } 97 | }; 98 | 99 | module.exports = Headers; 100 | -------------------------------------------------------------------------------- /lib/http-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var backend = require('./backend')(); 3 | var HttpResponse = require('./http-response'); 4 | 5 | var CLIENT_STATE_CLOSED = 0; 6 | var CLIENT_STATE_CONNECTING = 1; 7 | var CLIENT_STATE_CONNECTED = 2; 8 | 9 | var QUEUE_ITEM_REQUEST = 0; 10 | var QUEUE_ITEM_DONE = 1; 11 | 12 | class HttpClient { 13 | constructor(ip, port) { 14 | this._ip = ip; 15 | this._port = port; 16 | this._handle = backend.createClientHandle(this); 17 | this._state = CLIENT_STATE_CLOSED; 18 | this._requestQueue = []; 19 | this._requestsSent = 0; 20 | } 21 | 22 | request(request, cb) { 23 | var buf = request._getBuffer(); 24 | this._requestQueue.push([buf, cb, new HttpResponse(0, null), QUEUE_ITEM_REQUEST]); 25 | this._nextRequest(); 26 | 27 | } 28 | 29 | close() { 30 | this._requestQueue.push([null, null, null, QUEUE_ITEM_DONE]); 31 | } 32 | 33 | _nextRequest() { 34 | if (this._requestQueue.length === 0) { 35 | return; 36 | } 37 | 38 | if (this._requestQueue[0][3] === QUEUE_ITEM_DONE) { 39 | this._doneHandler(); 40 | return; 41 | } 42 | 43 | if (this._state === CLIENT_STATE_CONNECTING) { 44 | return; 45 | } 46 | 47 | // HTTP/1.1 can handle only one request at a time 48 | if (this._requestsSent > 0) { 49 | return; 50 | } 51 | 52 | if (this._state === CLIENT_STATE_CLOSED) { 53 | this._state = CLIENT_STATE_CONNECTING; 54 | backend.connect(this._handle, this._ip, this._port); 55 | return; 56 | } 57 | 58 | var request = this._requestQueue[0]; 59 | backend.send(this._handle, request[0]); 60 | } 61 | 62 | _doneHandler() { 63 | if (this._state === CLIENT_STATE_CONNECTED) { 64 | backend.closeClientHandle(this._handle); 65 | } 66 | } 67 | 68 | _openHandler() { 69 | if (this._state === CLIENT_STATE_CONNECTING) { 70 | this._state = CLIENT_STATE_CONNECTED; 71 | this._nextRequest(); 72 | } 73 | } 74 | 75 | _dataHandler(u8) { 76 | if (this._state !== CLIENT_STATE_CONNECTED) { 77 | return; 78 | } 79 | 80 | if (this._requestQueue.length === 0) { 81 | return; 82 | } 83 | 84 | var request = this._requestQueue[0]; 85 | var callback = request[1]; 86 | var response = request[2]; 87 | 88 | response._chunk(u8); 89 | 90 | if (response._parser._headersReceived) { 91 | if (callback) { 92 | callback(null, response); 93 | request[1] = null; 94 | } 95 | 96 | if (response.ondata && response._parser._lastBodyChunk) { 97 | response.ondata(response._parser._lastBodyChunk); 98 | response._parser._lastBodyChunk = null; 99 | } 100 | } 101 | 102 | if (response._parser.isComplete()) { 103 | this._requestQueue.shift(); 104 | 105 | if (response.onend) { 106 | response.onend(); 107 | } 108 | 109 | this._nextRequest(); 110 | } 111 | } 112 | 113 | _endHandler() { 114 | } 115 | 116 | _closeHandler() { 117 | } 118 | } 119 | 120 | module.exports = HttpClient; 121 | -------------------------------------------------------------------------------- /lib/http-codes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 100: 'Continue', 5 | 101: 'Switching Protocols', 6 | 200: 'OK', 7 | 201: 'Created', 8 | 202: 'Accepted', 9 | 203: 'Non-Authoritative Information', 10 | 204: 'No Content', 11 | 205: 'Reset Content', 12 | 206: 'Partial Content', 13 | 300: 'Multiple Choices', 14 | 301: 'Moved Permanently', 15 | 302: 'Found', 16 | 303: 'See Other', 17 | 304: 'Not Modified', 18 | 305: 'Use Proxy', 19 | 307: 'Temporary Redirect', 20 | 400: 'Bad Request', 21 | 401: 'Unauthorized', 22 | 402: 'Payment Required', 23 | 403: 'Forbidden', 24 | 404: 'Not Found', 25 | 405: 'Method Not Allowed', 26 | 406: 'Not Acceptable', 27 | 407: 'Proxy Authentication Required', 28 | 408: 'Request Timeout', 29 | 409: 'Conflict', 30 | 410: 'Gone', 31 | 411: 'Length Required', 32 | 412: 'Precondition Failed', 33 | 413: 'Request Entity Too Large', 34 | 414: 'Request-URI Too Long', 35 | 415: 'Unsupported Media Type', 36 | 416: 'Requested Range Not Satisfiable', 37 | 417: 'Expectation Failed', 38 | 500: 'Internal Server Error', 39 | 501: 'Not Implemented', 40 | 502: 'Bad Gateway', 41 | 503: 'Service Unavailable', 42 | 504: 'Gateway Timeout', 43 | 505: 'HTTP Version Not Supported' 44 | }; 45 | -------------------------------------------------------------------------------- /lib/http-connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var backend = require('./backend')(); 3 | var HttpRequest = require('./http-request'); 4 | 5 | class HttpConnection { 6 | constructor(server, socket) { 7 | this._server = server; 8 | this._socket = socket; 9 | this._request = new HttpRequest('', '', null); 10 | this._request._connection = this; 11 | this._request._setupParser(); 12 | this._requestHandled = false; 13 | this._done = false; 14 | this._timeoutTicks = 0; 15 | } 16 | 17 | _timeoutTick() { 18 | if (this._done) { 19 | return; 20 | } 21 | 22 | if (++this._timeoutTicks > 4) { 23 | this._done = true; 24 | backend.close(this._socket); 25 | } 26 | } 27 | 28 | _dataHandler(u8) { 29 | if (this._done) { 30 | return; 31 | } 32 | 33 | this._timeoutTicks = 0; 34 | 35 | var offset = 0; 36 | while (offset < u8.length) { 37 | this._request._chunk(u8, offset); 38 | offset = this._request._parser.lastParsedIndex + 1; 39 | 40 | if (this._request._parser.isError()) { 41 | this._done = true; 42 | backend.close(this._socket); 43 | return; 44 | } 45 | 46 | if (!this._requestHandled && this._request._parser.headersReceived) { 47 | this._requestHandled = true; 48 | this._server.onrequest(this._request); 49 | } else { 50 | var lastChunk = this._request._parser.lastBodyChunk; 51 | if (lastChunk && this._request.ondata) { 52 | this._request.ondata(lastChunk); 53 | } 54 | } 55 | 56 | if (this._request.isComplete()) { 57 | if (this._request.onend) { 58 | this._request.onend(); 59 | } 60 | this._request._sendResponse(); 61 | this._request = new HttpRequest('', '', null); 62 | this._request._connection = this; 63 | this._request._setupParser(); 64 | this._requestHandled = false; 65 | } 66 | } 67 | } 68 | 69 | _endHandler() { 70 | } 71 | 72 | _closeHandler() { 73 | } 74 | 75 | _sendAndClose(u8) { 76 | this._done = true; 77 | backend.sendAndClose(this._socket, u8); 78 | } 79 | 80 | _send(u8) { 81 | backend.send(this._socket, u8); 82 | } 83 | } 84 | 85 | module.exports = HttpConnection; 86 | -------------------------------------------------------------------------------- /lib/http-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Headers = require('./headers'); 3 | var tokens = require('./tokens'); 4 | var isValidMethodCharCode = tokens.isValidMethodCharCode; 5 | var isValidCharCode = tokens.isValidCharCode; 6 | 7 | const PARSER_MAX_HEADERS_LENGTH = 1024 * 16; /* 16 KiB */ 8 | const PARSER_MAX_TRAILERS_LENGTH = 1024 * 1; /* 1 KiB */ 9 | 10 | var PARSER_STATE_METHOD = 0; 11 | var PARSER_STATE_PATH = 1; 12 | var PARSER_STATE_PROTOCOL = 2; 13 | var PARSER_STATE_PROTOCOL_H = 3; 14 | var PARSER_STATE_PROTOCOL_HT = 4; 15 | var PARSER_STATE_PROTOCOL_HTT = 5; 16 | var PARSER_STATE_PROTOCOL_HTTP = 6; 17 | var PARSER_STATE_PROTOCOL_SLASH = 7; 18 | var PARSER_STATE_VERSION_MAJOR = 8; 19 | var PARSER_STATE_VERSION_DOT = 9; 20 | var PARSER_STATE_VERSION_MINOR = 10; 21 | var PARSER_STATE_CODE_1 = 11; 22 | var PARSER_STATE_CODE_2 = 12; 23 | var PARSER_STATE_CODE_3 = 13; 24 | var PARSER_STATE_CODE_SPACE = 14; 25 | var PARSER_STATE_PHRASE = 15; 26 | var PARSER_STATE_HTTP_HEADERS = 16; 27 | var PARSER_STATE_HTTP_HEADER_KEY = 17; 28 | var PARSER_STATE_HTTP_HEADER_VALUE = 18; 29 | var PARSER_STATE_BODY_CHUNK = 19; 30 | var PARSER_STATE_BODY_CHUNK_SIZE = 20; 31 | var PARSER_STATE_BODY_CHUNK_NEXT = 21; 32 | var PARSER_STATE_DONE = 22; 33 | var PARSER_STATE_ERROR = 23; 34 | var PARSER_STATE_HTTP_TRAILERS = 24; 35 | var PARSER_STATE_HTTP_TRAILER_KEY = 25; 36 | var PARSER_STATE_HTTP_TRAILER_VALUE = 26; 37 | 38 | var HTTP_CONNECTION_CLOSE = 0; 39 | var HTTP_CONNECTION_KEEP_ALIVE = 1; 40 | 41 | class HttpParser { 42 | constructor(isRequest, headers) { 43 | this._isRequest = isRequest; 44 | this._method = ''; 45 | this._path = ''; 46 | this._versionMajor = 0; 47 | this._versionMinor = 0; 48 | this._state = isRequest ? PARSER_STATE_METHOD : PARSER_STATE_PROTOCOL; 49 | this._code = 0; 50 | this._phrase = ''; 51 | this._tmpStringKey = ''; 52 | this._tmpStringValue = ''; 53 | this._headersReceived = false; 54 | this._bodyReceived = false; 55 | this._hasBody = false; 56 | this._lastParsedIndex = 0; 57 | this._chunkLength = 0; 58 | this._isChunked = false; 59 | this._lastBodyChunk = null; 60 | this._headersLength = 0; 61 | this._connection = HTTP_CONNECTION_KEEP_ALIVE; 62 | this.headers = headers ? headers : new Headers(); 63 | this._trailers = null; 64 | } 65 | 66 | get path() { return this._path; } 67 | get method() { return this._method; } 68 | get versionMajor() { return this._versionMajor; } 69 | get versionMinor() { return this._versionMinor; } 70 | get statusCode() { return this._code; } 71 | get statusMessage() { return this._phrase; } 72 | get lastBodyChunk() { return this._lastBodyChunk; } 73 | get headersReceived() { return this._headersReceived; } 74 | get lastParsedIndex() { return this._lastParsedIndex; } 75 | 76 | get trailers() { 77 | return this._trailers ? this._trailers : new Headers(); 78 | } 79 | 80 | hasBody() { 81 | return this._hasBody; 82 | } 83 | 84 | _error() { 85 | this._state = PARSER_STATE_ERROR; 86 | } 87 | 88 | _addHeader(name, value) { 89 | name = name.toLowerCase(); 90 | value = value.trimRight(); 91 | 92 | // RFC2616 (section 4.3) 93 | // the presence of a message-body is signaled by the inclusion 94 | // of a Content-Length or Transfer-Encoding header 95 | switch (name) { 96 | case 'content-length': 97 | this._chunkLength = Number(value); 98 | this._hasBody = true; 99 | return; 100 | case 'transfer-encoding': 101 | this._isChunked = value.indexOf('chunked') >= 0; 102 | this._hasBody = true; 103 | return; 104 | case 'connection': 105 | if (value === 'close') { 106 | this._connection = HTTP_CONNECTION_CLOSE; 107 | } 108 | return; 109 | } 110 | 111 | // push directly into private state to avoid possible validation 112 | this.headers._names.push(name); 113 | this.headers._values.push(value); 114 | } 115 | 116 | _addTrailer(name, value) { 117 | name = name.toLowerCase(); 118 | value = value.trimRight(); 119 | 120 | // lazy-create trailers object 121 | if (!this._trailers) { 122 | this._trailers = new Headers(); 123 | } 124 | 125 | // push directly into private state to avoid possible validation 126 | this._trailers._names.push(name); 127 | this._trailers._values.push(value); 128 | } 129 | 130 | isComplete() { 131 | return this._state === PARSER_STATE_DONE; 132 | } 133 | 134 | isKeepAlive() { 135 | return this._connection === HTTP_CONNECTION_KEEP_ALIVE; 136 | } 137 | 138 | isChunked() { 139 | return this._isChunked; 140 | } 141 | 142 | isError() { 143 | return this._state === PARSER_STATE_ERROR; 144 | } 145 | 146 | chunk(u8, offset) { 147 | offset = offset | 0; 148 | this._lastBodyChunk = null; 149 | 150 | for (var i = offset, l = u8.length; i < l; ++i) { 151 | var c = String.fromCharCode(u8[i]); 152 | this._lastParsedIndex = i; 153 | 154 | if (!this._headersReceived && ++this._headersLength > PARSER_MAX_HEADERS_LENGTH) { 155 | this._error(); 156 | return; 157 | } 158 | 159 | if (this._bodyReceived && ++this._headersLength > PARSER_MAX_TRAILERS_LENGTH) { 160 | this._error(); 161 | return; 162 | } 163 | 164 | if (c === '\r') { 165 | continue; 166 | } 167 | 168 | switch (this._state) { 169 | case PARSER_STATE_METHOD: 170 | // allow any number of newlines before the request method 171 | if (c === '\n') { 172 | continue; 173 | } 174 | 175 | if (c === ' ') { 176 | this._state = PARSER_STATE_PATH; 177 | continue; 178 | } 179 | 180 | if (this._method.length < 10 && isValidMethodCharCode(u8[i])) { 181 | this._method += c; 182 | } else { 183 | this._error(); 184 | return; 185 | } 186 | continue; 187 | case PARSER_STATE_PATH: 188 | if (c === ' ') { 189 | this._state = PARSER_STATE_PROTOCOL; 190 | this._tokenPosition = 0; 191 | continue; 192 | } 193 | 194 | if (c === '\n') { 195 | this._error(); 196 | return; 197 | } 198 | 199 | this._path += c; 200 | continue; 201 | case PARSER_STATE_PROTOCOL: 202 | if (c === 'H') { 203 | this._state = PARSER_STATE_PROTOCOL_H; 204 | continue; 205 | } 206 | this._error(); 207 | return; 208 | case PARSER_STATE_PROTOCOL_H: 209 | if (c === 'T') { 210 | this._state = PARSER_STATE_PROTOCOL_HT; 211 | continue; 212 | } 213 | this._error(); 214 | return; 215 | case PARSER_STATE_PROTOCOL_HT: 216 | if (c === 'T') { 217 | this._state = PARSER_STATE_PROTOCOL_HTT; 218 | continue; 219 | } 220 | this._error(); 221 | return; 222 | case PARSER_STATE_PROTOCOL_HTT: 223 | if (c === 'P') { 224 | this._state = PARSER_STATE_PROTOCOL_HTTP; 225 | continue; 226 | } 227 | this._error(); 228 | return; 229 | case PARSER_STATE_PROTOCOL_HTTP: 230 | if (c === '/') { 231 | this._state = PARSER_STATE_PROTOCOL_SLASH; 232 | continue; 233 | } 234 | this._error(); 235 | return; 236 | case PARSER_STATE_PROTOCOL_SLASH: 237 | if (c === '1') { 238 | this._state = PARSER_STATE_VERSION_MAJOR; 239 | this._versionMajor = c | 0; 240 | continue; 241 | } 242 | this._error(); 243 | return; 244 | case PARSER_STATE_VERSION_MAJOR: 245 | if (c === '.') { 246 | this._state = PARSER_STATE_VERSION_DOT; 247 | continue; 248 | } 249 | this._error(); 250 | return; 251 | case PARSER_STATE_VERSION_DOT: 252 | if (c === '0' || c === '1') { 253 | this._state = PARSER_STATE_VERSION_MINOR; 254 | this._versionMinor = c | 0; 255 | 256 | // HTTP/1.0 defaults to connection: close 257 | if (this._versionMajor === 1 && this._versionMinor === 0) { 258 | this._connection = HTTP_CONNECTION_CLOSE; 259 | } 260 | 261 | continue; 262 | } 263 | this._error(); 264 | return; 265 | case PARSER_STATE_VERSION_MINOR: 266 | if (this._isRequest) { 267 | if (c === '\n') { 268 | this._state = PARSER_STATE_HTTP_HEADERS; 269 | continue; 270 | } 271 | } else { 272 | if (c === ' ') { 273 | this._state = PARSER_STATE_CODE_1; 274 | continue; 275 | } 276 | } 277 | this._error(); 278 | return; 279 | case PARSER_STATE_CODE_1: 280 | this._code = Number(c) * 100; 281 | this._state = PARSER_STATE_CODE_2; 282 | continue; 283 | case PARSER_STATE_CODE_2: 284 | this._code += Number(c) * 10; 285 | this._state = PARSER_STATE_CODE_3; 286 | continue; 287 | case PARSER_STATE_CODE_3: 288 | this._code += Number(c); 289 | this._state = PARSER_STATE_CODE_SPACE; 290 | continue; 291 | case PARSER_STATE_CODE_SPACE: 292 | if (c === ' ') { 293 | this._state = PARSER_STATE_PHRASE; 294 | continue; 295 | } 296 | // response phrase is optional 297 | if (c === '\n') { 298 | this._state = PARSER_STATE_HTTP_HEADERS; 299 | continue; 300 | } 301 | this._error(); 302 | return; 303 | case PARSER_STATE_PHRASE: 304 | if (c === '\n') { 305 | this._state = PARSER_STATE_HTTP_HEADERS; 306 | continue; 307 | } 308 | this._phrase += c; 309 | break; 310 | case PARSER_STATE_HTTP_HEADERS: 311 | if (c === '\n') { 312 | if (this._tmpStringKey) { 313 | this._addHeader(this._tmpStringKey, this._tmpStringValue); 314 | } 315 | this._headersReceived = true; 316 | if (this._hasBody) { 317 | this._state = this._isChunked ? PARSER_STATE_BODY_CHUNK_SIZE : PARSER_STATE_BODY_CHUNK; 318 | this._tmpStringKey = ''; 319 | } else { 320 | this._state = PARSER_STATE_DONE; 321 | } 322 | 323 | return; 324 | } 325 | 326 | // multiline header value 327 | if (c === ' ') { 328 | if (this._tmpStringKey === '') { 329 | this._error(); 330 | return; 331 | } 332 | 333 | if (this._tmpStringValue.length > 0) { 334 | this._tmpStringValue += c; 335 | } 336 | 337 | this._state = PARSER_STATE_HTTP_HEADER_VALUE; 338 | continue; 339 | } 340 | 341 | if (!isValidCharCode(u8[i])) { 342 | this._error(); 343 | return; 344 | } 345 | 346 | if (this._tmpStringKey) { 347 | this._addHeader(this._tmpStringKey, this._tmpStringValue); 348 | } 349 | this._tmpStringKey = c; 350 | this._state = PARSER_STATE_HTTP_HEADER_KEY; 351 | break; 352 | case PARSER_STATE_HTTP_HEADER_KEY: 353 | if (c === ':') { 354 | this._state = PARSER_STATE_HTTP_HEADER_VALUE; 355 | this._tmpStringValue = ''; 356 | continue; 357 | } 358 | 359 | if (!isValidCharCode(u8[i])) { 360 | this._error(); 361 | return; 362 | } 363 | 364 | this._tmpStringKey += c; 365 | break; 366 | case PARSER_STATE_HTTP_HEADER_VALUE: 367 | if (c === '\n') { 368 | this._state = PARSER_STATE_HTTP_HEADERS; 369 | continue; 370 | } 371 | 372 | // skip spaces before header value 373 | // "x-header: value" => "x-header" = "value" 374 | if (c === ' ' && this._tmpStringValue.length === 0) { 375 | continue; 376 | } 377 | 378 | this._tmpStringValue += c; 379 | break; 380 | case PARSER_STATE_BODY_CHUNK: 381 | this._lastBodyChunk = u8.subarray(i, Math.min(i + this._chunkLength, u8.length)); 382 | this._chunkLength -= this._lastBodyChunk.length; 383 | i += this._lastBodyChunk.length; 384 | this._lastParsedIndex = i; 385 | if (this._chunkLength === 0) { 386 | this._state = this._isChunked ? PARSER_STATE_BODY_CHUNK_NEXT : PARSER_STATE_DONE; 387 | this._tmpStringKey = ''; 388 | if (this._state === PARSER_STATE_DONE) { 389 | this._bodyReceived = true; 390 | return; 391 | } 392 | } 393 | 394 | return; 395 | case PARSER_STATE_BODY_CHUNK_SIZE: 396 | if (c === '\n') { 397 | this._chunkLength = parseInt(this._tmpStringKey, 16); 398 | if (this._chunkLength === 0) { 399 | this._tmpStringKey = ''; 400 | this._headersLength = 0; 401 | this._bodyReceived = true; 402 | this._state = PARSER_STATE_HTTP_TRAILERS; 403 | } else { 404 | this._state = PARSER_STATE_BODY_CHUNK; 405 | } 406 | continue; 407 | } 408 | this._tmpStringKey += c; 409 | break; 410 | case PARSER_STATE_BODY_CHUNK_NEXT: 411 | if (c === '\n') { 412 | this._state = PARSER_STATE_BODY_CHUNK_SIZE; 413 | continue; 414 | } 415 | this._error(); 416 | return; 417 | case PARSER_STATE_HTTP_TRAILERS: 418 | if (c === '\n') { 419 | if (this._tmpStringKey) { 420 | this._addTrailer(this._tmpStringKey, this._tmpStringValue); 421 | } 422 | 423 | this._state = PARSER_STATE_DONE; 424 | return; 425 | } 426 | 427 | // multiline trailer value 428 | if (c === ' ') { 429 | if (this._tmpStringKey === '') { 430 | this._error(); 431 | return; 432 | } 433 | 434 | if (this._tmpStringValue.length > 0) { 435 | this._tmpStringValue += c; 436 | } 437 | 438 | this._state = PARSER_STATE_HTTP_TRAILER_VALUE; 439 | continue; 440 | } 441 | 442 | if (!isValidCharCode(u8[i])) { 443 | this._error(); 444 | return; 445 | } 446 | 447 | if (this._tmpStringKey) { 448 | this._addTrailer(this._tmpStringKey, this._tmpStringValue); 449 | } 450 | 451 | this._tmpStringKey = c; 452 | this._state = PARSER_STATE_HTTP_TRAILER_KEY; 453 | continue; 454 | case PARSER_STATE_HTTP_TRAILER_KEY: 455 | if (c === ':') { 456 | this._state = PARSER_STATE_HTTP_TRAILER_VALUE; 457 | this._tmpStringValue = ''; 458 | continue; 459 | } 460 | 461 | if (!isValidCharCode(u8[i])) { 462 | this._error(); 463 | return; 464 | } 465 | 466 | this._tmpStringKey += c; 467 | continue; 468 | case PARSER_STATE_HTTP_TRAILER_VALUE: 469 | if (c === '\n') { 470 | this._state = PARSER_STATE_HTTP_TRAILERS; 471 | continue; 472 | } 473 | 474 | // skip spaces before trailer value 475 | // "x-trailer: value" => "x-trailer" = "value" 476 | if (c === ' ' && this._tmpStringValue.length === 0) { 477 | continue; 478 | } 479 | 480 | this._tmpStringValue += c; 481 | continue; 482 | } 483 | } 484 | } 485 | } 486 | 487 | module.exports = HttpParser; 488 | -------------------------------------------------------------------------------- /lib/http-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var HttpParser = require('./http-parser'); 3 | var HttpResponse = require('./http-response'); 4 | var Headers = require('./headers'); 5 | var concatBuffers = require('concat-buffers'); 6 | var CRLF = '\r\n'; 7 | 8 | function makeHeader(method, path, headers, body) { 9 | var header = String(method) + ' ' + String(path) + ' HTTP/1.1' + CRLF; 10 | 11 | if (body) { 12 | header += 'content-length: ' + String(body.length) + CRLF; 13 | } 14 | 15 | for (var i = 0, l = headers._names.length; i < l; ++i) { 16 | header += String(headers._names[i]) + ': ' + String(headers._values[i]) + CRLF; 17 | } 18 | 19 | if (!body) { 20 | return header + CRLF; 21 | } 22 | 23 | return header + CRLF + body + CRLF; 24 | } 25 | 26 | function stringToBuffer(str) { 27 | return new Buffer(str); 28 | } 29 | 30 | class HttpRequest { 31 | constructor(method, path, headers, body, opts) { 32 | this._method = method; 33 | this._path = path; 34 | if (headers instanceof Headers) { 35 | this._headers = headers; 36 | } else { 37 | this._headers = new Headers(headers); 38 | } 39 | this._body = body ? body : ''; 40 | this._u8cache = null; 41 | this._parser = null; 42 | this._connection = null; 43 | // this._bodyChunks = []; 44 | this._response = null; 45 | this._responseSent = false; 46 | this.ondata = null; 47 | this.onend = null; 48 | } 49 | 50 | // arrayBuffer(cb) { 51 | // this.ondata = function(chunk) { 52 | // this._bodyChunks.push(chunk); 53 | // }; 54 | // this.onend = function() { 55 | // cb(concatBuffers(this._bodyChunks)); 56 | // }; 57 | // } 58 | // 59 | // text() { 60 | // this.ondata = function(chunk) { 61 | // this._bodyChunks.push(chunk); 62 | // }; 63 | // this.onend = function() { 64 | // var decoder = new TextDecoder('utf-8'); 65 | // cb(decoder.decode(concatBuffers(this._bodyChunks))); 66 | // }; 67 | // } 68 | 69 | get method() { 70 | return this._parser ? this._parser._method : this._method; 71 | } 72 | 73 | get path() { 74 | return this._parser ? this._parser._path : this._path; 75 | } 76 | 77 | get httpVersion() { 78 | return this._parser._versionMajor + '.' + this._parser._versionMinor; 79 | } 80 | 81 | get hasBody() { 82 | return this._parser ? this._parser.hasBody() : true; 83 | } 84 | 85 | get headers() { 86 | return this._headers; 87 | } 88 | 89 | isComplete() { 90 | return this._parser ? this._parser.isComplete() : true; 91 | } 92 | 93 | respondWith(response, a, b) { 94 | if (!this._connection || !this._parser) { 95 | throw new Error('no connection'); 96 | } 97 | 98 | if (this._response) { 99 | throw new Error('response has already been sent'); 100 | } 101 | 102 | if (typeof response === 'number') { 103 | this._response = new HttpResponse(response, a, b); 104 | } else { 105 | this._response = response; 106 | } 107 | 108 | if (this._parser.isComplete()) { 109 | this._sendResponse(); 110 | } 111 | } 112 | 113 | _sendResponse() { 114 | if (this._responseSent || !this._response) { 115 | return; 116 | } 117 | 118 | this._responseSent = true; 119 | if (this._parser.isKeepAlive()) { 120 | this._connection._send(this._response._getBuffer(true)); 121 | } else { 122 | this._connection._sendAndClose(this._response._getBuffer(false)); 123 | } 124 | } 125 | 126 | _setupParser() { 127 | this._parser = new HttpParser(true, this._headers); 128 | } 129 | 130 | _chunk(u8, offset) { 131 | this._parser.chunk(u8, offset); 132 | } 133 | 134 | _getBuffer() { 135 | if (this._u8cache) { 136 | return this._u8cache; 137 | } 138 | 139 | this._u8cache = stringToBuffer(makeHeader(this._method, this._path, this._headers, this._body)); 140 | return this._u8cache; 141 | } 142 | 143 | toString() { 144 | return makeHeader(this._method, this._path, this._headers, this._body); 145 | } 146 | } 147 | 148 | module.exports = HttpRequest; 149 | -------------------------------------------------------------------------------- /lib/http-response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var backend = require('./backend')(); 3 | var date = require('./date'); 4 | var Headers = require('./headers'); 5 | var HttpParser = require('./http-parser'); 6 | var codes = require('./http-codes'); 7 | var makeHeader = require('./response-header'); 8 | var stringToSocketData = backend.stringToSocketData; 9 | var CRLF = '\r\n'; 10 | 11 | class HttpResponse { 12 | constructor(code, headers, body, opts) { 13 | this._code = code | 0; 14 | this._headers = null; 15 | if (headers instanceof Headers) { 16 | this._headers = headers; 17 | } else { 18 | this._headers = new Headers(headers); 19 | } 20 | this._body = body; 21 | this._chunked = body === void 0 || body === null; 22 | this._opts = opts; 23 | 24 | // transmit buffer cache 25 | this._u8cache = null; 26 | this._u8cacheKeepAlive = null; 27 | this._u8cacheDate = 0; 28 | 29 | // client response 30 | this._parser = null; 31 | this.ondata = null; 32 | this.onend = null; 33 | } 34 | 35 | get trailers() { 36 | return this._parser ? this._parser.trailers : new Headers(); 37 | } 38 | 39 | get statusCode() { 40 | return this._parser ? this._parser._code : this._code; 41 | } 42 | 43 | get statusMessage() { 44 | if (this._parser) { 45 | return this._parser._phrase; 46 | } 47 | 48 | return codes[this._code] || 'Unknown'; 49 | } 50 | 51 | _chunk(u8) { 52 | if (!this._parser) { 53 | this._parser = new HttpParser(false, this._headers); 54 | } 55 | 56 | this._parser.chunk(u8); 57 | } 58 | 59 | _getBuffer(keepAlive) { 60 | var cachedValue = keepAlive ? this._u8cacheKeepAlive : this._u8cache; 61 | var dateValue = date.getDateValue(); 62 | if (cachedValue && this._u8cacheDate === dateValue) { 63 | return cachedValue; 64 | } 65 | 66 | // recreate headers every time for the date header 67 | this._u8cacheDate = dateValue; 68 | cachedValue = stringToSocketData(makeHeader(this._code, 69 | this._headers, this._body.length, this._chunked, keepAlive) + CRLF + this._body); 70 | 71 | if (keepAlive) { 72 | this._u8cacheKeepAlive = cachedValue; 73 | } else { 74 | this._u8cache = cachedValue; 75 | } 76 | 77 | return cachedValue; 78 | } 79 | } 80 | 81 | module.exports = HttpResponse; 82 | -------------------------------------------------------------------------------- /lib/http-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var backend = require('./backend')(); 3 | var date = require('./date'); 4 | var HttpConnection = require('./http-connection'); 5 | var connections = new Map(); 6 | 7 | class HttpServer { 8 | constructor() { 9 | this.onrequest = function() {}; 10 | this.onclose = function() {}; 11 | this._handle = backend.createServerHandle(this); 12 | } 13 | 14 | _connectionHandler(socket) { 15 | connections.set(socket, new HttpConnection(this, socket)); 16 | } 17 | 18 | _dataHandler(socket, u8) { 19 | var conn = connections.get(socket); 20 | if (!conn) { 21 | return; 22 | } 23 | 24 | conn._dataHandler(u8); 25 | } 26 | 27 | _endHandler(socket) { 28 | var conn = connections.get(socket); 29 | if (!conn) { 30 | return; 31 | } 32 | 33 | conn._endHandler(); 34 | } 35 | 36 | _timeoutTick() { 37 | for (var conn of connections.values()) { 38 | conn._timeoutTick(); 39 | } 40 | } 41 | 42 | _closeHandler(socket) { 43 | var conn = connections.get(socket); 44 | if (!conn) { 45 | return; 46 | } 47 | 48 | conn._closeHandler(); 49 | connections.delete(socket); 50 | } 51 | 52 | listen(port) { 53 | date.ref(this); 54 | backend.listen(this._handle, port); 55 | } 56 | 57 | close() { 58 | date.unref(this); 59 | backend.unlisten(this._handle); 60 | } 61 | } 62 | 63 | module.exports = HttpServer; 64 | -------------------------------------------------------------------------------- /lib/response-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var CRLF = '\r\n'; 3 | var date = require('./date'); 4 | var codes = require('./http-codes'); 5 | 6 | function statusLine(code) { 7 | if (code === 200) { 8 | return 'HTTP/1.1 200 OK'; 9 | } 10 | 11 | var phrase = codes[code] || 'Unknown'; 12 | return 'HTTP/1.1 ' + String(code) + ' ' + String(phrase); 13 | } 14 | 15 | function makeHeader(code, headers, bodyLength, isChunked, isKeepAlive) { 16 | var header = String(statusLine(code)) + CRLF; 17 | 18 | if (isKeepAlive) { 19 | header += 'connection: keep-alive' + CRLF; 20 | } else { 21 | header += 'connection: close' + CRLF; 22 | } 23 | 24 | // append date header 25 | header += 'date: ' + String(date.getDateHeaderString()) + CRLF; 26 | 27 | if (isChunked) { 28 | header += 'transfer-encoding: chunked' + CRLF; 29 | } else { 30 | header += 'content-length: ' + String(bodyLength) + CRLF; 31 | } 32 | 33 | for (var i = 0, l = headers._names.length; i < l; ++i) { 34 | header += String(headers._names[i]) + ': ' + String(headers._values[i]) + CRLF; 35 | } 36 | 37 | return header; 38 | } 39 | 40 | module.exports = makeHeader; 41 | -------------------------------------------------------------------------------- /lib/tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // according to rfc 2616 4 | function isValidToken(c) { 5 | var isInvalid = c <= 0x20 || c >= 0x7f || 6 | c === 40 || c === 41 || // ( ) 7 | c === 60 || c === 62 || // < > 8 | c === 64 || c === 44 || // @ , 9 | c === 59 || c === 58 || // ; : 10 | c === 92 || c === 34 || // \ " 11 | c === 47 || c === 91 || // / [ 12 | c === 93 || c === 63 || // ] ? 13 | c === 61 || c === 123 || // = { 14 | c === 125; // } 15 | return !isInvalid; 16 | } 17 | 18 | var table = new Uint8Array(256); 19 | for (var i = 0; i < 256; ++i) { 20 | table[i] = isValidToken(i) ? i : 0; 21 | } 22 | 23 | function isValidCharCode(charCode) { 24 | return table[charCode & 0xff] !== 0; 25 | } 26 | 27 | exports.isValidCharCode = isValidCharCode; 28 | 29 | exports.isValidMethodCharCode = function(charCode) { 30 | return charCode >= 65 && charCode <= 90; // A-Z 31 | }; 32 | 33 | exports.isValidToken = function(str) { 34 | if (!str) { 35 | return false; 36 | } 37 | 38 | for (var i = 0; i < str.length; ++i) { 39 | if (!isValidCharCode(str.charCodeAt(i))) { 40 | return false; 41 | } 42 | } 43 | 44 | return true; 45 | }; 46 | 47 | exports.isValidHeaderName = isValidToken; 48 | 49 | exports.isValidHeaderValue = function(str) { 50 | for (var i = 0; i < str.length; ++i) { 51 | var code = str.charCodeAt(i); 52 | if (code > 127 || code === 0 || code === 10 /* \n */ || code === 13 /* \r */) { 53 | return false; 54 | } 55 | } 56 | 57 | return true; 58 | }; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eshttp", 3 | "version": "0.5.2", 4 | "description": "ES6-style pure JavaScript HTTP library", 5 | "main": "index-node.js", 6 | "runtime": "index-runtime.js", 7 | "scripts": { 8 | "lint": "eslint .", 9 | "test": "tape test/index.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:iefserge/eshttp.git" 14 | }, 15 | "author": "Sergii Iefremov", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/iefserge/eshttp/issues" 19 | }, 20 | "homepage": "http://runtimejs.org", 21 | "engines": { 22 | "node": ">=4" 23 | }, 24 | "devDependencies": { 25 | "eslint": "^1.0.0", 26 | "performance-now": "^0.2.0", 27 | "tape": "^4.0.1" 28 | }, 29 | "dependencies": { 30 | "concat-buffers": "^1.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/headers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tape'); 3 | var Headers = require('../lib/headers'); 4 | 5 | test('construct basic headers object', function(t) { 6 | var h = new Headers({ 7 | 'x-header-1': 'value1', 8 | 'X-HEADER-2': 'value2' 9 | }); 10 | 11 | t.equal(h.get('x-header-1'), 'value1'); 12 | t.equal(h.get('X-Header-1'), 'value1'); 13 | t.equal(h.get('X-HEADER-1'), 'value1'); 14 | t.equal(h.get('x-header-2'), 'value2'); 15 | t.equal(h.get('X-Header-2'), 'value2'); 16 | t.equal(h.get('X-HEADER-2'), 'value2'); 17 | t.equal(h.has('x-header-1'), true); 18 | t.equal(h.has('X-Header-1'), true); 19 | t.equal(h.has('X-HEADER-1'), true); 20 | t.equal(h.has('x-header-2'), true); 21 | t.equal(h.has('X-Header-2'), true); 22 | t.equal(h.has('X-HEADER-2'), true); 23 | t.end(); 24 | }); 25 | 26 | test('append header', function(t) { 27 | var h = new Headers({ 28 | 'x-header-1': 'value1', 29 | 'X-HEADER-2': 'value2' 30 | }); 31 | 32 | h.append('x-HEADER-3', 'value3'); 33 | t.equal(h.get('x-header-3'), 'value3'); 34 | t.equal(h.has('x-header-3'), true); 35 | t.end(); 36 | }); 37 | 38 | test('set new header', function(t) { 39 | var h = new Headers({ 40 | 'x-header-1': 'value1', 41 | 'X-HEADER-2': 'value2' 42 | }); 43 | 44 | h.set('x-HEADER-3', 'value3'); 45 | t.equal(h.get('x-header-3'), 'value3'); 46 | t.equal(h.has('x-header-3'), true); 47 | t.end(); 48 | }); 49 | 50 | test('set existing header', function(t) { 51 | var h = new Headers({ 52 | 'x-header-1': 'value1', 53 | 'X-HEADER-2': 'value2' 54 | }); 55 | 56 | h.set('x-HEADER-2', 'value3'); 57 | t.equal(h.get('x-header-2'), 'value3'); 58 | t.equal(h.has('x-header-2'), true); 59 | t.end(); 60 | }); 61 | 62 | test('delele existing header', function(t) { 63 | var h = new Headers({ 64 | 'x-header-1': 'value1', 65 | 'X-HEADER-2': 'value2' 66 | }); 67 | 68 | h.delete('x-HEADER-2', 'value3'); 69 | t.equal(h.get('x-header-1'), 'value1'); 70 | t.equal(h.has('x-header-1'), true); 71 | t.equal(h.get('x-header-2'), null); 72 | t.equal(h.has('x-header-2'), false); 73 | t.end(); 74 | }); 75 | 76 | test('delele non-existing header', function(t) { 77 | var h = new Headers({ 78 | 'x-header-1': 'value1', 79 | 'X-HEADER-2': 'value2' 80 | }); 81 | 82 | h.delete('x-HEADER-10', 'value3'); 83 | t.equal(h.get('x-header-1'), 'value1'); 84 | t.equal(h.has('x-header-1'), true); 85 | t.equal(h.get('x-header-2'), 'value2'); 86 | t.equal(h.has('x-header-2'), true); 87 | t.end(); 88 | }); 89 | 90 | test('headers iterator', function(t) { 91 | var h = new Headers({ 92 | 'x-header-1': 'value1', 93 | 'X-HEADER-2': 'value2' 94 | }); 95 | 96 | var index = 0; 97 | for (var header of h) { 98 | if (index++ === 0) { 99 | t.equal(header[0], 'x-header-1'); 100 | t.equal(header[1], 'value1'); 101 | } else { 102 | t.equal(header[0], 'x-header-2'); 103 | t.equal(header[1], 'value2'); 104 | } 105 | } 106 | 107 | index = 0; 108 | for (var headerName of h.keys()) { 109 | if (index++ === 0) { 110 | t.equal(headerName, 'x-header-1'); 111 | } else { 112 | t.equal(headerName, 'x-header-2'); 113 | } 114 | } 115 | 116 | index = 0; 117 | for (var headerValue of h.values()) { 118 | if (index++ === 0) { 119 | t.equal(headerValue, 'value1'); 120 | } else { 121 | t.equal(headerValue, 'value2'); 122 | } 123 | } 124 | 125 | t.end(); 126 | }); 127 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('../lib/backend').setBackend(require('../backend/backend-node')); 3 | require('./headers'); 4 | require('./parser'); 5 | require('./request'); 6 | require('./server'); 7 | -------------------------------------------------------------------------------- /test/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var testCaseParser = require('./test-case-parser'); 3 | var CRLF = '\r\n'; 4 | 5 | // *************************************************************** 6 | // REQUESTS 7 | // ************************************************************** 8 | 9 | testCaseParser({ 10 | name: 'basic http request', 11 | type: 'request', 12 | input: [ 13 | 'GET / HTTP/1.1', 14 | 'Connection: close', 15 | 'Host: localhost:8080', 16 | 'Accept: text/html, text/plain', 17 | 'User-Agent: test', 18 | CRLF 19 | ], 20 | checks: { 21 | method: 'GET', 22 | path: '/', 23 | versionMajor: 1, 24 | versionMinor: 1, 25 | chunked: false, 26 | keepAlive: false, 27 | status: 'complete', 28 | headers: { 29 | 'host': 'localhost:8080', 30 | 'accept': 'text/html, text/plain', 31 | 'user-agent': 'test' 32 | } 33 | } 34 | }); 35 | 36 | testCaseParser(function() { 37 | function makeTest(method) { 38 | return { 39 | name: 'request method ' + method, 40 | type: 'request', 41 | input: [ 42 | method + ' / HTTP/1.1', 43 | 'Connection: close', 44 | CRLF 45 | ], 46 | checks: { 47 | method: method, 48 | path: '/', 49 | versionMajor: 1, 50 | versionMinor: 1, 51 | chunked: false, 52 | keepAlive: false, 53 | status: 'complete', 54 | headers: {} 55 | } 56 | }; 57 | } 58 | 59 | return [ 60 | 'OPTIONS', 61 | 'GET', 62 | 'HEAD', 63 | 'POST', 64 | 'PUT', 65 | 'DELETE', 66 | 'CONNECT', 67 | 'TRACE', 68 | 'PATCH', 69 | 'PURGE', 70 | 'REPORT', 71 | 'MKACTIVITY', 72 | 'CHECKOUT', 73 | 'MERGE', 74 | 'COPY', 75 | 'MOVE', 76 | 'LOCK', 77 | 'UNLOCK', 78 | 'MKCOL', 79 | 'PROPPATCH', 80 | 'PROPFIND', 81 | 'SEARCH' 82 | ].map(makeTest); 83 | }); 84 | 85 | testCaseParser(function() { 86 | function makeTest(path) { 87 | return { 88 | name: 'request path ' + path, 89 | type: 'request', 90 | input: [ 91 | 'GET ' + path + ' HTTP/1.1', 92 | 'Connection: close', 93 | CRLF 94 | ], 95 | checks: { 96 | method: 'GET', 97 | path: path, 98 | versionMajor: 1, 99 | versionMinor: 1, 100 | chunked: false, 101 | keepAlive: false, 102 | status: 'complete', 103 | headers: {} 104 | } 105 | }; 106 | } 107 | 108 | return [ 109 | '/', 110 | '/hello', 111 | '/hello-world', 112 | '/a/b/c/d/e/f/g/h', 113 | '/some/resource.json?x=hello-world&z=23&token=badsjejfd234j2k', 114 | '/data/index.html' 115 | ].map(makeTest); 116 | }); 117 | 118 | testCaseParser({ 119 | name: 'headers test', 120 | type: 'request', 121 | input: [ 122 | 'GET / HTTP/1.1', 123 | 'connection: keep-alive', 124 | 'x-header: ok', 125 | 'other-header: header-value', 126 | CRLF 127 | ], 128 | checks: { 129 | method: 'GET', 130 | path: '/', 131 | versionMajor: 1, 132 | versionMinor: 1, 133 | chunked: false, 134 | keepAlive: true, 135 | status: 'complete', 136 | headers: { 137 | 'x-header': 'ok', 138 | 'other-header': 'header-value' 139 | } 140 | } 141 | }); 142 | 143 | testCaseParser({ 144 | name: 'header multiline value', 145 | type: 'request', 146 | input: [ 147 | 'GET / HTTP/1.1', 148 | 'connection: keep-alive', 149 | 'x-header: hello', 150 | ' world', 151 | 'other-header: header-value', 152 | 'x-header-2: test', 153 | ' value', 154 | CRLF 155 | ], 156 | checks: { 157 | method: 'GET', 158 | path: '/', 159 | versionMajor: 1, 160 | versionMinor: 1, 161 | chunked: false, 162 | keepAlive: true, 163 | status: 'complete', 164 | headers: { 165 | 'x-header': 'hello world', 166 | 'other-header': 'header-value', 167 | 'x-header-2': 'test value' 168 | } 169 | } 170 | }); 171 | 172 | testCaseParser({ 173 | name: 'header multiline value on the next lines', 174 | type: 'request', 175 | input: [ 176 | 'GET / HTTP/1.1', 177 | 'connection: keep-alive', 178 | 'x-header:', 179 | ' hello', 180 | ' world', 181 | CRLF 182 | ], 183 | checks: { 184 | method: 'GET', 185 | path: '/', 186 | versionMajor: 1, 187 | versionMinor: 1, 188 | chunked: false, 189 | keepAlive: true, 190 | status: 'complete', 191 | headers: { 192 | 'x-header': 'hello world' 193 | } 194 | } 195 | }); 196 | 197 | testCaseParser({ 198 | name: 'header multiline value on the next lines and extra spaces', 199 | type: 'request', 200 | input: [ 201 | 'GET / HTTP/1.1', 202 | 'connection: keep-alive', 203 | 'x-header: ', 204 | ' hello', 205 | ' world', 206 | CRLF 207 | ], 208 | checks: { 209 | method: 'GET', 210 | path: '/', 211 | versionMajor: 1, 212 | versionMinor: 1, 213 | chunked: false, 214 | keepAlive: true, 215 | status: 'complete', 216 | headers: { 217 | 'x-header': 'hello world' 218 | } 219 | } 220 | }); 221 | 222 | testCaseParser({ 223 | name: 'http 1.1 keep alive enabled', 224 | type: 'request', 225 | input: [ 226 | 'GET / HTTP/1.1', 227 | 'Connection: keep-alive', 228 | 'Host: localhost:8080', 229 | 'User-Agent: test', 230 | CRLF 231 | ], 232 | checks: { 233 | method: 'GET', 234 | path: '/', 235 | versionMajor: 1, 236 | versionMinor: 1, 237 | chunked: false, 238 | keepAlive: true, 239 | status: 'complete', 240 | headers: { 241 | 'host': 'localhost:8080', 242 | 'user-agent': 'test' 243 | } 244 | } 245 | }); 246 | 247 | testCaseParser({ 248 | name: 'http 1.0 keep alive disabled', 249 | type: 'request', 250 | input: [ 251 | 'GET / HTTP/1.0', 252 | 'Connection: keep-alive', 253 | 'Host: localhost:8080', 254 | 'User-Agent: test', 255 | CRLF 256 | ], 257 | checks: { 258 | method: 'GET', 259 | path: '/', 260 | versionMajor: 1, 261 | versionMinor: 0, 262 | chunked: false, 263 | keepAlive: false, 264 | status: 'complete', 265 | headers: { 266 | 'host': 'localhost:8080', 267 | 'user-agent': 'test' 268 | } 269 | } 270 | }); 271 | 272 | testCaseParser({ 273 | name: 'http 1.1 no connection header default to keep-alive', 274 | type: 'request', 275 | input: [ 276 | 'GET / HTTP/1.1', 277 | 'Host: localhost:8080', 278 | CRLF 279 | ], 280 | checks: { 281 | method: 'GET', 282 | path: '/', 283 | versionMajor: 1, 284 | versionMinor: 1, 285 | chunked: false, 286 | keepAlive: true, 287 | status: 'complete', 288 | headers: {} 289 | } 290 | }); 291 | 292 | testCaseParser({ 293 | name: 'http 1.0 no connection header default to close', 294 | type: 'request', 295 | input: [ 296 | 'GET / HTTP/1.0', 297 | 'Host: localhost:8080', 298 | CRLF 299 | ], 300 | checks: { 301 | method: 'GET', 302 | path: '/', 303 | versionMajor: 1, 304 | versionMinor: 0, 305 | chunked: false, 306 | keepAlive: false, 307 | status: 'complete', 308 | headers: {} 309 | } 310 | }); 311 | 312 | testCaseParser({ 313 | name: 'http request content length body', 314 | type: 'request', 315 | input: [ 316 | 'POST /write HTTP/1.0', 317 | 'Server: test', 318 | 'Content-Length: 16' + CRLF, 319 | 'This is the body', 320 | CRLF 321 | ], 322 | checks: { 323 | method: 'POST', 324 | path: '/write', 325 | versionMajor: 1, 326 | versionMinor: 0, 327 | chunked: false, 328 | keepAlive: false, 329 | status: 'complete', 330 | body: 'This is the body', 331 | headers: { 332 | 'server': 'test' 333 | } 334 | } 335 | }); 336 | 337 | testCaseParser({ 338 | name: 'http request chunked', 339 | type: 'request', 340 | input: [ 341 | 'POST /write HTTP/1.1', 342 | 'Transfer-encoding: chunked', 343 | 'Server: test' + CRLF, 344 | '5', 345 | 'hello', 346 | '7', 347 | ' world!', 348 | '0' + CRLF, 349 | CRLF 350 | ], 351 | checks: { 352 | method: 'POST', 353 | path: '/write', 354 | versionMajor: 1, 355 | versionMinor: 1, 356 | chunked: true, 357 | keepAlive: true, 358 | status: 'complete', 359 | body: 'hello world!', 360 | headers: { 361 | 'server': 'test' 362 | } 363 | } 364 | }); 365 | 366 | testCaseParser({ 367 | name: 'http request chunked with trailers', 368 | type: 'request', 369 | input: [ 370 | 'POST /write HTTP/1.1', 371 | 'Transfer-encoding: chunked', 372 | 'Server: test' + CRLF, 373 | '5', 374 | 'hello', 375 | '7', 376 | ' world!', 377 | '0', 378 | 'Trailer1: value1', 379 | 'Trailer2: value2', 380 | CRLF 381 | ], 382 | checks: { 383 | method: 'POST', 384 | path: '/write', 385 | versionMajor: 1, 386 | versionMinor: 1, 387 | chunked: true, 388 | keepAlive: true, 389 | status: 'complete', 390 | body: 'hello world!', 391 | headers: { 392 | 'server': 'test' 393 | }, 394 | trailers: { 395 | 'trailer1': 'value1', 396 | 'trailer2': 'value2' 397 | } 398 | } 399 | }); 400 | 401 | testCaseParser({ 402 | name: 'http request chunked with multiline trailers', 403 | type: 'request', 404 | input: [ 405 | 'POST /write HTTP/1.1', 406 | 'Transfer-encoding: chunked', 407 | 'Server: test' + CRLF, 408 | '5', 409 | 'hello', 410 | '7', 411 | ' world!', 412 | '0', 413 | 'Trailer1:', 414 | ' value1', 415 | ' value2', 416 | 'Trailer2: value3', 417 | ' value4', 418 | CRLF 419 | ], 420 | checks: { 421 | method: 'POST', 422 | path: '/write', 423 | versionMajor: 1, 424 | versionMinor: 1, 425 | chunked: true, 426 | keepAlive: true, 427 | status: 'complete', 428 | body: 'hello world!', 429 | headers: { 430 | 'server': 'test' 431 | }, 432 | trailers: { 433 | 'trailer1': 'value1 value2', 434 | 'trailer2': 'value3 value4' 435 | } 436 | } 437 | }); 438 | 439 | testCaseParser(function() { 440 | var common = { 441 | name: 'request parse errors', 442 | type: 'request', 443 | checks: { 444 | status: 'error' 445 | } 446 | }; 447 | 448 | return [ 449 | [ 450 | 'VERYLONGREQUESTMETHOD / HTTP/1.1' 451 | ], 452 | [ 453 | 'hello /' 454 | ], 455 | [ 456 | 'GET', 457 | '/hello' 458 | ], 459 | [ 460 | 'GET /hello', 461 | 'HTTP/1.1' 462 | ], 463 | [ 464 | 'GET /hello TCP/1.0' 465 | ], 466 | [ 467 | 'GET /hello H/1.0' 468 | ], 469 | [ 470 | 'GET /hello HT/1.0' 471 | ], 472 | [ 473 | 'GET /hello HTT/1.0' 474 | ], 475 | [ 476 | 'GET /hello HTTP.1.0' 477 | ], 478 | [ 479 | 'GET /hello HTTP/0.0' 480 | ], 481 | [ 482 | 'GET /hello HTTP/1.2' 483 | ], 484 | [ 485 | 'GET /hello HTTP/2.0' 486 | ], 487 | [ 488 | 'GET /hello HTTP/1', 489 | '.0' 490 | ], 491 | [ 492 | 'GET /hello HTTP/1.1', 493 | 'conn', 494 | 'ection: close' 495 | ], 496 | [ 497 | 'GET /hello HTTP/1.1', 498 | 'conn ection: close' 499 | ], 500 | [ 501 | 'GET /hello HTTP/1.1', 502 | '{connection: close' 503 | ], 504 | [ 505 | 'GET /hello HTTP/1.1', 506 | ';connection: close' 507 | ], 508 | [ 509 | 'GET /hello HTTP/1.1', 510 | 'connection : close' 511 | ], 512 | [ 513 | 'GET /hello HTTP/1.1', 514 | 'conne=ction : close' 515 | ], 516 | [ 517 | 'GET /hello HTTP/1.1', 518 | ' world' 519 | ], 520 | [ 521 | 'GET /hello HTTP/1.1', 522 | 'Header: ' + 'a'.repeat(1024 * 20) // >16 KiB value 523 | ], 524 | [ 525 | 'POST /write HTTP/1.1', 526 | 'Transfer-encoding: chunked', 527 | 'Server: test' + CRLF, 528 | '5', 529 | 'hello', 530 | '7', 531 | ' world!', 532 | '0', 533 | 'Trailer: ' + 'a'.repeat(1024 * 2) // >1 KiB value 534 | ], 535 | [ 536 | 'POST /write HTTP/1.1', 537 | 'Transfer-encoding: chunked', 538 | 'Server: test' + CRLF, 539 | '5', 540 | 'hello', 541 | '0', 542 | 'Trailer: value', 543 | 'Trailer2' + CRLF 544 | ] 545 | ].map(function(input) { 546 | return Object.assign({ input }, common); 547 | }); 548 | }); 549 | 550 | testCaseParser({ 551 | name: 'http request chunked', 552 | type: 'request', 553 | input: [ 554 | 'POST /write HTTP/1.1', 555 | 'Transfer-encoding: chunked', 556 | 'Server: test' + CRLF, 557 | '5', 558 | 'hello', 559 | '7', 560 | ' world!', 561 | '0', 562 | CRLF 563 | ], 564 | checks: { 565 | method: 'POST', 566 | path: '/write', 567 | versionMajor: 1, 568 | versionMinor: 1, 569 | chunked: true, 570 | keepAlive: true, 571 | status: 'complete', 572 | body: 'hello world!', 573 | headers: { 574 | 'server': 'test' 575 | } 576 | } 577 | }); 578 | 579 | testCaseParser({ 580 | name: 'http request chunked hex numbers', 581 | type: 'request', 582 | input: [ 583 | 'POST /write HTTP/1.1', 584 | 'Transfer-encoding: chunked', 585 | 'Server: test' + CRLF, 586 | 'a', 587 | 'hellohello', 588 | 'f', 589 | ' world! hello!!', 590 | '0', 591 | CRLF 592 | ], 593 | checks: { 594 | method: 'POST', 595 | path: '/write', 596 | versionMajor: 1, 597 | versionMinor: 1, 598 | chunked: true, 599 | keepAlive: true, 600 | status: 'complete', 601 | body: 'hellohello world! hello!!', 602 | headers: { 603 | 'server': 'test' 604 | } 605 | } 606 | }); 607 | 608 | // *************************************************************** 609 | // RESPONSES 610 | // ************************************************************** 611 | 612 | testCaseParser({ 613 | name: 'basic http response', 614 | type: 'response', 615 | input: [ 616 | 'HTTP/1.1 200 OK', 617 | 'Server: test', 618 | 'X-Header1: value1', 619 | 'X-Header2: value2', 620 | CRLF 621 | ], 622 | checks: { 623 | statusCode: 200, 624 | statusMessage: 'OK', 625 | versionMajor: 1, 626 | versionMinor: 1, 627 | chunked: false, 628 | keepAlive: true, 629 | status: 'complete', 630 | headers: { 631 | 'server': 'test', 632 | 'x-header1': 'value1', 633 | 'x-header2': 'value2' 634 | } 635 | } 636 | }); 637 | 638 | testCaseParser({ 639 | name: 'http response no messgae', 640 | type: 'response', 641 | input: [ 642 | 'HTTP/1.1 200', 643 | 'Server: test', 644 | 'X-Header1: value1', 645 | 'X-Header2: value2', 646 | CRLF 647 | ], 648 | checks: { 649 | statusCode: 200, 650 | statusMessage: '', 651 | versionMajor: 1, 652 | versionMinor: 1, 653 | chunked: false, 654 | keepAlive: true, 655 | status: 'complete', 656 | headers: { 657 | 'server': 'test', 658 | 'x-header1': 'value1', 659 | 'x-header2': 'value2' 660 | } 661 | } 662 | }); 663 | 664 | testCaseParser({ 665 | name: 'http response custom message', 666 | type: 'response', 667 | input: [ 668 | 'HTTP/1.1 200 Hello World', 669 | 'Server: test', 670 | CRLF 671 | ], 672 | checks: { 673 | statusCode: 200, 674 | statusMessage: 'Hello World', 675 | versionMajor: 1, 676 | versionMinor: 1, 677 | chunked: false, 678 | keepAlive: true, 679 | status: 'complete', 680 | headers: {} 681 | } 682 | }); 683 | 684 | testCaseParser({ 685 | name: 'http response custom code', 686 | type: 'response', 687 | input: [ 688 | 'HTTP/1.1 707 Who am I?', 689 | 'Server: test', 690 | CRLF 691 | ], 692 | checks: { 693 | statusCode: 707, 694 | statusMessage: 'Who am I?', 695 | versionMajor: 1, 696 | versionMinor: 1, 697 | chunked: false, 698 | keepAlive: true, 699 | status: 'complete', 700 | headers: {} 701 | } 702 | }); 703 | 704 | testCaseParser({ 705 | name: 'http response content length body', 706 | type: 'response', 707 | input: [ 708 | 'HTTP/1.1 200 OK', 709 | 'Server: test', 710 | 'Content-Length: 16' + CRLF, 711 | 'This is the body', 712 | CRLF 713 | ], 714 | checks: { 715 | statusCode: 200, 716 | statusMessage: 'OK', 717 | versionMajor: 1, 718 | versionMinor: 1, 719 | chunked: false, 720 | keepAlive: true, 721 | status: 'complete', 722 | body: 'This is the body', 723 | headers: {} 724 | } 725 | }); 726 | 727 | testCaseParser({ 728 | name: 'http response chunked', 729 | type: 'response', 730 | input: [ 731 | 'HTTP/1.1 200 OK', 732 | 'Transfer-encoding: chunked', 733 | 'Server: test' + CRLF, 734 | '5', 735 | 'hello', 736 | '7', 737 | ' world!', 738 | '0', 739 | CRLF 740 | ], 741 | checks: { 742 | statusCode: 200, 743 | statusMessage: 'OK', 744 | versionMajor: 1, 745 | versionMinor: 1, 746 | chunked: true, 747 | keepAlive: true, 748 | status: 'complete', 749 | body: 'hello world!', 750 | headers: { 751 | 'server': 'test' 752 | } 753 | } 754 | }); 755 | 756 | testCaseParser({ 757 | name: 'http response chunked empty body', 758 | type: 'response', 759 | input: [ 760 | 'HTTP/1.1 200 OK', 761 | 'Transfer-encoding: chunked', 762 | 'Server: test' + CRLF, 763 | '0', 764 | CRLF 765 | ], 766 | checks: { 767 | statusCode: 200, 768 | statusMessage: 'OK', 769 | versionMajor: 1, 770 | versionMinor: 1, 771 | chunked: true, 772 | keepAlive: true, 773 | status: 'complete', 774 | body: '', 775 | headers: { 776 | 'server': 'test' 777 | } 778 | } 779 | }); 780 | -------------------------------------------------------------------------------- /test/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tape'); 3 | var HttpRequest = require('../lib/http-request'); 4 | 5 | test('http request instance', function(t) { 6 | var request = new HttpRequest('GET', '/', { 'x-header': 'value' }); 7 | t.equal(request.method, 'GET'); 8 | t.equal(request.path, '/'); 9 | t.ok(request.headers.has('X-HEADER')); 10 | t.equal(request.headers.get('X-HEADER'), 'value'); 11 | t.end(); 12 | }); 13 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tape'); 3 | var HttpServer = require('../lib/http-server'); 4 | var HttpResponse = require('../lib/http-response'); 5 | var http = require('http'); 6 | var net = require('net'); 7 | var concatBuffers = require('concat-buffers'); 8 | var CRLF = '\r\n'; 9 | 10 | function U8(str) { 11 | var u8 = new Uint8Array(str.length); 12 | for (var i = 0; i < str.length; ++i) { 13 | u8[i] = str.charCodeAt(i); 14 | } 15 | return u8; 16 | } 17 | 18 | test('http server get', function(t) { 19 | t.plan(9); 20 | var response = new HttpResponse(200, {}, 'ok'); 21 | var server = new HttpServer(); 22 | 23 | server.onrequest = request => { 24 | request.respondWith(response); 25 | }; 26 | 27 | t.on('end', function() { 28 | server.close(); 29 | }); 30 | 31 | server.listen(7777); 32 | 33 | process.nextTick(function() { 34 | http.get('http://localhost:7777/', function(res) { 35 | t.equal(res.statusCode, 200); 36 | 37 | res.on('data', function(value) { 38 | t.equal(value.toString(), 'ok'); 39 | }); 40 | 41 | res.on('end', function() { 42 | t.ok(true); 43 | }); 44 | }); 45 | 46 | http.get('http://localhost:7777/hello', function(res) { 47 | t.equal(res.statusCode, 200); 48 | 49 | res.on('data', function(value) { 50 | t.equal(value.toString(), 'ok'); 51 | }); 52 | 53 | res.on('end', function() { 54 | t.ok(true); 55 | }); 56 | }); 57 | 58 | http.get('http://localhost:7777/hello?x=1#tag', function(res) { 59 | t.equal(res.statusCode, 200); 60 | 61 | res.on('data', function(value) { 62 | t.equal(value.toString(), 'ok'); 63 | }); 64 | 65 | res.on('end', function() { 66 | t.ok(true); 67 | }); 68 | }); 69 | }); 70 | }); 71 | 72 | test('http server post request', function(t) { 73 | t.plan(6); 74 | var response = new HttpResponse(200, {}, 'ok'); 75 | var server = new HttpServer(); 76 | 77 | server.onrequest = request => { 78 | console.log('request', request.path); 79 | var chunks = []; 80 | 81 | request.ondata = chunk => { 82 | t.ok(true, 'request data'); 83 | chunks.push(chunk); 84 | } 85 | 86 | request.onend = () => { 87 | t.ok(true, 'request end'); 88 | 89 | var b = concatBuffers(chunks); 90 | var body = String.fromCharCode.apply(null, b); 91 | t.equal(body, JSON.stringify({ data: 'value' })); 92 | 93 | request.respondWith(response); 94 | }; 95 | }; 96 | 97 | t.on('end', server.close.bind(server)); 98 | 99 | server.listen(7777); 100 | 101 | process.nextTick(function() { 102 | var json = JSON.stringify({ 103 | data: 'value' 104 | }); 105 | 106 | var req = http.request({ 107 | hostname: '127.0.0.1', 108 | port: 7777, 109 | path: '/message', 110 | method: 'POST', 111 | headers: { 112 | 'Content-Type': 'application/json', 113 | 'Content-Length': json.length 114 | } 115 | }, function(res) { 116 | t.equal(res.statusCode, 200); 117 | 118 | res.on('data', function(buf) { 119 | t.equal(buf.toString(), 'ok'); 120 | }); 121 | 122 | res.on('end', function() { 123 | t.ok(true, 'http response end'); 124 | }); 125 | }); 126 | 127 | req.end(json); 128 | }); 129 | }); 130 | 131 | test('http pipelining', function(t) { 132 | t.plan(8); 133 | var response = new HttpResponse(200, {}, 'ok'); 134 | var server = new HttpServer(); 135 | 136 | server.onrequest = request => { 137 | request.respondWith(response); 138 | }; 139 | 140 | t.on('end', server.close.bind(server)); 141 | 142 | server.listen(7777); 143 | 144 | function testInput(input) { 145 | var socket = net.createConnection(7777, '127.0.0.1', () => { 146 | socket.write(Buffer(U8(input.join(CRLF)))); 147 | socket.end(); 148 | }); 149 | 150 | socket.on('data', (buf) => { 151 | var s = buf.toString(); 152 | var responsesCount = (s.match(/HTTP\/1.1 200 OK/g) || []).length; 153 | t.equal(responsesCount, 4); 154 | }); 155 | 156 | socket.on('end', () => { 157 | t.ok(true, 'done'); 158 | }); 159 | } 160 | 161 | process.nextTick(function() { 162 | testInput([ 163 | 'GET / HTTP/1.1', 164 | 'Request-Id: 1' + CRLF, 165 | 'GET / HTTP/1.1', 166 | 'Request-Id: 2' + CRLF, 167 | 'GET / HTTP/1.1', 168 | 'Request-Id: 3' + CRLF, 169 | 'GET / HTTP/1.1', 170 | 'Request-Id: 4' + CRLF, 171 | CRLF 172 | ]); 173 | 174 | testInput([ 175 | 'POST / HTTP/1.1', 176 | 'Content-length: 4', 177 | 'Request-Id: 1' + CRLF, 178 | 'testPOST / HTTP/1.1', 179 | 'Content-length: 4', 180 | 'Request-Id: 2' + CRLF, 181 | 'testPOST / HTTP/1.1', 182 | 'Content-length: 4', 183 | 'Request-Id: 3' + CRLF, 184 | 'testPOST / HTTP/1.1', 185 | 'Content-length: 4', 186 | 'Request-Id: 4' + CRLF, 187 | 'test' 188 | ]); 189 | 190 | testInput([ 191 | 'POST / HTTP/1.1', 192 | 'transfer-Encoding: chunked', 193 | 'Request-Id: 1' + CRLF, 194 | '4', 195 | 'test', 196 | '0' + CRLF, 197 | 'POST / HTTP/1.1', 198 | 'transfer-Encoding: chunked', 199 | 'Request-Id: 2' + CRLF, 200 | '4', 201 | 'test', 202 | '0' + CRLF, 203 | 'POST / HTTP/1.1', 204 | 'transfer-Encoding: chunked', 205 | 'Request-Id: 3' + CRLF, 206 | '4', 207 | 'test', 208 | '0' + CRLF, 209 | 'POST / HTTP/1.1', 210 | 'transfer-Encoding: chunked', 211 | 'Request-Id: 4' + CRLF, 212 | '4', 213 | 'test', 214 | '0' + CRLF + CRLF 215 | ]); 216 | 217 | testInput([ 218 | 'POST / HTTP/1.1', 219 | 'transfer-Encoding: chunked', 220 | 'Request-Id: 1' + CRLF, 221 | '4', 222 | 'test', 223 | '0', 224 | 'Trailer: value' + CRLF, 225 | 'POST / HTTP/1.1', 226 | 'transfer-Encoding: chunked', 227 | 'Request-Id: 2' + CRLF, 228 | '4', 229 | 'test', 230 | '5', 231 | 'hello', 232 | '0', 233 | 'Trailer: value' + CRLF, 234 | 'POST / HTTP/1.1', 235 | 'transfer-Encoding: chunked', 236 | 'Request-Id: 3' + CRLF, 237 | '4', 238 | 'test', 239 | '6', 240 | 'world!', 241 | '0', 242 | 'Trailer: value', 243 | 'Trailer2: value2' + CRLF, 244 | 'POST / HTTP/1.1', 245 | 'transfer-Encoding: chunked', 246 | 'Request-Id: 4' + CRLF, 247 | '4', 248 | 'test', 249 | '3', 250 | 'abc', 251 | '3', 252 | 'def', 253 | '2', 254 | 'kk', 255 | '0', 256 | 'Trailer: value' + CRLF + CRLF 257 | ]); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /test/test-case-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tape'); 3 | var concatBuffers = require('concat-buffers'); 4 | var HttpParser = require('../lib/http-parser'); 5 | var CRLF = '\r\n'; 6 | 7 | function U8(str) { 8 | var u8 = new Uint8Array(str.length); 9 | for (var i = 0; i < str.length; ++i) { 10 | u8[i] = str.charCodeAt(i); 11 | } 12 | return u8; 13 | } 14 | 15 | function getRequestParser() { 16 | return new HttpParser(true); 17 | } 18 | 19 | function getResponseParser() { 20 | return new HttpParser(false); 21 | } 22 | 23 | function runTestCase(currentTest) { 24 | test(currentTest.name, function(t) { 25 | var checks = currentTest.checks; 26 | 27 | function getParser() { 28 | return currentTest.type === 'request' 29 | ? getRequestParser() : getResponseParser(); 30 | } 31 | 32 | function runChecks(parser, bodyChunks, prefix) { 33 | if ('statusCode' in checks) { 34 | t.equal(checks.statusCode, parser.statusCode, prefix + ' status code ok'); 35 | } 36 | if ('statusMessage' in checks) { 37 | t.equal(checks.statusMessage, parser.statusMessage, prefix + ' status message ok'); 38 | } 39 | if ('method' in checks) { 40 | t.equal(checks.method, parser.method, prefix + ' method ok'); 41 | } 42 | if ('path' in checks) { 43 | t.equal(checks.path, parser.path, prefix + ' path ok'); 44 | } 45 | if ('versionMajor' in checks) { 46 | t.equal(checks.versionMajor, parser.versionMajor, prefix + ' major version ok'); 47 | } 48 | if ('versionMinor' in checks) { 49 | t.equal(checks.versionMinor, parser.versionMinor, prefix + ' minor version ok'); 50 | } 51 | if ('chunked' in checks) { 52 | t.equal(checks.chunked, parser.isChunked(), prefix + ' chunked flag ok'); 53 | } 54 | if ('keepAlive' in checks) { 55 | t.equal(checks.keepAlive, parser.isKeepAlive(), prefix + ' keep-alive flag ok'); 56 | } 57 | if ('headers' in checks) { 58 | for (let key in checks.headers) { 59 | if (!checks.headers.hasOwnProperty(key)) { 60 | continue; 61 | } 62 | 63 | t.ok(parser.headers.has(key), prefix + ' has header "' + key + '"'); 64 | t.equal(parser.headers.get(key), checks.headers[key], prefix + ' header value ok "' + key + '"'); 65 | } 66 | } 67 | if ('body' in checks) { 68 | t.deepEqual(concatBuffers(bodyChunks), U8(checks.body), prefix + ' body ok'); 69 | } 70 | if ('trailers' in checks) { 71 | for (let key in checks.trailers) { 72 | if (!checks.trailers.hasOwnProperty(key)) { 73 | continue; 74 | } 75 | 76 | t.ok(parser.trailers.has(key), prefix + ' has trailer "' + key + '"'); 77 | t.equal(parser.trailers.get(key), checks.trailers[key], prefix + ' header value ok "' + key + '"'); 78 | } 79 | } 80 | 81 | var status = checks.status || 'complete'; 82 | 83 | if (status === 'complete') { 84 | t.ok(parser.isComplete(), prefix + ' input complete'); 85 | } 86 | 87 | if (status === 'error') { 88 | t.ok(parser.isError(), prefix + ' input error'); 89 | } 90 | } 91 | 92 | { 93 | let bodyChunks = []; 94 | let parser = getParser(); 95 | let offset = 0; 96 | let u8 = U8(currentTest.input.join(CRLF)); 97 | while (offset < u8.length) { 98 | parser.chunk(u8, offset); 99 | if (parser.isError()) { 100 | break; 101 | } 102 | 103 | if (parser.lastBodyChunk) { 104 | bodyChunks.push(parser.lastBodyChunk); 105 | } 106 | 107 | if (offset < parser.lastParsedIndex + 1) { 108 | offset = parser.lastParsedIndex + 1; 109 | continue; 110 | } 111 | 112 | break; 113 | } 114 | 115 | runChecks(parser, bodyChunks, 'single packet'); 116 | } 117 | 118 | 119 | { 120 | let bodyChunks = []; 121 | let parser = getParser(); 122 | let u8 = U8(currentTest.input.join(CRLF)); 123 | for (let i = 0; i < u8.length; ++i) { 124 | let subu8 = u8.subarray(i, i + 1); 125 | let offset = 0; 126 | while (offset < subu8.length) { 127 | parser.chunk(subu8, offset); 128 | if (parser.isError()) { 129 | break; 130 | } 131 | 132 | if (parser.lastBodyChunk) { 133 | bodyChunks.push(parser.lastBodyChunk); 134 | } 135 | 136 | if (offset < parser.lastParsedIndex + 1) { 137 | offset = parser.lastParsedIndex + 1; 138 | continue; 139 | } 140 | 141 | break; 142 | } 143 | } 144 | runChecks(parser, bodyChunks, 'split into 1 byte packets'); 145 | } 146 | 147 | t.end(); 148 | }); 149 | } 150 | 151 | function runTestCases(list) { 152 | for (let currentTest of list) { 153 | testCase(currentTest); 154 | } 155 | } 156 | 157 | function testCase(currentTest) { 158 | if (typeof currentTest === 'function') { 159 | runTestCases(currentTest()); 160 | } else { 161 | runTestCase(currentTest); 162 | } 163 | } 164 | 165 | module.exports = testCase; 166 | --------------------------------------------------------------------------------