├── .npmignore ├── .gitignore ├── .travis.yml ├── lib ├── spdy.js └── spdy │ ├── request.js │ ├── socket.js │ ├── response.js │ ├── handle.js │ ├── server.js │ └── agent.js ├── package.json ├── test ├── fixtures.js ├── client-test.js └── server-test.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | keys/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | /*.Makefile 4 | /Makefile 5 | /*.xcodeproj 6 | /*.target.mk 7 | /gyp-mac-tool 8 | /out 9 | /build 10 | .lock-wscript 11 | npm-debug.log 12 | *.node 13 | test.js 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "0.10" 7 | - "0.12" 8 | - "4" 9 | - "6" 10 | - "stable" 11 | 12 | script: 13 | - npm run lint 14 | - npm test 15 | - npm run coverage 16 | -------------------------------------------------------------------------------- /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 | 20 | // Not first listener 21 | if (req.listeners('push').length !== 0) { 22 | return 23 | } 24 | 25 | if (!req.socket) { 26 | req.on('socket', function () { 27 | attachPush(req) 28 | }) 29 | return 30 | } 31 | 32 | attachPush(req) 33 | } 34 | -------------------------------------------------------------------------------- /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.4", 4 | "description": "Implementation of the SPDY protocol on node.js.", 5 | "license": "MIT", 6 | "scripts": { 7 | "lint": "standard", 8 | "test": "mocha --reporter=spec test/*-test.js", 9 | "coverage": "istanbul cover node_modules/.bin/_mocha -- --reporter=spec test/**/*-test.js" 10 | }, 11 | "pre-commit": [ 12 | "lint", 13 | "test" 14 | ], 15 | "keywords": [ 16 | "spdy" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/indutny/node-spdy.git" 21 | }, 22 | "homepage": "https://github.com/indutny/node-spdy", 23 | "bugs": { 24 | "email": "node-spdy+bugs@indutny.com", 25 | "url": "https://github.com/spdy-http2/node-spdy/issues" 26 | }, 27 | "author": "Fedor Indutny ", 28 | "contributors": [ 29 | "Chris Storm ", 30 | "François de Metz ", 31 | "Ilya Grigorik ", 32 | "Roberto Peon", 33 | "Tatsuhiro Tsujikawa", 34 | "Jesse Cravens " 35 | ], 36 | "dependencies": { 37 | "debug": "^2.2.0", 38 | "handle-thing": "^1.2.4", 39 | "http-deceiver": "^1.2.4", 40 | "select-hose": "^2.0.0", 41 | "spdy-transport": "^2.0.15" 42 | }, 43 | "devDependencies": { 44 | "istanbul": "^0.4.5", 45 | "mocha": "^2.2.x", 46 | "pre-commit": "^1.2.2", 47 | "standard": "^8.6.0" 48 | }, 49 | "engines": [ 50 | "node >= 0.7.0" 51 | ], 52 | "main": "./lib/spdy", 53 | "optionalDependencies": {} 54 | } 55 | -------------------------------------------------------------------------------- /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 | (statusCode >= 100 && 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) { this.socket._handle._spdyState.stream.respond(this.statusCode, headers) } 60 | } 61 | 62 | exports.end = function end (data, encoding, callback) { 63 | if (!this._headerSent) { 64 | this.writeHead(this.statusCode) 65 | } 66 | 67 | if (!this.socket._handle) { 68 | return 69 | } 70 | 71 | // Compatibility with Node.js core 72 | this.finished = true 73 | 74 | var self = this 75 | var handle = this.socket._handle 76 | handle._spdyState.ending = true 77 | this.socket.end(data, encoding, function () { 78 | self.constructor.prototype.end.call(self, '', 'utf8', callback) 79 | }) 80 | } 81 | 82 | exports.push = function push (path, headers, callback) { 83 | var frame = { 84 | path: path, 85 | method: headers.method ? headers.method.toString() : 'GET', 86 | status: headers.status ? parseInt(headers.status, 10) : 200, 87 | host: this._req.headers.host, 88 | headers: headers.request, 89 | response: headers.response 90 | } 91 | 92 | var stream = this.spdyStream 93 | return stream.pushPromise(frame, callback) 94 | } 95 | 96 | exports.writeContinue = function writeContinue (callback) { 97 | if (this.socket._handle) { 98 | this.socket._handle._spdyState.stream.respond(100, {}, callback) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/fixtures.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | var assert = require('assert') 5 | 6 | exports.port = 23433 7 | 8 | exports.keys = { 9 | key: '-----BEGIN RSA PRIVATE KEY-----\n' + 10 | 'MIIEogIBAAKCAQEA1ARXSoyizYSnHDYickxX4x2UG/8uNWnQWKlWR97NAwRsspN6\n' + 11 | 'aFF1+LnyN9bvLNnhxIowcYy68+LpZ7pYAQgBZSyAhnF1S4qz2w/rxH4CNn96B/je\n' + 12 | 'vQGo3e8vIQ8ChhfuYvGAtTEYJzW8aRoxWSPcukZZdxPQ1Wgbhd9DSXhgkUnkEEET\n' + 13 | 'owyn8ufQFRnQHfc9Fn5DrJilI7vD+ZyRU3gZoBj2GVMQuxJLqQEHy2XsJ6ZWTea/\n' + 14 | 'EfK93XfDyY7ZxyeK0ZdWCVoTqw9QNJSkGjesCBkcY4Rjxi9LbLJwW3Es4wgW4N4Y\n' + 15 | 'cltfygjltSis+RVKJyGeDqTWAxceih3mlkdGIQIDAQABAoIBAB6akc8dBdMMtuKH\n' + 16 | 'nelJw9XwyxRPfWgQYhaqOt4c9xLcbKRKTX0JZTIGBUSyLcwXl1M7b0q0ubfCpVZn\n' + 17 | 'u5RKh4kHJ3ZAomHJH7UbUzkFx2P+eqrz7ZLyzmFayT7IX+DjS3HU0nNVJttiElRJ\n' + 18 | 'h54KYy4wQXHC1n43jOGCHMBaM/ZEpO3xA7PLfVD/BpYLzL+FAYoFBb/x2buLv8Mg\n' + 19 | 'D6QAWkS70mu8ER13NapKjg6PUsYPxHYU30BjGMTXw95Iz5PSAK8+/xQ6YaW7MEVM\n' + 20 | 'twxxfJfZ+9u9nJMfJANqxCi6iZ6ft/e5cbhvNhV/X97XeoPWxqSpx98M6BC/vvBc\n' + 21 | 'UjSmaRECgYEA4NH8Y20zC8zF4ALcBgqgrx+U9upov2UGa+kjS1/So4r/dpG4T8pT\n' + 22 | 'T2tMW6zR5qe7g11kgJm0oI/I6x9P2qwFJONO3MdLYVKd2mSxG2fniCktLg2j6BAX\n' + 23 | 'QTt5zjIEWvhRP2vkrS8gAaJbVMLTMg4s374bE/IdKT+c59tYpcVaXXMCgYEA8WvJ\n' + 24 | 'dfPXoagEgaHRd++R2COMG19euOTFRle0MSq+S9ZeeSe9ejb9CIpWYZ3WVviKvf+E\n' + 25 | 'zksmKTZJnig5pGEgU+2ka1C9PthCGlTlQagD6Ey4hblQgi+pOFgBjE9Yn3FxfppH\n' + 26 | '25ICXNY89EF6klEqKV67E/5O+nBZo+Y2TM4YKRsCgYAaEV8RbEUB9kFvYwV+Eddl\n' + 27 | '1uSf6LgykRU4h/TWtYqn+eL7LZRQdCZKzCczbgt8kjBU4AxaOPhPsbxbPus0cMO7\n' + 28 | '7jtjsBwWcczp2MkMY3TePeAGOgCqVMtNfgb2mKgWoDpTf0ApsJAmgFvUrS5t3GTp\n' + 29 | 'oJJlMqqc8MpRvAZAWmzK7wKBgEVBFlmvyXumJyTItr4hC0VlbRutEA8aET1Mi3RP\n' + 30 | 'Pqeipxc6PzB/9bYtePonvQTV53b5ha9n/1pzKEsmXuK4uf1ZfoEKeD8+6jeDgwCC\n' + 31 | 'ohxRZd12e5Hc+j4fgNIvMM0MTfJzb4mdKPBYxMOMxQyUG/QiKKhjm2RcNlq9/3Wo\n' + 32 | '6WVhAoGAG4QPWoE4ccFECp8eyGw8rjE45y5uqUI/f/RssX7bnKbCRY0otDsPlJd6\n' + 33 | 'Kf0XFssLnYsCXO+ua03gw2N+2mrcsuA5FXHmQMrbfnuojHIVY05nt4Wa5iqV/gqH\n' + 34 | 'PJXWyOgD+Kd6eR/cih/SCoKl4tSGCSJG5TDEpMt+r8EJkCXJ7Fw=\n' + 35 | '-----END RSA PRIVATE KEY-----', 36 | cert: '-----BEGIN CERTIFICATE-----\n' + 37 | 'MIICuTCCAaOgAwIBAgIDAQABMAsGCSqGSIb3DQEBCzAUMRIwEAYDVQQDFglub2Rl\n' + 38 | 'LnNwZHkwHhcNNjkwMTAxMDAwMDAwWhcNMjUwNzA2MDUwMzQzWjAUMRIwEAYDVQQD\n' + 39 | 'Fglub2RlLnNwZHkwggEgMAsGCSqGSIb3DQEBAQOCAQ8AMIIBCgKCAQEA1ARXSoyi\n' + 40 | 'zYSnHDYickxX4x2UG/8uNWnQWKlWR97NAwRsspN6aFF1+LnyN9bvLNnhxIowcYy6\n' + 41 | '8+LpZ7pYAQgBZSyAhnF1S4qz2w/rxH4CNn96B/jevQGo3e8vIQ8ChhfuYvGAtTEY\n' + 42 | 'JzW8aRoxWSPcukZZdxPQ1Wgbhd9DSXhgkUnkEEETowyn8ufQFRnQHfc9Fn5DrJil\n' + 43 | 'I7vD+ZyRU3gZoBj2GVMQuxJLqQEHy2XsJ6ZWTea/EfK93XfDyY7ZxyeK0ZdWCVoT\n' + 44 | 'qw9QNJSkGjesCBkcY4Rjxi9LbLJwW3Es4wgW4N4YcltfygjltSis+RVKJyGeDqTW\n' + 45 | 'Axceih3mlkdGIQIDAQABoxowGDAWBgNVHREEDzANggsqLm5vZGUuc3BkeTALBgkq\n' + 46 | 'hkiG9w0BAQsDggEBALn2FQSDMsyu+oqUnJgTVdGpnzKmfXoBPlQuznRdibri8ABO\n' + 47 | 'kOo8FC72Iy6leVSsB26KtAdhpURZ3mv1Oyt4cGeeyQln2Olzp5flIos+GqYSztAq\n' + 48 | '5ZnrzTLLlip7KHkmastYRXhEwTLmo2JCU8RkRP1X/m1xONF/YkURxmqj6cQTahPY\n' + 49 | 'FzzLP1clW3arJwPlUcKKby6WpxO5MihYEliheBr7fL2TDUA96eG+B/SKxvwaGF2v\n' + 50 | 'gWF8rg5prjPaLW8HH3Efq59AimFqUVQ4HtcJApjLJDYUKlvsMNMvBqh/pQRRPafj\n' + 51 | '0Cp8dyS45sbZ2RgXdyfl6gNEj+DiPbaFliIuFmM=\n' + 52 | '-----END CERTIFICATE-----' 53 | } 54 | 55 | function expectData (stream, expected, callback) { 56 | var actual = '' 57 | 58 | stream.on('data', function (chunk) { 59 | actual += chunk 60 | }) 61 | stream.on('end', function () { 62 | assert.equal(actual, expected) 63 | callback() 64 | }) 65 | } 66 | exports.expectData = expectData 67 | 68 | exports.everyProtocol = function everyProtocol (body) { 69 | var protocols = [ 70 | { protocol: 'http2', npn: 'h2', version: 4 }, 71 | { protocol: 'spdy', npn: 'spdy/3.1', version: 3.1 }, 72 | { protocol: 'spdy', npn: 'spdy/3', version: 3 }, 73 | { protocol: 'spdy', npn: 'spdy/2', version: 2 } 74 | ] 75 | 76 | protocols.forEach(function (protocol) { 77 | describe(protocol.npn, function () { 78 | body(protocol.protocol, protocol.npn, protocol.version) 79 | }) 80 | }) 81 | } 82 | 83 | exports.everyConfig = function everyConfig (body) { 84 | exports.everyProtocol(function (protocol, npn, version) { 85 | if (npn === 'spdy/2') { 86 | return 87 | } 88 | 89 | [ false, true ].forEach(function (plain) { 90 | describe(plain ? 'plain mode' : 'ssl mode', function () { 91 | body(protocol, npn, version, plain) 92 | }) 93 | }) 94 | }) 95 | } 96 | 97 | -------------------------------------------------------------------------------- /test/client-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | var https = require('https') 5 | var http = require('http') 6 | var util = require('util') 7 | 8 | var fixtures = require('./fixtures') 9 | var spdy = require('../') 10 | 11 | describe('SPDY Client', function () { 12 | describe('regular', function () { 13 | fixtures.everyConfig(function (protocol, npn, version, plain) { 14 | var server 15 | var agent 16 | var hmodule 17 | 18 | beforeEach(function (done) { 19 | hmodule = plain ? http : https 20 | 21 | var options = util._extend({ 22 | spdy: { 23 | plain: plain 24 | } 25 | }, fixtures.keys) 26 | server = spdy.createServer(options, function (req, res) { 27 | var body = '' 28 | req.on('data', function (chunk) { 29 | body += chunk 30 | }) 31 | req.on('end', function () { 32 | res.writeHead(200, req.headers) 33 | res.addTrailers({ trai: 'ler' }) 34 | 35 | var push = res.push('/push', { 36 | request: { 37 | push: 'yes' 38 | } 39 | }, function (err) { 40 | assert(!err) 41 | 42 | push.end('push') 43 | push.on('error', function () { 44 | }) 45 | 46 | res.end(body || 'okay') 47 | }) 48 | }) 49 | }) 50 | 51 | server.listen(fixtures.port, function () { 52 | agent = spdy.createAgent({ 53 | rejectUnauthorized: false, 54 | port: fixtures.port, 55 | spdy: { 56 | plain: plain, 57 | protocol: plain ? npn : null, 58 | protocols: [ npn ] 59 | } 60 | }) 61 | 62 | done() 63 | }) 64 | }) 65 | 66 | afterEach(function (done) { 67 | var waiting = 2 68 | agent.close(next) 69 | server.close(next) 70 | 71 | function next () { 72 | if (--waiting === 0) { 73 | done() 74 | } 75 | } 76 | }) 77 | 78 | it('should send GET request', function (done) { 79 | var req = hmodule.request({ 80 | agent: agent, 81 | 82 | method: 'GET', 83 | path: '/get', 84 | headers: { 85 | a: 'b' 86 | } 87 | }, function (res) { 88 | assert.equal(res.statusCode, 200) 89 | assert.equal(res.headers.a, 'b') 90 | 91 | fixtures.expectData(res, 'okay', done) 92 | }) 93 | req.end() 94 | }) 95 | 96 | it('should send POST request', function (done) { 97 | var req = hmodule.request({ 98 | agent: agent, 99 | 100 | method: 'POST', 101 | path: '/post', 102 | 103 | headers: { 104 | post: 'headers' 105 | } 106 | }, function (res) { 107 | assert.equal(res.statusCode, 200) 108 | assert.equal(res.headers.post, 'headers') 109 | 110 | fixtures.expectData(res, 'post body', done) 111 | }) 112 | 113 | agent._spdyState.socket.once(plain ? 'connect' : 'secureConnect', 114 | function () { 115 | req.end('post body') 116 | }) 117 | }) 118 | 119 | it('should receive PUSH_PROMISE', function (done) { 120 | var req = hmodule.request({ 121 | agent: agent, 122 | 123 | method: 'GET', 124 | path: '/get' 125 | }, function (res) { 126 | assert.equal(res.statusCode, 200) 127 | 128 | res.resume() 129 | }) 130 | req.on('push', function (push) { 131 | assert.equal(push.path, '/push') 132 | assert.equal(push.headers.push, 'yes') 133 | 134 | push.resume() 135 | push.once('end', done) 136 | }) 137 | req.end() 138 | }) 139 | 140 | it('should receive trailing headers', function (done) { 141 | var req = hmodule.request({ 142 | agent: agent, 143 | 144 | method: 'GET', 145 | path: '/get' 146 | }, function (res) { 147 | assert.equal(res.statusCode, 200) 148 | 149 | res.on('trailers', function (headers) { 150 | assert.equal(headers.trai, 'ler') 151 | fixtures.expectData(res, 'okay', done) 152 | }) 153 | }) 154 | req.end() 155 | }) 156 | }) 157 | }) 158 | 159 | describe('x-forwarded-for', function () { 160 | fixtures.everyConfig(function (protocol, npn, version, plain) { 161 | var server 162 | var agent 163 | var hmodule 164 | 165 | beforeEach(function (done) { 166 | hmodule = plain ? http : https 167 | 168 | var options = util._extend({ 169 | spdy: { 170 | plain: plain, 171 | 'x-forwarded-for': true 172 | } 173 | }, fixtures.keys) 174 | server = spdy.createServer(options, function (req, res) { 175 | res.writeHead(200, req.headers) 176 | res.end() 177 | }) 178 | 179 | server.listen(fixtures.port, function () { 180 | agent = spdy.createAgent({ 181 | rejectUnauthorized: false, 182 | port: fixtures.port, 183 | spdy: { 184 | 'x-forwarded-for': '1.2.3.4', 185 | plain: plain, 186 | protocol: plain ? npn : null, 187 | protocols: [ npn ] 188 | } 189 | }) 190 | 191 | done() 192 | }) 193 | }) 194 | 195 | afterEach(function (done) { 196 | var waiting = 2 197 | agent.close(next) 198 | server.close(next) 199 | 200 | function next () { 201 | if (--waiting === 0) { 202 | done() 203 | } 204 | } 205 | }) 206 | 207 | it('should send x-forwarded-for', function (done) { 208 | var req = hmodule.request({ 209 | agent: agent, 210 | 211 | method: 'GET', 212 | path: '/get' 213 | }, function (res) { 214 | assert.equal(res.statusCode, 200) 215 | assert.equal(res.headers['x-forwarded-for'], '1.2.3.4') 216 | 217 | res.resume() 218 | res.once('end', done) 219 | }) 220 | req.end() 221 | }) 222 | }) 223 | }) 224 | }) 225 | -------------------------------------------------------------------------------- /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 | 50 | return null 51 | } 52 | 53 | Handle.prototype._closeCallback = function _closeCallback (callback) { 54 | var state = this._spdyState 55 | var stream = state.stream 56 | 57 | if (state.ending) { 58 | // The .end() method of the stream may be called by us or by the 59 | // .shutdown() method in our super-class. If the latter has already been 60 | // called, then calling the .end() method below will have no effect, with 61 | // the result that the callback will never get executed, leading to an ever 62 | // so subtle memory leak. 63 | if (stream._writableState.finished) { 64 | // NOTE: it is important to call `setImmediate` instead of `nextTick`, 65 | // since this is how regular `handle.close()` works in node.js core. 66 | // 67 | // Using `nextTick` will lead to `net.Socket` emitting `close` before 68 | // `end` on UV_EOF. This results in aborted request without `end` event. 69 | setImmediate(callback) 70 | } else if (stream._writableState.ending) { 71 | stream.once('finish', function () { 72 | callback(null) 73 | }) 74 | } else { 75 | stream.end(callback) 76 | } 77 | } else { 78 | stream.abort(callback) 79 | } 80 | 81 | // Only a single end is allowed 82 | state.ending = false 83 | } 84 | 85 | Handle.prototype.getStream = function getStream (callback) { 86 | var state = this._spdyState 87 | 88 | if (!callback) { 89 | assert(state.stream) 90 | return state.stream 91 | } 92 | 93 | if (state.stream) { 94 | process.nextTick(function () { 95 | callback(state.stream) 96 | }) 97 | return 98 | } 99 | 100 | this.on('stream', callback) 101 | } 102 | 103 | Handle.prototype.assignSocket = function assignSocket (socket, options) { 104 | var state = this._spdyState 105 | 106 | state.socket = socket 107 | state.deceiver = httpDeceiver.create(socket, options) 108 | 109 | function onStreamError (err) { 110 | state.socket.emit('error', err) 111 | } 112 | 113 | this.getStream(function (stream) { 114 | stream.on('error', onStreamError) 115 | }) 116 | } 117 | 118 | Handle.prototype.assignClientRequest = function assignClientRequest (req) { 119 | var state = this._spdyState 120 | var oldEnd = req.end 121 | var oldSend = req._send 122 | 123 | // Catch the headers before request will be sent 124 | var self = this 125 | 126 | // For old nodes 127 | if (thing.mode !== 'modern') { 128 | req.end = function end () { 129 | this.end = oldEnd 130 | 131 | this._send('') 132 | 133 | return this.end.apply(this, arguments) 134 | } 135 | } 136 | 137 | req._send = function send (data) { 138 | this._headerSent = true 139 | 140 | // for v0.10 and below, otherwise it will set `hot = false` and include 141 | // headers in first write 142 | this._header = 'ignore me' 143 | 144 | // To prevent exception 145 | this.connection = state.socket 146 | 147 | // It is very important to leave this here, otherwise it will be executed 148 | // on a next tick, after `_send` will perform write 149 | self.getStream(function (stream) { 150 | stream.send() 151 | }) 152 | 153 | // We are ready to create stream 154 | self.emit('needStream') 155 | 156 | req._send = oldSend 157 | 158 | // Ignore empty writes 159 | if (req.method === 'GET' && data.length === 0) { 160 | return 161 | } 162 | 163 | return req._send.apply(this, arguments) 164 | } 165 | 166 | // No chunked encoding 167 | req.useChunkedEncodingByDefault = false 168 | 169 | req.on('finish', function () { 170 | req.socket.end() 171 | }) 172 | } 173 | 174 | Handle.prototype.assignRequest = function assignRequest (req) { 175 | // Emit trailing headers 176 | this.getStream(function (stream) { 177 | stream.on('headers', function (headers) { 178 | req.emit('trailers', headers) 179 | }) 180 | }) 181 | } 182 | 183 | Handle.prototype.assignResponse = function assignResponse (res) { 184 | var self = this 185 | 186 | res.addTrailers = function addTrailers (headers) { 187 | self.getStream(function (stream) { 188 | stream.sendHeaders(headers) 189 | }) 190 | } 191 | } 192 | 193 | Handle.prototype._transformHeaders = function _transformHeaders (kind, headers) { 194 | var state = this._spdyState 195 | 196 | var res = {} 197 | var keys = Object.keys(headers) 198 | 199 | if (kind === 'request' && state.options['x-forwarded-for']) { 200 | var xforwarded = state.stream.connection.getXForwardedFor() 201 | if (xforwarded !== null) { 202 | res['x-forwarded-for'] = xforwarded 203 | } 204 | } 205 | 206 | for (var i = 0; i < keys.length; i++) { 207 | var key = keys[i] 208 | var value = headers[key] 209 | 210 | if (key === ':authority') { 211 | res.host = value 212 | } 213 | if (/^:/.test(key)) { 214 | continue 215 | } 216 | 217 | res[key] = value 218 | } 219 | return res 220 | } 221 | 222 | Handle.prototype.emitRequest = function emitRequest () { 223 | var state = this._spdyState 224 | var stream = state.stream 225 | 226 | state.deceiver.emitRequest({ 227 | method: stream.method, 228 | path: stream.path, 229 | headers: this._transformHeaders('request', stream.headers) 230 | }) 231 | } 232 | 233 | Handle.prototype.emitResponse = function emitResponse (status, headers) { 234 | var state = this._spdyState 235 | 236 | state.deceiver.emitResponse({ 237 | status: status, 238 | headers: this._transformHeaders('response', headers) 239 | }) 240 | } 241 | -------------------------------------------------------------------------------- /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 | 62 | // Support HEADERS+FIN 63 | this.httpAllowHalfOpen = true 64 | 65 | var event = state.secure ? 'secureConnection' : 'connection' 66 | 67 | state.listeners = this.listeners(event).slice() 68 | assert(state.listeners.length > 0, 'Server does not have default listeners') 69 | this.removeAllListeners(event) 70 | 71 | if (state.options.plain) { 72 | this.on(event, this._onPlainConnection) 73 | } else { this.on(event, this._onConnection) } 74 | 75 | if (handler) { 76 | this.on('request', handler) 77 | } 78 | 79 | debug('server init secure=%d', state.secure) 80 | } 81 | 82 | proto._onConnection = function _onConnection (socket) { 83 | var state = this._spdyState 84 | 85 | var protocol 86 | if (state.secure) { 87 | protocol = socket.npnProtocol || socket.alpnProtocol 88 | } 89 | 90 | this._handleConnection(socket, protocol) 91 | } 92 | 93 | proto._handleConnection = function _handleConnection (socket, protocol) { 94 | var state = this._spdyState 95 | 96 | if (!protocol) { 97 | protocol = state.options.protocol 98 | } 99 | 100 | debug('incoming socket protocol=%j', protocol) 101 | 102 | // No way we can do anything with the socket 103 | if (!protocol || protocol === 'http/1.1' || protocol === 'http/1.0') { 104 | debug('to default handler it goes') 105 | return this._invokeDefault(socket) 106 | } 107 | 108 | socket.setNoDelay(true) 109 | 110 | var connection = transport.connection.create(socket, util._extend({ 111 | protocol: /spdy/.test(protocol) ? 'spdy' : 'http2', 112 | isServer: true 113 | }, state.options.connection || {})) 114 | 115 | // Set version when we are certain 116 | if (protocol === 'http2') { connection.start(4) } else if (protocol === 'spdy/3.1') { 117 | connection.start(3.1) 118 | } else if (protocol === 'spdy/3') { connection.start(3) } else if (protocol === 'spdy/2') { 119 | connection.start(2) 120 | } 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 | 141 | // SPDY! 142 | if (data[0] === 0x80) { 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]) { return callback(null, 'http/1.1') } 147 | } 148 | 149 | // Not enough bytes to be sure about HTTP2 150 | if (avail !== PREFACE_BUFFER.length) { return callback(null, null) } 151 | 152 | return callback(null, 'h2') 153 | } 154 | 155 | proto._onPlainConnection = function _onPlainConnection (socket) { 156 | var hose = selectHose.create(socket, {}, hoseFilter) 157 | 158 | var self = this 159 | hose.on('select', function (protocol, socket) { 160 | self._handleConnection(socket, protocol) 161 | }) 162 | 163 | hose.on('error', function (err) { 164 | debug('hose error %j', err.message) 165 | socket.destroy() 166 | }) 167 | } 168 | 169 | proto._invokeDefault = function _invokeDefault (socket) { 170 | var state = this._spdyState 171 | 172 | for (var i = 0; i < state.listeners.length; i++) { state.listeners[i].call(this, socket) } 173 | } 174 | 175 | proto._onStream = function _onStream (stream) { 176 | var state = this._spdyState 177 | 178 | var handle = spdy.handle.create(this._spdyState.options, stream) 179 | 180 | var socketOptions = { 181 | handle: handle, 182 | allowHalfOpen: true 183 | } 184 | 185 | var socket 186 | if (state.secure) { 187 | socket = new spdy.Socket(stream.connection.socket, socketOptions) 188 | } else { 189 | socket = new net.Socket(socketOptions) 190 | } 191 | 192 | handle.assignSocket(socket) 193 | 194 | // For v0.8 195 | socket.readable = true 196 | socket.writable = true 197 | 198 | this._invokeDefault(socket) 199 | 200 | // Add lazy `checkContinue` listener, otherwise `res.writeContinue` will be 201 | // called before the response object was patched by us. 202 | if (stream.headers.expect !== undefined && 203 | /100-continue/i.test(stream.headers.expect) && 204 | EventEmitter.listenerCount(this, 'checkContinue') === 0) { 205 | this.once('checkContinue', function (req, res) { 206 | res.writeContinue() 207 | 208 | this.emit('request', req, res) 209 | }) 210 | } 211 | 212 | handle.emitRequest() 213 | } 214 | 215 | proto.emit = function emit (event, req, res) { 216 | if (event !== 'request' && event !== 'checkContinue') { 217 | return EventEmitter.prototype.emit.apply(this, arguments) 218 | } 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 | 266 | if (options.spdy && options.spdy.plain) { return exports.PlainServer.create(options, handler) } else { 267 | return exports.Server.create(options, handler) 268 | } 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) 16 | ? 'rusty' 17 | : /^v0\.(9|10)\./.test(process.version) 18 | ? 'old' 19 | : /^v0\.12\./.test(process.version) 20 | ? 'normal' 21 | : 'modern' 22 | 23 | var proto = {} 24 | 25 | function instantiate (base) { 26 | function Agent (options) { 27 | this._init(base, options) 28 | } 29 | util.inherits(Agent, base) 30 | 31 | Agent.create = function create (options) { 32 | return new Agent(options) 33 | } 34 | 35 | Object.keys(proto).forEach(function (key) { 36 | Agent.prototype[key] = proto[key] 37 | }) 38 | 39 | return Agent 40 | } 41 | 42 | proto._init = function _init (base, options) { 43 | base.call(this, options) 44 | 45 | var state = {} 46 | this._spdyState = state 47 | 48 | state.host = options.host 49 | state.options = options.spdy || {} 50 | state.secure = this instanceof https.Agent 51 | state.fallback = false 52 | state.createSocket = this._getCreateSocket() 53 | state.socket = null 54 | state.connection = null 55 | 56 | // No chunked encoding 57 | this.keepAlive = false 58 | 59 | var self = this 60 | this._connect(options, function (err, connection) { 61 | if (err) { 62 | return self.emit('error', err) 63 | } 64 | 65 | state.connection = connection 66 | self.emit('_connect') 67 | }) 68 | } 69 | 70 | proto._getCreateSocket = function _getCreateSocket () { 71 | // Find super's `createSocket` method 72 | var createSocket 73 | var cons = this.constructor.super_ 74 | do { 75 | createSocket = cons.prototype.createSocket 76 | 77 | if (cons.super_ === EventEmitter || !cons.super_) { 78 | break 79 | } 80 | cons = cons.super_ 81 | } while (!createSocket) 82 | if (!createSocket) { 83 | createSocket = http.Agent.prototype.createSocket 84 | } 85 | 86 | assert(createSocket, '.createSocket() method not found') 87 | 88 | return createSocket 89 | } 90 | 91 | proto._connect = function _connect (options, callback) { 92 | var state = this._spdyState 93 | 94 | var protocols = state.options.protocols || [ 95 | 'h2', 96 | 'spdy/3.1', 'spdy/3', 'spdy/2', 97 | 'http/1.1', 'http/1.0' 98 | ] 99 | 100 | // TODO(indutny): reconnect automatically? 101 | var socket = this.createConnection(util._extend({ 102 | NPNProtocols: protocols, 103 | ALPNProtocols: protocols, 104 | servername: options.servername || options.host 105 | }, options)) 106 | state.socket = socket 107 | 108 | socket.setNoDelay(true) 109 | 110 | function onError (err) { 111 | return callback(err) 112 | } 113 | socket.on('error', onError) 114 | 115 | socket.on(state.secure ? 'secureConnect' : 'connect', function () { 116 | socket.removeListener('error', onError) 117 | 118 | var protocol 119 | if (state.secure) { 120 | protocol = socket.npnProtocol || 121 | socket.alpnProtocol || 122 | state.options.protocol 123 | } else { 124 | protocol = state.options.protocol 125 | } 126 | 127 | // HTTP server - kill socket and switch to the fallback mode 128 | if (!protocol || protocol === 'http/1.1' || protocol === 'http/1.0') { 129 | debug('activating fallback') 130 | socket.destroy() 131 | state.fallback = true 132 | return 133 | } 134 | 135 | debug('connected protocol=%j', protocol) 136 | var connection = transport.connection.create(socket, util._extend({ 137 | protocol: /spdy/.test(protocol) ? 'spdy' : 'http2', 138 | isServer: false 139 | }, state.options.connection || {})) 140 | 141 | // Set version when we are certain 142 | if (protocol === 'h2') { 143 | connection.start(4) 144 | } else if (protocol === 'spdy/3.1') { 145 | connection.start(3.1) 146 | } else if (protocol === 'spdy/3') { 147 | connection.start(3) 148 | } else if (protocol === 'spdy/2') { 149 | connection.start(2) 150 | } else { 151 | socket.destroy() 152 | callback(new Error('Unexpected protocol: ' + protocol)) 153 | return 154 | } 155 | 156 | if (state.options['x-forwarded-for'] !== undefined) { 157 | connection.sendXForwardedFor(state.options['x-forwarded-for']) 158 | } 159 | 160 | callback(null, connection) 161 | }) 162 | } 163 | 164 | proto._createSocket = function _createSocket (req, options, callback) { 165 | var state = this._spdyState 166 | if (state.fallback) { return state.createSocket(req, options) } 167 | 168 | var handle = spdy.handle.create(null, null, state.socket) 169 | 170 | var socketOptions = { 171 | handle: handle, 172 | allowHalfOpen: true 173 | } 174 | 175 | var socket 176 | if (state.secure) { 177 | socket = new spdy.Socket(state.socket, socketOptions) 178 | } else { 179 | socket = new net.Socket(socketOptions) 180 | } 181 | 182 | handle.assignSocket(socket) 183 | handle.assignClientRequest(req) 184 | 185 | // Create stream only once `req.end()` is called 186 | var self = this 187 | handle.once('needStream', function () { 188 | if (state.connection === null) { 189 | self.once('_connect', function () { 190 | handle.setStream(self._createStream(req, handle)) 191 | }) 192 | } else { 193 | handle.setStream(self._createStream(req, handle)) 194 | } 195 | }) 196 | 197 | // Yes, it is in reverse 198 | req.on('response', function (res) { 199 | handle.assignRequest(res) 200 | }) 201 | handle.assignResponse(req) 202 | 203 | // Handle PUSH 204 | req.addListener('newListener', spdy.request.onNewListener) 205 | 206 | // For v0.8 207 | socket.readable = true 208 | socket.writable = true 209 | 210 | if (callback) { 211 | return callback(null, socket) 212 | } 213 | 214 | return socket 215 | } 216 | 217 | if (mode === 'modern' || mode === 'normal') { 218 | proto.createSocket = proto._createSocket 219 | } else { 220 | proto.createSocket = function createSocket (name, host, port, addr, req) { 221 | var state = this._spdyState 222 | if (state.fallback) { 223 | return state.createSocket(name, host, port, addr, req) 224 | } 225 | 226 | return this._createSocket(req, { 227 | host: host, 228 | port: port 229 | }) 230 | } 231 | } 232 | 233 | proto._createStream = function _createStream (req, handle) { 234 | var state = this._spdyState 235 | 236 | var self = this 237 | return state.connection.reserveStream({ 238 | method: req.method, 239 | path: req.path, 240 | headers: req._headers, 241 | host: state.host 242 | }, function (err, stream) { 243 | if (err) { 244 | return self.emit('error', err) 245 | } 246 | 247 | stream.on('response', function (status, headers) { 248 | handle.emitResponse(status, headers) 249 | }) 250 | }) 251 | } 252 | 253 | // Public APIs 254 | 255 | proto.close = function close (callback) { 256 | var state = this._spdyState 257 | 258 | if (state.connection === null) { 259 | this.once('_connect', function () { 260 | this.close(callback) 261 | }) 262 | return 263 | } 264 | 265 | state.connection.end(callback) 266 | } 267 | 268 | exports.Agent = instantiate(https.Agent) 269 | exports.PlainAgent = instantiate(http.Agent) 270 | 271 | exports.create = function create (base, options) { 272 | if (typeof base === 'object') { 273 | options = base 274 | base = null 275 | } 276 | 277 | if (base) { 278 | return instantiate(base).create(options) 279 | } 280 | 281 | if (options.spdy && options.spdy.plain) { 282 | return exports.PlainAgent.create(options) 283 | } else { return exports.Agent.create(options) } 284 | } 285 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SPDY Server for node.js 2 | 3 | [![Build Status](https://travis-ci.org/spdy-http2/node-spdy.svg?branch=master)](http://travis-ci.org/spdy-http2/node-spdy) 4 | [![NPM version](https://badge.fury.io/js/node-spdy.svg)](http://badge.fury.io/js/node-spdy) 5 | [![dependencies Status](https://david-dm.org/spdy-http2/node-spdy/status.svg?style=flat-square)](https://david-dm.org/spdy-http2/node-spdy) 6 | [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square)](http://standardjs.com/) 7 | [![Waffle](https://img.shields.io/badge/track-waffle-blue.svg?style=flat-square)](https://waffle.io/spdy-http2/node-spdy) 8 | 9 | With this module you can create [HTTP2][0] / [SPDY][1] servers 10 | in node.js with natural http module interface and fallback to regular https 11 | (for browsers that don't support neither HTTP2, nor SPDY yet). 12 | 13 | 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. 14 | 15 | ## Usage 16 | 17 | ### Examples 18 | 19 | Server: 20 | ```javascript 21 | var spdy = require('spdy'), 22 | fs = require('fs'); 23 | 24 | var options = { 25 | // Private key 26 | key: fs.readFileSync(__dirname + '/keys/spdy-key.pem'), 27 | 28 | // Fullchain file or cert file (prefer the former) 29 | cert: fs.readFileSync(__dirname + '/keys/spdy-fullchain.pem'), 30 | 31 | // **optional** SPDY-specific options 32 | spdy: { 33 | protocols: [ 'h2', 'spdy/3.1', ..., 'http/1.1' ], 34 | plain: false, 35 | 36 | // **optional** 37 | // Parse first incoming X_FORWARDED_FOR frame and put it to the 38 | // headers of every request. 39 | // NOTE: Use with care! This should not be used without some proxy that 40 | // will *always* send X_FORWARDED_FOR 41 | 'x-forwarded-for': true, 42 | 43 | connection: { 44 | windowSize: 1024 * 1024, // Server's window size 45 | 46 | // **optional** if true - server will send 3.1 frames on 3.0 *plain* spdy 47 | autoSpdy31: false 48 | } 49 | } 50 | }; 51 | 52 | var server = spdy.createServer(options, function(req, res) { 53 | res.writeHead(200); 54 | res.end('hello world!'); 55 | }); 56 | 57 | server.listen(3000); 58 | ``` 59 | 60 | Client: 61 | ```javascript 62 | var spdy = require('spdy'); 63 | var https = require('https'); 64 | 65 | var agent = spdy.createAgent({ 66 | host: 'www.google.com', 67 | port: 443, 68 | 69 | // Optional SPDY options 70 | spdy: { 71 | plain: false, 72 | ssl: true, 73 | 74 | // **optional** send X_FORWARDED_FOR 75 | 'x-forwarded-for': '127.0.0.1' 76 | } 77 | }); 78 | 79 | https.get({ 80 | host: 'www.google.com', 81 | agent: agent 82 | }, function(response) { 83 | console.log('yikes'); 84 | // Here it goes like with any other node.js HTTP request 85 | // ... 86 | // And once we're done - we may close TCP connection to server 87 | // NOTE: All non-closed requests will die! 88 | agent.close(); 89 | }).end(); 90 | ``` 91 | 92 | Please note that if you use a custom agent, by default all connection-level 93 | errors will result in an uncaught exception. To handle these errors subscribe 94 | to the `error` event and re-emit the captured error: 95 | 96 | ```javascript 97 | var agent = spdy.createAgent({ 98 | host: 'www.google.com', 99 | port: 443 100 | }).once('error', function (err) { 101 | this.emit(err); 102 | }); 103 | ``` 104 | 105 | #### Push streams 106 | 107 | It is possible to initiate [PUSH_PROMISE][5] to send content to clients _before_ 108 | the client requests it. 109 | 110 | ```javascript 111 | spdy.createServer(options, function(req, res) { 112 | var stream = res.push('/main.js', { 113 | status: 200, // optional 114 | method: 'GET', // optional 115 | request: { 116 | accept: '*/*' 117 | }, 118 | response: { 119 | 'content-type': 'application/javascript' 120 | } 121 | }); 122 | stream.on('error', function() { 123 | }); 124 | stream.end('alert("hello from push stream!");'); 125 | 126 | res.end(''); 127 | }).listen(3000); 128 | ``` 129 | 130 | [PUSH_PROMISE][5] may be sent using the `push()` method on the current response 131 | object. The signature of the `push()` method is: 132 | 133 | `.push('/some/relative/url', { request: {...}, response: {...} }, callback)` 134 | 135 | Second argument contains headers for both PUSH_PROMISE and emulated response. 136 | `callback` will receive two arguments: `err` (if any error is happened) and a 137 | [Duplex][4] stream as the second argument. 138 | 139 | Client usage: 140 | ```javascript 141 | var agent = spdy.createAgent({ /* ... */ }); 142 | var req = http.get({ 143 | host: 'www.google.com', 144 | agent: agent 145 | }, function(response) { 146 | }); 147 | req.on('push', function(stream) { 148 | stream.on('error', function(err) { 149 | // Handle error 150 | }); 151 | // Read data from stream 152 | }); 153 | ``` 154 | 155 | NOTE: You're responsible for the `stream` object once given it in `.push()` 156 | callback or `push` event. Hence ignoring `error` event on it will result in 157 | uncaught exception and crash your program. 158 | 159 | #### Trailing headers 160 | 161 | Server usage: 162 | ```javascript 163 | function (req, res) { 164 | // Send trailing headers to client 165 | res.addTrailers({ header1: 'value1', header2: 'value2' }); 166 | 167 | // On client's trailing headers 168 | req.on('trailers', function(headers) { 169 | // ... 170 | }); 171 | } 172 | ``` 173 | 174 | Client usage: 175 | ```javascript 176 | var req = http.request({ agent: spdyAgent, /* ... */ }).function (res) { 177 | // On server's trailing headers 178 | res.on('trailers', function(headers) { 179 | // ... 180 | }); 181 | }); 182 | req.write('stuff'); 183 | req.addTrailers({ /* ... */ }); 184 | req.end(); 185 | ``` 186 | 187 | #### Options 188 | 189 | All options supported by [tls][2] work with node-spdy. 190 | 191 | Additional options may be passed via `spdy` sub-object: 192 | 193 | * `plain` - if defined, server will ignore NPN and ALPN data and choose whether 194 | to use spdy or plain http by looking at first data packet. 195 | * `ssl` - if `false` and `options.plain` is `true`, `http.Server` will be used 196 | as a `base` class for created server. 197 | * `maxChunk` - if set and non-falsy, limits number of bytes sent in one DATA 198 | chunk. Setting it to non-zero value is recommended if you care about 199 | interleaving of outgoing data from multiple different streams. 200 | (defaults to 8192) 201 | * `protocols` - list of NPN/ALPN protocols to use (default is: 202 | `['h2','spdy/3.1', 'spdy/3', 'spdy/2','http/1.1', 'http/1.0']`) 203 | * `protocol` - use specific protocol if no NPN/ALPN ex In addition, 204 | * `maxStreams` - set "[maximum concurrent streams][3]" protocol option 205 | 206 | ### API 207 | 208 | API is compatible with `http` and `https` module, but you can use another 209 | function as base class for SPDYServer. 210 | 211 | ```javascript 212 | spdy.createServer( 213 | [base class constructor, i.e. https.Server], 214 | { /* keys and options */ }, // <- the only one required argument 215 | [request listener] 216 | ).listen([port], [host], [callback]); 217 | ``` 218 | 219 | Request listener will receive two arguments: `request` and `response`. They're 220 | both instances of `http`'s `IncomingMessage` and `OutgoingMessage`. But three 221 | custom properties are added to both of them: `isSpdy`, `spdyVersion`. `isSpdy` 222 | is `true` when the request was processed using HTTP2/SPDY protocols, it is 223 | `false` in case of HTTP/1.1 fallback. `spdyVersion` is either of: `2`, `3`, 224 | `3.1`, or `4` (for HTTP2). 225 | 226 | 227 | #### Contributors 228 | 229 | * [Fedor Indutny](https://github.com/indutny) 230 | * [Chris Strom](https://github.com/eee-c) 231 | * [François de Metz](https://github.com/francois2metz) 232 | * [Ilya Grigorik](https://github.com/igrigorik) 233 | * [Roberto Peon](https://github.com/grmocg) 234 | * [Tatsuhiro Tsujikawa](https://github.com/tatsuhiro-t) 235 | * [Jesse Cravens](https://github.com/jessecravens) 236 | 237 | #### LICENSE 238 | 239 | This software is licensed under the MIT License. 240 | 241 | Copyright Fedor Indutny, 2015. 242 | 243 | Permission is hereby granted, free of charge, to any person obtaining a 244 | copy of this software and associated documentation files (the 245 | "Software"), to deal in the Software without restriction, including 246 | without limitation the rights to use, copy, modify, merge, publish, 247 | distribute, sublicense, and/or sell copies of the Software, and to permit 248 | persons to whom the Software is furnished to do so, subject to the 249 | following conditions: 250 | 251 | The above copyright notice and this permission notice shall be included 252 | in all copies or substantial portions of the Software. 253 | 254 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 255 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 256 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 257 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 258 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 259 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 260 | USE OR OTHER DEALINGS IN THE SOFTWARE. 261 | 262 | [0]: https://http2.github.io/ 263 | [1]: http://www.chromium.org/spdy 264 | [2]: http://nodejs.org/docs/latest/api/tls.html#tls.createServer 265 | [3]: https://httpwg.github.io/specs/rfc7540.html#SETTINGS_MAX_CONCURRENT_STREAMS 266 | [4]: https://iojs.org/api/stream.html#stream_class_stream_duplex 267 | [5]: https://httpwg.github.io/specs/rfc7540.html#PUSH_PROMISE 268 | -------------------------------------------------------------------------------- /test/server-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | var tls = require('tls') 5 | var net = require('net') 6 | var https = require('https') 7 | var transport = require('spdy-transport') 8 | var util = require('util') 9 | 10 | var fixtures = require('./fixtures') 11 | var spdy = require('../') 12 | 13 | describe('SPDY Server', function () { 14 | fixtures.everyConfig(function (protocol, npn, version, plain) { 15 | var server 16 | var client 17 | 18 | beforeEach(function (done) { 19 | server = spdy.createServer(util._extend({ 20 | spdy: { 21 | 'x-forwarded-for': true, 22 | plain: plain 23 | } 24 | }, fixtures.keys)) 25 | 26 | server.listen(fixtures.port, function () { 27 | var socket = (plain ? net : tls).connect({ 28 | rejectUnauthorized: false, 29 | port: fixtures.port, 30 | NPNProtocols: [ npn ] 31 | }, function () { 32 | client = transport.connection.create(socket, { 33 | protocol: protocol, 34 | isServer: false 35 | }) 36 | client.start(version) 37 | done() 38 | }) 39 | }) 40 | }) 41 | 42 | afterEach(function (done) { 43 | client.socket.destroy() 44 | server.close(done) 45 | }) 46 | 47 | it('should process GET request', function (done) { 48 | var stream = client.request({ 49 | method: 'GET', 50 | path: '/get', 51 | headers: { 52 | a: 'b' 53 | } 54 | }, function (err) { 55 | assert(!err) 56 | 57 | stream.on('response', function (status, headers) { 58 | assert.equal(status, 200) 59 | assert.equal(headers.ok, 'yes') 60 | 61 | fixtures.expectData(stream, 'response', done) 62 | }) 63 | 64 | stream.end() 65 | }) 66 | 67 | server.on('request', function (req, res) { 68 | assert.equal(req.isSpdy, res.isSpdy) 69 | assert.equal(req.spdyVersion, res.spdyVersion) 70 | assert(req.isSpdy) 71 | if (!plain) { 72 | assert(req.socket.encrypted) 73 | assert(req.socket.getPeerCertificate()) 74 | } 75 | 76 | // Auto-detection 77 | if (version === 3.1) { 78 | assert(req.spdyVersion >= 3 && req.spdyVersion <= 3.1) 79 | } else { 80 | assert.equal(req.spdyVersion, version) 81 | } 82 | assert(req.spdyStream) 83 | assert(res.spdyStream) 84 | 85 | assert.equal(req.method, 'GET') 86 | assert.equal(req.url, '/get') 87 | assert.deepEqual(req.headers, { a: 'b', host: 'localhost' }) 88 | 89 | req.on('end', function () { 90 | res.writeHead(200, { 91 | ok: 'yes' 92 | }) 93 | res.end('response') 94 | assert(res.finished, 'res.finished should be set') 95 | }) 96 | req.resume() 97 | }) 98 | }) 99 | 100 | it('should process POST request', function (done) { 101 | var stream = client.request({ 102 | method: 'POST', 103 | path: '/post' 104 | }, function (err) { 105 | assert(!err) 106 | 107 | stream.on('response', function (status, headers) { 108 | assert.equal(status, 200) 109 | assert.equal(headers.ok, 'yes') 110 | 111 | fixtures.expectData(stream, 'response', next) 112 | }) 113 | 114 | stream.end('request') 115 | }) 116 | 117 | server.on('request', function (req, res) { 118 | assert.equal(req.method, 'POST') 119 | assert.equal(req.url, '/post') 120 | 121 | res.writeHead(200, { 122 | ok: 'yes' 123 | }) 124 | res.end('response') 125 | 126 | fixtures.expectData(req, 'request', next) 127 | }) 128 | 129 | var waiting = 2 130 | function next () { 131 | if (--waiting === 0) { 132 | return done() 133 | } 134 | } 135 | }) 136 | 137 | it('should process expect-continue request', function (done) { 138 | var stream = client.request({ 139 | method: 'GET', 140 | path: '/get', 141 | headers: { 142 | Expect: '100-continue' 143 | } 144 | }, function (err) { 145 | assert(!err) 146 | 147 | stream.on('response', function (status, headers) { 148 | assert.equal(status, 100) 149 | 150 | fixtures.expectData(stream, 'response', done) 151 | }) 152 | 153 | stream.end() 154 | }) 155 | 156 | server.on('request', function (req, res) { 157 | req.on('end', function () { 158 | res.end('response') 159 | }) 160 | req.resume() 161 | }) 162 | }) 163 | 164 | it('should emit `checkContinue` request', function (done) { 165 | var stream = client.request({ 166 | method: 'GET', 167 | path: '/get', 168 | headers: { 169 | Expect: '100-continue' 170 | } 171 | }, function (err) { 172 | assert(!err) 173 | 174 | stream.on('response', function (status, headers) { 175 | assert.equal(status, 100) 176 | 177 | fixtures.expectData(stream, 'response', done) 178 | }) 179 | 180 | stream.end() 181 | }) 182 | 183 | server.on('checkContinue', function (req, res) { 184 | req.on('end', function () { 185 | res.writeContinue() 186 | res.end('response') 187 | }) 188 | req.resume() 189 | }) 190 | }) 191 | 192 | it('should send PUSH_PROMISE', function (done) { 193 | var stream = client.request({ 194 | method: 'POST', 195 | path: '/page' 196 | }, function (err) { 197 | assert(!err) 198 | 199 | stream.on('pushPromise', function (push) { 200 | assert.equal(push.path, '/push') 201 | assert.equal(push.headers.yes, 'push') 202 | 203 | fixtures.expectData(push, 'push', next) 204 | fixtures.expectData(stream, 'response', next) 205 | }) 206 | 207 | stream.end('request') 208 | }) 209 | 210 | server.on('request', function (req, res) { 211 | assert.equal(req.method, 'POST') 212 | assert.equal(req.url, '/page') 213 | 214 | res.writeHead(200, { 215 | ok: 'yes' 216 | }) 217 | 218 | var push = res.push('/push', { 219 | request: { 220 | yes: 'push' 221 | } 222 | }) 223 | push.end('push') 224 | 225 | res.end('response') 226 | 227 | fixtures.expectData(req, 'request', next) 228 | }) 229 | 230 | var waiting = 3 231 | function next () { 232 | if (--waiting === 0) { 233 | return done() 234 | } 235 | } 236 | }) 237 | 238 | it('should receive trailing headers', function (done) { 239 | var stream = client.request({ 240 | method: 'POST', 241 | path: '/post' 242 | }, function (err) { 243 | assert(!err) 244 | 245 | stream.sendHeaders({ trai: 'ler' }) 246 | stream.end() 247 | 248 | stream.on('response', function (status, headers) { 249 | assert.equal(status, 200) 250 | assert.equal(headers.ok, 'yes') 251 | 252 | fixtures.expectData(stream, 'response', done) 253 | }) 254 | }) 255 | 256 | server.on('request', function (req, res) { 257 | var gotHeaders = false 258 | req.on('trailers', function (headers) { 259 | gotHeaders = true 260 | assert.equal(headers.trai, 'ler') 261 | }) 262 | 263 | req.on('end', function () { 264 | assert(gotHeaders) 265 | 266 | res.writeHead(200, { 267 | ok: 'yes' 268 | }) 269 | res.end('response') 270 | }) 271 | req.resume() 272 | }) 273 | }) 274 | 275 | it('should call .writeHead() automatically', function (done) { 276 | var stream = client.request({ 277 | method: 'POST', 278 | path: '/post' 279 | }, function (err) { 280 | assert(!err) 281 | 282 | stream.on('response', function (status, headers) { 283 | assert.equal(status, 300) 284 | 285 | fixtures.expectData(stream, 'response', done) 286 | }) 287 | stream.end() 288 | }) 289 | 290 | server.on('request', function (req, res) { 291 | req.on('end', function () { 292 | res.statusCode = 300 293 | res.end('response') 294 | }) 295 | req.resume() 296 | }) 297 | }) 298 | 299 | it('should not crash on .writeHead() after socket close', function (done) { 300 | var stream = client.request({ 301 | method: 'POST', 302 | path: '/post' 303 | }, function (err) { 304 | assert(!err) 305 | 306 | setTimeout(function () { 307 | client.socket.destroy() 308 | }, 50) 309 | stream.on('error', function () {}) 310 | stream.end() 311 | }) 312 | 313 | server.on('request', function (req, res) { 314 | req.connection.on('close', function () { 315 | assert.doesNotThrow(function () { 316 | res.writeHead(200) 317 | res.end('response') 318 | }) 319 | done() 320 | }) 321 | }) 322 | }) 323 | 324 | it('should not crash on .push() after socket close', function (done) { 325 | var stream = client.request({ 326 | method: 'POST', 327 | path: '/post' 328 | }, function (err) { 329 | assert(!err) 330 | 331 | setTimeout(function () { 332 | client.socket.destroy() 333 | }, 50) 334 | stream.on('error', function () {}) 335 | stream.end() 336 | }) 337 | 338 | server.on('request', function (req, res) { 339 | req.connection.on('close', function () { 340 | assert.doesNotThrow(function () { 341 | assert.equal(res.push('/push', {}), undefined) 342 | res.end('response') 343 | }) 344 | done() 345 | }) 346 | }) 347 | }) 348 | 349 | it('should end response after writing everything down', function (done) { 350 | var stream = client.request({ 351 | method: 'GET', 352 | path: '/post' 353 | }, function (err) { 354 | assert(!err) 355 | 356 | stream.on('response', function (status, headers) { 357 | assert.equal(status, 200) 358 | 359 | fixtures.expectData(stream, 'hello world, what\'s up?', done) 360 | }) 361 | 362 | stream.end() 363 | }) 364 | 365 | server.on('request', function (req, res) { 366 | req.resume() 367 | res.writeHead(200) 368 | res.write('hello ') 369 | res.write('world') 370 | res.write(', what\'s') 371 | res.write(' up?') 372 | res.end() 373 | }) 374 | }) 375 | 376 | it('should handle x-forwarded-for', function (done) { 377 | client.sendXForwardedFor('1.2.3.4') 378 | 379 | var stream = client.request({ 380 | method: 'GET', 381 | path: '/post' 382 | }, function (err) { 383 | assert(!err) 384 | 385 | stream.resume() 386 | stream.on('end', done) 387 | stream.end() 388 | }) 389 | 390 | server.on('request', function (req, res) { 391 | assert.equal(req.headers['x-forwarded-for'], '1.2.3.4') 392 | req.resume() 393 | res.end() 394 | }) 395 | }) 396 | 397 | it('should destroy request after end', function (done) { 398 | var stream = client.request({ 399 | method: 'POST', 400 | path: '/post' 401 | }, function (err) { 402 | assert(!err) 403 | }) 404 | stream.end() 405 | stream.on('error', function () {}) 406 | 407 | server.on('request', function (req, res) { 408 | res.end() 409 | res.destroy() 410 | res.socket.on('close', function () { 411 | done() 412 | }) 413 | }) 414 | }) 415 | }) 416 | 417 | it('should respond to http/1.1', function (done) { 418 | var server = spdy.createServer(fixtures.keys, function (req, res) { 419 | assert.equal(req.isSpdy, res.isSpdy) 420 | assert.equal(req.spdyVersion, res.spdyVersion) 421 | assert(!req.isSpdy) 422 | assert.equal(req.spdyVersion, 1) 423 | 424 | res.writeHead(200) 425 | res.end() 426 | }) 427 | 428 | server.listen(fixtures.port, function () { 429 | var req = https.request({ 430 | agent: false, 431 | rejectUnauthorized: false, 432 | NPNProtocols: [ 'http/1.1' ], 433 | port: fixtures.port, 434 | method: 'GET', 435 | path: '/' 436 | }, function (res) { 437 | assert.equal(res.statusCode, 200) 438 | res.resume() 439 | res.on('end', function () { 440 | server.close(done) 441 | }) 442 | }) 443 | 444 | req.end() 445 | }) 446 | }) 447 | 448 | it('should support custom base', function (done) { 449 | function Pseuver (options, listener) { 450 | https.Server.call(this, options, listener) 451 | } 452 | util.inherits(Pseuver, https.Server) 453 | 454 | var server = spdy.createServer(Pseuver, fixtures.keys, function (req, res) { 455 | assert.equal(req.isSpdy, res.isSpdy) 456 | assert.equal(req.spdyVersion, res.spdyVersion) 457 | assert(!req.isSpdy) 458 | assert.equal(req.spdyVersion, 1) 459 | 460 | res.writeHead(200) 461 | res.end() 462 | }) 463 | 464 | server.listen(fixtures.port, function () { 465 | var req = https.request({ 466 | agent: false, 467 | rejectUnauthorized: false, 468 | NPNProtocols: [ 'http/1.1' ], 469 | port: fixtures.port, 470 | method: 'GET', 471 | path: '/' 472 | }, function (res) { 473 | assert.equal(res.statusCode, 200) 474 | res.resume() 475 | res.on('end', function () { 476 | server.close(done) 477 | }) 478 | }) 479 | 480 | req.end() 481 | }) 482 | }) 483 | }) 484 | --------------------------------------------------------------------------------