diff --git a/lib/agent.js b/lib/agent.js index 600d493..962bf50 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,12 +1,94 @@ -var http2 = require('http2.js'); +/* global console */ +var OutgoingRequest = require('http2.js').OutgoingRequest; +var Agent = require('http2.js').Agent; +var Endpoint = require('http2.js').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..e6df09d --- /dev/null +++ b/lib/server.js @@ -0,0 +1,54 @@ +/* global console */ +var Server = require('http2.js').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/lib/xhr.js b/lib/xhr.js index 4ff1587..bae2597 100644 --- a/lib/xhr.js +++ b/lib/xhr.js @@ -47,6 +47,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; diff --git a/test/http2-proxy-test.js b/test/http2-proxy-test.js index 7fd0969..f6100bb 100644 --- a/test/http2-proxy-test.js +++ b/test/http2-proxy-test.js @@ -93,7 +93,7 @@ describe('http2-proxy', function () { done(); } } - + // start config http2 server socket = getSocketServer({ port: 7081 diff --git a/test/http2-xhr-test.js b/test/http2-xhr-test.js index 5b22fe6..fac4b01 100644 --- a/test/http2-xhr-test.js +++ b/test/http2-xhr-test.js @@ -7,7 +7,6 @@ if (typeof XMLHttpRequest === 'undefined') { XMLHttpRequest = require("xhr2").XMLHttpRequest; } /* jshint ignore:end */ - require("../lib/http2-cache"); var FormData = require("../lib/form-data").FormData, @@ -48,7 +47,7 @@ describe('http2-xhr', function () { socketOnRequest = function (request, response) { throw new Error("socketOnRequest Unexpected request: " + request.url); }; - + // start config http2 server socket = getSocketServer({ port: 7081 diff --git a/test/server.js b/test/server.js new file mode 100644 index 0000000..d18a273 --- /dev/null +++ b/test/server.js @@ -0,0 +1,140 @@ +/* global console */ +var chai = require('chai'); +var assert = chai.assert; + +/* jshint ignore:start */ +if (typeof XMLHttpRequest === 'undefined') { + XMLHttpRequest = require("xhr2").XMLHttpRequest; +} +/* jshint ignore:end */ +require("../lib/http2-cache"); + +var getSocketServer = require('./test-utils.js').getSocketServer, + getConfigServer = require('./test-utils.js').getConfigServer; + +describe('http2-proxy', function () { + + var config = { + 'transport': 'ws://localhost:7081/', + 'push': 'http://cache-endpoint1/stream', + 'proxy': [ + 'http://cache-endpoint1/' + ] + }; + + var config2 = { + 'transport': 'ws://localhost:7082/path', + 'proxy': [ + 'http://cache-endpoint2/' + ] + }; + + // serves the config files + var configServer, configServer2; + + before(function (done) { + + var completed = 0; + + function doneOn2() { + completed++; + if (completed === 2) { + done(); + } + } + + configServer = getConfigServer({ + config: config, + port: 7080 + }, doneOn2); + + configServer2 = getConfigServer({ + config: config2, + port: 7090 + }, doneOn2); + }); + + after(function (done) { + + var completed = 0; + + function doneOn2() { + completed++; + if (completed === 2) { + done(); + } + } + + configServer.close(doneOn2); + configServer2.close(doneOn2); + }); + + + var socket; + var socket2; + var socketOnRequest; + var socket2OnRequest; + + beforeEach(function (done) { + // starts the 2 h2overWs servers + socketOnRequest = function (request, response) { + throw new Error("socketOnRequest Unexpected request: " + request.url); + }; + // + socket2OnRequest = function (request, response) { + throw new Error("socket2OnRequest Unexpected request: " + request.url); + }; + + var completed = 0; + + function doneOn2() { + completed++; + if (completed === 2) { + done(); + } + } + + // start config http2 server + socket = getSocketServer({ + port: 7081 + }, function (request, response) { + socketOnRequest(request, response); + }, doneOn2); + + // start config2 http2 server + socket2 = getSocketServer({ + port: 7082 + }, function (request, response) { + socket2OnRequest(request, response); + }, doneOn2); + }); + + afterEach(function (done) { + + var completed = 0; + + function doneOn2() { + completed++; + if (completed === 2) { + done(); + } + } + + socket.close(doneOn2); + socket2.close(doneOn2); + }); + + it('proxy() with empty params throws exception', function () { + assert.throws(function () { + XMLHttpRequest.proxy(); + }); + }); + + it('should load config and start stream for pushs when h2PushPath is set in config', function (done) { + socketOnRequest = function (request) { + assert.equal(request.url, '/stream', 'should be on streaming url'); + done(); + }; + XMLHttpRequest.proxy(["http://localhost:7080/config"]); + }); +}); \ No newline at end of file