From 2c130c492bc1a41aa2a8993a5dbd9e98c48986a6 Mon Sep 17 00:00:00 2001 From: Harold Thetiot Date: Tue, 27 Jun 2017 16:06:38 -0700 Subject: [PATCH 1/3] use http2 official module and implement custom Agent instead of fork --- lib/agent.js | 92 +++++++++++++++++++++++++++++++++++++--- lib/server.js | 54 +++++++++++++++++++++++ package.json | 8 ++-- test/http2-proxy-test.js | 5 ++- test/http2-xhr-test.js | 5 ++- 5 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 lib/server.js diff --git a/lib/agent.js b/lib/agent.js index 933ef33..4966790 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,12 +1,94 @@ -var http2 = require('http2'); +/* global console */ +var OutgoingRequest = require('http2').OutgoingRequest; +var Agent = require('http2').Agent; +var Endpoint = require('http2').protocol.Endpoint; +var url = require('url'); -// TODO extend AGENT function Http2CacheAgent() { - http2.Agent.apply(this, arguments); + Agent.apply(this, arguments); } -Http2CacheAgent.prototype = Object.create(http2.Agent.prototype, { - // TODO override http2.Agent +Http2CacheAgent.prototype = Object.create(Agent.prototype, { + // Overide Server here + request: { + value: function request(options, callback) { + + if (typeof options === 'string') { + options = url.parse(options); + } else { + options = Object.assign({}, options); + } + + options.method = (options.method || 'GET').toUpperCase(); + options.protocol = options.protocol || 'https:'; + options.host = options.hostname || options.host || 'localhost'; + options.port = options.port || 443; + options.path = options.path || '/'; + + if (!options.plain && options.protocol === 'http:') { + this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1'); + this.emit('error', new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.')); + } + + var request = new OutgoingRequest(this._log); + + if (callback) { + request.on('response', callback); + } + + // Re-use transportUrl endPoint if specified + var key = ([ + options.transportUrl + ]).join(':'); + + // * There's an existing HTTP/2 connection to this host + var endpoint; + if (key in this.endpoints && this.endpoints[key]) { + 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; + + var self = this; + + 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); + }); + + 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); + + // Fallback + } else { + request = Agent.prototype.request.apply(this, arguments); + } + + return request; + } + } }); exports.Agent = Http2CacheAgent; \ No newline at end of file diff --git a/lib/server.js b/lib/server.js new file mode 100644 index 0000000..1639dc4 --- /dev/null +++ b/lib/server.js @@ -0,0 +1,54 @@ +/* global console */ +var Server = require('http2').Server, + logger = require('./logger'); + +function Http2CacheServer(options) { + + options = Object.assign({}, options); + + this._log = (options.log || logger.defaultLogger).child({ + component: 'http' + }); + this._settings = options.settings; + + var start = this._start.bind(this); + var fallback = this._fallback.bind(this); + + // HTTP2 over any generic transport + if (options.transport) { + this._mode = 'plain'; + this._server = options.transport(options, start); + this._server.on('close', this.emit.bind(this, 'close')); + } else { + Server.apply(this, arguments); + } +} + +Http2CacheServer.prototype = Object.create(Server.prototype, { + // Overide Server here +}); + +function createServer(options, requestListener) { + if (typeof options === 'function') { + requestListener = options; + options = {}; + } + + if (options.pfx || (options.key && options.cert)) { + throw new Error('options.pfx, options.key, and options.cert are nonsensical!'); + } + + options.plain = true; + var server = new Http2CacheServer(options); + + if (requestListener) { + server.on('request', requestListener); + } + + return server; +} + +module.exports = { + Http2CacheServer: Http2CacheServer, + createServer: createServer +}; \ No newline at end of file diff --git a/package.json b/package.json index 4da12a9..de7b65a 100644 --- a/package.json +++ b/package.json @@ -31,12 +31,12 @@ }, "homepage": "https://github.com/kaazing/http2-cache.js#readme", "dependencies": { + "bluebird": "~3.4.x", "browserify": "14.0.0 ", - "k3po-mocha.js": "https://github.com/dpwspoon/k3po-mocha.js#develop", - "http2": "https://github.com/dpwspoon/node-http2#http2ws", - "websocket-stream": "3.3.3", "collections": "~5.0.x", - "bluebird": "~3.4.x" + "http2": "^3.3.6", + "k3po-mocha.js": "https://github.com/dpwspoon/k3po-mocha.js#develop", + "websocket-stream": "3.3.3" }, "devDependencies": { "npm-k3po": "https://github.com/dpwspoon/npm-k3po#develop", diff --git a/test/http2-proxy-test.js b/test/http2-proxy-test.js index 9388f0b..5eb4a6e 100644 --- a/test/http2-proxy-test.js +++ b/test/http2-proxy-test.js @@ -5,6 +5,7 @@ XMLHttpRequest = require("xhr2").XMLHttpRequest; /* jshint ignore:end */ require("../lib/http2-cache"); var assert = require('assert'), + createServer = require('../lib/server').createServer, http = require('http'), http2 = require('http2'), getWSTransportServer = require('./test-utils.js').getWSTransportServer; @@ -77,13 +78,13 @@ describe('H2 Proxy', function () { } // start config1 http2 server - s1 = http2.raw.createServer(getWSTransportServer(), function (request, response) { + s1 = createServer(getWSTransportServer(), function (request, response) { s1OnRequest(request, response); }); s1.listen(7081, doneOn2); // start config2 http2 server - s2 = http2.raw.createServer(getWSTransportServer(), function (request, response) { + s2 = createServer(getWSTransportServer(), function (request, response) { s2OnRequest(request, response); }); s2.listen(7082, doneOn2); diff --git a/test/http2-xhr-test.js b/test/http2-xhr-test.js index be11f7f..7a98731 100644 --- a/test/http2-xhr-test.js +++ b/test/http2-xhr-test.js @@ -8,6 +8,7 @@ require("../lib/http2-cache"); var assert = require('assert'), http = require('http'), http2 = require('http2'), + createServer = require('../lib/server').createServer, getWSTransportServer = require('./test-utils').getWSTransportServer; describe('H2 XHR', function () { @@ -79,13 +80,13 @@ describe('H2 XHR', function () { } // start config1 http2 server - s1 = http2.raw.createServer(getWSTransportServer(), function (request, response) { + s1 = createServer(getWSTransportServer(), function (request, response) { s1OnRequest(request, response); }); s1.listen(7081, doneOn2); // start config2 http2 server - s2 = http2.raw.createServer(getWSTransportServer(), function (request, response) { + s2 = createServer(getWSTransportServer(), function (request, response) { s2OnRequest(request, response); }); s2.listen(7082, doneOn2); From 8637c2b23ca44bec462471bd0099051a2a9507a9 Mon Sep 17 00:00:00 2001 From: Harold Thetiot Date: Tue, 27 Jun 2017 16:08:28 -0700 Subject: [PATCH 2/3] use http2 official module and implement custom Agent instead of fork --- lib/xhr.js | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/lib/xhr.js b/lib/xhr.js index 9542f23..52cfe5e 100644 --- a/lib/xhr.js +++ b/lib/xhr.js @@ -42,6 +42,62 @@ var HTTP_METHODS = [ 'CONNECT' ]; +var 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' +}; + // ProgressEvent function ProgressEvent(type) { this.type = type; @@ -133,7 +189,7 @@ function enableXHROverH2(xhrProto, configuration) { case XMLHttpRequest.HEADERS_RECEIVED: var statusCode = options.response.statusCode; redefine(this, 'status', statusCode); - var statusMessage = http2.STATUS_CODES[statusCode]; + var statusMessage = STATUS_CODES[statusCode]; if (statusMessage) { this.statusText = statusMessage; } else { @@ -215,6 +271,7 @@ function enableXHROverH2(xhrProto, configuration) { } var transport = configuration.getTransport(proxyTransportUrl); var request = http2.raw.request({ + agent: configuration.agent, // protocol has already been matched by getting transport url // protocol: destination.protocol, hostname: destination.hostname, From 6d972a5018a08e32172cf7330d090d80bc6944b8 Mon Sep 17 00:00:00 2001 From: Harold Thetiot Date: Wed, 13 Dec 2017 16:37:27 -0800 Subject: [PATCH 3/3] fix spec --- lib/agent.js | 6 +++--- lib/server.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 4966790..962bf50 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,7 +1,7 @@ /* global console */ -var OutgoingRequest = require('http2').OutgoingRequest; -var Agent = require('http2').Agent; -var Endpoint = require('http2').protocol.Endpoint; +var OutgoingRequest = require('http2.js').OutgoingRequest; +var Agent = require('http2.js').Agent; +var Endpoint = require('http2.js').protocol.Endpoint; var url = require('url'); function Http2CacheAgent() { diff --git a/lib/server.js b/lib/server.js index 1639dc4..e6df09d 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,5 +1,5 @@ /* global console */ -var Server = require('http2').Server, +var Server = require('http2.js').Server, logger = require('./logger'); function Http2CacheServer(options) {