Skip to content
92 changes: 87 additions & 5 deletions lib/agent.js
Original file line number Diff line number Diff line change
@@ -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;
54 changes: 54 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
@@ -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
};
56 changes: 56 additions & 0 deletions lib/xhr.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion test/http2-proxy-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe('http2-proxy', function () {
done();
}
}

// start config http2 server
socket = getSocketServer({
port: 7081
Expand Down
3 changes: 1 addition & 2 deletions test/http2-xhr-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
140 changes: 140 additions & 0 deletions test/server.js
Original file line number Diff line number Diff line change
@@ -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"]);
});
});