diff --git a/.gitignore b/.gitignore index bc483625..08b69ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ coverage doc .vscode/.browse* npm-debug.log -typings \ No newline at end of file +typings +builds diff --git a/lib/http.js b/lib/http.js index 4c4234c5..affc8ca9 100644 --- a/lib/http.js +++ b/lib/http.js @@ -135,10 +135,64 @@ var Readable = require('stream').Readable; var Writable = require('stream').Writable; var protocol = require('./protocol'); var Endpoint = protocol.Endpoint; -var http = require('http'); var https = require('https'); -exports.STATUS_CODES = http.STATUS_CODES; + +exports.STATUS_CODES = { + '202': 'Accepted', + '502': 'Bad Gateway', + '400': 'Bad Request', + '409': 'Conflict', + '100': 'Continue', + '201': 'Created', + '417': 'Expectation Failed', + '424': 'Failed Dependency', + '403': 'Forbidden', + '504': 'Gateway Timeout', + '410': 'Gone', + '505': 'HTTP Version Not Supported', + '419': 'Insufficient Space on Resource', + '507': 'Insufficient Storage', + '500': 'Server Error', + '411': 'Length Required', + '423': 'Locked', + '420': 'Method Failure', + '405': 'Method Not Allowed', + '301': 'Moved Permanently', + '302': 'Moved Temporarily', + '207': 'Multi-Status', + '300': 'Multiple Choices', + '511': 'Network Authentication Required', + '204': 'No Content', + '203': 'Non Authoritative Information', + '406': 'Not Acceptable', + '404': 'Not Found', + '501': 'Not Implemented', + '304': 'Not Modified', + '200': 'OK', + '206': 'Partial Content', + '402': 'Payment Required', + '308': 'Permanent Redirect', + '412': 'Precondition Failed', + '428': 'Precondition Required', + '102': 'Processing', + '407': 'Proxy Authentication Required', + '431': 'Request Header Fields Too Large', + '408': 'Request Timeout', + '413': 'Request Entity Too Large', + '414': 'Request-URI Too Long', + '416': 'Requested Range Not Satisfiable', + '205': 'Reset Content', + '303': 'See Other', + '503': 'Service Unavailable', + '101': 'Switching Protocols', + '307': 'Temporary Redirect', + '429': 'Too Many Requests', + '401': 'Unauthorized', + '422': 'Unprocessable Entity', + '415': 'Unsupported Media Type', + '305': 'Use Proxy' +}; exports.IncomingMessage = IncomingMessage; exports.OutgoingMessage = OutgoingMessage; exports.protocol = protocol; @@ -293,8 +347,8 @@ IncomingMessage.prototype._validateHeaders = function _validateHeaders(headers) for (var i = 0; i < deprecatedHeaders.length; i++) { var key = deprecatedHeaders[i]; if (key in headers || (key === 'te' && headers[key] !== 'trailers')) { - this._log.error({ key: key, value: headers[key] }, 'Deprecated header found'); - this.stream.reset('PROTOCOL_ERROR'); + this._log.error({ key: key, value: headers[key] }, 'Deprecated header found'); + this.stream.reset('PROTOCOL_ERROR'); return; } } @@ -354,14 +408,13 @@ OutgoingMessage.prototype._finish = function _finish() { this.once('socket', this._finish.bind(this)); } }; - OutgoingMessage.prototype.setHeader = function setHeader(name, value) { if (this.headersSent) { return this.emit('error', new Error('Can\'t set headers after they are sent.')); } else { name = name.toLowerCase(); if (deprecatedHeaders.indexOf(name) !== -1) { - return this.emit('error', new Error('Cannot set deprecated header: ' + name)); + return this.emit('error', new Error('Cannot set deprecated header: ' + name)); } this._headers[name] = value; } @@ -429,7 +482,7 @@ function forwardEvent(event, source, target) { // ------------ function Server(options) { - options = util._extend({}, options); + options = Object.assign({}, options); this._log = (options.log || defaultLogger).child({ component: 'http' }); this._settings = options.settings; @@ -464,7 +517,13 @@ function Server(options) { forwardEvent('listening', this._server, this); } - // HTTP2 over plain TCP + // HTTP2 over any generic transport + else if (options.transport){ + this._mode = 'plain'; + this._server = options.transport(options, start); + } + + // HTTP2 over plain TCP (Perhaps this should be deprecated??) else if (options.plain) { this._log.info('Creating HTTP/2 server over plain TCP'); this._mode = 'plain'; @@ -766,7 +825,7 @@ OutgoingResponse.prototype.push = function push(options) { throw new Error('`path` option is mandatory.'); } - var promise = util._extend({ + var promise = Object.assign({ ':method': (options.method || 'GET').toUpperCase(), ':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'], ':authority': options.hostname || options.host || this._requestHeaders[':authority'], @@ -817,7 +876,7 @@ function requestRaw(options, callback) { throw new Error('This interface only supports http-schemed URLs'); } if (options.agent && typeof(options.agent.request) === 'function') { - var agentOptions = util._extend({}, options); + var agentOptions = Object.assign({}, options); delete agentOptions.agent; return options.agent.request(agentOptions, callback); } @@ -833,7 +892,7 @@ function requestTLS(options, callback) { throw new Error('This interface only supports https-schemed URLs'); } if (options.agent && typeof(options.agent.request) === 'function') { - var agentOptions = util._extend({}, options); + var agentOptions = Object.assign({}, options); delete agentOptions.agent; return options.agent.request(agentOptions, callback); } @@ -849,7 +908,7 @@ function getRaw(options, callback) { throw new Error('This interface only supports http-schemed URLs'); } if (options.agent && typeof(options.agent.get) === 'function') { - var agentOptions = util._extend({}, options); + var agentOptions = Object.assign({}, options); delete agentOptions.agent; return options.agent.get(agentOptions, callback); } @@ -865,7 +924,7 @@ function getTLS(options, callback) { throw new Error('This interface only supports https-schemed URLs'); } if (options.agent && typeof(options.agent.get) === 'function') { - var agentOptions = util._extend({}, options); + var agentOptions = Object.assign({}, options); delete agentOptions.agent; return options.agent.get(agentOptions, callback); } @@ -879,7 +938,7 @@ function Agent(options) { EventEmitter.call(this); this.setMaxListeners(0); - options = util._extend({}, options); + options = Object.assign({}, options); this._settings = options.settings; this._log = (options.log || defaultLogger).child({ component: 'http' }); @@ -902,7 +961,7 @@ Agent.prototype.request = function request(options, callback) { if (typeof options === 'string') { options = url.parse(options); } else { - options = util._extend({}, options); + options = Object.assign({}, options); } options.method = (options.method || 'GET').toUpperCase(); @@ -922,20 +981,63 @@ Agent.prototype.request = function request(options, callback) { request.on('response', callback); } - var key = [ - !!options.plain, - options.host, - options.port - ].join(':'); + // Re-use transportUrl endPoint if specified + if (options.transportUrl && options.transport) { + key = ([ + options.transportUrl + ]).join(':'); + + // Re-use host:port endPoint + } else { + key = ([ + !!options.plain, + options.host, + options.port + ]).join(':'); + } + var self = this; // * There's an existing HTTP/2 connection to this host - if (key in this.endpoints) { + if (key in this.endpoints && this.endpoints[key]) { var endpoint = this.endpoints[key]; request._start(endpoint.createStream(), options); } + // * HTTP/2 over generic stream transport + else if(options.transport) { + endpoint = new Endpoint(this._log, 'CLIENT', this._settings); + endpoint.socket = options.transport; + + endpoint.socket.on('error', function (error) { + self._log.error('Socket error: ' + error.toString()); + request.emit('error', error); + }); + + endpoint.on('error', function(error){ + self._log.error('Connection error: ' + error.toString()); + request.emit('error', error); + }); + + var self = this; + endpoint.socket.on('close', function (error) { + // DPW This is sort of a hack to protect against + // the reuse of a endpoint that has the underlying + // connection closed. It would probably be better + // to implement this near lin 933 (if (key in this.endpoints)) + // by checking the endpoint state (requires new API to expose) + + // Alternatively, this could be a bug with my WS connection + // not emitting an error when it is unexpectedly closed ?? + delete self.endpoints[key]; + }); + + this.endpoints[key] = endpoint; + endpoint.pipe(endpoint.socket).pipe(endpoint); + request._start(endpoint.createStream(), options); +} // * HTTP/2 over plain TCP + // TODO deprecate? else if (options.plain) { endpoint = new Endpoint(this._log, 'CLIENT', this._settings); endpoint.socket = net.connect({ @@ -1109,7 +1211,14 @@ OutgoingRequest.prototype._start = function _start(stream, options) { headers[':scheme'] = options.protocol.slice(0, -1); headers[':method'] = options.method; - headers[':authority'] = options.host; + if(options.port) + { + headers[':authority'] = options.host + ':' + options.port; + } + else + { + headers[':authority'] = options.host; + } headers[':path'] = options.path; this._log.info({ scheme: headers[':scheme'], method: headers[':method'], diff --git a/lib/protocol/connection.js b/lib/protocol/connection.js index 2b86b7f1..8a5fa511 100644 --- a/lib/protocol/connection.js +++ b/lib/protocol/connection.js @@ -8,6 +8,7 @@ var assert = require('assert'); // [Flow](flow.html) subclass. var Flow = require('./flow').Flow; +var timers = require('timers'); exports.Connection = Connection; @@ -272,7 +273,7 @@ Connection.prototype._send = function _send(immediate) { } else { if (!this._sendScheduled) { this._sendScheduled = true; - setImmediate(this._send.bind(this, true)); + timers.setImmediate(this._send.bind(this, true)); } return; } diff --git a/lib/protocol/endpoint.js b/lib/protocol/endpoint.js index a218db04..22e17ce1 100644 --- a/lib/protocol/endpoint.js +++ b/lib/protocol/endpoint.js @@ -7,6 +7,7 @@ var Decompressor = require('./compressor').Decompressor; var Connection = require('./connection').Connection; var Duplex = require('stream').Duplex; var Transform = require('stream').Transform; +var timers = require('timers'); exports.Endpoint = Endpoint; @@ -240,7 +241,7 @@ Endpoint.prototype._initializeErrorHandling = function _initializeErrorHandling( Endpoint.prototype._error = function _error(component, error) { this._log.fatal({ source: component, message: error }, 'Fatal error, closing connection'); this.close(error); - setImmediate(this.emit.bind(this, 'error', error)); + timers.setImmediate(this.emit.bind(this, 'error', error)); }; Endpoint.prototype.close = function close(error) { diff --git a/lib/protocol/flow.js b/lib/protocol/flow.js index 4ec5649b..9529c0a3 100644 --- a/lib/protocol/flow.js +++ b/lib/protocol/flow.js @@ -1,4 +1,5 @@ var assert = require('assert'); +var timers = require('timers'); // The Flow class // ============== @@ -91,7 +92,7 @@ Flow.prototype._write = function _write(frame, encoding, callback) { this._receive(frame, function() { this._received += frame.data.length; if (!this._restoreWindowTimer) { - this._restoreWindowTimer = setImmediate(this._restoreWindow.bind(this)); + this._restoreWindowTimer = timers.setImmediate(this._restoreWindow.bind(this)); } callback(); }.bind(this)); diff --git a/lib/protocol/framer.js b/lib/protocol/framer.js index 244e60ae..5df10f13 100644 --- a/lib/protocol/framer.js +++ b/lib/protocol/framer.js @@ -9,7 +9,7 @@ var Transform = require('stream').Transform; exports.Serializer = Serializer; exports.Deserializer = Deserializer; -var logData = Boolean(process.env.HTTP2_LOG_DATA); +var logData = (process !== 'undefined' && process.env !== 'undefined' && process.env.HTTP2_LOG_DATA); var MAX_PAYLOAD_SIZE = 16384; var WINDOW_UPDATE_PAYLOAD_SIZE = 4; diff --git a/lib/protocol/index.js b/lib/protocol/index.js index 0f3720e2..46a079f9 100644 --- a/lib/protocol/index.js +++ b/lib/protocol/index.js @@ -41,13 +41,19 @@ exports.VERSION = 'h2'; exports.Endpoint = require('./endpoint').Endpoint; /* Bunyan serializers exported by submodules that are worth adding when creating a logger. */ + exports.serializers = {}; var modules = ['./framer', './compressor', './flow', './connection', './stream', './endpoint']; -modules.map(require).forEach(function(module) { - for (var name in module.serializers) { - exports.serializers[name] = module.serializers[name]; - } -}); +try { + modules.map(require).forEach(function (module) { + for (var name in module.serializers) { + exports.serializers[name] = module.serializers[name]; + } + }); +} catch (e) { + // NOOP, probably in browser +} + /* Stream API Endpoint API diff --git a/package.json b/package.json index 5372f17f..f5d98a5a 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,25 @@ "version": "3.3.6", "description": "An HTTP/2 client and server implementation", "main": "lib/index.js", - "engines" : { - "node" : ">=0.12.0" + "engines": { + "node": ">=0.12.0" + }, + "dependencies": { + "assert": "1.4.1", + "stream-browserify": "2.0.1", + "websocket-stream": "3.3.3", + "url": "0.11.0", + "setimmediate": "1.0.5", + "https-browserify": "0.0.1", + "timers-browserify": "2.0.2", + "events": "1.1.1" }, "devDependencies": { "istanbul": "*", "chai": "*", "mocha": "*", "docco": "*", + "browserify": "14.0.0 ", "bunyan": "*" }, "scripts": { diff --git a/test/http.js b/test/http.js index 95a074e4..a75d0cab 100644 --- a/test/http.js +++ b/test/http.js @@ -7,6 +7,8 @@ var net = require('net'); var http2 = require('../lib/http'); var https = require('https'); +var http = require('http'); +var websocket = require('websocket-stream'); var serverOptions = { key: fs.readFileSync(path.join(__dirname, '../example/localhost.key')), @@ -390,6 +392,127 @@ describe('http.js', function() { }); }); }); + describe('request over generic plain transport (example WebSocket)', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + var portnum = 1239; + + var server = http2.raw.createServer({ + log: util.serverLog, + transport: function(options, start){ + var httpServer = http.createServer(); + options.server = httpServer; + var res = websocket.createServer(options, start); + res.listen = function(options, cb){ + httpServer.listen(options, cb); + }; + res.close = function (cb) { + httpServer.close(cb); + }; + return res; + } + }, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + server.listen(portnum, function() { + var request = http2.raw.request({ + plain: true, + host: 'localhost', + port: portnum, + path: path, + transport: websocket('ws://localhost:' + portnum) + }, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + server.close(); + done(); + }); + }); + request.end(); + }); + }); + }); + + describe('get over plain generic transport (example WebSocket)', function() { + it('should work as expected', function(done) { + var path = '/x'; + var portnum = 1239; + var message = 'Hello world'; + + var server = http2.raw.createServer({ + log: util.serverLog, + transport: function(options, start){ + var httpServer = http.createServer(); + options.server = httpServer; + var res = websocket.createServer(options, start); + res.listen = function(options, cb){ + httpServer.listen(options, cb); + }; + res.close = function (cb) { + httpServer.close(cb); + }; + return res; + } + }, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(portnum, function() { + var request = http2.raw.get({ + path: path, + transport: websocket('ws://localhost:' + portnum) + }, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + server.close(); + done(); + }); + }); + request.end(); + }); + }); + }); + describe('get over plain generic transport (example WebSocket) 2', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = http2.raw.createServer({ + log: util.serverLog, + transport: function(options, start){ + var httpServer = http.createServer(); + options.server = httpServer; + var res = websocket.createServer(options, start); + res.listen = function(options, cb){ + httpServer.listen(options, cb); + }; + res.close = function (cb) { + httpServer.close(cb); + }; + return res; + } + }, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(1239, function() { + var request = http2.raw.get({path : path, transport: function(){ + return websocket('ws://localhost:' + 1239); + }}, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + server.close(); + done(); + }); + }); + request.end(); + }); + }); + }); describe('request over plain TCP', function() { it('should work as expected', function(done) { var path = '/x'; diff --git a/test/stream.js b/test/stream.js index 90e0ef64..e99f74c3 100644 --- a/test/stream.js +++ b/test/stream.js @@ -1,5 +1,6 @@ var expect = require('chai').expect; var util = require('./util'); +var timers = require('timers'); var stream = require('../lib/protocol/stream'); var Stream = stream.Stream; @@ -97,7 +98,7 @@ function execute_sequence(stream, sequence, done) { done(); } - setImmediate(execute.bind(null, check)); + timers.setImmediate(execute.bind(null, check)); } var example_frames = [ diff --git a/test/util.js b/test/util.js index 52c6a1be..b2c24940 100644 --- a/test/util.js +++ b/test/util.js @@ -87,3 +87,4 @@ exports.shuffleBuffers = function shuffleBuffers(buffers) { return output; }; +