UNPKG

secure-spdy

Version:

Implementation of SPDY-based Secure Session Management System on node.js.

253 lines (201 loc) 6.05 kB
'use strict'; var assert = require('assert'); var http = require('http'); var https = require('https'); var net = require('net'); var util = require('util'); var transport = require('secure-spdy-transport'); var debug = require('debug')('spdy:client'); var EventEmitter = require('events').EventEmitter; var spdy = require('../spdy'); var mode = /^v0\.8\./.test(process.version) ? 'rusty' : /^v0\.(9|10)\./.test(process.version) ? 'old' : /^v0\.12\./.test(process.version) ? 'normal' : 'modern'; var proto = {}; function instantiate(base) { function Agent(options) { this._init(base, options); } util.inherits(Agent, base); Agent.create = function create(options) { return new Agent(options); }; Object.keys(proto).forEach(function(key) { Agent.prototype[key] = proto[key]; }); return Agent; } proto._init = function _init(base, options) { base.call(this, options); var state = {}; this._spdyState = state; state.host = options.host; state.options = options.spdy || {}; state.secure = this instanceof https.Agent; state.fallback = false; state.createSocket = this._getCreateSocket(); state.socket = null; state.connection = null; // No chunked encoding this.keepAlive = false; var self = this; this._connect(options, function(err, connection) { if (err) return self.emit('error', err); state.connection = connection; self.emit('_connect'); }); }; proto._getCreateSocket = function _getCreateSocket() { // Find super's `createSocket` method var createSocket; var cons = this.constructor.super_; do { createSocket = cons.prototype.createSocket; if (cons.super_ === EventEmitter || !cons.super_) break; cons = cons.super_; } while (!createSocket); if (!createSocket) createSocket = http.Agent.prototype.createSocket; assert(createSocket, '.createSocket() method not found'); return createSocket; }; proto._connect = function _connect(options, callback) { var state = this._spdyState; var protocols = state.options.protocols || [ 'h2', 'spdy/3.1', 'spdy/3', 'spdy/2', 'http/1.1', 'http/1.0' ]; // TODO(indutny): reconnect automatically? var socket = this.createConnection(util._extend({ NPNProtocols: protocols, ALPNProtocols: protocols }, options)); state.socket = socket; function onError(err) { return callback(err); } socket.on('error', onError); socket.on(state.secure ? 'secureConnect' : 'connect', function() { socket.removeListener('error', onError); var protocol; if (state.secure) protocol = socket.npnProtocol || socket.alpnProtocol; else protocol = state.options.protocol; // HTTP server - kill socket and switch to the fallback mode if (!protocol || protocol === 'http/1.1' || protocol === 'http/1.0') { debug('activating fallback'); socket.destroy(); state.fallback = true; return; } debug('connected protocol=%j', protocol); var connection = transport.connection.create(socket, util._extend({ protocol: /spdy/.test(protocol) ? 'spdy' : 'http2', isServer: false }, state.options.connection || {})); // Set version when we are certain if (protocol === 'h2') { connection.start(4); } else if (protocol === 'spdy/3.1') { connection.start(3.1); } else if (protocol === 'spdy/3') { connection.start(3); } else if (protocol === 'spdy/2') { connection.start(2); } else { socket.destroy(); callback(new Error('Unexpected protocol: ' + protocol)); return; } callback(null, connection); }); }; proto._createSocket = function _createSocket(req, options) { var state = this._spdyState; if (state.fallback) return state.createSocket(req, options); var handle = spdy.handle.create(null, state.socket); if (state.connection === null) { this.once('_connect', function() { handle.setStream(this._createStream(req, handle)); }); } else { handle.setStream(this._createStream(req, handle)); } var socket = new net.Socket({ handle: handle, allowHalfOpen: true }); socket.encrypted = true; handle.assignSocket(socket); handle.assignClientRequest(req); // Yes, it is in reverse req.on('response', function(res) { handle.assignRequest(res); }); handle.assignResponse(req); // Handle PUSH req.addListener('newListener', spdy.request.onNewListener); // For v0.8 socket.readable = true; socket.writable = true; return socket; }; if (mode === 'modern' || mode === 'normal') { proto.createSocket = proto._createSocket; } else { proto.createSocket = function createSocket(name, host, port, addr, req) { var state = this._spdyState; if (state.fallback) return state.createSocket(name, host, port, addr, req); return this._createSocket(req, { host: host, port: port }); }; } proto._createStream = function _createStream(req, handle) { var state = this._spdyState; var self = this; return state.connection.reserveStream({ method: req.method, path: req.path, host: state.host }, function(err, stream) { if (err) self.emit('error', err); stream.on('response', function(status, headers) { handle.emitResponse(status, headers); }); }); }; // Public APIs proto.close = function close(callback) { var state = this._spdyState; if (state.connection === null) { this.once('_connect', function() { this.close(callback); }); return; } state.connection.end(callback); }; exports.Agent = instantiate(https.Agent); exports.PlainAgent = instantiate(http.Agent); exports.create = function create(base, options) { if (typeof base === 'object') { options = base; base = null; } if (base) return instantiate(base).create(options); if (options.spdy && options.spdy.plain) return exports.PlainAgent.create(options); else return exports.Agent.create(options); };