├── .npmignore ├── .travis.yml ├── .gitignore ├── lib ├── spdy.js └── spdy │ ├── request.js │ ├── socket.js │ ├── response.js │ ├── handle.js │ ├── server.js │ └── agent.js ├── package.json ├── .jscsrc ├── test ├── fixtures.js ├── client-test.js └── server-test.js ├── .jshintrc └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | keys/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "0.10" 5 | - "0.12" 6 | - "4" 7 | - "5" 8 | branches: 9 | only: 10 | - master 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /*.Makefile 3 | /Makefile 4 | /*.xcodeproj 5 | /*.target.mk 6 | /gyp-mac-tool 7 | /out 8 | /build 9 | .lock-wscript 10 | npm-debug.log 11 | *.node 12 | test.js 13 | -------------------------------------------------------------------------------- /lib/spdy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var spdy = exports; 4 | 5 | // Export tools 6 | spdy.handle = require('./spdy/handle'); 7 | spdy.request = require('./spdy/request'); 8 | spdy.response = require('./spdy/response'); 9 | spdy.Socket = require('./spdy/socket'); 10 | 11 | // Export client 12 | spdy.agent = require('./spdy/agent'); 13 | spdy.Agent = spdy.agent.Agent; 14 | spdy.createAgent = spdy.agent.create; 15 | 16 | // Export server 17 | spdy.server = require('./spdy/server'); 18 | spdy.Server = spdy.server.Server; 19 | spdy.PlainServer = spdy.server.PlainServer; 20 | spdy.createServer = spdy.server.create; 21 | -------------------------------------------------------------------------------- /lib/spdy/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function attachPush(req) { 4 | var handle = req.socket._handle; 5 | 6 | handle.getStream(function(stream) { 7 | stream.on('pushPromise', function(push) { 8 | req.emit('push', push); 9 | }); 10 | }); 11 | } 12 | 13 | exports.onNewListener = function onNewListener(type) { 14 | var req = this; 15 | 16 | if (type !== 'push') 17 | return; 18 | 19 | // Not first listener 20 | if (req.listeners('push').length !== 0) 21 | return; 22 | 23 | if (!req.socket) { 24 | req.on('socket', function() { 25 | attachPush(req); 26 | }); 27 | return; 28 | } 29 | 30 | attachPush(req); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/spdy/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var net = require('net'); 5 | 6 | function Socket(parent, options) { 7 | net.Socket.call(this, options); 8 | 9 | var state = {}; 10 | 11 | this._spdyState = state; 12 | 13 | state.parent = parent; 14 | 15 | this.servername = parent.servername; 16 | this.npnProtocol = parent.npnProtocol; 17 | this.alpnProtocol = parent.alpnProtocol; 18 | this.authorized = parent.authorized; 19 | this.authorizationError = parent.authorizationError; 20 | this.encrypted = true; 21 | } 22 | util.inherits(Socket, net.Socket); 23 | 24 | module.exports = Socket; 25 | 26 | var methods = [ 27 | 'renegotiate', 'setMaxSendFragment', 'getTLSTicket', 'setServername', 28 | 'setSession', 'getPeerCertificate', 'getSession', 'isSessionReused', 29 | 'getCipher', 'getEphemeralKeyInfo' 30 | ]; 31 | 32 | methods.forEach(function(method) { 33 | Socket.prototype[method] = function methodWrap() { 34 | var parent = this._spdyState.parent; 35 | return parent[method].apply(parent, arguments); 36 | }; 37 | }); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spdy", 3 | "version": "3.4.0", 4 | "description": "Implementation of the SPDY protocol on node.js.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "spdy" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/indutny/node-spdy.git" 12 | }, 13 | "homepage": "https://github.com/indutny/node-spdy", 14 | "bugs": { 15 | "email": "node-spdy+bugs@indutny.com", 16 | "url": "https://github.com/indutny/node-spdy/issues" 17 | }, 18 | "author": "Fedor Indutny ", 19 | "contributors": [ 20 | "Chris Storm ", 21 | "François de Metz ", 22 | "Ilya Grigorik ", 23 | "Roberto Peon", 24 | "Tatsuhiro Tsujikawa", 25 | "Jesse Cravens " 26 | ], 27 | "dependencies": { 28 | "debug": "^2.2.0", 29 | "handle-thing": "^1.2.4", 30 | "http-deceiver": "^1.2.4", 31 | "select-hose": "^2.0.0", 32 | "spdy-transport": "^2.0.0" 33 | }, 34 | "devDependencies": { 35 | "jscs": "^1.13.1", 36 | "jshint": "^2.8.0", 37 | "mocha": "^2.2.x" 38 | }, 39 | "scripts": { 40 | "test": "jscs lib/**/*.js test/*.js && jshint lib/**/*.js && mocha --reporter=spec test/*-test.js" 41 | }, 42 | "engines": [ 43 | "node >= 0.7.0" 44 | ], 45 | "main": "./lib/spdy", 46 | "optionalDependencies": {} 47 | } 48 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowKeywordsOnNewLine": [ "else" ], 3 | "disallowMixedSpacesAndTabs": true, 4 | "disallowMultipleLineStrings": true, 5 | "disallowMultipleVarDecl": true, 6 | "disallowNewlineBeforeBlockStatements": true, 7 | "disallowQuotedKeysInObjects": true, 8 | "disallowSpaceAfterObjectKeys": true, 9 | "disallowSpaceAfterPrefixUnaryOperators": true, 10 | "disallowSpaceBeforePostfixUnaryOperators": true, 11 | "disallowSpacesInCallExpression": true, 12 | "disallowTrailingComma": true, 13 | "disallowTrailingWhitespace": true, 14 | 15 | "requireCommaBeforeLineBreak": true, 16 | "requireOperatorBeforeLineBreak": true, 17 | "requireSpaceAfterBinaryOperators": true, 18 | "requireSpaceAfterKeywords": [ "if", "for", "while", "else", "try", "catch" ], 19 | "requireSpaceAfterLineComment": true, 20 | "requireSpaceBeforeBinaryOperators": true, 21 | "requireSpaceBeforeBlockStatements": true, 22 | "requireSpaceBeforeKeywords": [ "else", "catch" ], 23 | "requireSpaceBeforeObjectValues": true, 24 | "requireSpaceBetweenArguments": true, 25 | "requireSpacesInAnonymousFunctionExpression": { 26 | "beforeOpeningCurlyBrace": true 27 | }, 28 | "requireSpacesInFunctionDeclaration": { 29 | "beforeOpeningCurlyBrace": true 30 | }, 31 | "requireSpacesInFunctionExpression": { 32 | "beforeOpeningCurlyBrace": true 33 | }, 34 | "requireSpacesInConditionalExpression": true, 35 | "requireSpacesInForStatement": true, 36 | "requireSpacesInsideArrayBrackets": "all", 37 | "requireSpacesInsideObjectBrackets": "all", 38 | "requireDotNotation": true, 39 | 40 | "maximumLineLength": 80, 41 | "validateIndentation": 2, 42 | "validateLineBreaks": "LF", 43 | "validateParameterSeparator": ", ", 44 | "validateQuoteMarks": "'" 45 | } 46 | -------------------------------------------------------------------------------- /lib/spdy/response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // NOTE: Mostly copy paste from node 4 | exports.writeHead = function writeHead(statusCode, reason, obj) { 5 | var headers; 6 | 7 | if (typeof reason === 'string') { 8 | // writeHead(statusCode, reasonPhrase[, headers]) 9 | this.statusMessage = reason; 10 | } else { 11 | // writeHead(statusCode[, headers]) 12 | this.statusMessage = 13 | this.statusMessage || 'unknown'; 14 | obj = reason; 15 | } 16 | this.statusCode = statusCode; 17 | 18 | if (this._headers) { 19 | // Slow-case: when progressive API and header fields are passed. 20 | if (obj) { 21 | var keys = Object.keys(obj); 22 | for (var i = 0; i < keys.length; i++) { 23 | var k = keys[i]; 24 | if (k) this.setHeader(k, obj[k]); 25 | } 26 | } 27 | // only progressive api is used 28 | headers = this._renderHeaders(); 29 | } else { 30 | // only writeHead() called 31 | headers = obj; 32 | } 33 | 34 | if (statusCode === 204 || statusCode === 304 || 35 | (100 <= statusCode && statusCode <= 199)) { 36 | // RFC 2616, 10.2.5: 37 | // The 204 response MUST NOT include a message-body, and thus is always 38 | // terminated by the first empty line after the header fields. 39 | // RFC 2616, 10.3.5: 40 | // The 304 response MUST NOT contain a message-body, and thus is always 41 | // terminated by the first empty line after the header fields. 42 | // RFC 2616, 10.1 Informational 1xx: 43 | // This class of status code indicates a provisional response, 44 | // consisting only of the Status-Line and optional headers, and is 45 | // terminated by an empty line. 46 | this._hasBody = false; 47 | } 48 | 49 | // don't keep alive connections where the client expects 100 Continue 50 | // but we sent a final status; they may put extra bytes on the wire. 51 | if (this._expect_continue && !this._sent100) { 52 | this.shouldKeepAlive = false; 53 | } 54 | 55 | // Implicit headers sent! 56 | this._header = true; 57 | this._headerSent = true; 58 | 59 | if (this.socket._handle) 60 | this.socket._handle._spdyState.stream.respond(this.statusCode, headers); 61 | }; 62 | 63 | exports.end = function end(data, encoding, callback) { 64 | if (!this._headerSent) 65 | this.writeHead(this.statusCode); 66 | 67 | if (!this.socket._handle) 68 | return; 69 | 70 | var self = this; 71 | var handle = this.socket._handle; 72 | handle._spdyState.ending = true; 73 | this.socket.end(data, encoding, function() { 74 | self.constructor.prototype.end.call(self, '', 'utf8', callback); 75 | }); 76 | }; 77 | 78 | exports.push = function push(path, headers, callback) { 79 | var frame = { 80 | path: path, 81 | method: headers.method ? headers.method.toString() : 'GET', 82 | status: headers.status ? parseInt(headers.status, 10) : 200, 83 | host: this._req.headers.host, 84 | headers: headers.request, 85 | response: headers.response 86 | }; 87 | 88 | var stream = this.spdyStream; 89 | return stream.pushPromise(frame, callback); 90 | }; 91 | 92 | exports.writeContinue = function writeContinue(callback) { 93 | if (this.socket._handle) 94 | this.socket._handle._spdyState.stream.respond(100, {}, callback); 95 | }; 96 | -------------------------------------------------------------------------------- /test/fixtures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | 5 | exports.port = 23433; 6 | 7 | exports.keys = { 8 | key: '-----BEGIN RSA PRIVATE KEY-----\n' + 9 | 'MIIEogIBAAKCAQEA1ARXSoyizYSnHDYickxX4x2UG/8uNWnQWKlWR97NAwRsspN6\n' + 10 | 'aFF1+LnyN9bvLNnhxIowcYy68+LpZ7pYAQgBZSyAhnF1S4qz2w/rxH4CNn96B/je\n' + 11 | 'vQGo3e8vIQ8ChhfuYvGAtTEYJzW8aRoxWSPcukZZdxPQ1Wgbhd9DSXhgkUnkEEET\n' + 12 | 'owyn8ufQFRnQHfc9Fn5DrJilI7vD+ZyRU3gZoBj2GVMQuxJLqQEHy2XsJ6ZWTea/\n' + 13 | 'EfK93XfDyY7ZxyeK0ZdWCVoTqw9QNJSkGjesCBkcY4Rjxi9LbLJwW3Es4wgW4N4Y\n' + 14 | 'cltfygjltSis+RVKJyGeDqTWAxceih3mlkdGIQIDAQABAoIBAB6akc8dBdMMtuKH\n' + 15 | 'nelJw9XwyxRPfWgQYhaqOt4c9xLcbKRKTX0JZTIGBUSyLcwXl1M7b0q0ubfCpVZn\n' + 16 | 'u5RKh4kHJ3ZAomHJH7UbUzkFx2P+eqrz7ZLyzmFayT7IX+DjS3HU0nNVJttiElRJ\n' + 17 | 'h54KYy4wQXHC1n43jOGCHMBaM/ZEpO3xA7PLfVD/BpYLzL+FAYoFBb/x2buLv8Mg\n' + 18 | 'D6QAWkS70mu8ER13NapKjg6PUsYPxHYU30BjGMTXw95Iz5PSAK8+/xQ6YaW7MEVM\n' + 19 | 'twxxfJfZ+9u9nJMfJANqxCi6iZ6ft/e5cbhvNhV/X97XeoPWxqSpx98M6BC/vvBc\n' + 20 | 'UjSmaRECgYEA4NH8Y20zC8zF4ALcBgqgrx+U9upov2UGa+kjS1/So4r/dpG4T8pT\n' + 21 | 'T2tMW6zR5qe7g11kgJm0oI/I6x9P2qwFJONO3MdLYVKd2mSxG2fniCktLg2j6BAX\n' + 22 | 'QTt5zjIEWvhRP2vkrS8gAaJbVMLTMg4s374bE/IdKT+c59tYpcVaXXMCgYEA8WvJ\n' + 23 | 'dfPXoagEgaHRd++R2COMG19euOTFRle0MSq+S9ZeeSe9ejb9CIpWYZ3WVviKvf+E\n' + 24 | 'zksmKTZJnig5pGEgU+2ka1C9PthCGlTlQagD6Ey4hblQgi+pOFgBjE9Yn3FxfppH\n' + 25 | '25ICXNY89EF6klEqKV67E/5O+nBZo+Y2TM4YKRsCgYAaEV8RbEUB9kFvYwV+Eddl\n' + 26 | '1uSf6LgykRU4h/TWtYqn+eL7LZRQdCZKzCczbgt8kjBU4AxaOPhPsbxbPus0cMO7\n' + 27 | '7jtjsBwWcczp2MkMY3TePeAGOgCqVMtNfgb2mKgWoDpTf0ApsJAmgFvUrS5t3GTp\n' + 28 | 'oJJlMqqc8MpRvAZAWmzK7wKBgEVBFlmvyXumJyTItr4hC0VlbRutEA8aET1Mi3RP\n' + 29 | 'Pqeipxc6PzB/9bYtePonvQTV53b5ha9n/1pzKEsmXuK4uf1ZfoEKeD8+6jeDgwCC\n' + 30 | 'ohxRZd12e5Hc+j4fgNIvMM0MTfJzb4mdKPBYxMOMxQyUG/QiKKhjm2RcNlq9/3Wo\n' + 31 | '6WVhAoGAG4QPWoE4ccFECp8eyGw8rjE45y5uqUI/f/RssX7bnKbCRY0otDsPlJd6\n' + 32 | 'Kf0XFssLnYsCXO+ua03gw2N+2mrcsuA5FXHmQMrbfnuojHIVY05nt4Wa5iqV/gqH\n' + 33 | 'PJXWyOgD+Kd6eR/cih/SCoKl4tSGCSJG5TDEpMt+r8EJkCXJ7Fw=\n' + 34 | '-----END RSA PRIVATE KEY-----', 35 | cert: '-----BEGIN CERTIFICATE-----\n' + 36 | 'MIICuTCCAaOgAwIBAgIDAQABMAsGCSqGSIb3DQEBCzAUMRIwEAYDVQQDFglub2Rl\n' + 37 | 'LnNwZHkwHhcNNjkwMTAxMDAwMDAwWhcNMjUwNzA2MDUwMzQzWjAUMRIwEAYDVQQD\n' + 38 | 'Fglub2RlLnNwZHkwggEgMAsGCSqGSIb3DQEBAQOCAQ8AMIIBCgKCAQEA1ARXSoyi\n' + 39 | 'zYSnHDYickxX4x2UG/8uNWnQWKlWR97NAwRsspN6aFF1+LnyN9bvLNnhxIowcYy6\n' + 40 | '8+LpZ7pYAQgBZSyAhnF1S4qz2w/rxH4CNn96B/jevQGo3e8vIQ8ChhfuYvGAtTEY\n' + 41 | 'JzW8aRoxWSPcukZZdxPQ1Wgbhd9DSXhgkUnkEEETowyn8ufQFRnQHfc9Fn5DrJil\n' + 42 | 'I7vD+ZyRU3gZoBj2GVMQuxJLqQEHy2XsJ6ZWTea/EfK93XfDyY7ZxyeK0ZdWCVoT\n' + 43 | 'qw9QNJSkGjesCBkcY4Rjxi9LbLJwW3Es4wgW4N4YcltfygjltSis+RVKJyGeDqTW\n' + 44 | 'Axceih3mlkdGIQIDAQABoxowGDAWBgNVHREEDzANggsqLm5vZGUuc3BkeTALBgkq\n' + 45 | 'hkiG9w0BAQsDggEBALn2FQSDMsyu+oqUnJgTVdGpnzKmfXoBPlQuznRdibri8ABO\n' + 46 | 'kOo8FC72Iy6leVSsB26KtAdhpURZ3mv1Oyt4cGeeyQln2Olzp5flIos+GqYSztAq\n' + 47 | '5ZnrzTLLlip7KHkmastYRXhEwTLmo2JCU8RkRP1X/m1xONF/YkURxmqj6cQTahPY\n' + 48 | 'FzzLP1clW3arJwPlUcKKby6WpxO5MihYEliheBr7fL2TDUA96eG+B/SKxvwaGF2v\n' + 49 | 'gWF8rg5prjPaLW8HH3Efq59AimFqUVQ4HtcJApjLJDYUKlvsMNMvBqh/pQRRPafj\n' + 50 | '0Cp8dyS45sbZ2RgXdyfl6gNEj+DiPbaFliIuFmM=\n' + 51 | '-----END CERTIFICATE-----' 52 | }; 53 | 54 | function expectData(stream, expected, callback) { 55 | var actual = ''; 56 | 57 | stream.on('data', function(chunk) { 58 | actual += chunk; 59 | }); 60 | stream.on('end', function() { 61 | assert.equal(actual, expected); 62 | callback(); 63 | }); 64 | } 65 | exports.expectData = expectData; 66 | 67 | exports.everyProtocol = function everyProtocol(body) { 68 | var protocols = [ 69 | { protocol: 'http2', npn: 'h2', version: 4 }, 70 | { protocol: 'spdy', npn: 'spdy/3.1', version: 3.1 }, 71 | { protocol: 'spdy', npn: 'spdy/3', version: 3 }, 72 | { protocol: 'spdy', npn: 'spdy/2', version: 2 } 73 | ]; 74 | 75 | protocols.forEach(function(protocol) { 76 | describe(protocol.npn, function() { 77 | body(protocol.protocol, protocol.npn, protocol.version); 78 | }); 79 | }); 80 | }; 81 | 82 | exports.everyConfig = function everyConfig(body) { 83 | exports.everyProtocol(function(protocol, npn, version) { 84 | if (npn === 'spdy/2') 85 | return; 86 | 87 | [ false, true ].forEach(function(plain) { 88 | describe(plain ? 'plain mode' : 'ssl mode', function() { 89 | body(protocol, npn, version, plain); 90 | }); 91 | }); 92 | }); 93 | } 94 | 95 | -------------------------------------------------------------------------------- /lib/spdy/handle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var thing = require('handle-thing'); 5 | var httpDeceiver = require('http-deceiver'); 6 | var util = require('util'); 7 | 8 | function Handle(options, stream, socket) { 9 | var state = {}; 10 | this._spdyState = state; 11 | 12 | state.options = options || {}; 13 | 14 | state.stream = stream; 15 | state.socket = null; 16 | state.rawSocket = socket || stream.connection.socket; 17 | state.deceiver = null; 18 | state.ending = false; 19 | 20 | var self = this; 21 | thing.call(this, stream, { 22 | getPeerName: function() { 23 | return self._getPeerName(); 24 | }, 25 | close: function(callback) { 26 | return self._closeCallback(callback); 27 | } 28 | }); 29 | 30 | if (!state.stream) { 31 | this.on('stream', function(stream) { 32 | state.stream = stream; 33 | }); 34 | } 35 | } 36 | util.inherits(Handle, thing); 37 | module.exports = Handle; 38 | 39 | Handle.create = function create(options, stream, socket) { 40 | return new Handle(options, stream, socket); 41 | }; 42 | 43 | Handle.prototype._getPeerName = function _getPeerName() { 44 | var state = this._spdyState; 45 | 46 | if (state.rawSocket._getpeername) 47 | return state.rawSocket._getpeername(); 48 | 49 | return null; 50 | }; 51 | 52 | Handle.prototype._closeCallback = function _closeCallback(callback) { 53 | var state = this._spdyState; 54 | 55 | if (state.ending) 56 | state.stream.end(callback); 57 | else 58 | state.stream.abort(callback); 59 | 60 | // Only a single end is allowed 61 | state.ending = false; 62 | }; 63 | 64 | Handle.prototype.getStream = function getStream(callback) { 65 | var state = this._spdyState; 66 | 67 | if (!callback) { 68 | assert(state.stream); 69 | return state.stream; 70 | } 71 | 72 | if (state.stream) { 73 | process.nextTick(function() { 74 | callback(state.stream); 75 | }); 76 | return; 77 | } 78 | 79 | this.on('stream', callback); 80 | }; 81 | 82 | Handle.prototype.assignSocket = function assignSocket(socket, options) { 83 | var state = this._spdyState; 84 | 85 | state.socket = socket; 86 | state.deceiver = httpDeceiver.create(socket, options); 87 | 88 | function onStreamError(err) { 89 | state.socket.emit('error', err); 90 | } 91 | 92 | this.getStream(function(stream) { 93 | stream.on('error', onStreamError); 94 | }); 95 | }; 96 | 97 | Handle.prototype.assignClientRequest = function assignClientRequest(req) { 98 | var state = this._spdyState; 99 | var oldEnd = req.end; 100 | var oldSend = req._send; 101 | 102 | // Catch the headers before request will be sent 103 | var self = this; 104 | 105 | // For old nodes 106 | if (thing.mode !== 'modern') { 107 | req.end = function end() { 108 | this.end = oldEnd; 109 | 110 | this._send(''); 111 | 112 | return this.end.apply(this, arguments); 113 | }; 114 | } 115 | 116 | req._send = function send(data) { 117 | this._headerSent = true; 118 | 119 | // for v0.10 and below, otherwise it will set `hot = false` and include 120 | // headers in first write 121 | this._header = 'ignore me'; 122 | 123 | // To prevent exception 124 | this.connection = state.socket; 125 | 126 | // It is very important to leave this here, otherwise it will be executed 127 | // on a next tick, after `_send` will perform write 128 | self.getStream(function(stream) { 129 | stream.send(); 130 | }); 131 | 132 | // We are ready to create stream 133 | self.emit('needStream'); 134 | 135 | req._send = oldSend; 136 | 137 | // Ignore empty writes 138 | if (req.method === 'GET' && data.length === 0) 139 | return; 140 | 141 | return req._send.apply(this, arguments); 142 | }; 143 | 144 | // No chunked encoding 145 | req.useChunkedEncodingByDefault = false; 146 | 147 | req.on('finish', function() { 148 | req.socket.end(); 149 | }); 150 | }; 151 | 152 | Handle.prototype.assignRequest = function assignRequest(req) { 153 | // Emit trailing headers 154 | this.getStream(function(stream) { 155 | stream.on('headers', function(headers) { 156 | req.emit('trailers', headers); 157 | }); 158 | }); 159 | }; 160 | 161 | Handle.prototype.assignResponse = function assignResponse(res) { 162 | var self = this; 163 | 164 | res.addTrailers = function addTrailers(headers) { 165 | self.getStream(function(stream) { 166 | stream.sendHeaders(headers); 167 | }); 168 | }; 169 | }; 170 | 171 | Handle.prototype._transformHeaders = function _transformHeaders(kind, headers) { 172 | var state = this._spdyState; 173 | 174 | var res = {}; 175 | var keys = Object.keys(headers); 176 | 177 | if (kind === 'request' && state.options['x-forwarded-for']) { 178 | var xforwarded = state.stream.connection.getXForwardedFor(); 179 | if (xforwarded !== null) 180 | res['x-forwarded-for'] = xforwarded; 181 | } 182 | 183 | for (var i = 0; i < keys.length; i++) { 184 | var key = keys[i]; 185 | var value = headers[key]; 186 | 187 | if (key === ':authority') 188 | res.host = value; 189 | if (/^:/.test(key)) 190 | continue; 191 | 192 | res[key] = value; 193 | } 194 | return res; 195 | }; 196 | 197 | Handle.prototype.emitRequest = function emitRequest() { 198 | var state = this._spdyState; 199 | var stream = state.stream; 200 | 201 | state.deceiver.emitRequest({ 202 | method: stream.method, 203 | path: stream.path, 204 | headers: this._transformHeaders('request', stream.headers) 205 | }); 206 | }; 207 | 208 | Handle.prototype.emitResponse = function emitResponse(status, headers) { 209 | var state = this._spdyState; 210 | 211 | state.deceiver.emitResponse({ 212 | status: status, 213 | headers: this._transformHeaders('response', headers) 214 | }); 215 | }; 216 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : false, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : false, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "indent" : 2, // {int} Number of spaces to use for indentation 16 | "latedef" : true, // true: Require variables/functions to be defined before being used 17 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 18 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 19 | "noempty" : false, // true: Prohibit use of empty blocks 20 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 21 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 22 | "plusplus" : false, // true: Prohibit use of `++` & `--` 23 | "quotmark" : "single", // Quotation mark consistency: 24 | // false : do nothing (default) 25 | // true : ensure whatever is used is consistent 26 | // "single" : require single quotes 27 | // "double" : require double quotes 28 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 29 | "unused" : true, // true: Require all defined variables be used 30 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 31 | "maxparams" : false, // {int} Max number of formal params allowed per function 32 | "maxdepth" : 4, // {int} Max depth of nested blocks (within functions) 33 | "maxstatements" : false, // {int} Max number statements per function 34 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 35 | "maxlen" : false, // {int} Max number of characters per line 36 | 37 | // Relaxing 38 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 39 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 40 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 41 | "eqnull" : false, // true: Tolerate use of `== null` 42 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 43 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 44 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 45 | // (ex: `for each`, multiple try/catch, function expression…) 46 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 47 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 48 | "funcscope" : false, // true: Tolerate defining variables inside control statements 49 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 50 | "iterator" : false, // true: Tolerate using the `__iterator__` property 51 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 52 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 53 | "laxcomma" : false, // true: Tolerate comma-first style coding 54 | "loopfunc" : false, // true: Tolerate functions being defined in loops 55 | "multistr" : false, // true: Tolerate multi-line strings 56 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 57 | "notypeof" : false, // true: Tolerate invalid typeof operator values 58 | "proto" : false, // true: Tolerate using the `__proto__` property 59 | "scripturl" : false, // true: Tolerate script-targeted URLs 60 | "shadow" : true, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 61 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 62 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 63 | "validthis" : false, // true: Tolerate using this in a non-constructor function 64 | 65 | // Environments 66 | "browser" : true, // Web Browser (window, document, etc) 67 | "browserify" : true, // Browserify (node.js code in the browser) 68 | "couch" : false, // CouchDB 69 | "devel" : true, // Development/debugging (alert, confirm, etc) 70 | "dojo" : false, // Dojo Toolkit 71 | "jasmine" : false, // Jasmine 72 | "jquery" : false, // jQuery 73 | "mocha" : true, // Mocha 74 | "mootools" : false, // MooTools 75 | "node" : true, // Node.js 76 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 77 | "prototypejs" : false, // Prototype and Scriptaculous 78 | "qunit" : false, // QUnit 79 | "rhino" : false, // Rhino 80 | "shelljs" : false, // ShellJS 81 | "worker" : false, // Web Workers 82 | "wsh" : false, // Windows Scripting Host 83 | "yui" : false, // Yahoo User Interface 84 | 85 | // Custom Globals 86 | "globals" : { 87 | "module": true 88 | } // additional predefined global variables 89 | } 90 | -------------------------------------------------------------------------------- /test/client-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var https = require('https'); 3 | var http = require('http'); 4 | var util = require('util'); 5 | var transport = require('spdy-transport'); 6 | 7 | var fixtures = require('./fixtures'); 8 | var spdy = require('../'); 9 | 10 | describe('SPDY Client', function() { 11 | describe('regular', function() { 12 | fixtures.everyConfig(function(protocol, npn, version, plain) { 13 | var server; 14 | var agent; 15 | var hmodule; 16 | 17 | beforeEach(function(done) { 18 | hmodule = plain ? http : https; 19 | 20 | var options = util._extend({ 21 | spdy: { 22 | plain: plain 23 | } 24 | }, fixtures.keys); 25 | server = spdy.createServer(options, function(req, res) { 26 | var body = ''; 27 | req.on('data', function(chunk) { 28 | body += chunk; 29 | }); 30 | req.on('end', function() { 31 | res.writeHead(200, req.headers); 32 | res.addTrailers({ trai: 'ler' }); 33 | 34 | var push = res.push('/push', { 35 | request: { 36 | push: 'yes' 37 | } 38 | }, function(err) { 39 | assert(!err); 40 | 41 | push.end('push'); 42 | push.on('error', function() { 43 | }); 44 | 45 | res.end(body || 'okay'); 46 | }); 47 | }); 48 | }); 49 | 50 | server.listen(fixtures.port, function() { 51 | agent = spdy.createAgent({ 52 | rejectUnauthorized: false, 53 | port: fixtures.port, 54 | spdy: { 55 | plain: plain, 56 | protocol: plain ? npn : null, 57 | protocols: [ npn ] 58 | } 59 | }); 60 | 61 | done(); 62 | }); 63 | }); 64 | 65 | afterEach(function(done) { 66 | var waiting = 2; 67 | agent.close(next); 68 | server.close(next); 69 | 70 | function next() { 71 | if (--waiting === 0) 72 | done(); 73 | } 74 | }); 75 | 76 | it('should send GET request', function(done) { 77 | var req = hmodule.request({ 78 | agent: agent, 79 | 80 | method: 'GET', 81 | path: '/get', 82 | headers: { 83 | a: 'b' 84 | } 85 | }, function(res) { 86 | assert.equal(res.statusCode, 200); 87 | assert.equal(res.headers.a, 'b'); 88 | 89 | fixtures.expectData(res, 'okay', done); 90 | }); 91 | req.end(); 92 | }); 93 | 94 | it('should send POST request', function(done) { 95 | var req = hmodule.request({ 96 | agent: agent, 97 | 98 | method: 'POST', 99 | path: '/post', 100 | 101 | headers: { 102 | post: 'headers' 103 | } 104 | }, function(res) { 105 | assert.equal(res.statusCode, 200); 106 | assert.equal(res.headers.post, 'headers'); 107 | 108 | fixtures.expectData(res, 'post body', done); 109 | }); 110 | 111 | agent._spdyState.socket.once(plain ? 'connect' : 'secureConnect', 112 | function() { 113 | req.end('post body'); 114 | }); 115 | }); 116 | 117 | it('should receive PUSH_PROMISE', function(done) { 118 | var req = hmodule.request({ 119 | agent: agent, 120 | 121 | method: 'GET', 122 | path: '/get' 123 | }, function(res) { 124 | assert.equal(res.statusCode, 200); 125 | 126 | res.resume(); 127 | }); 128 | req.on('push', function(push) { 129 | assert.equal(push.path, '/push'); 130 | assert.equal(push.headers.push, 'yes'); 131 | 132 | push.resume(); 133 | push.once('end', done); 134 | }); 135 | req.end(); 136 | }); 137 | 138 | it('should receive trailing headers', function(done) { 139 | var req = hmodule.request({ 140 | agent: agent, 141 | 142 | method: 'GET', 143 | path: '/get' 144 | }, function(res) { 145 | assert.equal(res.statusCode, 200); 146 | 147 | res.on('trailers', function(headers) { 148 | assert.equal(headers.trai, 'ler'); 149 | fixtures.expectData(res, 'okay', done); 150 | }); 151 | }); 152 | req.end(); 153 | }); 154 | }); 155 | }); 156 | 157 | describe('x-forwarded-for', function() { 158 | fixtures.everyConfig(function(protocol, npn, version, plain) { 159 | var server; 160 | var agent; 161 | var hmodule; 162 | 163 | beforeEach(function(done) { 164 | hmodule = plain ? http : https; 165 | 166 | var options = util._extend({ 167 | spdy: { 168 | plain: plain, 169 | 'x-forwarded-for': true 170 | } 171 | }, fixtures.keys); 172 | server = spdy.createServer(options, function(req, res) { 173 | res.writeHead(200, req.headers); 174 | res.end(); 175 | }); 176 | 177 | server.listen(fixtures.port, function() { 178 | agent = spdy.createAgent({ 179 | rejectUnauthorized: false, 180 | port: fixtures.port, 181 | spdy: { 182 | 'x-forwarded-for': '1.2.3.4', 183 | plain: plain, 184 | protocol: plain ? npn : null, 185 | protocols: [ npn ] 186 | } 187 | }); 188 | 189 | done(); 190 | }); 191 | }); 192 | 193 | afterEach(function(done) { 194 | var waiting = 2; 195 | agent.close(next); 196 | server.close(next); 197 | 198 | function next() { 199 | if (--waiting === 0) 200 | done(); 201 | } 202 | }); 203 | 204 | it('should send x-forwarded-for', function(done) { 205 | var req = hmodule.request({ 206 | agent: agent, 207 | 208 | method: 'GET', 209 | path: '/get' 210 | }, function(res) { 211 | assert.equal(res.statusCode, 200); 212 | assert.equal(res.headers['x-forwarded-for'], '1.2.3.4'); 213 | 214 | res.resume(); 215 | res.once('end', done); 216 | }); 217 | req.end(); 218 | }); 219 | }); 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /lib/spdy/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var https = require('https'); 5 | var http = require('http'); 6 | var tls = require('tls'); 7 | var net = require('net'); 8 | var util = require('util'); 9 | var selectHose = require('select-hose'); 10 | var transport = require('spdy-transport'); 11 | var debug = require('debug')('spdy:server'); 12 | var EventEmitter = require('events').EventEmitter; 13 | 14 | var spdy = require('../spdy'); 15 | 16 | var proto = {}; 17 | 18 | function instantiate(base) { 19 | function Server(options, handler) { 20 | this._init(base, options, handler); 21 | } 22 | util.inherits(Server, base); 23 | 24 | Server.create = function create(options, handler) { 25 | return new Server(options, handler); 26 | }; 27 | 28 | Object.keys(proto).forEach(function(key) { 29 | Server.prototype[key] = proto[key]; 30 | }); 31 | 32 | return Server; 33 | } 34 | 35 | proto._init = function _init(base, options, handler) { 36 | var state = {}; 37 | this._spdyState = state; 38 | 39 | state.options = options.spdy || {}; 40 | 41 | var protocols = state.options.protocols || [ 42 | 'h2', 43 | 'spdy/3.1', 'spdy/3', 'spdy/2', 44 | 'http/1.1', 'http/1.0' 45 | ]; 46 | 47 | var actualOptions = util._extend({ 48 | NPNProtocols: protocols, 49 | 50 | // Future-proof 51 | ALPNProtocols: protocols 52 | }, options); 53 | 54 | state.secure = this instanceof tls.Server; 55 | 56 | if (state.secure) 57 | base.call(this, actualOptions); 58 | else 59 | base.call(this); 60 | 61 | // Support HEADERS+FIN 62 | this.httpAllowHalfOpen = true; 63 | 64 | var event = state.secure ? 'secureConnection' : 'connection'; 65 | 66 | state.listeners = this.listeners(event).slice(); 67 | assert(state.listeners.length > 0, 'Server does not have default listeners'); 68 | this.removeAllListeners(event); 69 | 70 | if (state.options.plain) 71 | this.on(event, this._onPlainConnection); 72 | else 73 | this.on(event, this._onConnection); 74 | 75 | if (handler) 76 | this.on('request', handler); 77 | 78 | debug('server init secure=%d', state.secure); 79 | }; 80 | 81 | proto._onConnection = function _onConnection(socket) { 82 | var state = this._spdyState; 83 | 84 | var protocol; 85 | if (state.secure) 86 | protocol = socket.npnProtocol || socket.alpnProtocol; 87 | 88 | this._handleConnection(socket, protocol); 89 | }; 90 | 91 | proto._handleConnection = function _handleConnection(socket, protocol) { 92 | var state = this._spdyState; 93 | 94 | if (!protocol) 95 | protocol = state.options.protocol; 96 | 97 | debug('incoming socket protocol=%j', protocol); 98 | 99 | // No way we can do anything with the socket 100 | if (!protocol || protocol === 'http/1.1' || protocol === 'http/1.0') { 101 | debug('to default handler it goes'); 102 | return this._invokeDefault(socket); 103 | } 104 | 105 | socket.setNoDelay(true); 106 | 107 | var connection = transport.connection.create(socket, util._extend({ 108 | protocol: /spdy/.test(protocol) ? 'spdy' : 'http2', 109 | isServer: true 110 | }, state.options.connection || {})); 111 | 112 | // Set version when we are certain 113 | if (protocol === 'http2') 114 | connection.start(4); 115 | else if (protocol === 'spdy/3.1') 116 | connection.start(3.1); 117 | else if (protocol === 'spdy/3') 118 | connection.start(3); 119 | else if (protocol === 'spdy/2') 120 | connection.start(2); 121 | 122 | connection.on('error', function() { 123 | socket.destroy(); 124 | }); 125 | 126 | var self = this; 127 | connection.on('stream', function(stream) { 128 | self._onStream(stream); 129 | }); 130 | }; 131 | 132 | // HTTP2 preface 133 | var PREFACE = 'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'; 134 | var PREFACE_BUFFER = new Buffer(PREFACE); 135 | 136 | function hoseFilter(data, callback) { 137 | if (data.length < 1) 138 | return callback(null, null); 139 | 140 | // SPDY! 141 | if (data[0] === 0x80) 142 | return callback(null, 'spdy'); 143 | 144 | var avail = Math.min(data.length, PREFACE_BUFFER.length); 145 | for (var i = 0; i < avail; i++) 146 | if (data[i] !== PREFACE_BUFFER[i]) 147 | return callback(null, 'http/1.1'); 148 | 149 | // Not enough bytes to be sure about HTTP2 150 | if (avail !== PREFACE_BUFFER.length) 151 | return callback(null, null); 152 | 153 | return callback(null, 'h2'); 154 | } 155 | 156 | proto._onPlainConnection = function _onPlainConnection(socket) { 157 | var hose = selectHose.create(socket, {}, hoseFilter); 158 | 159 | var self = this; 160 | hose.on('select', function(protocol, socket) { 161 | self._handleConnection(socket, protocol); 162 | }); 163 | 164 | hose.on('error', function(err) { 165 | debug('hose error %j', err.message); 166 | socket.destroy(); 167 | }); 168 | }; 169 | 170 | proto._invokeDefault = function _invokeDefault(socket) { 171 | var state = this._spdyState; 172 | 173 | for (var i = 0; i < state.listeners.length; i++) 174 | state.listeners[i].call(this, socket); 175 | }; 176 | 177 | proto._onStream = function _onStream(stream) { 178 | var state = this._spdyState; 179 | 180 | var handle = spdy.handle.create(this._spdyState.options, stream); 181 | 182 | var socketOptions = { 183 | handle: handle, 184 | allowHalfOpen: true 185 | }; 186 | 187 | var socket; 188 | if (state.secure) 189 | socket = new spdy.Socket(stream.connection.socket, socketOptions); 190 | else 191 | socket = new net.Socket(socketOptions); 192 | 193 | handle.assignSocket(socket); 194 | 195 | // For v0.8 196 | socket.readable = true; 197 | socket.writable = true; 198 | 199 | this._invokeDefault(socket); 200 | 201 | // Add lazy `checkContinue` listener, otherwise `res.writeContinue` will be 202 | // called before the response object was patched by us. 203 | if (stream.headers.expect !== undefined && 204 | /100-continue/i.test(stream.headers.expect) && 205 | EventEmitter.listenerCount(this, 'checkContinue') === 0) { 206 | this.once('checkContinue', function(req, res) { 207 | res.writeContinue(); 208 | 209 | this.emit('request', req, res); 210 | }); 211 | } 212 | 213 | handle.emitRequest(); 214 | }; 215 | 216 | proto.emit = function emit(event, req, res) { 217 | if (event !== 'request' && event !== 'checkContinue') 218 | return EventEmitter.prototype.emit.apply(this, arguments); 219 | 220 | if (!(req.socket._handle instanceof spdy.handle)) { 221 | debug('not spdy req/res'); 222 | req.isSpdy = false; 223 | req.spdyVersion = 1; 224 | res.isSpdy = false; 225 | res.spdyVersion = 1; 226 | return EventEmitter.prototype.emit.apply(this, arguments); 227 | } 228 | 229 | var handle = req.connection._handle; 230 | 231 | req.isSpdy = true; 232 | req.spdyVersion = handle.getStream().connection.getVersion(); 233 | res.isSpdy = true; 234 | res.spdyVersion = req.spdyVersion; 235 | req.spdyStream = handle.getStream(); 236 | 237 | debug('override req/res'); 238 | res.writeHead = spdy.response.writeHead; 239 | res.end = spdy.response.end; 240 | res.push = spdy.response.push; 241 | res.writeContinue = spdy.response.writeContinue; 242 | res.spdyStream = handle.getStream(); 243 | 244 | res._req = req; 245 | 246 | handle.assignRequest(req); 247 | handle.assignResponse(res); 248 | 249 | return EventEmitter.prototype.emit.apply(this, arguments); 250 | }; 251 | 252 | exports.Server = instantiate(https.Server); 253 | exports.PlainServer = instantiate(http.Server); 254 | 255 | exports.create = function create(base, options, handler) { 256 | if (typeof base === 'object') { 257 | handler = options; 258 | options = base; 259 | base = null; 260 | } 261 | 262 | if (base) 263 | return instantiate(base).create(options, handler); 264 | 265 | if (options.spdy && options.spdy.plain) 266 | return exports.PlainServer.create(options, handler); 267 | else 268 | return exports.Server.create(options, handler); 269 | }; 270 | -------------------------------------------------------------------------------- /lib/spdy/agent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var http = require('http'); 5 | var https = require('https'); 6 | var net = require('net'); 7 | var util = require('util'); 8 | var transport = require('spdy-transport'); 9 | var debug = require('debug')('spdy:client'); 10 | 11 | var EventEmitter = require('events').EventEmitter; 12 | 13 | var spdy = require('../spdy'); 14 | 15 | var mode = /^v0\.8\./.test(process.version) ? 'rusty' : 16 | /^v0\.(9|10)\./.test(process.version) ? 'old' : 17 | /^v0\.12\./.test(process.version) ? 'normal' : 18 | 'modern'; 19 | 20 | var proto = {}; 21 | 22 | function instantiate(base) { 23 | function Agent(options) { 24 | this._init(base, options); 25 | } 26 | util.inherits(Agent, base); 27 | 28 | Agent.create = function create(options) { 29 | return new Agent(options); 30 | }; 31 | 32 | Object.keys(proto).forEach(function(key) { 33 | Agent.prototype[key] = proto[key]; 34 | }); 35 | 36 | return Agent; 37 | } 38 | 39 | proto._init = function _init(base, options) { 40 | base.call(this, options); 41 | 42 | var state = {}; 43 | this._spdyState = state; 44 | 45 | state.host = options.host; 46 | state.options = options.spdy || {}; 47 | state.secure = this instanceof https.Agent; 48 | state.fallback = false; 49 | state.createSocket = this._getCreateSocket(); 50 | state.socket = null; 51 | state.connection = null; 52 | 53 | // No chunked encoding 54 | this.keepAlive = false; 55 | 56 | var self = this; 57 | this._connect(options, function(err, connection) { 58 | if (err) 59 | return self.emit('error', err); 60 | 61 | state.connection = connection; 62 | 63 | function onClose() { 64 | state.connection.removeListener('close', onClose); 65 | state.connection.removeListener('error', onError); 66 | self.emit('close'); 67 | } 68 | function onError(error) { 69 | state.connection.removeListener('close', onClose); 70 | state.connection.removeListener('error', onError); 71 | self.emit('error', error); 72 | } 73 | state.connection.once('close', onClose); 74 | state.connection.once('error', onError); 75 | 76 | self.emit('_connect'); 77 | }); 78 | }; 79 | 80 | proto._getCreateSocket = function _getCreateSocket() { 81 | // Find super's `createSocket` method 82 | var createSocket; 83 | var cons = this.constructor.super_; 84 | do { 85 | createSocket = cons.prototype.createSocket; 86 | 87 | if (cons.super_ === EventEmitter || !cons.super_) 88 | break; 89 | cons = cons.super_; 90 | } while (!createSocket); 91 | if (!createSocket) 92 | createSocket = http.Agent.prototype.createSocket; 93 | 94 | assert(createSocket, '.createSocket() method not found'); 95 | 96 | return createSocket; 97 | }; 98 | 99 | proto._connect = function _connect(options, callback) { 100 | var state = this._spdyState; 101 | 102 | var protocols = state.options.protocols || [ 103 | 'h2', 104 | 'spdy/3.1', 'spdy/3', 'spdy/2', 105 | 'http/1.1', 'http/1.0' 106 | ]; 107 | 108 | // TODO(indutny): reconnect automatically? 109 | var socket = this.createConnection(util._extend({ 110 | NPNProtocols: protocols, 111 | ALPNProtocols: protocols, 112 | servername: options.servername || options.host 113 | }, options)); 114 | state.socket = socket; 115 | 116 | socket.setNoDelay(true); 117 | 118 | function onError(err) { 119 | return callback(err); 120 | } 121 | socket.on('error', onError); 122 | 123 | socket.on(state.secure ? 'secureConnect' : 'connect', function() { 124 | socket.removeListener('error', onError); 125 | 126 | var protocol; 127 | if (state.secure) 128 | protocol = socket.npnProtocol || socket.alpnProtocol; 129 | else 130 | protocol = state.options.protocol; 131 | 132 | // HTTP server - kill socket and switch to the fallback mode 133 | if (!protocol || protocol === 'http/1.1' || protocol === 'http/1.0') { 134 | debug('activating fallback'); 135 | socket.destroy(); 136 | state.fallback = true; 137 | return; 138 | } 139 | 140 | debug('connected protocol=%j', protocol); 141 | var connection = transport.connection.create(socket, util._extend({ 142 | protocol: /spdy/.test(protocol) ? 'spdy' : 'http2', 143 | isServer: false 144 | }, state.options.connection || {})); 145 | 146 | // Set version when we are certain 147 | if (protocol === 'h2') { 148 | connection.start(4); 149 | } else if (protocol === 'spdy/3.1') { 150 | connection.start(3.1); 151 | } else if (protocol === 'spdy/3') { 152 | connection.start(3); 153 | } else if (protocol === 'spdy/2') { 154 | connection.start(2); 155 | } else { 156 | socket.destroy(); 157 | callback(new Error('Unexpected protocol: ' + protocol)); 158 | return; 159 | } 160 | 161 | if (state.options['x-forwarded-for'] !== undefined) 162 | connection.sendXForwardedFor(state.options['x-forwarded-for']); 163 | 164 | callback(null, connection); 165 | }); 166 | }; 167 | 168 | proto._createSocket = function _createSocket(req, options, callback) { 169 | var state = this._spdyState; 170 | if (state.fallback) 171 | return state.createSocket(req, options); 172 | 173 | var handle = spdy.handle.create(null, null, state.socket); 174 | 175 | var socketOptions = { 176 | handle: handle, 177 | allowHalfOpen: true 178 | }; 179 | 180 | var socket; 181 | if (state.secure) 182 | socket = new spdy.Socket(state.socket, socketOptions); 183 | else 184 | socket = new net.Socket(socketOptions); 185 | 186 | handle.assignSocket(socket); 187 | handle.assignClientRequest(req); 188 | 189 | // Create stream only once `req.end()` is called 190 | var self = this; 191 | handle.once('needStream', function() { 192 | if (state.connection === null) { 193 | self.once('_connect', function() { 194 | handle.setStream(self._createStream(req, handle)); 195 | }); 196 | } else { 197 | handle.setStream(self._createStream(req, handle)); 198 | } 199 | }); 200 | 201 | // Yes, it is in reverse 202 | req.on('response', function(res) { 203 | handle.assignRequest(res); 204 | }); 205 | handle.assignResponse(req); 206 | 207 | // Handle PUSH 208 | req.addListener('newListener', spdy.request.onNewListener); 209 | 210 | // For v0.8 211 | socket.readable = true; 212 | socket.writable = true; 213 | 214 | if (callback) 215 | return callback(null, socket); 216 | 217 | return socket; 218 | }; 219 | 220 | if (mode === 'modern' || mode === 'normal') { 221 | proto.createSocket = proto._createSocket; 222 | } else { 223 | proto.createSocket = function createSocket(name, host, port, addr, req) { 224 | var state = this._spdyState; 225 | if (state.fallback) 226 | return state.createSocket(name, host, port, addr, req); 227 | 228 | return this._createSocket(req, { 229 | host: host, 230 | port: port 231 | }); 232 | }; 233 | } 234 | 235 | proto._createStream = function _createStream(req, handle) { 236 | var state = this._spdyState; 237 | 238 | var self = this; 239 | return state.connection.reserveStream({ 240 | method: req.method, 241 | path: req.path, 242 | headers: req._headers, 243 | host: state.host 244 | }, function(err, stream) { 245 | if (err) 246 | return self.emit('error', err); 247 | 248 | stream.on('response', function(status, headers) { 249 | handle.emitResponse(status, headers); 250 | }); 251 | }); 252 | }; 253 | 254 | // Public APIs 255 | 256 | proto.close = function close(callback) { 257 | var state = this._spdyState; 258 | 259 | if (state.connection === null) { 260 | this.once('_connect', function() { 261 | this.close(callback); 262 | }); 263 | return; 264 | } 265 | 266 | state.connection.end(callback); 267 | }; 268 | 269 | exports.Agent = instantiate(https.Agent); 270 | exports.PlainAgent = instantiate(http.Agent); 271 | 272 | exports.create = function create(base, options) { 273 | if (typeof base === 'object') { 274 | options = base; 275 | base = null; 276 | } 277 | 278 | if (base) 279 | return instantiate(base).create(options); 280 | 281 | if (options.spdy && options.spdy.plain) 282 | return exports.PlainAgent.create(options); 283 | else 284 | return exports.Agent.create(options); 285 | }; 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SPDY Server for node.js 2 | 3 | [![Build Status](https://travis-ci.org/indutny/node-spdy.svg?branch=master)](https://travis-ci.org/indutny/node-spdy) 4 | [![NPM version](https://badge.fury.io/js/spdy.svg)](http://badge.fury.io/js/spdy) 5 | 6 | With this module you can create [HTTP2][0] / [SPDY][1] servers 7 | in node.js with natural http module interface and fallback to regular https 8 | (for browsers that don't support neither HTTP2, nor SPDY yet). 9 | 10 | This module named `spdy` but it [provides](https://github.com/indutny/node-spdy/issues/269#issuecomment-239014184) support for both http/2 (h2) and spdy (2,3,3.1). Also, `spdy` is compatible with Express. 11 | 12 | ## Usage 13 | 14 | Server: 15 | ```javascript 16 | var spdy = require('spdy'), 17 | fs = require('fs'); 18 | 19 | var options = { 20 | // Private key 21 | key: fs.readFileSync(__dirname + '/keys/spdy-key.pem'), 22 | 23 | // Fullchain file or cert file (prefer the former) 24 | cert: fs.readFileSync(__dirname + '/keys/spdy-fullchain.pem'), 25 | 26 | // **optional** SPDY-specific options 27 | spdy: { 28 | protocols: [ 'h2', 'spdy/3.1', ..., 'http/1.1' ], 29 | plain: false, 30 | 31 | // **optional** 32 | // Parse first incoming X_FORWARDED_FOR frame and put it to the 33 | // headers of every request. 34 | // NOTE: Use with care! This should not be used without some proxy that 35 | // will *always* send X_FORWARDED_FOR 36 | 'x-forwarded-for': true, 37 | 38 | connection: { 39 | windowSize: 1024 * 1024, // Server's window size 40 | 41 | // **optional** if true - server will send 3.1 frames on 3.0 *plain* spdy 42 | autoSpdy31: false 43 | } 44 | } 45 | }; 46 | 47 | var server = spdy.createServer(options, function(req, res) { 48 | res.writeHead(200); 49 | res.end('hello world!'); 50 | }); 51 | 52 | server.listen(3000); 53 | ``` 54 | 55 | Client: 56 | ```javascript 57 | var spdy = require('spdy'); 58 | var http = require('http'); 59 | 60 | var agent = spdy.createAgent({ 61 | host: 'www.google.com', 62 | port: 443, 63 | 64 | // Optional SPDY options 65 | spdy: { 66 | plain: false or true, 67 | ssl: false or true, 68 | 69 | // **optional** send X_FORWARDED_FOR 70 | 'x-forwarded-for': '127.0.0.1' 71 | } 72 | }); 73 | 74 | http.get({ 75 | host: 'www.google.com', 76 | agent: agent 77 | }, function(response) { 78 | console.log('yikes'); 79 | // Here it goes like with any other node.js HTTP request 80 | // ... 81 | // And once we're done - we may close TCP connection to server 82 | // NOTE: All non-closed requests will die! 83 | agent.close(); 84 | }).end(); 85 | ``` 86 | 87 | Please note that if you use a custom agent, by default all connection-level 88 | errors will result in an uncaught exception. To handle these errors subscribe 89 | to the `error` event and re-emit the captured error: 90 | 91 | ```javascript 92 | var agent = spdy.createAgent({ 93 | host: 'www.google.com', 94 | port: 443 95 | }).once('error', function (err) { 96 | this.emit(err); 97 | }); 98 | ``` 99 | 100 | ## API 101 | 102 | API is compatible with `http` and `https` module, but you can use another 103 | function as base class for SPDYServer. 104 | 105 | ```javascript 106 | spdy.createServer( 107 | [base class constructor, i.e. https.Server], 108 | { /* keys and options */ }, // <- the only one required argument 109 | [request listener] 110 | ).listen([port], [host], [callback]); 111 | ``` 112 | 113 | Request listener will receive two arguments: `request` and `response`. They're 114 | both instances of `http`'s `IncomingMessage` and `OutgoingMessage`. But three 115 | custom properties are added to both of them: `isSpdy`, `spdyVersion`. `isSpdy` 116 | is `true` when the request was processed using HTTP2/SPDY protocols, it is 117 | `false` in case of HTTP/1.1 fallback. `spdyVersion` is either of: `2`, `3`, 118 | `3.1`, or `4` (for HTTP2). 119 | 120 | ### Push streams 121 | 122 | It is possible to initiate [PUSH_PROMISE][5] to send content to clients _before_ 123 | the client requests it. 124 | 125 | ```javascript 126 | spdy.createServer(options, function(req, res) { 127 | var stream = res.push('/main.js', { 128 | status: 200, // optional 129 | method: 'GET', // optional 130 | request: { 131 | accept: '*/*' 132 | }, 133 | response: { 134 | 'content-type': 'application/javascript' 135 | } 136 | }); 137 | stream.on('error', function() { 138 | }); 139 | stream.end('alert("hello from push stream!");'); 140 | 141 | res.end(''); 142 | }).listen(3000); 143 | ``` 144 | 145 | [PUSH_PROMISE][5] may be sent using the `push()` method on the current response 146 | object. The signature of the `push()` method is: 147 | 148 | `.push('/some/relative/url', { request: {...}, response: {...} }, callback)` 149 | 150 | Second argument contains headers for both PUSH_PROMISE and emulated response. 151 | `callback` will receive two arguments: `err` (if any error is happened) and a 152 | [Duplex][4] stream as the second argument. 153 | 154 | Client usage: 155 | ```javascript 156 | var agent = spdy.createAgent({ /* ... */ }); 157 | var req = http.get({ 158 | host: 'www.google.com', 159 | agent: agent 160 | }, function(response) { 161 | }); 162 | req.on('push', function(stream) { 163 | stream.on('error', function(err) { 164 | // Handle error 165 | }); 166 | // Read data from stream 167 | }); 168 | ``` 169 | 170 | NOTE: You're responsible for the `stream` object once given it in `.push()` 171 | callback or `push` event. Hence ignoring `error` event on it will result in 172 | uncaught exception and crash your program. 173 | 174 | ### Trailing headers 175 | 176 | Server usage: 177 | ```javascript 178 | function (req, res) { 179 | // Send trailing headers to client 180 | res.addTrailers({ header1: 'value1', header2: 'value2' }); 181 | 182 | // On client's trailing headers 183 | req.on('trailers', function(headers) { 184 | // ... 185 | }); 186 | } 187 | ``` 188 | 189 | Client usage: 190 | ```javascript 191 | var req = http.request({ agent: spdyAgent, /* ... */ }).function (res) { 192 | // On server's trailing headers 193 | res.on('trailers', function(headers) { 194 | // ... 195 | }); 196 | }); 197 | req.write('stuff'); 198 | req.addTrailers({ /* ... */ }); 199 | req.end(); 200 | ``` 201 | 202 | ### Options 203 | 204 | All options supported by [tls][2] work with node-spdy. 205 | 206 | Additional options may be passed via `spdy` sub-object: 207 | 208 | * `plain` - if defined, server will ignore NPN and ALPN data and choose whether 209 | to use spdy or plain http by looking at first data packet. 210 | * `ssl` - if `false` and `options.plain` is `true`, `http.Server` will be used 211 | as a `base` class for created server. 212 | * `maxChunk` - if set and non-falsy, limits number of bytes sent in one DATA 213 | chunk. Setting it to non-zero value is recommended if you care about 214 | interleaving of outgoing data from multiple different streams. 215 | (defaults to 8192) 216 | * `protocols` - list of NPN/ALPN protocols to use (default is: 217 | `['h2','spdy/3.1', 'spdy/3', 'spdy/2','http/1.1', 'http/1.0']`) 218 | * `protocol` - use specific protocol if no NPN/ALPN ex In addition, 219 | * `maxStreams` - set "[maximum concurrent streams][3]" protocol option 220 | 221 | #### Contributors 222 | 223 | * [Fedor Indutny](https://github.com/indutny) 224 | * [Chris Strom](https://github.com/eee-c) 225 | * [François de Metz](https://github.com/francois2metz) 226 | * [Ilya Grigorik](https://github.com/igrigorik) 227 | * [Roberto Peon](https://github.com/grmocg) 228 | * [Tatsuhiro Tsujikawa](https://github.com/tatsuhiro-t) 229 | * [Jesse Cravens](https://github.com/jessecravens) 230 | 231 | #### LICENSE 232 | 233 | This software is licensed under the MIT License. 234 | 235 | Copyright Fedor Indutny, 2015. 236 | 237 | Permission is hereby granted, free of charge, to any person obtaining a 238 | copy of this software and associated documentation files (the 239 | "Software"), to deal in the Software without restriction, including 240 | without limitation the rights to use, copy, modify, merge, publish, 241 | distribute, sublicense, and/or sell copies of the Software, and to permit 242 | persons to whom the Software is furnished to do so, subject to the 243 | following conditions: 244 | 245 | The above copyright notice and this permission notice shall be included 246 | in all copies or substantial portions of the Software. 247 | 248 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 249 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 250 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 251 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 252 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 253 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 254 | USE OR OTHER DEALINGS IN THE SOFTWARE. 255 | 256 | [0]: https://http2.github.io/ 257 | [1]: http://www.chromium.org/spdy 258 | [2]: http://nodejs.org/docs/latest/api/tls.html#tls.createServer 259 | [3]: https://httpwg.github.io/specs/rfc7540.html#SETTINGS_MAX_CONCURRENT_STREAMS 260 | [4]: https://iojs.org/api/stream.html#stream_class_stream_duplex 261 | [5]: https://httpwg.github.io/specs/rfc7540.html#PUSH_PROMISE 262 | -------------------------------------------------------------------------------- /test/server-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var tls = require('tls'); 3 | var net = require('net'); 4 | var https = require('https'); 5 | var transport = require('spdy-transport'); 6 | var util = require('util'); 7 | 8 | var fixtures = require('./fixtures'); 9 | var spdy = require('../'); 10 | 11 | describe('SPDY Server', function() { 12 | fixtures.everyConfig(function(protocol, npn, version, plain) { 13 | var server; 14 | var client; 15 | 16 | beforeEach(function(done) { 17 | server = spdy.createServer(util._extend({ 18 | spdy: { 19 | 'x-forwarded-for': true, 20 | plain: plain 21 | } 22 | }, fixtures.keys)); 23 | 24 | server.listen(fixtures.port, function() { 25 | var socket = (plain ? net : tls).connect({ 26 | rejectUnauthorized: false, 27 | port: fixtures.port, 28 | NPNProtocols: [ npn ] 29 | }, function() { 30 | client = transport.connection.create(socket, { 31 | protocol: protocol, 32 | isServer: false 33 | }); 34 | client.start(version); 35 | done(); 36 | }); 37 | }); 38 | }); 39 | 40 | afterEach(function(done) { 41 | client.socket.destroy(); 42 | server.close(done); 43 | }); 44 | 45 | it('should process GET request', function(done) { 46 | var stream = client.request({ 47 | method: 'GET', 48 | path: '/get', 49 | headers: { 50 | a: 'b' 51 | } 52 | }, function(err) { 53 | assert(!err); 54 | 55 | stream.on('response', function(status, headers) { 56 | assert.equal(status, 200); 57 | assert.equal(headers.ok, 'yes'); 58 | 59 | fixtures.expectData(stream, 'response', done); 60 | }); 61 | 62 | stream.end(); 63 | }); 64 | 65 | server.on('request', function(req, res) { 66 | assert.equal(req.isSpdy, res.isSpdy); 67 | assert.equal(req.spdyVersion, res.spdyVersion); 68 | assert(req.isSpdy); 69 | if (!plain) { 70 | assert(req.socket.encrypted); 71 | assert(req.socket.getPeerCertificate()); 72 | } 73 | 74 | // Auto-detection 75 | if (version === 3.1) 76 | assert(req.spdyVersion >= 3 && req.spdyVersion <= 3.1); 77 | else 78 | assert.equal(req.spdyVersion, version); 79 | assert(req.spdyStream); 80 | assert(res.spdyStream); 81 | 82 | assert.equal(req.method, 'GET'); 83 | assert.equal(req.url, '/get'); 84 | assert.deepEqual(req.headers, { a: 'b', host: 'localhost' }); 85 | 86 | req.on('end', function() { 87 | res.writeHead(200, { 88 | ok: 'yes' 89 | }); 90 | res.end('response'); 91 | }); 92 | req.resume(); 93 | }); 94 | }); 95 | 96 | it('should process POST request', function(done) { 97 | var stream = client.request({ 98 | method: 'POST', 99 | path: '/post' 100 | }, function(err) { 101 | assert(!err); 102 | 103 | stream.on('response', function(status, headers) { 104 | assert.equal(status, 200); 105 | assert.equal(headers.ok, 'yes'); 106 | 107 | fixtures.expectData(stream, 'response', next); 108 | }); 109 | 110 | stream.end('request'); 111 | }); 112 | 113 | server.on('request', function(req, res) { 114 | assert.equal(req.method, 'POST'); 115 | assert.equal(req.url, '/post'); 116 | 117 | res.writeHead(200, { 118 | ok: 'yes' 119 | }); 120 | res.end('response'); 121 | 122 | fixtures.expectData(req, 'request', next); 123 | }); 124 | 125 | var waiting = 2; 126 | function next() { 127 | if (--waiting === 0) 128 | return done(); 129 | } 130 | }); 131 | 132 | it('should process expect-continue request', function(done) { 133 | var stream = client.request({ 134 | method: 'GET', 135 | path: '/get', 136 | headers: { 137 | Expect: '100-continue' 138 | } 139 | }, function(err) { 140 | assert(!err); 141 | 142 | stream.on('response', function(status, headers) { 143 | assert.equal(status, 100); 144 | 145 | fixtures.expectData(stream, 'response', done); 146 | }); 147 | 148 | stream.end(); 149 | }); 150 | 151 | server.on('request', function(req, res) { 152 | req.on('end', function() { 153 | res.end('response'); 154 | }); 155 | req.resume(); 156 | }); 157 | }); 158 | 159 | it('should emit `checkContinue` request', function(done) { 160 | var stream = client.request({ 161 | method: 'GET', 162 | path: '/get', 163 | headers: { 164 | Expect: '100-continue' 165 | } 166 | }, function(err) { 167 | assert(!err); 168 | 169 | stream.on('response', function(status, headers) { 170 | assert.equal(status, 100); 171 | 172 | fixtures.expectData(stream, 'response', done); 173 | }); 174 | 175 | stream.end(); 176 | }); 177 | 178 | server.on('checkContinue', function(req, res) { 179 | req.on('end', function() { 180 | res.writeContinue(); 181 | res.end('response'); 182 | }); 183 | req.resume(); 184 | }); 185 | }); 186 | 187 | it('should send PUSH_PROMISE', function(done) { 188 | var stream = client.request({ 189 | method: 'POST', 190 | path: '/page' 191 | }, function(err) { 192 | assert(!err); 193 | 194 | stream.on('pushPromise', function(push) { 195 | assert.equal(push.path, '/push'); 196 | assert.equal(push.headers.yes, 'push'); 197 | 198 | fixtures.expectData(push, 'push', next); 199 | fixtures.expectData(stream, 'response', next); 200 | }); 201 | 202 | stream.end('request'); 203 | }); 204 | 205 | server.on('request', function(req, res) { 206 | assert.equal(req.method, 'POST'); 207 | assert.equal(req.url, '/page'); 208 | 209 | res.writeHead(200, { 210 | ok: 'yes' 211 | }); 212 | 213 | var push = res.push('/push', { 214 | request: { 215 | yes: 'push' 216 | } 217 | }); 218 | push.end('push'); 219 | 220 | res.end('response'); 221 | 222 | fixtures.expectData(req, 'request', next); 223 | }); 224 | 225 | var waiting = 3; 226 | function next() { 227 | if (--waiting === 0) 228 | return done(); 229 | } 230 | }); 231 | 232 | it('should receive trailing headers', function(done) { 233 | var stream = client.request({ 234 | method: 'POST', 235 | path: '/post' 236 | }, function(err) { 237 | assert(!err); 238 | 239 | stream.sendHeaders({ trai: 'ler' }); 240 | stream.end(); 241 | 242 | stream.on('response', function(status, headers) { 243 | assert.equal(status, 200); 244 | assert.equal(headers.ok, 'yes'); 245 | 246 | fixtures.expectData(stream, 'response', done); 247 | }); 248 | }); 249 | 250 | server.on('request', function(req, res) { 251 | var gotHeaders = false; 252 | req.on('trailers', function(headers) { 253 | gotHeaders = true; 254 | assert.equal(headers.trai, 'ler'); 255 | }); 256 | 257 | req.on('end', function() { 258 | assert(gotHeaders); 259 | 260 | res.writeHead(200, { 261 | ok: 'yes' 262 | }); 263 | res.end('response'); 264 | }); 265 | req.resume(); 266 | }); 267 | }); 268 | 269 | it('should call .writeHead() automatically', function(done) { 270 | var stream = client.request({ 271 | method: 'POST', 272 | path: '/post' 273 | }, function(err) { 274 | assert(!err); 275 | 276 | stream.on('response', function(status, headers) { 277 | assert.equal(status, 300); 278 | 279 | fixtures.expectData(stream, 'response', done); 280 | }); 281 | stream.end(); 282 | }); 283 | 284 | server.on('request', function(req, res) { 285 | req.on('end', function() { 286 | res.statusCode = 300; 287 | res.end('response'); 288 | }); 289 | req.resume(); 290 | }); 291 | }); 292 | 293 | it('should not crash on .writeHead() after socket close', function(done) { 294 | var stream = client.request({ 295 | method: 'POST', 296 | path: '/post' 297 | }, function(err) { 298 | assert(!err); 299 | 300 | setTimeout(function() { 301 | client.socket.destroy(); 302 | }, 50); 303 | stream.on('error', function() {}); 304 | stream.end(); 305 | }); 306 | 307 | server.on('request', function(req, res) { 308 | req.connection.on('close', function() { 309 | assert.doesNotThrow(function() { 310 | res.writeHead(200); 311 | res.end('response'); 312 | }); 313 | done(); 314 | }); 315 | }); 316 | }); 317 | 318 | it('should not crash on .push() after socket close', function(done) { 319 | var stream = client.request({ 320 | method: 'POST', 321 | path: '/post' 322 | }, function(err) { 323 | assert(!err); 324 | 325 | setTimeout(function() { 326 | client.socket.destroy(); 327 | }, 50); 328 | stream.on('error', function() {}); 329 | stream.end(); 330 | }); 331 | 332 | server.on('request', function(req, res) { 333 | req.connection.on('close', function() { 334 | assert.doesNotThrow(function() { 335 | assert.equal(res.push('/push', {}), undefined); 336 | res.end('response'); 337 | }); 338 | done(); 339 | }); 340 | }); 341 | }); 342 | 343 | it('should end response after writing everything down', function(done) { 344 | var stream = client.request({ 345 | method: 'GET', 346 | path: '/post' 347 | }, function(err) { 348 | assert(!err); 349 | 350 | stream.on('response', function(status, headers) { 351 | assert.equal(status, 200); 352 | 353 | fixtures.expectData(stream, 'hello world, what\'s up?', done); 354 | }); 355 | 356 | stream.end(); 357 | }); 358 | 359 | server.on('request', function(req, res) { 360 | req.resume(); 361 | res.writeHead(200); 362 | res.write('hello '); 363 | res.write('world'); 364 | res.write(', what\'s'); 365 | res.write(' up?'); 366 | res.end(); 367 | }); 368 | }); 369 | 370 | it('should handle x-forwarded-for', function(done) { 371 | client.sendXForwardedFor('1.2.3.4'); 372 | 373 | var stream = client.request({ 374 | method: 'GET', 375 | path: '/post' 376 | }, function(err) { 377 | assert(!err); 378 | 379 | stream.resume(); 380 | stream.on('end', done); 381 | stream.end(); 382 | }); 383 | 384 | server.on('request', function(req, res) { 385 | assert.equal(req.headers['x-forwarded-for'], '1.2.3.4'); 386 | req.resume(); 387 | res.end(); 388 | }); 389 | }); 390 | }); 391 | 392 | it('should respond to http/1.1', function(done) { 393 | var server = spdy.createServer(fixtures.keys, function(req, res) { 394 | assert.equal(req.isSpdy, res.isSpdy); 395 | assert.equal(req.spdyVersion, res.spdyVersion); 396 | assert(!req.isSpdy); 397 | assert.equal(req.spdyVersion, 1); 398 | 399 | res.writeHead(200); 400 | res.end(); 401 | }); 402 | 403 | server.listen(fixtures.port, function() { 404 | var req = https.request({ 405 | agent: false, 406 | rejectUnauthorized: false, 407 | NPNProtocols: [ 'http/1.1' ], 408 | port: fixtures.port, 409 | method: 'GET', 410 | path: '/' 411 | }, function(res) { 412 | assert.equal(res.statusCode, 200); 413 | res.resume(); 414 | res.on('end', function() { 415 | server.close(done); 416 | }); 417 | }); 418 | 419 | req.end(); 420 | }); 421 | }); 422 | 423 | it('should support custom base', function(done) { 424 | function Pseuver(options, listener) { 425 | https.Server.call(this, options, listener); 426 | } 427 | util.inherits(Pseuver, https.Server); 428 | 429 | var server = spdy.createServer(Pseuver, fixtures.keys, function(req, res) { 430 | assert.equal(req.isSpdy, res.isSpdy); 431 | assert.equal(req.spdyVersion, res.spdyVersion); 432 | assert(!req.isSpdy); 433 | assert.equal(req.spdyVersion, 1); 434 | 435 | res.writeHead(200); 436 | res.end(); 437 | }); 438 | 439 | server.listen(fixtures.port, function() { 440 | var req = https.request({ 441 | agent: false, 442 | rejectUnauthorized: false, 443 | NPNProtocols: [ 'http/1.1' ], 444 | port: fixtures.port, 445 | method: 'GET', 446 | path: '/' 447 | }, function(res) { 448 | assert.equal(res.statusCode, 200); 449 | res.resume(); 450 | res.on('end', function() { 451 | server.close(done); 452 | }); 453 | }); 454 | 455 | req.end(); 456 | }); 457 | }); 458 | }); 459 | --------------------------------------------------------------------------------