├── .gitignore ├── .travis.yml ├── README.md ├── lib ├── spdy-transport.js └── spdy-transport │ ├── connection.js │ ├── priority.js │ ├── protocol │ ├── base │ │ ├── constants.js │ │ ├── framer.js │ │ ├── index.js │ │ ├── parser.js │ │ ├── scheduler.js │ │ └── utils.js │ ├── http2 │ │ ├── constants.js │ │ ├── framer.js │ │ ├── hpack-pool.js │ │ ├── index.js │ │ └── parser.js │ └── spdy │ │ ├── constants.js │ │ ├── dictionary.js │ │ ├── framer.js │ │ ├── index.js │ │ ├── parser.js │ │ └── zlib-pool.js │ ├── stream.js │ ├── utils.js │ └── window.js ├── package-lock.json ├── package.json └── test ├── base ├── priority-test.js ├── scheduler-test.js └── utils-test.js ├── both ├── framer-test.js └── transport │ ├── connection-test.js │ ├── fixtures.js │ ├── push-test.js │ └── stream-test.js ├── http2 └── parser-test.js └── spdy ├── transport-test.js └── v3-parser-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vimrc 3 | **.log 4 | coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "6" 7 | - "8" 8 | - "10" 9 | - "stable" 10 | 11 | script: 12 | - npm run lint 13 | - npm test 14 | - npm run coverage 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spdy-transport 2 | 3 | [![Build Status](https://travis-ci.org/spdy-http2/spdy-transport.svg?branch=master)](http://travis-ci.org/spdy-http2/spdy-transport) 4 | [![NPM version](https://badge.fury.io/js/spdy-transport.svg)](http://badge.fury.io/js/spdy-transport) 5 | [![dependencies Status](https://david-dm.org/spdy-http2/spdy-transport/status.svg?style=flat-square)](https://david-dm.org/spdy-http2/spdy-transport) 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 | > SPDY/HTTP2 generic transport implementation. 10 | 11 | ## Usage 12 | 13 | ```javascript 14 | var transport = require('spdy-transport'); 15 | 16 | // NOTE: socket is some stream or net.Socket instance, may be an argument 17 | // of `net.createServer`'s connection handler. 18 | 19 | var server = transport.connection.create(socket, { 20 | protocol: 'http2', 21 | isServer: true 22 | }); 23 | 24 | server.on('stream', function(stream) { 25 | console.log(stream.method, stream.path, stream.headers); 26 | stream.respond(200, { 27 | header: 'value' 28 | }); 29 | 30 | stream.on('readable', function() { 31 | var chunk = stream.read(); 32 | if (!chunk) 33 | return; 34 | 35 | console.log(chunk); 36 | }); 37 | 38 | stream.on('end', function() { 39 | console.log('end'); 40 | }); 41 | 42 | // And other node.js Stream APIs 43 | // ... 44 | }); 45 | ``` 46 | 47 | ## LICENSE 48 | 49 | This software is licensed under the MIT License. 50 | 51 | Copyright Fedor Indutny, 2015. 52 | 53 | Permission is hereby granted, free of charge, to any person obtaining a 54 | copy of this software and associated documentation files (the 55 | "Software"), to deal in the Software without restriction, including 56 | without limitation the rights to use, copy, modify, merge, publish, 57 | distribute, sublicense, and/or sell copies of the Software, and to permit 58 | persons to whom the Software is furnished to do so, subject to the 59 | following conditions: 60 | 61 | The above copyright notice and this permission notice shall be included 62 | in all copies or substantial portions of the Software. 63 | 64 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 65 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 66 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 67 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 68 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 69 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 70 | USE OR OTHER DEALINGS IN THE SOFTWARE. 71 | 72 | [0]: http://json.org/ 73 | [1]: http://github.com/indutny/bud-backend 74 | [2]: https://github.com/nodejs/io.js 75 | [3]: https://github.com/libuv/libuv 76 | [4]: http://openssl.org/ 77 | -------------------------------------------------------------------------------- /lib/spdy-transport.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transport = exports 4 | 5 | // Exports utils 6 | transport.utils = require('./spdy-transport/utils') 7 | 8 | // Export parser&framer 9 | transport.protocol = {} 10 | transport.protocol.base = require('./spdy-transport/protocol/base') 11 | transport.protocol.spdy = require('./spdy-transport/protocol/spdy') 12 | transport.protocol.http2 = require('./spdy-transport/protocol/http2') 13 | 14 | // Window 15 | transport.Window = require('./spdy-transport/window') 16 | 17 | // Priority Tree 18 | transport.Priority = require('./spdy-transport/priority') 19 | 20 | // Export Connection and Stream 21 | transport.Stream = require('./spdy-transport/stream').Stream 22 | transport.Connection = require('./spdy-transport/connection').Connection 23 | 24 | // Just for `transport.connection.create()` 25 | transport.connection = transport.Connection 26 | -------------------------------------------------------------------------------- /lib/spdy-transport/priority.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transport = require('../spdy-transport') 4 | var utils = transport.utils 5 | 6 | var assert = require('assert') 7 | var debug = require('debug')('spdy:priority') 8 | 9 | function PriorityNode (tree, options) { 10 | this.tree = tree 11 | 12 | this.id = options.id 13 | this.parent = options.parent 14 | this.weight = options.weight 15 | 16 | // To be calculated in `addChild` 17 | this.priorityFrom = 0 18 | this.priorityTo = 1 19 | this.priority = 1 20 | 21 | this.children = { 22 | list: [], 23 | weight: 0 24 | } 25 | 26 | if (this.parent !== null) { 27 | this.parent.addChild(this) 28 | } 29 | } 30 | 31 | function compareChildren (a, b) { 32 | return a.weight === b.weight ? a.id - b.id : a.weight - b.weight 33 | } 34 | 35 | PriorityNode.prototype.toJSON = function toJSON () { 36 | return { 37 | parent: this.parent, 38 | weight: this.weight, 39 | exclusive: this.exclusive 40 | } 41 | } 42 | 43 | PriorityNode.prototype.getPriority = function getPriority () { 44 | return this.priority 45 | } 46 | 47 | PriorityNode.prototype.getPriorityRange = function getPriorityRange () { 48 | return { from: this.priorityFrom, to: this.priorityTo } 49 | } 50 | 51 | PriorityNode.prototype.addChild = function addChild (child) { 52 | child.parent = this 53 | utils.binaryInsert(this.children.list, child, compareChildren) 54 | this.children.weight += child.weight 55 | 56 | this._updatePriority(this.priorityFrom, this.priorityTo) 57 | } 58 | 59 | PriorityNode.prototype.remove = function remove () { 60 | assert(this.parent, 'Can\'t remove root node') 61 | 62 | this.parent.removeChild(this) 63 | this.tree._removeNode(this) 64 | 65 | // Move all children to the parent 66 | for (var i = 0; i < this.children.list.length; i++) { 67 | this.parent.addChild(this.children.list[i]) 68 | } 69 | } 70 | 71 | PriorityNode.prototype.removeChild = function removeChild (child) { 72 | this.children.weight -= child.weight 73 | var index = utils.binarySearch(this.children.list, child, compareChildren) 74 | if (index !== -1 && this.children.list.length >= index) { 75 | this.children.list.splice(index, 1) 76 | } 77 | } 78 | 79 | PriorityNode.prototype.removeChildren = function removeChildren () { 80 | var children = this.children.list 81 | this.children.list = [] 82 | this.children.weight = 0 83 | return children 84 | } 85 | 86 | PriorityNode.prototype._updatePriority = function _updatePriority (from, to) { 87 | this.priority = to - from 88 | this.priorityFrom = from 89 | this.priorityTo = to 90 | 91 | var weight = 0 92 | for (var i = 0; i < this.children.list.length; i++) { 93 | var node = this.children.list[i] 94 | var nextWeight = weight + node.weight 95 | 96 | node._updatePriority( 97 | from + this.priority * (weight / this.children.weight), 98 | from + this.priority * (nextWeight / this.children.weight) 99 | ) 100 | weight = nextWeight 101 | } 102 | } 103 | 104 | function PriorityTree (options) { 105 | this.map = {} 106 | this.list = [] 107 | this.defaultWeight = options.defaultWeight || 16 108 | 109 | this.count = 0 110 | this.maxCount = options.maxCount 111 | 112 | // Root 113 | this.root = this.add({ 114 | id: 0, 115 | parent: null, 116 | weight: 1 117 | }) 118 | } 119 | module.exports = PriorityTree 120 | 121 | PriorityTree.create = function create (options) { 122 | return new PriorityTree(options) 123 | } 124 | 125 | PriorityTree.prototype.add = function add (options) { 126 | if (options.id === options.parent) { 127 | return this.addDefault(options.id) 128 | } 129 | 130 | var parent = options.parent === null ? null : this.map[options.parent] 131 | if (parent === undefined) { 132 | return this.addDefault(options.id) 133 | } 134 | 135 | debug('add node=%d parent=%d weight=%d exclusive=%d', 136 | options.id, 137 | options.parent === null ? -1 : options.parent, 138 | options.weight || this.defaultWeight, 139 | options.exclusive ? 1 : 0) 140 | 141 | var children 142 | if (options.exclusive) { 143 | children = parent.removeChildren() 144 | } 145 | 146 | var node = new PriorityNode(this, { 147 | id: options.id, 148 | parent: parent, 149 | weight: options.weight || this.defaultWeight 150 | }) 151 | this.map[options.id] = node 152 | 153 | if (options.exclusive) { 154 | for (var i = 0; i < children.length; i++) { 155 | node.addChild(children[i]) 156 | } 157 | } 158 | 159 | this.count++ 160 | if (this.count > this.maxCount) { 161 | debug('hit maximum remove id=%d', this.list[0].id) 162 | this.list.shift().remove() 163 | } 164 | 165 | // Root node is not subject to removal 166 | if (node.parent !== null) { 167 | this.list.push(node) 168 | } 169 | 170 | return node 171 | } 172 | 173 | // Only for testing, should use `node`'s methods 174 | PriorityTree.prototype.get = function get (id) { 175 | return this.map[id] 176 | } 177 | 178 | PriorityTree.prototype.addDefault = function addDefault (id) { 179 | debug('creating default node') 180 | return this.add({ id: id, parent: 0, weight: this.defaultWeight }) 181 | } 182 | 183 | PriorityTree.prototype._removeNode = function _removeNode (node) { 184 | delete this.map[node.id] 185 | var index = utils.binarySearch(this.list, node, compareChildren) 186 | this.list.splice(index, 1) 187 | this.count-- 188 | } 189 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/base/constants.js: -------------------------------------------------------------------------------- 1 | exports.DEFAULT_METHOD = 'GET' 2 | exports.DEFAULT_HOST = 'localhost' 3 | exports.MAX_PRIORITY_STREAMS = 100 4 | exports.DEFAULT_MAX_CHUNK = 8 * 1024 5 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/base/framer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var util = require('util') 4 | 5 | var transport = require('../../../spdy-transport') 6 | var base = require('./') 7 | var Scheduler = base.Scheduler 8 | 9 | function Framer (options) { 10 | Scheduler.call(this) 11 | 12 | this.version = null 13 | this.compress = null 14 | this.window = options.window 15 | this.timeout = options.timeout 16 | 17 | // Wait for `enablePush` 18 | this.pushEnabled = null 19 | } 20 | util.inherits(Framer, Scheduler) 21 | module.exports = Framer 22 | 23 | Framer.prototype.setVersion = function setVersion (version) { 24 | this.version = version 25 | this.emit('version') 26 | } 27 | 28 | Framer.prototype.setCompression = function setCompresion (pair) { 29 | this.compress = new transport.utils.LockStream(pair.compress) 30 | } 31 | 32 | Framer.prototype.enablePush = function enablePush (enable) { 33 | this.pushEnabled = enable 34 | this.emit('_pushEnabled') 35 | } 36 | 37 | Framer.prototype._checkPush = function _checkPush (callback) { 38 | if (this.pushEnabled === null) { 39 | this.once('_pushEnabled', function () { 40 | this._checkPush(callback) 41 | }) 42 | return 43 | } 44 | 45 | var err = null 46 | if (!this.pushEnabled) { 47 | err = new Error('PUSH_PROMISE disabled by other side') 48 | } 49 | process.nextTick(function () { 50 | return callback(err) 51 | }) 52 | } 53 | 54 | Framer.prototype._resetTimeout = function _resetTimeout () { 55 | if (this.timeout) { 56 | this.timeout.reset() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/base/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.utils = require('./utils') 4 | exports.constants = require('./constants') 5 | exports.Scheduler = require('./scheduler') 6 | exports.Parser = require('./parser') 7 | exports.Framer = require('./framer') 8 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/base/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transport = require('../../../spdy-transport') 4 | 5 | var util = require('util') 6 | var utils = require('./').utils 7 | var OffsetBuffer = require('obuf') 8 | var Transform = require('readable-stream').Transform 9 | 10 | function Parser (options) { 11 | Transform.call(this, { 12 | readableObjectMode: true 13 | }) 14 | 15 | this.buffer = new OffsetBuffer() 16 | this.partial = false 17 | this.waiting = 0 18 | 19 | this.window = options.window 20 | 21 | this.version = null 22 | this.decompress = null 23 | this.dead = false 24 | } 25 | module.exports = Parser 26 | util.inherits(Parser, Transform) 27 | 28 | Parser.prototype.error = utils.error 29 | 30 | Parser.prototype.kill = function kill () { 31 | this.dead = true 32 | } 33 | 34 | Parser.prototype._transform = function transform (data, encoding, cb) { 35 | if (!this.dead) { this.buffer.push(data) } 36 | 37 | this._consume(cb) 38 | } 39 | 40 | Parser.prototype._consume = function _consume (cb) { 41 | var self = this 42 | 43 | function next (err, frame) { 44 | if (err) { 45 | return cb(err) 46 | } 47 | 48 | if (Array.isArray(frame)) { 49 | for (var i = 0; i < frame.length; i++) { 50 | self.push(frame[i]) 51 | } 52 | } else if (frame) { 53 | self.push(frame) 54 | } 55 | 56 | // Consume more packets 57 | if (!sync) { 58 | return self._consume(cb) 59 | } 60 | 61 | process.nextTick(function () { 62 | self._consume(cb) 63 | }) 64 | } 65 | 66 | if (this.dead) { 67 | return cb() 68 | } 69 | 70 | if (this.buffer.size < this.waiting) { 71 | // No data at all 72 | if (this.buffer.size === 0) { 73 | return cb() 74 | } 75 | 76 | // Partial DATA frame or something that we can process partially 77 | if (this.partial) { 78 | var partial = this.buffer.clone(this.buffer.size) 79 | this.buffer.skip(partial.size) 80 | this.waiting -= partial.size 81 | 82 | this.executePartial(partial, next) 83 | return 84 | } 85 | 86 | // We shall not do anything until we get all expected data 87 | return cb() 88 | } 89 | 90 | var sync = true 91 | 92 | var content = this.buffer.clone(this.waiting) 93 | this.buffer.skip(this.waiting) 94 | 95 | this.execute(content, next) 96 | sync = false 97 | } 98 | 99 | Parser.prototype.setVersion = function setVersion (version) { 100 | this.version = version 101 | this.emit('version', version) 102 | } 103 | 104 | Parser.prototype.setCompression = function setCompresion (pair) { 105 | this.decompress = new transport.utils.LockStream(pair.decompress) 106 | } 107 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/base/scheduler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transport = require('../../../spdy-transport') 4 | var utils = transport.utils 5 | 6 | var assert = require('assert') 7 | var util = require('util') 8 | var debug = require('debug')('spdy:scheduler') 9 | var Readable = require('readable-stream').Readable 10 | 11 | /* 12 | * We create following structure in `pending`: 13 | * [ [ id = 0 ], [ id = 1 ], [ id = 2 ], [ id = 0 ] ] 14 | * chunks chunks chunks chunks 15 | * chunks chunks 16 | * chunks 17 | * 18 | * Then on the `.tick()` pass we pick one chunks from each item and remove the 19 | * item if it is empty: 20 | * 21 | * [ [ id = 0 ], [ id = 2 ] ] 22 | * chunks chunks 23 | * chunks 24 | * 25 | * Writing out: chunks for 0, chunks for 1, chunks for 2, chunks for 0 26 | * 27 | * This way data is interleaved between the different streams. 28 | */ 29 | 30 | function Scheduler (options) { 31 | Readable.call(this) 32 | 33 | // Pretty big window by default 34 | this.window = 0.25 35 | 36 | if (options && options.window) { this.window = options.window } 37 | 38 | this.sync = [] 39 | this.list = [] 40 | this.count = 0 41 | this.pendingTick = false 42 | } 43 | util.inherits(Scheduler, Readable) 44 | module.exports = Scheduler 45 | 46 | // Just for testing, really 47 | Scheduler.create = function create (options) { 48 | return new Scheduler(options) 49 | } 50 | 51 | function insertCompare (a, b) { 52 | return a.priority === b.priority 53 | ? a.stream - b.stream 54 | : b.priority - a.priority 55 | } 56 | 57 | Scheduler.prototype.schedule = function schedule (data) { 58 | var priority = data.priority 59 | var stream = data.stream 60 | var chunks = data.chunks 61 | 62 | // Synchronous frames should not be interleaved 63 | if (priority === false) { 64 | debug('queue sync', chunks) 65 | this.sync.push(data) 66 | this.count += chunks.length 67 | 68 | this._read() 69 | return 70 | } 71 | 72 | debug('queue async priority=%d stream=%d', priority, stream, chunks) 73 | var item = new SchedulerItem(stream, priority) 74 | var index = utils.binaryLookup(this.list, item, insertCompare) 75 | 76 | // Push new item 77 | if (index >= this.list.length || insertCompare(this.list[index], item) !== 0) { 78 | this.list.splice(index, 0, item) 79 | } else { // Coalesce 80 | item = this.list[index] 81 | } 82 | 83 | item.push(data) 84 | 85 | this.count += chunks.length 86 | 87 | this._read() 88 | } 89 | 90 | Scheduler.prototype._read = function _read () { 91 | if (this.count === 0) { 92 | return 93 | } 94 | 95 | if (this.pendingTick) { 96 | return 97 | } 98 | this.pendingTick = true 99 | 100 | var self = this 101 | process.nextTick(function () { 102 | self.pendingTick = false 103 | self.tick() 104 | }) 105 | } 106 | 107 | Scheduler.prototype.tick = function tick () { 108 | // No luck for async frames 109 | if (!this.tickSync()) { return false } 110 | 111 | return this.tickAsync() 112 | } 113 | 114 | Scheduler.prototype.tickSync = function tickSync () { 115 | // Empty sync queue first 116 | var sync = this.sync 117 | var res = true 118 | this.sync = [] 119 | for (var i = 0; i < sync.length; i++) { 120 | var item = sync[i] 121 | debug('tick sync pending=%d', this.count, item.chunks) 122 | for (var j = 0; j < item.chunks.length; j++) { 123 | this.count-- 124 | // TODO: handle stream backoff properly 125 | try { 126 | res = this.push(item.chunks[j]) 127 | } catch (err) { 128 | this.emit('error', err) 129 | return false 130 | } 131 | } 132 | debug('after tick sync pending=%d', this.count) 133 | 134 | // TODO(indutny): figure out the way to invoke callback on actual write 135 | if (item.callback) { 136 | item.callback(null) 137 | } 138 | } 139 | return res 140 | } 141 | 142 | Scheduler.prototype.tickAsync = function tickAsync () { 143 | var res = true 144 | var list = this.list 145 | if (list.length === 0) { 146 | return res 147 | } 148 | 149 | var startPriority = list[0].priority 150 | for (var index = 0; list.length > 0; index++) { 151 | // Loop index 152 | index %= list.length 153 | if (startPriority - list[index].priority > this.window) { index = 0 } 154 | debug('tick async index=%d start=%d', index, startPriority) 155 | 156 | var current = list[index] 157 | var item = current.shift() 158 | 159 | if (current.isEmpty()) { 160 | list.splice(index, 1) 161 | if (index === 0 && list.length > 0) { 162 | startPriority = list[0].priority 163 | } 164 | index-- 165 | } 166 | 167 | debug('tick async pending=%d', this.count, item.chunks) 168 | for (var i = 0; i < item.chunks.length; i++) { 169 | this.count-- 170 | // TODO: handle stream backoff properly 171 | try { 172 | res = this.push(item.chunks[i]) 173 | } catch (err) { 174 | this.emit('error', err) 175 | return false 176 | } 177 | } 178 | debug('after tick pending=%d', this.count) 179 | 180 | // TODO(indutny): figure out the way to invoke callback on actual write 181 | if (item.callback) { 182 | item.callback(null) 183 | } 184 | if (!res) { break } 185 | } 186 | 187 | return res 188 | } 189 | 190 | Scheduler.prototype.dump = function dump () { 191 | this.tickSync() 192 | 193 | // Write everything out 194 | while (!this.tickAsync()) { 195 | // Intentional no-op 196 | } 197 | assert.strictEqual(this.count, 0) 198 | } 199 | 200 | function SchedulerItem (stream, priority) { 201 | this.stream = stream 202 | this.priority = priority 203 | this.queue = [] 204 | } 205 | 206 | SchedulerItem.prototype.push = function push (chunks) { 207 | this.queue.push(chunks) 208 | } 209 | 210 | SchedulerItem.prototype.shift = function shift () { 211 | return this.queue.shift() 212 | } 213 | 214 | SchedulerItem.prototype.isEmpty = function isEmpty () { 215 | return this.queue.length === 0 216 | } 217 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/base/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var utils = exports 4 | 5 | var util = require('util') 6 | 7 | function ProtocolError (code, message) { 8 | this.code = code 9 | this.message = message 10 | } 11 | util.inherits(ProtocolError, Error) 12 | utils.ProtocolError = ProtocolError 13 | 14 | utils.error = function error (code, message) { 15 | return new ProtocolError(code, message) 16 | } 17 | 18 | utils.reverse = function reverse (object) { 19 | var result = [] 20 | 21 | Object.keys(object).forEach(function (key) { 22 | result[object[key] | 0] = key 23 | }) 24 | 25 | return result 26 | } 27 | 28 | // weight [1, 36] <=> priority [0, 7] 29 | // This way weight=16 is preserved and has priority=3 30 | utils.weightToPriority = function weightToPriority (weight) { 31 | return ((Math.min(35, (weight - 1)) / 35) * 7) | 0 32 | } 33 | 34 | utils.priorityToWeight = function priorityToWeight (priority) { 35 | return (((priority / 7) * 35) | 0) + 1 36 | } 37 | 38 | // Copy-Paste from node 39 | exports.addHeaderLine = function addHeaderLine (field, value, dest) { 40 | field = field.toLowerCase() 41 | if (/^:/.test(field)) { 42 | dest[field] = value 43 | return 44 | } 45 | 46 | switch (field) { 47 | // Array headers: 48 | case 'set-cookie': 49 | if (dest[field] !== undefined) { 50 | dest[field].push(value) 51 | } else { 52 | dest[field] = [ value ] 53 | } 54 | break 55 | 56 | /* eslint-disable max-len */ 57 | // list is taken from: 58 | /* eslint-enable max-len */ 59 | case 'content-type': 60 | case 'content-length': 61 | case 'user-agent': 62 | case 'referer': 63 | case 'host': 64 | case 'authorization': 65 | case 'proxy-authorization': 66 | case 'if-modified-since': 67 | case 'if-unmodified-since': 68 | case 'from': 69 | case 'location': 70 | case 'max-forwards': 71 | // drop duplicates 72 | if (dest[field] === undefined) { 73 | dest[field] = value 74 | } 75 | break 76 | 77 | case 'cookie': 78 | // make semicolon-separated list 79 | if (dest[field] !== undefined) { 80 | dest[field] += '; ' + value 81 | } else { 82 | dest[field] = value 83 | } 84 | break 85 | 86 | default: 87 | // make comma-separated list 88 | if (dest[field] !== undefined) { 89 | dest[field] += ', ' + value 90 | } else { 91 | dest[field] = value 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/http2/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transport = require('../../../spdy-transport') 4 | var base = transport.protocol.base 5 | 6 | exports.PREFACE_SIZE = 24 7 | exports.PREFACE = 'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' 8 | exports.PREFACE_BUFFER = Buffer.from(exports.PREFACE) 9 | 10 | exports.PING_OPAQUE_SIZE = 8 11 | 12 | exports.FRAME_HEADER_SIZE = 9 13 | exports.INITIAL_MAX_FRAME_SIZE = 16384 14 | exports.ABSOLUTE_MAX_FRAME_SIZE = 16777215 15 | exports.HEADER_TABLE_SIZE = 4096 16 | exports.DEFAULT_MAX_HEADER_LIST_SIZE = 80 * 1024 // as in http_parser 17 | exports.MAX_INITIAL_WINDOW_SIZE = 2147483647 18 | 19 | exports.DEFAULT_WEIGHT = 16 20 | 21 | exports.MAX_CONCURRENT_STREAMS = Infinity 22 | 23 | exports.frameType = { 24 | DATA: 0, 25 | HEADERS: 1, 26 | PRIORITY: 2, 27 | RST_STREAM: 3, 28 | SETTINGS: 4, 29 | PUSH_PROMISE: 5, 30 | PING: 6, 31 | GOAWAY: 7, 32 | WINDOW_UPDATE: 8, 33 | CONTINUATION: 9, 34 | 35 | // Custom 36 | X_FORWARDED_FOR: 0xde 37 | } 38 | 39 | exports.flags = { 40 | ACK: 0x01, // SETTINGS-only 41 | END_STREAM: 0x01, 42 | END_HEADERS: 0x04, 43 | PADDED: 0x08, 44 | PRIORITY: 0x20 45 | } 46 | 47 | exports.settings = { 48 | SETTINGS_HEADER_TABLE_SIZE: 0x01, 49 | SETTINGS_ENABLE_PUSH: 0x02, 50 | SETTINGS_MAX_CONCURRENT_STREAMS: 0x03, 51 | SETTINGS_INITIAL_WINDOW_SIZE: 0x04, 52 | SETTINGS_MAX_FRAME_SIZE: 0x05, 53 | SETTINGS_MAX_HEADER_LIST_SIZE: 0x06 54 | } 55 | 56 | exports.settingsIndex = [ 57 | null, 58 | 'header_table_size', 59 | 'enable_push', 60 | 'max_concurrent_streams', 61 | 'initial_window_size', 62 | 'max_frame_size', 63 | 'max_header_list_size' 64 | ] 65 | 66 | exports.error = { 67 | OK: 0, 68 | NO_ERROR: 0, 69 | 70 | PROTOCOL_ERROR: 1, 71 | INTERNAL_ERROR: 2, 72 | FLOW_CONTROL_ERROR: 3, 73 | SETTINGS_TIMEOUT: 4, 74 | 75 | STREAM_CLOSED: 5, 76 | INVALID_STREAM: 5, 77 | 78 | FRAME_SIZE_ERROR: 6, 79 | REFUSED_STREAM: 7, 80 | CANCEL: 8, 81 | COMPRESSION_ERROR: 9, 82 | CONNECT_ERROR: 10, 83 | ENHANCE_YOUR_CALM: 11, 84 | INADEQUATE_SECURITY: 12, 85 | HTTP_1_1_REQUIRED: 13 86 | } 87 | exports.errorByCode = base.utils.reverse(exports.error) 88 | 89 | exports.DEFAULT_WINDOW = 64 * 1024 - 1 90 | 91 | exports.goaway = exports.error 92 | exports.goawayByCode = Object.assign({}, exports.errorByCode) 93 | exports.goawayByCode[0] = 'OK' 94 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/http2/framer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transport = require('../../../spdy-transport') 4 | var base = transport.protocol.base 5 | var constants = require('./').constants 6 | 7 | var assert = require('assert') 8 | var util = require('util') 9 | var WriteBuffer = require('wbuf') 10 | var OffsetBuffer = require('obuf') 11 | var debug = require('debug')('spdy:framer') 12 | var debugExtra = require('debug')('spdy:framer:extra') 13 | 14 | function Framer (options) { 15 | base.Framer.call(this, options) 16 | 17 | this.maxFrameSize = constants.INITIAL_MAX_FRAME_SIZE 18 | } 19 | util.inherits(Framer, base.Framer) 20 | module.exports = Framer 21 | 22 | Framer.create = function create (options) { 23 | return new Framer(options) 24 | } 25 | 26 | Framer.prototype.setMaxFrameSize = function setMaxFrameSize (size) { 27 | this.maxFrameSize = size 28 | } 29 | 30 | Framer.prototype._frame = function _frame (frame, body, callback) { 31 | debug('id=%d type=%s', frame.id, frame.type) 32 | 33 | var buffer = new WriteBuffer() 34 | 35 | buffer.reserve(constants.FRAME_HEADER_SIZE) 36 | var len = buffer.skip(3) 37 | buffer.writeUInt8(constants.frameType[frame.type]) 38 | buffer.writeUInt8(frame.flags) 39 | buffer.writeUInt32BE(frame.id & 0x7fffffff) 40 | 41 | body(buffer) 42 | 43 | var frameSize = buffer.size - constants.FRAME_HEADER_SIZE 44 | len.writeUInt24BE(frameSize) 45 | 46 | var chunks = buffer.render() 47 | var toWrite = { 48 | stream: frame.id, 49 | priority: frame.priority === undefined ? false : frame.priority, 50 | chunks: chunks, 51 | callback: callback 52 | } 53 | 54 | if (this.window && frame.type === 'DATA') { 55 | var self = this 56 | this._resetTimeout() 57 | this.window.send.update(-frameSize, function () { 58 | self._resetTimeout() 59 | self.schedule(toWrite) 60 | }) 61 | } else { 62 | this._resetTimeout() 63 | this.schedule(toWrite) 64 | } 65 | 66 | return chunks 67 | } 68 | 69 | Framer.prototype._split = function _split (frame) { 70 | var buf = new OffsetBuffer() 71 | for (var i = 0; i < frame.chunks.length; i++) { buf.push(frame.chunks[i]) } 72 | 73 | var frames = [] 74 | while (!buf.isEmpty()) { 75 | // First frame may have reserved bytes in it 76 | var size = this.maxFrameSize 77 | if (frames.length === 0) { 78 | size -= frame.reserve 79 | } 80 | size = Math.min(size, buf.size) 81 | 82 | var frameBuf = buf.clone(size) 83 | buf.skip(size) 84 | 85 | frames.push({ 86 | size: frameBuf.size, 87 | chunks: frameBuf.toChunks() 88 | }) 89 | } 90 | 91 | return frames 92 | } 93 | 94 | Framer.prototype._continuationFrame = function _continuationFrame (frame, 95 | body, 96 | callback) { 97 | var frames = this._split(frame) 98 | 99 | frames.forEach(function (subFrame, i) { 100 | var isFirst = i === 0 101 | var isLast = i === frames.length - 1 102 | 103 | var flags = isLast ? constants.flags.END_HEADERS : 0 104 | 105 | // PRIORITY and friends 106 | if (isFirst) { 107 | flags |= frame.flags 108 | } 109 | 110 | this._frame({ 111 | id: frame.id, 112 | priority: false, 113 | type: isFirst ? frame.type : 'CONTINUATION', 114 | flags: flags 115 | }, function (buf) { 116 | // Fill those reserved bytes 117 | if (isFirst && body) { body(buf) } 118 | 119 | buf.reserve(subFrame.size) 120 | for (var i = 0; i < subFrame.chunks.length; i++) { buf.copyFrom(subFrame.chunks[i]) } 121 | }, isLast ? callback : null) 122 | }, this) 123 | 124 | if (frames.length === 0) { 125 | this._frame({ 126 | id: frame.id, 127 | priority: false, 128 | type: frame.type, 129 | flags: frame.flags | constants.flags.END_HEADERS 130 | }, function (buf) { 131 | if (body) { body(buf) } 132 | }, callback) 133 | } 134 | } 135 | 136 | Framer.prototype._compressHeaders = function _compressHeaders (headers, 137 | pairs, 138 | callback) { 139 | Object.keys(headers || {}).forEach(function (name) { 140 | var lowName = name.toLowerCase() 141 | 142 | // Not allowed in HTTP2 143 | switch (lowName) { 144 | case 'host': 145 | case 'connection': 146 | case 'keep-alive': 147 | case 'proxy-connection': 148 | case 'transfer-encoding': 149 | case 'upgrade': 150 | return 151 | } 152 | 153 | // Should be in `pairs` 154 | if (/^:/.test(lowName)) { 155 | return 156 | } 157 | 158 | // Do not compress, or index Cookie field (for security reasons) 159 | var neverIndex = lowName === 'cookie' || lowName === 'set-cookie' 160 | 161 | var value = headers[name] 162 | if (Array.isArray(value)) { 163 | for (var i = 0; i < value.length; i++) { 164 | pairs.push({ 165 | name: lowName, 166 | value: value[i] + '', 167 | neverIndex: neverIndex, 168 | huffman: !neverIndex 169 | }) 170 | } 171 | } else { 172 | pairs.push({ 173 | name: lowName, 174 | value: value + '', 175 | neverIndex: neverIndex, 176 | huffman: !neverIndex 177 | }) 178 | } 179 | }) 180 | 181 | assert(this.compress !== null, 'Framer version not initialized') 182 | debugExtra('compressing headers=%j', pairs) 183 | this.compress.write([ pairs ], callback) 184 | } 185 | 186 | Framer.prototype._isDefaultPriority = function _isDefaultPriority (priority) { 187 | if (!priority) { return true } 188 | 189 | return !priority.parent && 190 | priority.weight === constants.DEFAULT && 191 | !priority.exclusive 192 | } 193 | 194 | Framer.prototype._defaultHeaders = function _defaultHeaders (frame, pairs) { 195 | if (!frame.path) { 196 | throw new Error('`path` is required frame argument') 197 | } 198 | 199 | pairs.push({ 200 | name: ':method', 201 | value: frame.method || base.constants.DEFAULT_METHOD 202 | }) 203 | pairs.push({ name: ':path', value: frame.path }) 204 | pairs.push({ name: ':scheme', value: frame.scheme || 'https' }) 205 | pairs.push({ 206 | name: ':authority', 207 | value: frame.host || 208 | (frame.headers && frame.headers.host) || 209 | base.constants.DEFAULT_HOST 210 | }) 211 | } 212 | 213 | Framer.prototype._headersFrame = function _headersFrame (kind, frame, callback) { 214 | var pairs = [] 215 | 216 | if (kind === 'request') { 217 | this._defaultHeaders(frame, pairs) 218 | } else if (kind === 'response') { 219 | pairs.push({ name: ':status', value: (frame.status || 200) + '' }) 220 | } 221 | 222 | var self = this 223 | this._compressHeaders(frame.headers, pairs, function (err, chunks) { 224 | if (err) { 225 | if (callback) { 226 | return callback(err) 227 | } else { 228 | return self.emit('error', err) 229 | } 230 | } 231 | 232 | var reserve = 0 233 | 234 | // If priority info is present, and the values are not default ones 235 | // reserve space for the priority info and add PRIORITY flag 236 | var priority = frame.priority 237 | if (!self._isDefaultPriority(priority)) { reserve = 5 } 238 | 239 | var flags = reserve === 0 ? 0 : constants.flags.PRIORITY 240 | 241 | // Mostly for testing 242 | if (frame.fin) { 243 | flags |= constants.flags.END_STREAM 244 | } 245 | 246 | self._continuationFrame({ 247 | id: frame.id, 248 | type: 'HEADERS', 249 | flags: flags, 250 | reserve: reserve, 251 | chunks: chunks 252 | }, function (buf) { 253 | if (reserve === 0) { 254 | return 255 | } 256 | 257 | buf.writeUInt32BE(((priority.exclusive ? 0x80000000 : 0) | 258 | priority.parent) >>> 0) 259 | buf.writeUInt8((priority.weight | 0) - 1) 260 | }, callback) 261 | }) 262 | } 263 | 264 | Framer.prototype.requestFrame = function requestFrame (frame, callback) { 265 | return this._headersFrame('request', frame, callback) 266 | } 267 | 268 | Framer.prototype.responseFrame = function responseFrame (frame, callback) { 269 | return this._headersFrame('response', frame, callback) 270 | } 271 | 272 | Framer.prototype.headersFrame = function headersFrame (frame, callback) { 273 | return this._headersFrame('headers', frame, callback) 274 | } 275 | 276 | Framer.prototype.pushFrame = function pushFrame (frame, callback) { 277 | var self = this 278 | 279 | function compress (headers, pairs, callback) { 280 | self._compressHeaders(headers, pairs, function (err, chunks) { 281 | if (err) { 282 | if (callback) { 283 | return callback(err) 284 | } else { 285 | return self.emit('error', err) 286 | } 287 | } 288 | 289 | callback(chunks) 290 | }) 291 | } 292 | 293 | function sendPromise (chunks) { 294 | self._continuationFrame({ 295 | id: frame.id, 296 | type: 'PUSH_PROMISE', 297 | reserve: 4, 298 | chunks: chunks 299 | }, function (buf) { 300 | buf.writeUInt32BE(frame.promisedId) 301 | }) 302 | } 303 | 304 | function sendResponse (chunks, callback) { 305 | var priority = frame.priority 306 | var isDefaultPriority = self._isDefaultPriority(priority) 307 | var flags = isDefaultPriority ? 0 : constants.flags.PRIORITY 308 | 309 | // Mostly for testing 310 | if (frame.fin) { 311 | flags |= constants.flags.END_STREAM 312 | } 313 | 314 | self._continuationFrame({ 315 | id: frame.promisedId, 316 | type: 'HEADERS', 317 | flags: flags, 318 | reserve: isDefaultPriority ? 0 : 5, 319 | chunks: chunks 320 | }, function (buf) { 321 | if (isDefaultPriority) { 322 | return 323 | } 324 | 325 | buf.writeUInt32BE((priority.exclusive ? 0x80000000 : 0) | 326 | priority.parent) 327 | buf.writeUInt8((priority.weight | 0) - 1) 328 | }, callback) 329 | } 330 | 331 | this._checkPush(function (err) { 332 | if (err) { 333 | return callback(err) 334 | } 335 | 336 | var pairs = { 337 | promise: [], 338 | response: [] 339 | } 340 | 341 | self._defaultHeaders(frame, pairs.promise) 342 | pairs.response.push({ name: ':status', value: (frame.status || 200) + '' }) 343 | 344 | compress(frame.headers, pairs.promise, function (promiseChunks) { 345 | sendPromise(promiseChunks) 346 | if (frame.response === false) { 347 | return callback(null) 348 | } 349 | compress(frame.response, pairs.response, function (responseChunks) { 350 | sendResponse(responseChunks, callback) 351 | }) 352 | }) 353 | }) 354 | } 355 | 356 | Framer.prototype.priorityFrame = function priorityFrame (frame, callback) { 357 | this._frame({ 358 | id: frame.id, 359 | priority: false, 360 | type: 'PRIORITY', 361 | flags: 0 362 | }, function (buf) { 363 | var priority = frame.priority 364 | buf.writeUInt32BE((priority.exclusive ? 0x80000000 : 0) | 365 | priority.parent) 366 | buf.writeUInt8((priority.weight | 0) - 1) 367 | }, callback) 368 | } 369 | 370 | Framer.prototype.dataFrame = function dataFrame (frame, callback) { 371 | var frames = this._split({ 372 | reserve: 0, 373 | chunks: [ frame.data ] 374 | }) 375 | 376 | var fin = frame.fin ? constants.flags.END_STREAM : 0 377 | 378 | var self = this 379 | frames.forEach(function (subFrame, i) { 380 | var isLast = i === frames.length - 1 381 | var flags = 0 382 | if (isLast) { 383 | flags |= fin 384 | } 385 | 386 | self._frame({ 387 | id: frame.id, 388 | priority: frame.priority, 389 | type: 'DATA', 390 | flags: flags 391 | }, function (buf) { 392 | buf.reserve(subFrame.size) 393 | for (var i = 0; i < subFrame.chunks.length; i++) { buf.copyFrom(subFrame.chunks[i]) } 394 | }, isLast ? callback : null) 395 | }) 396 | 397 | // Empty DATA 398 | if (frames.length === 0) { 399 | this._frame({ 400 | id: frame.id, 401 | priority: frame.priority, 402 | type: 'DATA', 403 | flags: fin 404 | }, function (buf) { 405 | // No-op 406 | }, callback) 407 | } 408 | } 409 | 410 | Framer.prototype.pingFrame = function pingFrame (frame, callback) { 411 | this._frame({ 412 | id: 0, 413 | type: 'PING', 414 | flags: frame.ack ? constants.flags.ACK : 0 415 | }, function (buf) { 416 | buf.copyFrom(frame.opaque) 417 | }, callback) 418 | } 419 | 420 | Framer.prototype.rstFrame = function rstFrame (frame, callback) { 421 | this._frame({ 422 | id: frame.id, 423 | type: 'RST_STREAM', 424 | flags: 0 425 | }, function (buf) { 426 | buf.writeUInt32BE(constants.error[frame.code]) 427 | }, callback) 428 | } 429 | 430 | Framer.prototype.prefaceFrame = function prefaceFrame (callback) { 431 | debug('preface') 432 | this._resetTimeout() 433 | this.schedule({ 434 | stream: 0, 435 | priority: false, 436 | chunks: [ constants.PREFACE_BUFFER ], 437 | callback: callback 438 | }) 439 | } 440 | 441 | Framer.prototype.settingsFrame = function settingsFrame (options, callback) { 442 | var key = JSON.stringify(options) 443 | 444 | var settings = Framer.settingsCache[key] 445 | if (settings) { 446 | debug('cached settings') 447 | this._resetTimeout() 448 | this.schedule({ 449 | id: 0, 450 | priority: false, 451 | chunks: settings, 452 | callback: callback 453 | }) 454 | return 455 | } 456 | 457 | var params = [] 458 | for (var i = 0; i < constants.settingsIndex.length; i++) { 459 | var name = constants.settingsIndex[i] 460 | if (!name) { 461 | continue 462 | } 463 | 464 | // value: Infinity 465 | if (!isFinite(options[name])) { 466 | continue 467 | } 468 | 469 | if (options[name] !== undefined) { 470 | params.push({ key: i, value: options[name] }) 471 | } 472 | } 473 | 474 | var bodySize = params.length * 6 475 | 476 | var chunks = this._frame({ 477 | id: 0, 478 | type: 'SETTINGS', 479 | flags: 0 480 | }, function (buffer) { 481 | buffer.reserve(bodySize) 482 | for (var i = 0; i < params.length; i++) { 483 | var param = params[i] 484 | 485 | buffer.writeUInt16BE(param.key) 486 | buffer.writeUInt32BE(param.value) 487 | } 488 | }, callback) 489 | 490 | Framer.settingsCache[key] = chunks 491 | } 492 | Framer.settingsCache = {} 493 | 494 | Framer.prototype.ackSettingsFrame = function ackSettingsFrame (callback) { 495 | /* var chunks = */ this._frame({ 496 | id: 0, 497 | type: 'SETTINGS', 498 | flags: constants.flags.ACK 499 | }, function (buffer) { 500 | // No-op 501 | }, callback) 502 | } 503 | 504 | Framer.prototype.windowUpdateFrame = function windowUpdateFrame (frame, 505 | callback) { 506 | this._frame({ 507 | id: frame.id, 508 | type: 'WINDOW_UPDATE', 509 | flags: 0 510 | }, function (buffer) { 511 | buffer.reserve(4) 512 | buffer.writeInt32BE(frame.delta) 513 | }, callback) 514 | } 515 | 516 | Framer.prototype.goawayFrame = function goawayFrame (frame, callback) { 517 | this._frame({ 518 | type: 'GOAWAY', 519 | id: 0, 520 | flags: 0 521 | }, function (buf) { 522 | buf.reserve(8) 523 | 524 | // Last-good-stream-ID 525 | buf.writeUInt32BE(frame.lastId & 0x7fffffff) 526 | // Code 527 | buf.writeUInt32BE(constants.goaway[frame.code]) 528 | 529 | // Extra debugging information 530 | if (frame.extra) { buf.write(frame.extra) } 531 | }, callback) 532 | } 533 | 534 | Framer.prototype.xForwardedFor = function xForwardedFor (frame, callback) { 535 | this._frame({ 536 | type: 'X_FORWARDED_FOR', 537 | id: 0, 538 | flags: 0 539 | }, function (buf) { 540 | buf.write(frame.host) 541 | }, callback) 542 | } 543 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/http2/hpack-pool.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var constants = require('./').constants 4 | 5 | var hpack = require('hpack.js') 6 | 7 | function Pool () { 8 | } 9 | module.exports = Pool 10 | 11 | Pool.create = function create () { 12 | return new Pool() 13 | } 14 | 15 | Pool.prototype.get = function get (version) { 16 | var options = { 17 | table: { 18 | maxSize: constants.HEADER_TABLE_SIZE 19 | } 20 | } 21 | 22 | var compress = hpack.compressor.create(options) 23 | var decompress = hpack.decompressor.create(options) 24 | 25 | return { 26 | version: version, 27 | 28 | compress: compress, 29 | decompress: decompress 30 | } 31 | } 32 | 33 | Pool.prototype.put = function put () { 34 | } 35 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/http2/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.name = 'h2' 4 | 5 | exports.constants = require('./constants') 6 | exports.parser = require('./parser') 7 | exports.framer = require('./framer') 8 | exports.compressionPool = require('./hpack-pool') 9 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/http2/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var parser = exports 4 | 5 | var transport = require('../../../spdy-transport') 6 | var base = transport.protocol.base 7 | var utils = base.utils 8 | var constants = require('./').constants 9 | 10 | var assert = require('assert') 11 | var util = require('util') 12 | 13 | function Parser (options) { 14 | base.Parser.call(this, options) 15 | 16 | this.isServer = options.isServer 17 | 18 | this.waiting = constants.PREFACE_SIZE 19 | this.state = 'preface' 20 | this.pendingHeader = null 21 | 22 | // Header Block queue 23 | this._lastHeaderBlock = null 24 | this.maxFrameSize = constants.INITIAL_MAX_FRAME_SIZE 25 | this.maxHeaderListSize = constants.DEFAULT_MAX_HEADER_LIST_SIZE 26 | } 27 | util.inherits(Parser, base.Parser) 28 | 29 | parser.create = function create (options) { 30 | return new Parser(options) 31 | } 32 | 33 | Parser.prototype.setMaxFrameSize = function setMaxFrameSize (size) { 34 | this.maxFrameSize = size 35 | } 36 | 37 | Parser.prototype.setMaxHeaderListSize = function setMaxHeaderListSize (size) { 38 | this.maxHeaderListSize = size 39 | } 40 | 41 | // Only for testing 42 | Parser.prototype.skipPreface = function skipPreface () { 43 | // Just some number bigger than 3.1, doesn't really matter for HTTP2 44 | this.setVersion(4) 45 | 46 | // Parse frame header! 47 | this.state = 'frame-head' 48 | this.waiting = constants.FRAME_HEADER_SIZE 49 | } 50 | 51 | Parser.prototype.execute = function execute (buffer, callback) { 52 | if (this.state === 'preface') { return this.onPreface(buffer, callback) } 53 | 54 | if (this.state === 'frame-head') { 55 | return this.onFrameHead(buffer, callback) 56 | } 57 | 58 | assert(this.state === 'frame-body' && this.pendingHeader !== null) 59 | 60 | var self = this 61 | var header = this.pendingHeader 62 | this.pendingHeader = null 63 | 64 | this.onFrameBody(header, buffer, function (err, frame) { 65 | if (err) { 66 | return callback(err) 67 | } 68 | 69 | self.state = 'frame-head' 70 | self.partial = false 71 | self.waiting = constants.FRAME_HEADER_SIZE 72 | callback(null, frame) 73 | }) 74 | } 75 | 76 | Parser.prototype.executePartial = function executePartial (buffer, callback) { 77 | var header = this.pendingHeader 78 | 79 | assert.strictEqual(header.flags & constants.flags.PADDED, 0) 80 | 81 | if (this.window) { this.window.recv.update(-buffer.size) } 82 | 83 | callback(null, { 84 | type: 'DATA', 85 | id: header.id, 86 | 87 | // Partial DATA can't be FIN 88 | fin: false, 89 | data: buffer.take(buffer.size) 90 | }) 91 | } 92 | 93 | Parser.prototype.onPreface = function onPreface (buffer, callback) { 94 | if (buffer.take(buffer.size).toString() !== constants.PREFACE) { 95 | return callback(this.error(constants.error.PROTOCOL_ERROR, 96 | 'Invalid preface')) 97 | } 98 | 99 | this.skipPreface() 100 | callback(null, null) 101 | } 102 | 103 | Parser.prototype.onFrameHead = function onFrameHead (buffer, callback) { 104 | var header = { 105 | length: buffer.readUInt24BE(), 106 | control: true, 107 | type: buffer.readUInt8(), 108 | flags: buffer.readUInt8(), 109 | id: buffer.readUInt32BE() & 0x7fffffff 110 | } 111 | 112 | if (header.length > this.maxFrameSize) { 113 | return callback(this.error(constants.error.FRAME_SIZE_ERROR, 114 | 'Frame length OOB')) 115 | } 116 | 117 | header.control = header.type !== constants.frameType.DATA 118 | 119 | this.state = 'frame-body' 120 | this.pendingHeader = header 121 | this.waiting = header.length 122 | this.partial = !header.control 123 | 124 | // TODO(indutny): eventually support partial padded DATA 125 | if (this.partial) { 126 | this.partial = (header.flags & constants.flags.PADDED) === 0 127 | } 128 | 129 | callback(null, null) 130 | } 131 | 132 | Parser.prototype.onFrameBody = function onFrameBody (header, buffer, callback) { 133 | var frameType = constants.frameType 134 | 135 | if (header.type === frameType.DATA) { 136 | this.onDataFrame(header, buffer, callback) 137 | } else if (header.type === frameType.HEADERS) { 138 | this.onHeadersFrame(header, buffer, callback) 139 | } else if (header.type === frameType.CONTINUATION) { 140 | this.onContinuationFrame(header, buffer, callback) 141 | } else if (header.type === frameType.WINDOW_UPDATE) { 142 | this.onWindowUpdateFrame(header, buffer, callback) 143 | } else if (header.type === frameType.RST_STREAM) { 144 | this.onRSTFrame(header, buffer, callback) 145 | } else if (header.type === frameType.SETTINGS) { 146 | this.onSettingsFrame(header, buffer, callback) 147 | } else if (header.type === frameType.PUSH_PROMISE) { 148 | this.onPushPromiseFrame(header, buffer, callback) 149 | } else if (header.type === frameType.PING) { 150 | this.onPingFrame(header, buffer, callback) 151 | } else if (header.type === frameType.GOAWAY) { 152 | this.onGoawayFrame(header, buffer, callback) 153 | } else if (header.type === frameType.PRIORITY) { 154 | this.onPriorityFrame(header, buffer, callback) 155 | } else if (header.type === frameType.X_FORWARDED_FOR) { 156 | this.onXForwardedFrame(header, buffer, callback) 157 | } else { 158 | this.onUnknownFrame(header, buffer, callback) 159 | } 160 | } 161 | 162 | Parser.prototype.onUnknownFrame = function onUnknownFrame (header, buffer, callback) { 163 | if (this._lastHeaderBlock !== null) { 164 | callback(this.error(constants.error.PROTOCOL_ERROR, 165 | 'Received unknown frame in the middle of a header block')) 166 | return 167 | } 168 | callback(null, { type: 'unknown: ' + header.type }) 169 | } 170 | 171 | Parser.prototype.unpadData = function unpadData (header, body, callback) { 172 | var isPadded = (header.flags & constants.flags.PADDED) !== 0 173 | 174 | if (!isPadded) { return callback(null, body) } 175 | 176 | if (!body.has(1)) { 177 | return callback(this.error(constants.error.FRAME_SIZE_ERROR, 178 | 'Not enough space for padding')) 179 | } 180 | 181 | var pad = body.readUInt8() 182 | if (!body.has(pad)) { 183 | return callback(this.error(constants.error.PROTOCOL_ERROR, 184 | 'Invalid padding size')) 185 | } 186 | 187 | var contents = body.clone(body.size - pad) 188 | body.skip(body.size) 189 | callback(null, contents) 190 | } 191 | 192 | Parser.prototype.onDataFrame = function onDataFrame (header, body, callback) { 193 | var isEndStream = (header.flags & constants.flags.END_STREAM) !== 0 194 | 195 | if (header.id === 0) { 196 | return callback(this.error(constants.error.PROTOCOL_ERROR, 197 | 'Received DATA frame with stream=0')) 198 | } 199 | 200 | // Count received bytes 201 | if (this.window) { 202 | this.window.recv.update(-body.size) 203 | } 204 | 205 | this.unpadData(header, body, function (err, data) { 206 | if (err) { 207 | return callback(err) 208 | } 209 | 210 | callback(null, { 211 | type: 'DATA', 212 | id: header.id, 213 | fin: isEndStream, 214 | data: data.take(data.size) 215 | }) 216 | }) 217 | } 218 | 219 | Parser.prototype.initHeaderBlock = function initHeaderBlock (header, 220 | frame, 221 | block, 222 | callback) { 223 | if (this._lastHeaderBlock) { 224 | return callback(this.error(constants.error.PROTOCOL_ERROR, 225 | 'Duplicate Stream ID')) 226 | } 227 | 228 | this._lastHeaderBlock = { 229 | id: header.id, 230 | frame: frame, 231 | queue: [], 232 | size: 0 233 | } 234 | 235 | this.queueHeaderBlock(header, block, callback) 236 | } 237 | 238 | Parser.prototype.queueHeaderBlock = function queueHeaderBlock (header, 239 | block, 240 | callback) { 241 | var self = this 242 | var item = this._lastHeaderBlock 243 | if (!this._lastHeaderBlock || item.id !== header.id) { 244 | return callback(this.error(constants.error.PROTOCOL_ERROR, 245 | 'No matching stream for continuation')) 246 | } 247 | 248 | var fin = (header.flags & constants.flags.END_HEADERS) !== 0 249 | 250 | var chunks = block.toChunks() 251 | for (var i = 0; i < chunks.length; i++) { 252 | var chunk = chunks[i] 253 | item.queue.push(chunk) 254 | item.size += chunk.length 255 | } 256 | 257 | if (item.size >= self.maxHeaderListSize) { 258 | return callback(this.error(constants.error.PROTOCOL_ERROR, 259 | 'Compressed header list is too large')) 260 | } 261 | 262 | if (!fin) { return callback(null, null) } 263 | this._lastHeaderBlock = null 264 | 265 | this.decompress.write(item.queue, function (err, chunks) { 266 | if (err) { 267 | return callback(self.error(constants.error.COMPRESSION_ERROR, 268 | err.message)) 269 | } 270 | 271 | var headers = {} 272 | var size = 0 273 | for (var i = 0; i < chunks.length; i++) { 274 | var header = chunks[i] 275 | 276 | size += header.name.length + header.value.length + 32 277 | if (size >= self.maxHeaderListSize) { 278 | return callback(self.error(constants.error.PROTOCOL_ERROR, 279 | 'Header list is too large')) 280 | } 281 | 282 | if (/[A-Z]/.test(header.name)) { 283 | return callback(self.error(constants.error.PROTOCOL_ERROR, 284 | 'Header name must be lowercase')) 285 | } 286 | 287 | utils.addHeaderLine(header.name, header.value, headers) 288 | } 289 | 290 | item.frame.headers = headers 291 | item.frame.path = headers[':path'] 292 | 293 | callback(null, item.frame) 294 | }) 295 | } 296 | 297 | Parser.prototype.onHeadersFrame = function onHeadersFrame (header, 298 | body, 299 | callback) { 300 | var self = this 301 | 302 | if (header.id === 0) { 303 | return callback(this.error(constants.error.PROTOCOL_ERROR, 304 | 'Invalid stream id for HEADERS')) 305 | } 306 | 307 | this.unpadData(header, body, function (err, data) { 308 | if (err) { return callback(err) } 309 | 310 | var isPriority = (header.flags & constants.flags.PRIORITY) !== 0 311 | if (!data.has(isPriority ? 5 : 0)) { 312 | return callback(self.error(constants.error.FRAME_SIZE_ERROR, 313 | 'Not enough data for HEADERS')) 314 | } 315 | 316 | var exclusive = false 317 | var dependency = 0 318 | var weight = constants.DEFAULT_WEIGHT 319 | if (isPriority) { 320 | dependency = data.readUInt32BE() 321 | exclusive = (dependency & 0x80000000) !== 0 322 | dependency &= 0x7fffffff 323 | 324 | // Weight's range is [1, 256] 325 | weight = data.readUInt8() + 1 326 | } 327 | 328 | if (dependency === header.id) { 329 | return callback(self.error(constants.error.PROTOCOL_ERROR, 330 | 'Stream can\'t dependend on itself')) 331 | } 332 | 333 | var streamInfo = { 334 | type: 'HEADERS', 335 | id: header.id, 336 | priority: { 337 | parent: dependency, 338 | exclusive: exclusive, 339 | weight: weight 340 | }, 341 | fin: (header.flags & constants.flags.END_STREAM) !== 0, 342 | writable: true, 343 | headers: null, 344 | path: null 345 | } 346 | 347 | self.initHeaderBlock(header, streamInfo, data, callback) 348 | }) 349 | } 350 | 351 | Parser.prototype.onContinuationFrame = function onContinuationFrame (header, 352 | body, 353 | callback) { 354 | this.queueHeaderBlock(header, body, callback) 355 | } 356 | 357 | Parser.prototype.onRSTFrame = function onRSTFrame (header, body, callback) { 358 | if (body.size !== 4) { 359 | return callback(this.error(constants.error.FRAME_SIZE_ERROR, 360 | 'RST_STREAM length not 4')) 361 | } 362 | 363 | if (header.id === 0) { 364 | return callback(this.error(constants.error.PROTOCOL_ERROR, 365 | 'Invalid stream id for RST_STREAM')) 366 | } 367 | 368 | callback(null, { 369 | type: 'RST', 370 | id: header.id, 371 | code: constants.errorByCode[body.readUInt32BE()] 372 | }) 373 | } 374 | 375 | Parser.prototype._validateSettings = function _validateSettings (settings) { 376 | if (settings['enable_push'] !== undefined && 377 | settings['enable_push'] !== 0 && 378 | settings['enable_push'] !== 1) { 379 | return this.error(constants.error.PROTOCOL_ERROR, 380 | 'SETTINGS_ENABLE_PUSH must be 0 or 1') 381 | } 382 | 383 | if (settings['initial_window_size'] !== undefined && 384 | (settings['initial_window_size'] > constants.MAX_INITIAL_WINDOW_SIZE || 385 | settings['initial_window_size'] < 0)) { 386 | return this.error(constants.error.FLOW_CONTROL_ERROR, 387 | 'SETTINGS_INITIAL_WINDOW_SIZE is OOB') 388 | } 389 | 390 | if (settings['max_frame_size'] !== undefined && 391 | (settings['max_frame_size'] > constants.ABSOLUTE_MAX_FRAME_SIZE || 392 | settings['max_frame_size'] < constants.INITIAL_MAX_FRAME_SIZE)) { 393 | return this.error(constants.error.PROTOCOL_ERROR, 394 | 'SETTINGS_MAX_FRAME_SIZE is OOB') 395 | } 396 | 397 | return undefined 398 | } 399 | 400 | Parser.prototype.onSettingsFrame = function onSettingsFrame (header, 401 | body, 402 | callback) { 403 | if (header.id !== 0) { 404 | return callback(this.error(constants.error.PROTOCOL_ERROR, 405 | 'Invalid stream id for SETTINGS')) 406 | } 407 | 408 | var isAck = (header.flags & constants.flags.ACK) !== 0 409 | if (isAck && body.size !== 0) { 410 | return callback(this.error(constants.error.FRAME_SIZE_ERROR, 411 | 'SETTINGS with ACK and non-zero length')) 412 | } 413 | 414 | if (isAck) { 415 | return callback(null, { type: 'ACK_SETTINGS' }) 416 | } 417 | 418 | if (body.size % 6 !== 0) { 419 | return callback(this.error(constants.error.FRAME_SIZE_ERROR, 420 | 'SETTINGS length not multiple of 6')) 421 | } 422 | 423 | var settings = {} 424 | while (!body.isEmpty()) { 425 | var id = body.readUInt16BE() 426 | var value = body.readUInt32BE() 427 | var name = constants.settingsIndex[id] 428 | 429 | if (name) { 430 | settings[name] = value 431 | } 432 | } 433 | 434 | var err = this._validateSettings(settings) 435 | if (err !== undefined) { 436 | return callback(err) 437 | } 438 | 439 | callback(null, { 440 | type: 'SETTINGS', 441 | settings: settings 442 | }) 443 | } 444 | 445 | Parser.prototype.onPushPromiseFrame = function onPushPromiseFrame (header, 446 | body, 447 | callback) { 448 | if (header.id === 0) { 449 | return callback(this.error(constants.error.PROTOCOL_ERROR, 450 | 'Invalid stream id for PUSH_PROMISE')) 451 | } 452 | 453 | var self = this 454 | this.unpadData(header, body, function (err, data) { 455 | if (err) { 456 | return callback(err) 457 | } 458 | 459 | if (!data.has(4)) { 460 | return callback(self.error(constants.error.FRAME_SIZE_ERROR, 461 | 'PUSH_PROMISE length less than 4')) 462 | } 463 | 464 | var streamInfo = { 465 | type: 'PUSH_PROMISE', 466 | id: header.id, 467 | fin: false, 468 | promisedId: data.readUInt32BE() & 0x7fffffff, 469 | headers: null, 470 | path: null 471 | } 472 | 473 | self.initHeaderBlock(header, streamInfo, data, callback) 474 | }) 475 | } 476 | 477 | Parser.prototype.onPingFrame = function onPingFrame (header, body, callback) { 478 | if (body.size !== 8) { 479 | return callback(this.error(constants.error.FRAME_SIZE_ERROR, 480 | 'PING length != 8')) 481 | } 482 | 483 | if (header.id !== 0) { 484 | return callback(this.error(constants.error.PROTOCOL_ERROR, 485 | 'Invalid stream id for PING')) 486 | } 487 | 488 | var ack = (header.flags & constants.flags.ACK) !== 0 489 | callback(null, { type: 'PING', opaque: body.take(body.size), ack: ack }) 490 | } 491 | 492 | Parser.prototype.onGoawayFrame = function onGoawayFrame (header, 493 | body, 494 | callback) { 495 | if (!body.has(8)) { 496 | return callback(this.error(constants.error.FRAME_SIZE_ERROR, 497 | 'GOAWAY length < 8')) 498 | } 499 | 500 | if (header.id !== 0) { 501 | return callback(this.error(constants.error.PROTOCOL_ERROR, 502 | 'Invalid stream id for GOAWAY')) 503 | } 504 | 505 | var frame = { 506 | type: 'GOAWAY', 507 | lastId: body.readUInt32BE(), 508 | code: constants.goawayByCode[body.readUInt32BE()] 509 | } 510 | 511 | if (body.size !== 0) { frame.debug = body.take(body.size) } 512 | 513 | callback(null, frame) 514 | } 515 | 516 | Parser.prototype.onPriorityFrame = function onPriorityFrame (header, 517 | body, 518 | callback) { 519 | if (body.size !== 5) { 520 | return callback(this.error(constants.error.FRAME_SIZE_ERROR, 521 | 'PRIORITY length != 5')) 522 | } 523 | 524 | if (header.id === 0) { 525 | return callback(this.error(constants.error.PROTOCOL_ERROR, 526 | 'Invalid stream id for PRIORITY')) 527 | } 528 | 529 | var dependency = body.readUInt32BE() 530 | 531 | // Again the range is from 1 to 256 532 | var weight = body.readUInt8() + 1 533 | 534 | if (dependency === header.id) { 535 | return callback(this.error(constants.error.PROTOCOL_ERROR, 536 | 'Stream can\'t dependend on itself')) 537 | } 538 | 539 | callback(null, { 540 | type: 'PRIORITY', 541 | id: header.id, 542 | priority: { 543 | exclusive: (dependency & 0x80000000) !== 0, 544 | parent: dependency & 0x7fffffff, 545 | weight: weight 546 | } 547 | }) 548 | } 549 | 550 | Parser.prototype.onWindowUpdateFrame = function onWindowUpdateFrame (header, 551 | body, 552 | callback) { 553 | if (body.size !== 4) { 554 | return callback(this.error(constants.error.FRAME_SIZE_ERROR, 555 | 'WINDOW_UPDATE length != 4')) 556 | } 557 | 558 | var delta = body.readInt32BE() 559 | if (delta === 0) { 560 | return callback(this.error(constants.error.PROTOCOL_ERROR, 561 | 'WINDOW_UPDATE delta == 0')) 562 | } 563 | 564 | callback(null, { 565 | type: 'WINDOW_UPDATE', 566 | id: header.id, 567 | delta: delta 568 | }) 569 | } 570 | 571 | Parser.prototype.onXForwardedFrame = function onXForwardedFrame (header, 572 | body, 573 | callback) { 574 | callback(null, { 575 | type: 'X_FORWARDED_FOR', 576 | host: body.take(body.size).toString() 577 | }) 578 | } 579 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/spdy/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transport = require('../../../spdy-transport') 4 | var base = transport.protocol.base 5 | 6 | exports.FRAME_HEADER_SIZE = 8 7 | 8 | exports.PING_OPAQUE_SIZE = 4 9 | 10 | exports.MAX_CONCURRENT_STREAMS = Infinity 11 | exports.DEFAULT_MAX_HEADER_LIST_SIZE = Infinity 12 | 13 | exports.DEFAULT_WEIGHT = 16 14 | 15 | exports.frameType = { 16 | SYN_STREAM: 1, 17 | SYN_REPLY: 2, 18 | RST_STREAM: 3, 19 | SETTINGS: 4, 20 | PING: 6, 21 | GOAWAY: 7, 22 | HEADERS: 8, 23 | WINDOW_UPDATE: 9, 24 | 25 | // Custom 26 | X_FORWARDED_FOR: 0xf000 27 | } 28 | 29 | exports.flags = { 30 | FLAG_FIN: 0x01, 31 | FLAG_COMPRESSED: 0x02, 32 | FLAG_UNIDIRECTIONAL: 0x02 33 | } 34 | 35 | exports.error = { 36 | PROTOCOL_ERROR: 1, 37 | INVALID_STREAM: 2, 38 | REFUSED_STREAM: 3, 39 | UNSUPPORTED_VERSION: 4, 40 | CANCEL: 5, 41 | INTERNAL_ERROR: 6, 42 | FLOW_CONTROL_ERROR: 7, 43 | STREAM_IN_USE: 8, 44 | // STREAM_ALREADY_CLOSED: 9 45 | STREAM_CLOSED: 9, 46 | INVALID_CREDENTIALS: 10, 47 | FRAME_TOO_LARGE: 11 48 | } 49 | exports.errorByCode = base.utils.reverse(exports.error) 50 | 51 | exports.settings = { 52 | FLAG_SETTINGS_PERSIST_VALUE: 1, 53 | FLAG_SETTINGS_PERSISTED: 2, 54 | 55 | SETTINGS_UPLOAD_BANDWIDTH: 1, 56 | SETTINGS_DOWNLOAD_BANDWIDTH: 2, 57 | SETTINGS_ROUND_TRIP_TIME: 3, 58 | SETTINGS_MAX_CONCURRENT_STREAMS: 4, 59 | SETTINGS_CURRENT_CWND: 5, 60 | SETTINGS_DOWNLOAD_RETRANS_RATE: 6, 61 | SETTINGS_INITIAL_WINDOW_SIZE: 7, 62 | SETTINGS_CLIENT_CERTIFICATE_VECTOR_SIZE: 8 63 | } 64 | 65 | exports.settingsIndex = [ 66 | null, 67 | 68 | 'upload_bandwidth', 69 | 'download_bandwidth', 70 | 'round_trip_time', 71 | 'max_concurrent_streams', 72 | 'current_cwnd', 73 | 'download_retrans_rate', 74 | 'initial_window_size', 75 | 'client_certificate_vector_size' 76 | ] 77 | 78 | exports.DEFAULT_WINDOW = 64 * 1024 79 | exports.MAX_INITIAL_WINDOW_SIZE = 2147483647 80 | 81 | exports.goaway = { 82 | OK: 0, 83 | PROTOCOL_ERROR: 1, 84 | INTERNAL_ERROR: 2 85 | } 86 | exports.goawayByCode = base.utils.reverse(exports.goaway) 87 | 88 | exports.statusReason = { 89 | 100: 'Continue', 90 | 101: 'Switching Protocols', 91 | 102: 'Processing', // RFC 2518, obsoleted by RFC 4918 92 | 200: 'OK', 93 | 201: 'Created', 94 | 202: 'Accepted', 95 | 203: 'Non-Authoritative Information', 96 | 204: 'No Content', 97 | 205: 'Reset Content', 98 | 206: 'Partial Content', 99 | 207: 'Multi-Status', // RFC 4918 100 | 300: 'Multiple Choices', 101 | 301: 'Moved Permanently', 102 | 302: 'Moved Temporarily', 103 | 303: 'See Other', 104 | 304: 'Not Modified', 105 | 305: 'Use Proxy', 106 | 307: 'Temporary Redirect', 107 | 308: 'Permanent Redirect', // RFC 7238 108 | 400: 'Bad Request', 109 | 401: 'Unauthorized', 110 | 402: 'Payment Required', 111 | 403: 'Forbidden', 112 | 404: 'Not Found', 113 | 405: 'Method Not Allowed', 114 | 406: 'Not Acceptable', 115 | 407: 'Proxy Authentication Required', 116 | 408: 'Request Time-out', 117 | 409: 'Conflict', 118 | 410: 'Gone', 119 | 411: 'Length Required', 120 | 412: 'Precondition Failed', 121 | 413: 'Request Entity Too Large', 122 | 414: 'Request-URI Too Large', 123 | 415: 'Unsupported Media Type', 124 | 416: 'Requested Range Not Satisfiable', 125 | 417: 'Expectation Failed', 126 | 418: 'I\'m a teapot', // RFC 2324 127 | 422: 'Unprocessable Entity', // RFC 4918 128 | 423: 'Locked', // RFC 4918 129 | 424: 'Failed Dependency', // RFC 4918 130 | 425: 'Unordered Collection', // RFC 4918 131 | 426: 'Upgrade Required', // RFC 2817 132 | 428: 'Precondition Required', // RFC 6585 133 | 429: 'Too Many Requests', // RFC 6585 134 | 431: 'Request Header Fields Too Large', // RFC 6585 135 | 500: 'Internal Server Error', 136 | 501: 'Not Implemented', 137 | 502: 'Bad Gateway', 138 | 503: 'Service Unavailable', 139 | 504: 'Gateway Time-out', 140 | 505: 'HTTP Version Not Supported', 141 | 506: 'Variant Also Negotiates', // RFC 2295 142 | 507: 'Insufficient Storage', // RFC 4918 143 | 509: 'Bandwidth Limit Exceeded', 144 | 510: 'Not Extended', // RFC 2774 145 | 511: 'Network Authentication Required' // RFC 6585 146 | } 147 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/spdy/dictionary.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var dictionary = {} 4 | module.exports = dictionary 5 | 6 | dictionary[2] = Buffer.from([ 7 | 'optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-', 8 | 'languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi', 9 | 'f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser', 10 | '-agent10010120020120220320420520630030130230330430530630740040140240340440', 11 | '5406407408409410411412413414415416417500501502503504505accept-rangesageeta', 12 | 'glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic', 13 | 'ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran', 14 | 'sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati', 15 | 'oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo', 16 | 'ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe', 17 | 'pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic', 18 | 'ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1', 19 | '.1statusversionurl\x00' 20 | ].join('')) 21 | 22 | dictionary[3] = Buffer.from([ 23 | 0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69, // ....opti 24 | 0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68, // ons....h 25 | 0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70, // ead....p 26 | 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70, // ost....p 27 | 0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65, // ut....de 28 | 0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05, // lete.... 29 | 0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00, // trace... 30 | 0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00, // .accept. 31 | 0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, // ...accep 32 | 0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // t-charse 33 | 0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63, // t....acc 34 | 0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // ept-enco 35 | 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f, // ding.... 36 | 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, // accept-l 37 | 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00, // anguage. 38 | 0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, // ...accep 39 | 0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, // t-ranges 40 | 0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00, // ....age. 41 | 0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, // ...allow 42 | 0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68, // ....auth 43 | 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, // orizatio 44 | 0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63, // n....cac 45 | 0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, // he-contr 46 | 0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f, // ol....co 47 | 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, // nnection 48 | 0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, // ....cont 49 | 0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65, // ent-base 50 | 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74, // ....cont 51 | 0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // ent-enco 52 | 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, // ding.... 53 | 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, // content- 54 | 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, // language 55 | 0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74, // ....cont 56 | 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, // ent-leng 57 | 0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, // th....co 58 | 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f, // ntent-lo 59 | 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, // cation.. 60 | 0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // ..conten 61 | 0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00, // t-md5... 62 | 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, // .content 63 | 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, // -range.. 64 | 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // ..conten 65 | 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00, // t-type.. 66 | 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00, // ..date.. 67 | 0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00, // ..etag.. 68 | 0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, // ..expect 69 | 0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69, // ....expi 70 | 0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66, // res....f 71 | 0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68, // rom....h 72 | 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69, // ost....i 73 | 0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, // f-match. 74 | 0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f, // ...if-mo 75 | 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73, // dified-s 76 | 0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d, // ince.... 77 | 0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d, // if-none- 78 | 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00, // match... 79 | 0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67, // .if-rang 80 | 0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d, // e....if- 81 | 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, // unmodifi 82 | 0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65, // ed-since 83 | 0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74, // ....last 84 | 0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, // -modifie 85 | 0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63, // d....loc 86 | 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, // ation... 87 | 0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72, // .max-for 88 | 0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, // wards... 89 | 0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00, // .pragma. 90 | 0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79, // ...proxy 91 | 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, // -authent 92 | 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00, // icate... 93 | 0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61, // .proxy-a 94 | 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, // uthoriza 95 | 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, // tion.... 96 | 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00, // range... 97 | 0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72, // .referer 98 | 0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72, // ....retr 99 | 0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00, // y-after. 100 | 0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, // ...serve 101 | 0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00, // r....te. 102 | 0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, // ...trail 103 | 0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72, // er....tr 104 | 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65, // ansfer-e 105 | 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, // ncoding. 106 | 0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, // ...upgra 107 | 0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73, // de....us 108 | 0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, // er-agent 109 | 0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79, // ....vary 110 | 0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00, // ....via. 111 | 0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, // ...warni 112 | 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77, // ng....ww 113 | 0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, // w-authen 114 | 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, // ticate.. 115 | 0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, // ..method 116 | 0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00, // ....get. 117 | 0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, // ...statu 118 | 0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30, // s....200 119 | 0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76, // .OK....v 120 | 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, // ersion.. 121 | 0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, // ..HTTP.1 122 | 0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, // .1....ur 123 | 0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62, // l....pub 124 | 0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73, // lic....s 125 | 0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69, // et-cooki 126 | 0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65, // e....kee 127 | 0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00, // p-alive. 128 | 0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, // ...origi 129 | 0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32, // n1001012 130 | 0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35, // 01202205 131 | 0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30, // 20630030 132 | 0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33, // 23033043 133 | 0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37, // 05306307 134 | 0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30, // 40240540 135 | 0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34, // 64074084 136 | 0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31, // 09410411 137 | 0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31, // 41241341 138 | 0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34, // 44154164 139 | 0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34, // 17502504 140 | 0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e, // 505203.N 141 | 0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f, // on-Autho 142 | 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, // ritative 143 | 0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, // .Informa 144 | 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20, // tion204. 145 | 0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65, // No.Conte 146 | 0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f, // nt301.Mo 147 | 0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d, // ved.Perm 148 | 0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34, // anently4 149 | 0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52, // 00.Bad.R 150 | 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30, // equest40 151 | 0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, // 1.Unauth 152 | 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30, // orized40 153 | 0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, // 3.Forbid 154 | 0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e, // den404.N 155 | 0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64, // ot.Found 156 | 0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65, // 500.Inte 157 | 0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72, // rnal.Ser 158 | 0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f, // ver.Erro 159 | 0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74, // r501.Not 160 | 0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, // .Impleme 161 | 0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20, // nted503. 162 | 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, // Service. 163 | 0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, // Unavaila 164 | 0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46, // bleJan.F 165 | 0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41, // eb.Mar.A 166 | 0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a, // pr.May.J 167 | 0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41, // un.Jul.A 168 | 0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20, // ug.Sept. 169 | 0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20, // Oct.Nov. 170 | 0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30, // Dec.00.0 171 | 0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e, // 0.00.Mon 172 | 0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57, // ..Tue..W 173 | 0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c, // ed..Thu. 174 | 0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61, // .Fri..Sa 175 | 0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20, // t..Sun.. 176 | 0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b, // GMTchunk 177 | 0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, // ed.text. 178 | 0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61, // html.ima 179 | 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69, // ge.png.i 180 | 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67, // mage.jpg 181 | 0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67, // .image.g 182 | 0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // if.appli 183 | 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // cation.x 184 | 0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // ml.appli 185 | 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // cation.x 186 | 0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, // html.xml 187 | 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, // .text.pl 188 | 0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74, // ain.text 189 | 0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72, // .javascr 190 | 0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c, // ipt.publ 191 | 0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, // icprivat 192 | 0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65, // emax-age 193 | 0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65, // .gzip.de 194 | 0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64, // flate.sd 195 | 0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // chcharse 196 | 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63, // t.utf-8c 197 | 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69, // harset.i 198 | 0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d, // so-8859- 199 | 0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a, // 1.utf-.. 200 | 0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e // .enq.0. 201 | ]) 202 | 203 | dictionary[3.1] = dictionary[3] 204 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/spdy/framer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transport = require('../../../spdy-transport') 4 | var constants = require('./').constants 5 | var base = transport.protocol.base 6 | var utils = base.utils 7 | 8 | var assert = require('assert') 9 | var util = require('util') 10 | var Buffer = require('buffer').Buffer 11 | var WriteBuffer = require('wbuf') 12 | 13 | var debug = require('debug')('spdy:framer') 14 | 15 | function Framer (options) { 16 | base.Framer.call(this, options) 17 | } 18 | util.inherits(Framer, base.Framer) 19 | module.exports = Framer 20 | 21 | Framer.create = function create (options) { 22 | return new Framer(options) 23 | } 24 | 25 | Framer.prototype.setMaxFrameSize = function setMaxFrameSize (size) { 26 | // http2-only 27 | } 28 | 29 | Framer.prototype.headersToDict = function headersToDict (headers, 30 | preprocess, 31 | callback) { 32 | function stringify (value) { 33 | if (value !== undefined) { 34 | if (Array.isArray(value)) { 35 | return value.join('\x00') 36 | } else if (typeof value === 'string') { 37 | return value 38 | } else { 39 | return value.toString() 40 | } 41 | } else { 42 | return '' 43 | } 44 | } 45 | 46 | // Lower case of all headers keys 47 | var loweredHeaders = {} 48 | Object.keys(headers || {}).map(function (key) { 49 | loweredHeaders[key.toLowerCase()] = headers[key] 50 | }) 51 | 52 | // Allow outer code to add custom headers or remove something 53 | if (preprocess) { preprocess(loweredHeaders) } 54 | 55 | // Transform object into kv pairs 56 | var size = this.version === 2 ? 2 : 4 57 | var len = size 58 | var pairs = Object.keys(loweredHeaders).filter(function (key) { 59 | var lkey = key.toLowerCase() 60 | 61 | // Will be in `:host` 62 | if (lkey === 'host' && this.version >= 3) { 63 | return false 64 | } 65 | 66 | return lkey !== 'connection' && lkey !== 'keep-alive' && 67 | lkey !== 'proxy-connection' && lkey !== 'transfer-encoding' 68 | }, this).map(function (key) { 69 | var klen = Buffer.byteLength(key) 70 | var value = stringify(loweredHeaders[key]) 71 | var vlen = Buffer.byteLength(value) 72 | 73 | len += size * 2 + klen + vlen 74 | return [klen, key, vlen, value] 75 | }) 76 | 77 | var block = new WriteBuffer() 78 | block.reserve(len) 79 | 80 | if (this.version === 2) { 81 | block.writeUInt16BE(pairs.length) 82 | } else { 83 | block.writeUInt32BE(pairs.length) 84 | } 85 | 86 | pairs.forEach(function (pair) { 87 | // Write key length 88 | if (this.version === 2) { 89 | block.writeUInt16BE(pair[0]) 90 | } else { 91 | block.writeUInt32BE(pair[0]) 92 | } 93 | 94 | // Write key 95 | block.write(pair[1]) 96 | 97 | // Write value length 98 | if (this.version === 2) { 99 | block.writeUInt16BE(pair[2]) 100 | } else { 101 | block.writeUInt32BE(pair[2]) 102 | } 103 | // Write value 104 | block.write(pair[3]) 105 | }, this) 106 | 107 | assert(this.compress !== null, 'Framer version not initialized') 108 | this.compress.write(block.render(), callback) 109 | } 110 | 111 | Framer.prototype._frame = function _frame (frame, body, callback) { 112 | if (!this.version) { 113 | this.on('version', function () { 114 | this._frame(frame, body, callback) 115 | }) 116 | return 117 | } 118 | 119 | debug('id=%d type=%s', frame.id, frame.type) 120 | 121 | var buffer = new WriteBuffer() 122 | 123 | buffer.writeUInt16BE(0x8000 | this.version) 124 | buffer.writeUInt16BE(constants.frameType[frame.type]) 125 | buffer.writeUInt8(frame.flags) 126 | var len = buffer.skip(3) 127 | 128 | body(buffer) 129 | 130 | var frameSize = buffer.size - constants.FRAME_HEADER_SIZE 131 | len.writeUInt24BE(frameSize) 132 | 133 | var chunks = buffer.render() 134 | var toWrite = { 135 | stream: frame.id, 136 | priority: false, 137 | chunks: chunks, 138 | callback: callback 139 | } 140 | 141 | this._resetTimeout() 142 | this.schedule(toWrite) 143 | 144 | return chunks 145 | } 146 | 147 | Framer.prototype._synFrame = function _synFrame (frame, callback) { 148 | var self = this 149 | 150 | if (!frame.path) { 151 | throw new Error('`path` is required frame argument') 152 | } 153 | 154 | function preprocess (headers) { 155 | var method = frame.method || base.constants.DEFAULT_METHOD 156 | var version = frame.version || 'HTTP/1.1' 157 | var scheme = frame.scheme || 'https' 158 | var host = frame.host || 159 | (frame.headers && frame.headers.host) || 160 | base.constants.DEFAULT_HOST 161 | 162 | if (self.version === 2) { 163 | headers.method = method 164 | headers.version = version 165 | headers.url = frame.path 166 | headers.scheme = scheme 167 | headers.host = host 168 | if (frame.status) { 169 | headers.status = frame.status 170 | } 171 | } else { 172 | headers[':method'] = method 173 | headers[':version'] = version 174 | headers[':path'] = frame.path 175 | headers[':scheme'] = scheme 176 | headers[':host'] = host 177 | if (frame.status) { headers[':status'] = frame.status } 178 | } 179 | } 180 | 181 | this.headersToDict(frame.headers, preprocess, function (err, chunks) { 182 | if (err) { 183 | if (callback) { 184 | return callback(err) 185 | } else { 186 | return self.emit('error', err) 187 | } 188 | } 189 | 190 | self._frame({ 191 | type: 'SYN_STREAM', 192 | id: frame.id, 193 | flags: frame.fin ? constants.flags.FLAG_FIN : 0 194 | }, function (buf) { 195 | buf.reserve(10) 196 | 197 | buf.writeUInt32BE(frame.id & 0x7fffffff) 198 | buf.writeUInt32BE(frame.associated & 0x7fffffff) 199 | 200 | var weight = (frame.priority && frame.priority.weight) || 201 | constants.DEFAULT_WEIGHT 202 | 203 | // We only have 3 bits for priority in SPDY, try to fit it into this 204 | var priority = utils.weightToPriority(weight) 205 | buf.writeUInt8(priority << 5) 206 | 207 | // CREDENTIALS slot 208 | buf.writeUInt8(0) 209 | 210 | for (var i = 0; i < chunks.length; i++) { 211 | buf.copyFrom(chunks[i]) 212 | } 213 | }, callback) 214 | }) 215 | } 216 | 217 | Framer.prototype.requestFrame = function requestFrame (frame, callback) { 218 | this._synFrame({ 219 | id: frame.id, 220 | fin: frame.fin, 221 | associated: 0, 222 | method: frame.method, 223 | version: frame.version, 224 | scheme: frame.scheme, 225 | host: frame.host, 226 | path: frame.path, 227 | priority: frame.priority, 228 | headers: frame.headers 229 | }, callback) 230 | } 231 | 232 | Framer.prototype.responseFrame = function responseFrame (frame, callback) { 233 | var self = this 234 | 235 | var reason = frame.reason 236 | if (!reason) { 237 | reason = constants.statusReason[frame.status] 238 | } 239 | 240 | function preprocess (headers) { 241 | if (self.version === 2) { 242 | headers.status = frame.status + ' ' + reason 243 | headers.version = 'HTTP/1.1' 244 | } else { 245 | headers[':status'] = frame.status + ' ' + reason 246 | headers[':version'] = 'HTTP/1.1' 247 | } 248 | } 249 | 250 | this.headersToDict(frame.headers, preprocess, function (err, chunks) { 251 | if (err) { 252 | if (callback) { 253 | return callback(err) 254 | } else { 255 | return self.emit('error', err) 256 | } 257 | } 258 | 259 | self._frame({ 260 | type: 'SYN_REPLY', 261 | id: frame.id, 262 | flags: 0 263 | }, function (buf) { 264 | buf.reserve(self.version === 2 ? 6 : 4) 265 | 266 | buf.writeUInt32BE(frame.id & 0x7fffffff) 267 | 268 | // Unused data 269 | if (self.version === 2) { 270 | buf.writeUInt16BE(0) 271 | } 272 | 273 | for (var i = 0; i < chunks.length; i++) { 274 | buf.copyFrom(chunks[i]) 275 | } 276 | }, callback) 277 | }) 278 | } 279 | 280 | Framer.prototype.pushFrame = function pushFrame (frame, callback) { 281 | var self = this 282 | 283 | this._checkPush(function (err) { 284 | if (err) { return callback(err) } 285 | 286 | self._synFrame({ 287 | id: frame.promisedId, 288 | associated: frame.id, 289 | method: frame.method, 290 | status: frame.status || 200, 291 | version: frame.version, 292 | scheme: frame.scheme, 293 | host: frame.host, 294 | path: frame.path, 295 | priority: frame.priority, 296 | 297 | // Merge everything together, there is no difference in SPDY protocol 298 | headers: Object.assign(Object.assign({}, frame.headers), frame.response) 299 | }, callback) 300 | }) 301 | } 302 | 303 | Framer.prototype.headersFrame = function headersFrame (frame, callback) { 304 | var self = this 305 | 306 | this.headersToDict(frame.headers, null, function (err, chunks) { 307 | if (err) { 308 | if (callback) { return callback(err) } else { 309 | return self.emit('error', err) 310 | } 311 | } 312 | 313 | self._frame({ 314 | type: 'HEADERS', 315 | id: frame.id, 316 | priority: false, 317 | flags: 0 318 | }, function (buf) { 319 | buf.reserve(4 + (self.version === 2 ? 2 : 0)) 320 | buf.writeUInt32BE(frame.id & 0x7fffffff) 321 | 322 | // Unused data 323 | if (self.version === 2) { buf.writeUInt16BE(0) } 324 | 325 | for (var i = 0; i < chunks.length; i++) { 326 | buf.copyFrom(chunks[i]) 327 | } 328 | }, callback) 329 | }) 330 | } 331 | 332 | Framer.prototype.dataFrame = function dataFrame (frame, callback) { 333 | if (!this.version) { 334 | return this.on('version', function () { 335 | this.dataFrame(frame, callback) 336 | }) 337 | } 338 | 339 | debug('id=%d type=DATA', frame.id) 340 | 341 | var buffer = new WriteBuffer() 342 | buffer.reserve(8 + frame.data.length) 343 | 344 | buffer.writeUInt32BE(frame.id & 0x7fffffff) 345 | buffer.writeUInt8(frame.fin ? 0x01 : 0x0) 346 | buffer.writeUInt24BE(frame.data.length) 347 | buffer.copyFrom(frame.data) 348 | 349 | var chunks = buffer.render() 350 | var toWrite = { 351 | stream: frame.id, 352 | priority: frame.priority, 353 | chunks: chunks, 354 | callback: callback 355 | } 356 | 357 | var self = this 358 | this._resetTimeout() 359 | 360 | var bypass = this.version < 3.1 361 | this.window.send.update(-frame.data.length, bypass ? undefined : function () { 362 | self._resetTimeout() 363 | self.schedule(toWrite) 364 | }) 365 | 366 | if (bypass) { 367 | this._resetTimeout() 368 | this.schedule(toWrite) 369 | } 370 | } 371 | 372 | Framer.prototype.pingFrame = function pingFrame (frame, callback) { 373 | this._frame({ 374 | type: 'PING', 375 | id: 0, 376 | flags: 0 377 | }, function (buf, callback) { 378 | buf.reserve(4) 379 | 380 | var opaque = frame.opaque 381 | buf.writeUInt32BE(opaque.readUInt32BE(opaque.length - 4, true)) 382 | }, callback) 383 | } 384 | 385 | Framer.prototype.rstFrame = function rstFrame (frame, callback) { 386 | this._frame({ 387 | type: 'RST_STREAM', 388 | id: frame.id, 389 | flags: 0 390 | }, function (buf) { 391 | buf.reserve(8) 392 | 393 | // Stream ID 394 | buf.writeUInt32BE(frame.id & 0x7fffffff) 395 | // Status Code 396 | buf.writeUInt32BE(constants.error[frame.code]) 397 | 398 | // Extra debugging information 399 | if (frame.extra) { 400 | buf.write(frame.extra) 401 | } 402 | }, callback) 403 | } 404 | 405 | Framer.prototype.prefaceFrame = function prefaceFrame () { 406 | } 407 | 408 | Framer.prototype.settingsFrame = function settingsFrame (options, callback) { 409 | var self = this 410 | 411 | var key = this.version + '/' + JSON.stringify(options) 412 | 413 | var settings = Framer.settingsCache[key] 414 | if (settings) { 415 | debug('cached settings') 416 | this._resetTimeout() 417 | this.schedule({ 418 | stream: 0, 419 | priority: false, 420 | chunks: settings, 421 | callback: callback 422 | }) 423 | return 424 | } 425 | 426 | var params = [] 427 | for (var i = 0; i < constants.settingsIndex.length; i++) { 428 | var name = constants.settingsIndex[i] 429 | if (!name) { continue } 430 | 431 | // value: Infinity 432 | if (!isFinite(options[name])) { 433 | continue 434 | } 435 | 436 | if (options[name] !== undefined) { 437 | params.push({ key: i, value: options[name] }) 438 | } 439 | } 440 | 441 | var frame = this._frame({ 442 | type: 'SETTINGS', 443 | id: 0, 444 | flags: 0 445 | }, function (buf) { 446 | buf.reserve(4 + 8 * params.length) 447 | 448 | // Count of entries 449 | buf.writeUInt32BE(params.length) 450 | 451 | params.forEach(function (param) { 452 | var flag = constants.settings.FLAG_SETTINGS_PERSIST_VALUE << 24 453 | 454 | if (self.version === 2) { 455 | buf.writeUInt32LE(flag | param.key) 456 | } else { buf.writeUInt32BE(flag | param.key) } 457 | buf.writeUInt32BE(param.value & 0x7fffffff) 458 | }) 459 | }, callback) 460 | 461 | Framer.settingsCache[key] = frame 462 | } 463 | Framer.settingsCache = {} 464 | 465 | Framer.prototype.ackSettingsFrame = function ackSettingsFrame (callback) { 466 | if (callback) { 467 | process.nextTick(callback) 468 | } 469 | } 470 | 471 | Framer.prototype.windowUpdateFrame = function windowUpdateFrame (frame, 472 | callback) { 473 | this._frame({ 474 | type: 'WINDOW_UPDATE', 475 | id: frame.id, 476 | flags: 0 477 | }, function (buf) { 478 | buf.reserve(8) 479 | 480 | // ID 481 | buf.writeUInt32BE(frame.id & 0x7fffffff) 482 | 483 | // Delta 484 | buf.writeInt32BE(frame.delta) 485 | }, callback) 486 | } 487 | 488 | Framer.prototype.goawayFrame = function goawayFrame (frame, callback) { 489 | this._frame({ 490 | type: 'GOAWAY', 491 | id: 0, 492 | flags: 0 493 | }, function (buf) { 494 | buf.reserve(8) 495 | 496 | // Last-good-stream-ID 497 | buf.writeUInt32BE(frame.lastId & 0x7fffffff) 498 | // Status 499 | buf.writeUInt32BE(constants.goaway[frame.code]) 500 | }, callback) 501 | } 502 | 503 | Framer.prototype.priorityFrame = function priorityFrame (frame, callback) { 504 | // No such thing in SPDY 505 | if (callback) { 506 | process.nextTick(callback) 507 | } 508 | } 509 | 510 | Framer.prototype.xForwardedFor = function xForwardedFor (frame, callback) { 511 | this._frame({ 512 | type: 'X_FORWARDED_FOR', 513 | id: 0, 514 | flags: 0 515 | }, function (buf) { 516 | buf.writeUInt32BE(Buffer.byteLength(frame.host)) 517 | buf.write(frame.host) 518 | }, callback) 519 | } 520 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/spdy/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.name = 'spdy' 4 | 5 | exports.dictionary = require('./dictionary') 6 | exports.constants = require('./constants') 7 | exports.parser = require('./parser') 8 | exports.framer = require('./framer') 9 | exports.compressionPool = require('./zlib-pool') 10 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/spdy/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var parser = exports 4 | 5 | var transport = require('../../../spdy-transport') 6 | var base = transport.protocol.base 7 | var utils = base.utils 8 | var constants = require('./constants') 9 | 10 | var assert = require('assert') 11 | var util = require('util') 12 | var OffsetBuffer = require('obuf') 13 | 14 | function Parser (options) { 15 | base.Parser.call(this, options) 16 | 17 | this.isServer = options.isServer 18 | this.waiting = constants.FRAME_HEADER_SIZE 19 | this.state = 'frame-head' 20 | this.pendingHeader = null 21 | } 22 | util.inherits(Parser, base.Parser) 23 | 24 | parser.create = function create (options) { 25 | return new Parser(options) 26 | } 27 | 28 | Parser.prototype.setMaxFrameSize = function setMaxFrameSize (size) { 29 | // http2-only 30 | } 31 | 32 | Parser.prototype.setMaxHeaderListSize = function setMaxHeaderListSize (size) { 33 | // http2-only 34 | } 35 | 36 | // Only for testing 37 | Parser.prototype.skipPreface = function skipPreface () { 38 | } 39 | 40 | Parser.prototype.execute = function execute (buffer, callback) { 41 | if (this.state === 'frame-head') { return this.onFrameHead(buffer, callback) } 42 | 43 | assert(this.state === 'frame-body' && this.pendingHeader !== null) 44 | 45 | var self = this 46 | var header = this.pendingHeader 47 | this.pendingHeader = null 48 | 49 | this.onFrameBody(header, buffer, function (err, frame) { 50 | if (err) { 51 | return callback(err) 52 | } 53 | 54 | self.state = 'frame-head' 55 | self.waiting = constants.FRAME_HEADER_SIZE 56 | self.partial = false 57 | callback(null, frame) 58 | }) 59 | } 60 | 61 | Parser.prototype.executePartial = function executePartial (buffer, callback) { 62 | var header = this.pendingHeader 63 | 64 | if (this.window) { 65 | this.window.recv.update(-buffer.size) 66 | } 67 | 68 | // DATA frame 69 | callback(null, { 70 | type: 'DATA', 71 | id: header.id, 72 | 73 | // Partial DATA can't be FIN 74 | fin: false, 75 | data: buffer.take(buffer.size) 76 | }) 77 | } 78 | 79 | Parser.prototype.onFrameHead = function onFrameHead (buffer, callback) { 80 | var header = { 81 | control: (buffer.peekUInt8() & 0x80) === 0x80, 82 | version: null, 83 | type: null, 84 | id: null, 85 | flags: null, 86 | length: null 87 | } 88 | 89 | if (header.control) { 90 | header.version = buffer.readUInt16BE() & 0x7fff 91 | header.type = buffer.readUInt16BE() 92 | } else { 93 | header.id = buffer.readUInt32BE(0) & 0x7fffffff 94 | } 95 | header.flags = buffer.readUInt8() 96 | header.length = buffer.readUInt24BE() 97 | 98 | if (this.version === null && header.control) { 99 | // TODO(indutny): do ProtocolError here and in the rest of errors 100 | if (header.version !== 2 && header.version !== 3) { 101 | return callback(new Error('Unsupported SPDY version: ' + header.version)) 102 | } 103 | this.setVersion(header.version) 104 | } 105 | 106 | this.state = 'frame-body' 107 | this.waiting = header.length 108 | this.pendingHeader = header 109 | this.partial = !header.control 110 | 111 | callback(null, null) 112 | } 113 | 114 | Parser.prototype.onFrameBody = function onFrameBody (header, buffer, callback) { 115 | // Data frame 116 | if (!header.control) { 117 | // Count received bytes 118 | if (this.window) { 119 | this.window.recv.update(-buffer.size) 120 | } 121 | 122 | // No support for compressed DATA 123 | if ((header.flags & constants.flags.FLAG_COMPRESSED) !== 0) { 124 | return callback(new Error('DATA compression not supported')) 125 | } 126 | 127 | if (header.id === 0) { 128 | return callback(this.error(constants.error.PROTOCOL_ERROR, 129 | 'Invalid stream id for DATA')) 130 | } 131 | 132 | return callback(null, { 133 | type: 'DATA', 134 | id: header.id, 135 | fin: (header.flags & constants.flags.FLAG_FIN) !== 0, 136 | data: buffer.take(buffer.size) 137 | }) 138 | } 139 | 140 | if (header.type === 0x01 || header.type === 0x02) { // SYN_STREAM or SYN_REPLY 141 | this.onSynHeadFrame(header.type, header.flags, buffer, callback) 142 | } else if (header.type === 0x03) { // RST_STREAM 143 | this.onRSTFrame(buffer, callback) 144 | } else if (header.type === 0x04) { // SETTINGS 145 | this.onSettingsFrame(buffer, callback) 146 | } else if (header.type === 0x05) { 147 | callback(null, { type: 'NOOP' }) 148 | } else if (header.type === 0x06) { // PING 149 | this.onPingFrame(buffer, callback) 150 | } else if (header.type === 0x07) { // GOAWAY 151 | this.onGoawayFrame(buffer, callback) 152 | } else if (header.type === 0x08) { // HEADERS 153 | this.onHeaderFrames(buffer, callback) 154 | } else if (header.type === 0x09) { // WINDOW_UPDATE 155 | this.onWindowUpdateFrame(buffer, callback) 156 | } else if (header.type === 0xf000) { // X-FORWARDED 157 | this.onXForwardedFrame(buffer, callback) 158 | } else { 159 | callback(null, { type: 'unknown: ' + header.type }) 160 | } 161 | } 162 | 163 | Parser.prototype._filterHeader = function _filterHeader (headers, name) { 164 | var res = {} 165 | var keys = Object.keys(headers) 166 | 167 | for (var i = 0; i < keys.length; i++) { 168 | var key = keys[i] 169 | if (key !== name) { 170 | res[key] = headers[key] 171 | } 172 | } 173 | 174 | return res 175 | } 176 | 177 | Parser.prototype.onSynHeadFrame = function onSynHeadFrame (type, 178 | flags, 179 | body, 180 | callback) { 181 | var self = this 182 | var stream = type === 0x01 183 | var offset = stream ? 10 : this.version === 2 ? 6 : 4 184 | 185 | if (!body.has(offset)) { 186 | return callback(new Error('SynHead OOB')) 187 | } 188 | 189 | var head = body.clone(offset) 190 | body.skip(offset) 191 | this.parseKVs(body, function (err, headers) { 192 | if (err) { 193 | return callback(err) 194 | } 195 | 196 | if (stream && 197 | (!headers[':method'] || !headers[':path'])) { 198 | return callback(new Error('Missing `:method` and/or `:path` header')) 199 | } 200 | 201 | var id = head.readUInt32BE() & 0x7fffffff 202 | 203 | if (id === 0) { 204 | return callback(self.error(constants.error.PROTOCOL_ERROR, 205 | 'Invalid stream id for HEADERS')) 206 | } 207 | 208 | var associated = stream ? head.readUInt32BE() & 0x7fffffff : 0 209 | var priority = stream 210 | ? head.readUInt8() >> 5 211 | : utils.weightToPriority(constants.DEFAULT_WEIGHT) 212 | var fin = (flags & constants.flags.FLAG_FIN) !== 0 213 | var unidir = (flags & constants.flags.FLAG_UNIDIRECTIONAL) !== 0 214 | var path = headers[':path'] 215 | 216 | var isPush = stream && associated !== 0 217 | 218 | var weight = utils.priorityToWeight(priority) 219 | var priorityInfo = { 220 | weight: weight, 221 | exclusive: false, 222 | parent: 0 223 | } 224 | 225 | if (!isPush) { 226 | callback(null, { 227 | type: 'HEADERS', 228 | id: id, 229 | priority: priorityInfo, 230 | fin: fin, 231 | writable: !unidir, 232 | headers: headers, 233 | path: path 234 | }) 235 | return 236 | } 237 | 238 | if (stream && !headers[':status']) { 239 | return callback(new Error('Missing `:status` header')) 240 | } 241 | 242 | var filteredHeaders = self._filterHeader(headers, ':status') 243 | 244 | callback(null, [ { 245 | type: 'PUSH_PROMISE', 246 | id: associated, 247 | fin: false, 248 | promisedId: id, 249 | headers: filteredHeaders, 250 | path: path 251 | }, { 252 | type: 'HEADERS', 253 | id: id, 254 | fin: fin, 255 | priority: priorityInfo, 256 | writable: true, 257 | path: undefined, 258 | headers: { 259 | ':status': headers[':status'] 260 | } 261 | }]) 262 | }) 263 | } 264 | 265 | Parser.prototype.onHeaderFrames = function onHeaderFrames (body, callback) { 266 | var offset = this.version === 2 ? 6 : 4 267 | if (!body.has(offset)) { 268 | return callback(new Error('HEADERS OOB')) 269 | } 270 | 271 | var streamId = body.readUInt32BE() & 0x7fffffff 272 | if (this.version === 2) { body.skip(2) } 273 | 274 | this.parseKVs(body, function (err, headers) { 275 | if (err) { return callback(err) } 276 | 277 | callback(null, { 278 | type: 'HEADERS', 279 | priority: { 280 | parent: 0, 281 | exclusive: false, 282 | weight: constants.DEFAULT_WEIGHT 283 | }, 284 | id: streamId, 285 | fin: false, 286 | writable: true, 287 | path: undefined, 288 | headers: headers 289 | }) 290 | }) 291 | } 292 | 293 | Parser.prototype.parseKVs = function parseKVs (buffer, callback) { 294 | var self = this 295 | 296 | this.decompress.write(buffer.toChunks(), function (err, chunks) { 297 | if (err) { 298 | return callback(err) 299 | } 300 | 301 | var buffer = new OffsetBuffer() 302 | for (var i = 0; i < chunks.length; i++) { 303 | buffer.push(chunks[i]) 304 | } 305 | 306 | var size = self.version === 2 ? 2 : 4 307 | if (!buffer.has(size)) { return callback(new Error('KV OOB')) } 308 | 309 | var count = self.version === 2 310 | ? buffer.readUInt16BE() 311 | : buffer.readUInt32BE() 312 | 313 | var headers = {} 314 | 315 | function readString () { 316 | if (!buffer.has(size)) { return null } 317 | var len = self.version === 2 318 | ? buffer.readUInt16BE() 319 | : buffer.readUInt32BE() 320 | 321 | if (!buffer.has(len)) { return null } 322 | 323 | var value = buffer.take(len) 324 | return value.toString() 325 | } 326 | 327 | while (count > 0) { 328 | var key = readString() 329 | var value = readString() 330 | 331 | if (key === null || value === null) { 332 | return callback(new Error('Headers OOB')) 333 | } 334 | 335 | if (self.version < 3) { 336 | var isInternal = /^(method|version|url|host|scheme|status)$/.test(key) 337 | if (key === 'url') { 338 | key = 'path' 339 | } 340 | if (isInternal) { 341 | key = ':' + key 342 | } 343 | } 344 | 345 | // Compatibility with HTTP2 346 | if (key === ':status') { 347 | value = value.split(/ /g, 2)[0] 348 | } 349 | 350 | count-- 351 | if (key === ':host') { 352 | key = ':authority' 353 | } 354 | 355 | // Skip version, not present in HTTP2 356 | if (key === ':version') { 357 | continue 358 | } 359 | 360 | value = value.split(/\0/g) 361 | for (var j = 0; j < value.length; j++) { 362 | utils.addHeaderLine(key, value[j], headers) 363 | } 364 | } 365 | 366 | callback(null, headers) 367 | }) 368 | } 369 | 370 | Parser.prototype.onRSTFrame = function onRSTFrame (body, callback) { 371 | if (!body.has(8)) { return callback(new Error('RST OOB')) } 372 | 373 | var frame = { 374 | type: 'RST', 375 | id: body.readUInt32BE() & 0x7fffffff, 376 | code: constants.errorByCode[body.readUInt32BE()] 377 | } 378 | 379 | if (frame.id === 0) { 380 | return callback(this.error(constants.error.PROTOCOL_ERROR, 381 | 'Invalid stream id for RST')) 382 | } 383 | 384 | if (body.size !== 0) { 385 | frame.extra = body.take(body.size) 386 | } 387 | callback(null, frame) 388 | } 389 | 390 | Parser.prototype.onSettingsFrame = function onSettingsFrame (body, callback) { 391 | if (!body.has(4)) { 392 | return callback(new Error('SETTINGS OOB')) 393 | } 394 | 395 | var settings = {} 396 | var number = body.readUInt32BE() 397 | var idMap = { 398 | 1: 'upload_bandwidth', 399 | 2: 'download_bandwidth', 400 | 3: 'round_trip_time', 401 | 4: 'max_concurrent_streams', 402 | 5: 'current_cwnd', 403 | 6: 'download_retrans_rate', 404 | 7: 'initial_window_size', 405 | 8: 'client_certificate_vector_size' 406 | } 407 | 408 | if (!body.has(number * 8)) { 409 | return callback(new Error('SETTINGS OOB#2')) 410 | } 411 | 412 | for (var i = 0; i < number; i++) { 413 | var id = this.version === 2 414 | ? body.readUInt32LE() 415 | : body.readUInt32BE() 416 | 417 | var flags = (id >> 24) & 0xff 418 | id = id & 0xffffff 419 | 420 | // Skip persisted settings 421 | if (flags & 0x2) { continue } 422 | 423 | var name = idMap[id] 424 | 425 | settings[name] = body.readUInt32BE() 426 | } 427 | 428 | callback(null, { 429 | type: 'SETTINGS', 430 | settings: settings 431 | }) 432 | } 433 | 434 | Parser.prototype.onPingFrame = function onPingFrame (body, callback) { 435 | if (!body.has(4)) { 436 | return callback(new Error('PING OOB')) 437 | } 438 | 439 | var isServer = this.isServer 440 | var opaque = body.clone(body.size).take(body.size) 441 | var id = body.readUInt32BE() 442 | var ack = isServer ? (id % 2 === 0) : (id % 2 === 1) 443 | 444 | callback(null, { type: 'PING', opaque: opaque, ack: ack }) 445 | } 446 | 447 | Parser.prototype.onGoawayFrame = function onGoawayFrame (body, callback) { 448 | if (!body.has(8)) { 449 | return callback(new Error('GOAWAY OOB')) 450 | } 451 | 452 | callback(null, { 453 | type: 'GOAWAY', 454 | lastId: body.readUInt32BE() & 0x7fffffff, 455 | code: constants.goawayByCode[body.readUInt32BE()] 456 | }) 457 | } 458 | 459 | Parser.prototype.onWindowUpdateFrame = function onWindowUpdateFrame (body, 460 | callback) { 461 | if (!body.has(8)) { 462 | return callback(new Error('WINDOW_UPDATE OOB')) 463 | } 464 | 465 | callback(null, { 466 | type: 'WINDOW_UPDATE', 467 | id: body.readUInt32BE() & 0x7fffffff, 468 | delta: body.readInt32BE() 469 | }) 470 | } 471 | 472 | Parser.prototype.onXForwardedFrame = function onXForwardedFrame (body, 473 | callback) { 474 | if (!body.has(4)) { 475 | return callback(new Error('X_FORWARDED OOB')) 476 | } 477 | 478 | var len = body.readUInt32BE() 479 | if (!body.has(len)) { return callback(new Error('X_FORWARDED host length OOB')) } 480 | 481 | callback(null, { 482 | type: 'X_FORWARDED_FOR', 483 | host: body.take(len).toString() 484 | }) 485 | } 486 | -------------------------------------------------------------------------------- /lib/spdy-transport/protocol/spdy/zlib-pool.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var zlibpool = exports 4 | var zlib = require('zlib') 5 | 6 | var transport = require('../../../spdy-transport') 7 | 8 | // TODO(indutny): think about it, why has it always been Z_SYNC_FLUSH here. 9 | // It should be possible to manually flush stuff after the write instead 10 | function createDeflate (version, compression) { 11 | var deflate = zlib.createDeflate({ 12 | dictionary: transport.protocol.spdy.dictionary[version], 13 | flush: zlib.Z_SYNC_FLUSH, 14 | windowBits: 11, 15 | level: compression ? zlib.Z_DEFAULT_COMPRESSION : zlib.Z_NO_COMPRESSION 16 | }) 17 | 18 | // For node.js v0.8 19 | deflate._flush = zlib.Z_SYNC_FLUSH 20 | 21 | return deflate 22 | } 23 | 24 | function createInflate (version) { 25 | var inflate = zlib.createInflate({ 26 | dictionary: transport.protocol.spdy.dictionary[version], 27 | flush: zlib.Z_SYNC_FLUSH 28 | }) 29 | 30 | // For node.js v0.8 31 | inflate._flush = zlib.Z_SYNC_FLUSH 32 | 33 | return inflate 34 | } 35 | 36 | function Pool (compression) { 37 | this.compression = compression 38 | this.pool = { 39 | 2: [], 40 | 3: [], 41 | 3.1: [] 42 | } 43 | } 44 | 45 | zlibpool.create = function create (compression) { 46 | return new Pool(compression) 47 | } 48 | 49 | Pool.prototype.get = function get (version) { 50 | if (this.pool[version].length > 0) { 51 | return this.pool[version].pop() 52 | } else { 53 | var id = version 54 | 55 | return { 56 | version: version, 57 | compress: createDeflate(id, this.compression), 58 | decompress: createInflate(id) 59 | } 60 | } 61 | } 62 | 63 | Pool.prototype.put = function put (pair) { 64 | this.pool[pair.version].push(pair) 65 | } 66 | -------------------------------------------------------------------------------- /lib/spdy-transport/stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transport = require('../spdy-transport') 4 | 5 | var assert = require('assert') 6 | var util = require('util') 7 | 8 | var debug = { 9 | client: require('debug')('spdy:stream:client'), 10 | server: require('debug')('spdy:stream:server') 11 | } 12 | var Duplex = require('readable-stream').Duplex 13 | 14 | function Stream (connection, options) { 15 | Duplex.call(this) 16 | 17 | var connectionState = connection._spdyState 18 | 19 | var state = {} 20 | this._spdyState = state 21 | 22 | this.id = options.id 23 | this.method = options.method 24 | this.path = options.path 25 | this.host = options.host 26 | this.headers = options.headers || {} 27 | this.connection = connection 28 | this.parent = options.parent || null 29 | 30 | state.socket = null 31 | state.protocol = connectionState.protocol 32 | state.constants = state.protocol.constants 33 | 34 | // See _initPriority() 35 | state.priority = null 36 | 37 | state.version = this.connection.getVersion() 38 | state.isServer = this.connection.isServer() 39 | state.debug = state.isServer ? debug.server : debug.client 40 | 41 | state.framer = connectionState.framer 42 | state.parser = connectionState.parser 43 | 44 | state.request = options.request 45 | state.needResponse = options.request 46 | state.window = connectionState.streamWindow.clone(options.id) 47 | state.sessionWindow = connectionState.window 48 | state.maxChunk = connectionState.maxChunk 49 | 50 | // Can't send incoming request 51 | // (See `.send()` method) 52 | state.sent = !state.request 53 | 54 | state.readable = options.readable !== false 55 | state.writable = options.writable !== false 56 | 57 | state.aborted = false 58 | 59 | state.corked = 0 60 | state.corkQueue = [] 61 | 62 | state.timeout = new transport.utils.Timeout(this) 63 | 64 | this.on('finish', this._onFinish) 65 | this.on('end', this._onEnd) 66 | 67 | var self = this 68 | function _onWindowOverflow () { 69 | self._onWindowOverflow() 70 | } 71 | 72 | state.window.recv.on('overflow', _onWindowOverflow) 73 | state.window.send.on('overflow', _onWindowOverflow) 74 | 75 | this._initPriority(options.priority) 76 | 77 | if (!state.readable) { this.push(null) } 78 | if (!state.writable) { 79 | this._writableState.ended = true 80 | this._writableState.finished = true 81 | } 82 | } 83 | util.inherits(Stream, Duplex) 84 | exports.Stream = Stream 85 | 86 | Stream.prototype._init = function _init (socket) { 87 | this.socket = socket 88 | } 89 | 90 | Stream.prototype._initPriority = function _initPriority (priority) { 91 | var state = this._spdyState 92 | var connectionState = this.connection._spdyState 93 | var root = connectionState.priorityRoot 94 | 95 | if (!priority) { 96 | state.priority = root.addDefault(this.id) 97 | return 98 | } 99 | 100 | state.priority = root.add({ 101 | id: this.id, 102 | parent: priority.parent, 103 | weight: priority.weight, 104 | exclusive: priority.exclusive 105 | }) 106 | } 107 | 108 | Stream.prototype._handleFrame = function _handleFrame (frame) { 109 | var state = this._spdyState 110 | 111 | // Ignore any kind of data after abort 112 | if (state.aborted) { 113 | state.debug('id=%d ignoring frame=%s after abort', this.id, frame.type) 114 | return 115 | } 116 | 117 | // Restart the timer on incoming frames 118 | state.timeout.reset() 119 | 120 | if (frame.type === 'DATA') { 121 | this._handleData(frame) 122 | } else if (frame.type === 'HEADERS') { 123 | this._handleHeaders(frame) 124 | } else if (frame.type === 'RST') { 125 | this._handleRST(frame) 126 | } else if (frame.type === 'WINDOW_UPDATE') { this._handleWindowUpdate(frame) } else if (frame.type === 'PRIORITY') { 127 | this._handlePriority(frame) 128 | } else if (frame.type === 'PUSH_PROMISE') { this._handlePushPromise(frame) } 129 | 130 | if (frame.fin) { 131 | state.debug('id=%d end', this.id) 132 | this.push(null) 133 | } 134 | } 135 | 136 | function checkAborted (stream, state, callback) { 137 | if (state.aborted) { 138 | state.debug('id=%d abort write', stream.id) 139 | process.nextTick(function () { 140 | callback(new Error('Stream write aborted')) 141 | }) 142 | return true 143 | } 144 | 145 | return false 146 | } 147 | 148 | function _send (stream, state, data, callback) { 149 | if (checkAborted(stream, state, callback)) { 150 | return 151 | } 152 | 153 | state.debug('id=%d presend=%d', stream.id, data.length) 154 | 155 | state.timeout.reset() 156 | 157 | state.window.send.update(-data.length, function () { 158 | if (checkAborted(stream, state, callback)) { 159 | return 160 | } 161 | 162 | state.debug('id=%d send=%d', stream.id, data.length) 163 | 164 | state.timeout.reset() 165 | 166 | state.framer.dataFrame({ 167 | id: stream.id, 168 | priority: state.priority.getPriority(), 169 | fin: false, 170 | data: data 171 | }, function (err) { 172 | state.debug('id=%d postsend=%d', stream.id, data.length) 173 | callback(err) 174 | }) 175 | }) 176 | } 177 | 178 | Stream.prototype._write = function _write (data, enc, callback) { 179 | var state = this._spdyState 180 | 181 | // Send the request if it wasn't sent 182 | if (!state.sent) { this.send() } 183 | 184 | // Writes should come after pending control frames (response and headers) 185 | if (state.corked !== 0) { 186 | var self = this 187 | state.corkQueue.push(function () { 188 | self._write(data, enc, callback) 189 | }) 190 | return 191 | } 192 | 193 | // Split DATA in chunks to prevent window from going negative 194 | this._splitStart(data, _send, callback) 195 | } 196 | 197 | Stream.prototype._splitStart = function _splitStart (data, onChunk, callback) { 198 | return this._split(data, 0, onChunk, callback) 199 | } 200 | 201 | Stream.prototype._split = function _split (data, offset, onChunk, callback) { 202 | if (offset === data.length) { 203 | return process.nextTick(callback) 204 | } 205 | 206 | var state = this._spdyState 207 | var local = state.window.send 208 | var session = state.sessionWindow.send 209 | 210 | var availSession = Math.max(0, session.getCurrent()) 211 | if (availSession === 0) { 212 | availSession = session.getMax() 213 | } 214 | var availLocal = Math.max(0, local.getCurrent()) 215 | if (availLocal === 0) { 216 | availLocal = local.getMax() 217 | } 218 | 219 | var avail = Math.min(availSession, availLocal) 220 | avail = Math.min(avail, state.maxChunk) 221 | 222 | var self = this 223 | 224 | if (avail === 0) { 225 | state.window.send.update(0, function () { 226 | self._split(data, offset, onChunk, callback) 227 | }) 228 | return 229 | } 230 | 231 | // Split data in chunks in a following way: 232 | var limit = avail 233 | var size = Math.min(data.length - offset, limit) 234 | 235 | var chunk = data.slice(offset, offset + size) 236 | 237 | onChunk(this, state, chunk, function (err) { 238 | if (err) { return callback(err) } 239 | 240 | // Get the next chunk 241 | self._split(data, offset + size, onChunk, callback) 242 | }) 243 | } 244 | 245 | Stream.prototype._read = function _read () { 246 | var state = this._spdyState 247 | 248 | if (!state.window.recv.isDraining()) { 249 | return 250 | } 251 | 252 | var delta = state.window.recv.getDelta() 253 | 254 | state.debug('id=%d window emptying, update by %d', this.id, delta) 255 | 256 | state.window.recv.update(delta) 257 | state.framer.windowUpdateFrame({ 258 | id: this.id, 259 | delta: delta 260 | }) 261 | } 262 | 263 | Stream.prototype._handleData = function _handleData (frame) { 264 | var state = this._spdyState 265 | 266 | // DATA on ended or not readable stream! 267 | if (!state.readable || this._readableState.ended) { 268 | state.framer.rstFrame({ id: this.id, code: 'STREAM_CLOSED' }) 269 | return 270 | } 271 | 272 | state.debug('id=%d recv=%d', this.id, frame.data.length) 273 | state.window.recv.update(-frame.data.length) 274 | 275 | this.push(frame.data) 276 | } 277 | 278 | Stream.prototype._handleRST = function _handleRST (frame) { 279 | if (frame.code !== 'CANCEL') { 280 | this.emit('error', new Error('Got RST: ' + frame.code)) 281 | } 282 | this.abort() 283 | } 284 | 285 | Stream.prototype._handleWindowUpdate = function _handleWindowUpdate (frame) { 286 | var state = this._spdyState 287 | 288 | state.window.send.update(frame.delta) 289 | } 290 | 291 | Stream.prototype._onWindowOverflow = function _onWindowOverflow () { 292 | var state = this._spdyState 293 | 294 | state.debug('id=%d window overflow', this.id) 295 | state.framer.rstFrame({ id: this.id, code: 'FLOW_CONTROL_ERROR' }) 296 | 297 | this.aborted = true 298 | this.emit('error', new Error('HTTP2 window overflow')) 299 | } 300 | 301 | Stream.prototype._handlePriority = function _handlePriority (frame) { 302 | var state = this._spdyState 303 | 304 | state.priority.remove() 305 | state.priority = null 306 | this._initPriority(frame.priority) 307 | 308 | // Mostly for testing purposes 309 | this.emit('priority', frame.priority) 310 | } 311 | 312 | Stream.prototype._handleHeaders = function _handleHeaders (frame) { 313 | var state = this._spdyState 314 | 315 | if (!state.readable || this._readableState.ended) { 316 | state.framer.rstFrame({ id: this.id, code: 'STREAM_CLOSED' }) 317 | return 318 | } 319 | 320 | if (state.needResponse) { 321 | return this._handleResponse(frame) 322 | } 323 | 324 | this.emit('headers', frame.headers) 325 | } 326 | 327 | Stream.prototype._handleResponse = function _handleResponse (frame) { 328 | var state = this._spdyState 329 | 330 | if (frame.headers[':status'] === undefined) { 331 | state.framer.rstFrame({ id: this.id, code: 'PROTOCOL_ERROR' }) 332 | return 333 | } 334 | 335 | state.needResponse = false 336 | this.emit('response', frame.headers[':status'] | 0, frame.headers) 337 | } 338 | 339 | Stream.prototype._onFinish = function _onFinish () { 340 | var state = this._spdyState 341 | 342 | // Send the request if it wasn't sent 343 | if (!state.sent) { 344 | // NOTE: will send HEADERS with FIN flag 345 | this.send() 346 | } else { 347 | // Just an `.end()` without any writes will trigger immediate `finish` event 348 | // without any calls to `_write()`. 349 | if (state.corked !== 0) { 350 | var self = this 351 | state.corkQueue.push(function () { 352 | self._onFinish() 353 | }) 354 | return 355 | } 356 | 357 | state.framer.dataFrame({ 358 | id: this.id, 359 | priority: state.priority.getPriority(), 360 | fin: true, 361 | data: Buffer.alloc(0) 362 | }) 363 | } 364 | 365 | this._maybeClose() 366 | } 367 | 368 | Stream.prototype._onEnd = function _onEnd () { 369 | this._maybeClose() 370 | } 371 | 372 | Stream.prototype._checkEnded = function _checkEnded (callback) { 373 | var state = this._spdyState 374 | 375 | var ended = false 376 | if (state.aborted) { ended = true } 377 | 378 | if (!state.writable || this._writableState.finished) { ended = true } 379 | 380 | if (!ended) { 381 | return true 382 | } 383 | 384 | if (!callback) { 385 | return false 386 | } 387 | 388 | var err = new Error('Ended stream can\'t send frames') 389 | process.nextTick(function () { 390 | callback(err) 391 | }) 392 | 393 | return false 394 | } 395 | 396 | Stream.prototype._maybeClose = function _maybeClose () { 397 | var state = this._spdyState 398 | 399 | // .abort() emits `close` 400 | if (state.aborted) { 401 | return 402 | } 403 | 404 | if ((!state.readable || this._readableState.ended) && 405 | this._writableState.finished) { 406 | // Clear timeout 407 | state.timeout.set(0) 408 | 409 | this.emit('close') 410 | } 411 | } 412 | 413 | Stream.prototype._handlePushPromise = function _handlePushPromise (frame) { 414 | var push = this.connection._createStream({ 415 | id: frame.promisedId, 416 | parent: this, 417 | push: true, 418 | request: true, 419 | method: frame.headers[':method'], 420 | path: frame.headers[':path'], 421 | host: frame.headers[':authority'], 422 | priority: frame.priority, 423 | headers: frame.headers, 424 | writable: false 425 | }) 426 | 427 | // GOAWAY 428 | if (this.connection._isGoaway(push.id)) { 429 | return 430 | } 431 | 432 | if (!this.emit('pushPromise', push)) { 433 | push.abort() 434 | } 435 | } 436 | 437 | Stream.prototype._hardCork = function _hardCork () { 438 | var state = this._spdyState 439 | 440 | this.cork() 441 | state.corked++ 442 | } 443 | 444 | Stream.prototype._hardUncork = function _hardUncork () { 445 | var state = this._spdyState 446 | 447 | this.uncork() 448 | state.corked-- 449 | if (state.corked !== 0) { 450 | return 451 | } 452 | 453 | // Invoke callbacks 454 | var queue = state.corkQueue 455 | state.corkQueue = [] 456 | for (var i = 0; i < queue.length; i++) { 457 | queue[i]() 458 | } 459 | } 460 | 461 | Stream.prototype._sendPush = function _sendPush (status, response, callback) { 462 | var self = this 463 | var state = this._spdyState 464 | 465 | this._hardCork() 466 | state.framer.pushFrame({ 467 | id: this.parent.id, 468 | promisedId: this.id, 469 | priority: state.priority.toJSON(), 470 | path: this.path, 471 | host: this.host, 472 | method: this.method, 473 | status: status, 474 | headers: this.headers, 475 | response: response 476 | }, function (err) { 477 | self._hardUncork() 478 | 479 | callback(err) 480 | }) 481 | } 482 | 483 | Stream.prototype._wasSent = function _wasSent () { 484 | var state = this._spdyState 485 | return state.sent 486 | } 487 | 488 | // Public API 489 | 490 | Stream.prototype.send = function send (callback) { 491 | var state = this._spdyState 492 | 493 | if (state.sent) { 494 | var err = new Error('Stream was already sent') 495 | process.nextTick(function () { 496 | if (callback) { 497 | callback(err) 498 | } 499 | }) 500 | return 501 | } 502 | 503 | state.sent = true 504 | state.timeout.reset() 505 | 506 | // GET requests should always be auto-finished 507 | if (this.method === 'GET') { 508 | this._writableState.ended = true 509 | this._writableState.finished = true 510 | } 511 | 512 | // TODO(indunty): ideally it should just take a stream object as an input 513 | var self = this 514 | this._hardCork() 515 | state.framer.requestFrame({ 516 | id: this.id, 517 | method: this.method, 518 | path: this.path, 519 | host: this.host, 520 | priority: state.priority.toJSON(), 521 | headers: this.headers, 522 | fin: this._writableState.finished 523 | }, function (err) { 524 | self._hardUncork() 525 | 526 | if (!callback) { 527 | return 528 | } 529 | 530 | callback(err) 531 | }) 532 | } 533 | 534 | Stream.prototype.respond = function respond (status, headers, callback) { 535 | var self = this 536 | var state = this._spdyState 537 | assert(!state.request, 'Can\'t respond on request') 538 | 539 | state.timeout.reset() 540 | 541 | if (!this._checkEnded(callback)) { return } 542 | 543 | var frame = { 544 | id: this.id, 545 | status: status, 546 | headers: headers 547 | } 548 | this._hardCork() 549 | state.framer.responseFrame(frame, function (err) { 550 | self._hardUncork() 551 | if (callback) { callback(err) } 552 | }) 553 | } 554 | 555 | Stream.prototype.setWindow = function setWindow (size) { 556 | var state = this._spdyState 557 | 558 | state.timeout.reset() 559 | 560 | if (!this._checkEnded()) { 561 | return 562 | } 563 | 564 | state.debug('id=%d force window max=%d', this.id, size) 565 | state.window.recv.setMax(size) 566 | 567 | var delta = state.window.recv.getDelta() 568 | if (delta === 0) { return } 569 | 570 | state.framer.windowUpdateFrame({ 571 | id: this.id, 572 | delta: delta 573 | }) 574 | state.window.recv.update(delta) 575 | } 576 | 577 | Stream.prototype.sendHeaders = function sendHeaders (headers, callback) { 578 | var self = this 579 | var state = this._spdyState 580 | 581 | state.timeout.reset() 582 | 583 | if (!this._checkEnded(callback)) { 584 | return 585 | } 586 | 587 | // Request wasn't yet send, coalesce headers 588 | if (!state.sent) { 589 | this.headers = Object.assign({}, this.headers) 590 | Object.assign(this.headers, headers) 591 | process.nextTick(function () { 592 | if (callback) { 593 | callback(null) 594 | } 595 | }) 596 | return 597 | } 598 | 599 | this._hardCork() 600 | state.framer.headersFrame({ 601 | id: this.id, 602 | headers: headers 603 | }, function (err) { 604 | self._hardUncork() 605 | if (callback) { callback(err) } 606 | }) 607 | } 608 | 609 | Stream.prototype._destroy = function destroy () { 610 | this.abort() 611 | } 612 | 613 | Stream.prototype.abort = function abort (code, callback) { 614 | var state = this._spdyState 615 | 616 | // .abort(callback) 617 | if (typeof code === 'function') { 618 | callback = code 619 | code = null 620 | } 621 | 622 | if (this._readableState.ended && this._writableState.finished) { 623 | state.debug('id=%d already closed', this.id) 624 | if (callback) { 625 | process.nextTick(callback) 626 | } 627 | return 628 | } 629 | 630 | if (state.aborted) { 631 | state.debug('id=%d already aborted', this.id) 632 | if (callback) { process.nextTick(callback) } 633 | return 634 | } 635 | 636 | state.aborted = true 637 | state.debug('id=%d abort', this.id) 638 | 639 | this.setTimeout(0) 640 | 641 | var abortCode = code || 'CANCEL' 642 | 643 | state.framer.rstFrame({ 644 | id: this.id, 645 | code: abortCode 646 | }) 647 | 648 | var self = this 649 | process.nextTick(function () { 650 | if (callback) { 651 | callback(null) 652 | } 653 | self.emit('close', new Error('Aborted, code: ' + abortCode)) 654 | }) 655 | } 656 | 657 | Stream.prototype.setPriority = function setPriority (info) { 658 | var state = this._spdyState 659 | 660 | state.timeout.reset() 661 | 662 | if (!this._checkEnded()) { 663 | return 664 | } 665 | 666 | state.debug('id=%d priority change', this.id, info) 667 | 668 | var frame = { id: this.id, priority: info } 669 | 670 | // Change priority on this side 671 | this._handlePriority(frame) 672 | 673 | // And on the other too 674 | state.framer.priorityFrame(frame) 675 | } 676 | 677 | Stream.prototype.pushPromise = function pushPromise (uri, callback) { 678 | if (!this._checkEnded(callback)) { 679 | return 680 | } 681 | 682 | var self = this 683 | this._hardCork() 684 | var push = this.connection.pushPromise(this, uri, function (err) { 685 | self._hardUncork() 686 | if (!err) { 687 | push._hardUncork() 688 | } 689 | 690 | if (callback) { 691 | return callback(err, push) 692 | } 693 | 694 | if (err) { push.emit('error', err) } 695 | }) 696 | push._hardCork() 697 | 698 | return push 699 | } 700 | 701 | Stream.prototype.setMaxChunk = function setMaxChunk (size) { 702 | var state = this._spdyState 703 | state.maxChunk = size 704 | } 705 | 706 | Stream.prototype.setTimeout = function setTimeout (delay, callback) { 707 | var state = this._spdyState 708 | 709 | state.timeout.set(delay, callback) 710 | } 711 | -------------------------------------------------------------------------------- /lib/spdy-transport/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var util = require('util') 4 | var isNode = require('detect-node') 5 | 6 | // Node.js 0.8, 0.10 and 0.12 support 7 | Object.assign = (process.versions.modules >= 46 || !isNode) 8 | ? Object.assign // eslint-disable-next-line 9 | : util._extend 10 | 11 | function QueueItem () { 12 | this.prev = null 13 | this.next = null 14 | } 15 | exports.QueueItem = QueueItem 16 | 17 | function Queue () { 18 | QueueItem.call(this) 19 | 20 | this.prev = this 21 | this.next = this 22 | } 23 | util.inherits(Queue, QueueItem) 24 | exports.Queue = Queue 25 | 26 | Queue.prototype.insertTail = function insertTail (item) { 27 | item.prev = this.prev 28 | item.next = this 29 | item.prev.next = item 30 | item.next.prev = item 31 | } 32 | 33 | Queue.prototype.remove = function remove (item) { 34 | var next = item.next 35 | var prev = item.prev 36 | 37 | item.next = item 38 | item.prev = item 39 | next.prev = prev 40 | prev.next = next 41 | } 42 | 43 | Queue.prototype.head = function head () { 44 | return this.next 45 | } 46 | 47 | Queue.prototype.tail = function tail () { 48 | return this.prev 49 | } 50 | 51 | Queue.prototype.isEmpty = function isEmpty () { 52 | return this.next === this 53 | } 54 | 55 | Queue.prototype.isRoot = function isRoot (item) { 56 | return this === item 57 | } 58 | 59 | function LockStream (stream) { 60 | this.locked = false 61 | this.queue = [] 62 | this.stream = stream 63 | } 64 | exports.LockStream = LockStream 65 | 66 | LockStream.prototype.write = function write (chunks, callback) { 67 | var self = this 68 | 69 | // Do not let it interleave 70 | if (this.locked) { 71 | this.queue.push(function () { 72 | return self.write(chunks, callback) 73 | }) 74 | return 75 | } 76 | 77 | this.locked = true 78 | 79 | function done (err, chunks) { 80 | self.stream.removeListener('error', done) 81 | 82 | self.locked = false 83 | if (self.queue.length > 0) { self.queue.shift()() } 84 | callback(err, chunks) 85 | } 86 | 87 | this.stream.on('error', done) 88 | 89 | // Accumulate all output data 90 | var output = [] 91 | function onData (chunk) { 92 | output.push(chunk) 93 | } 94 | this.stream.on('data', onData) 95 | 96 | function next (err) { 97 | self.stream.removeListener('data', onData) 98 | if (err) { 99 | return done(err) 100 | } 101 | 102 | done(null, output) 103 | } 104 | 105 | for (var i = 0; i < chunks.length - 1; i++) { this.stream.write(chunks[i]) } 106 | 107 | if (chunks.length > 0) { 108 | this.stream.write(chunks[i], next) 109 | } else { process.nextTick(next) } 110 | 111 | if (this.stream.execute) { 112 | this.stream.execute(function (err) { 113 | if (err) { return done(err) } 114 | }) 115 | } 116 | } 117 | 118 | // Just finds the place in array to insert 119 | function binaryLookup (list, item, compare) { 120 | var start = 0 121 | var end = list.length 122 | 123 | while (start < end) { 124 | var pos = (start + end) >> 1 125 | var cmp = compare(item, list[pos]) 126 | 127 | if (cmp === 0) { 128 | start = pos 129 | end = pos 130 | break 131 | } else if (cmp < 0) { 132 | end = pos 133 | } else { 134 | start = pos + 1 135 | } 136 | } 137 | 138 | return start 139 | } 140 | exports.binaryLookup = binaryLookup 141 | 142 | function binaryInsert (list, item, compare) { 143 | var index = binaryLookup(list, item, compare) 144 | 145 | list.splice(index, 0, item) 146 | } 147 | exports.binaryInsert = binaryInsert 148 | 149 | function binarySearch (list, item, compare) { 150 | var index = binaryLookup(list, item, compare) 151 | 152 | if (index >= list.length) { 153 | return -1 154 | } 155 | 156 | if (compare(item, list[index]) === 0) { 157 | return index 158 | } 159 | 160 | return -1 161 | } 162 | exports.binarySearch = binarySearch 163 | 164 | function Timeout (object) { 165 | this.delay = 0 166 | this.timer = null 167 | this.object = object 168 | } 169 | exports.Timeout = Timeout 170 | 171 | Timeout.prototype.set = function set (delay, callback) { 172 | this.delay = delay 173 | this.reset() 174 | if (!callback) { return } 175 | 176 | if (this.delay === 0) { 177 | this.object.removeListener('timeout', callback) 178 | } else { 179 | this.object.once('timeout', callback) 180 | } 181 | } 182 | 183 | Timeout.prototype.reset = function reset () { 184 | if (this.timer !== null) { 185 | clearTimeout(this.timer) 186 | this.timer = null 187 | } 188 | 189 | if (this.delay === 0) { return } 190 | 191 | var self = this 192 | this.timer = setTimeout(function () { 193 | self.timer = null 194 | self.object.emit('timeout') 195 | }, this.delay) 196 | } 197 | -------------------------------------------------------------------------------- /lib/spdy-transport/window.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var util = require('util') 4 | var EventEmitter = require('events').EventEmitter 5 | var debug = { 6 | server: require('debug')('spdy:window:server'), 7 | client: require('debug')('spdy:window:client') 8 | } 9 | 10 | function Side (window, name, options) { 11 | EventEmitter.call(this) 12 | 13 | this.name = name 14 | this.window = window 15 | this.current = options.size 16 | this.max = options.size 17 | this.limit = options.max 18 | this.lowWaterMark = options.lowWaterMark === undefined 19 | ? this.max / 2 20 | : options.lowWaterMark 21 | 22 | this._refilling = false 23 | this._refillQueue = [] 24 | } 25 | util.inherits(Side, EventEmitter) 26 | 27 | Side.prototype.setMax = function setMax (max) { 28 | this.window.debug('id=%d side=%s setMax=%d', 29 | this.window.id, 30 | this.name, 31 | max) 32 | this.max = max 33 | this.lowWaterMark = this.max / 2 34 | } 35 | 36 | Side.prototype.updateMax = function updateMax (max) { 37 | var delta = max - this.max 38 | this.window.debug('id=%d side=%s updateMax=%d delta=%d', 39 | this.window.id, 40 | this.name, 41 | max, 42 | delta) 43 | 44 | this.max = max 45 | this.lowWaterMark = max / 2 46 | 47 | this.update(delta) 48 | } 49 | 50 | Side.prototype.setLowWaterMark = function setLowWaterMark (lwm) { 51 | this.lowWaterMark = lwm 52 | } 53 | 54 | Side.prototype.update = function update (size, callback) { 55 | // Not enough space for the update, wait for refill 56 | if (size <= 0 && callback && this.isEmpty()) { 57 | this.window.debug('id=%d side=%s wait for refill=%d [%d/%d]', 58 | this.window.id, 59 | this.name, 60 | -size, 61 | this.current, 62 | this.max) 63 | this._refillQueue.push({ 64 | size: size, 65 | callback: callback 66 | }) 67 | return 68 | } 69 | 70 | this.current += size 71 | 72 | if (this.current > this.limit) { 73 | this.emit('overflow') 74 | return 75 | } 76 | 77 | this.window.debug('id=%d side=%s update by=%d [%d/%d]', 78 | this.window.id, 79 | this.name, 80 | size, 81 | this.current, 82 | this.max) 83 | 84 | // Time to send WINDOW_UPDATE 85 | if (size < 0 && this.isDraining()) { 86 | this.window.debug('id=%d side=%s drained', this.window.id, this.name) 87 | this.emit('drain') 88 | } 89 | 90 | // Time to write 91 | if (size > 0 && this.current > 0 && this.current <= size) { 92 | this.window.debug('id=%d side=%s full', this.window.id, this.name) 93 | this.emit('full') 94 | } 95 | 96 | this._processRefillQueue() 97 | 98 | if (callback) { process.nextTick(callback) } 99 | } 100 | 101 | Side.prototype.getCurrent = function getCurrent () { 102 | return this.current 103 | } 104 | 105 | Side.prototype.getMax = function getMax () { 106 | return this.max 107 | } 108 | 109 | Side.prototype.getDelta = function getDelta () { 110 | return this.max - this.current 111 | } 112 | 113 | Side.prototype.isDraining = function isDraining () { 114 | return this.current <= this.lowWaterMark 115 | } 116 | 117 | Side.prototype.isEmpty = function isEmpty () { 118 | return this.current <= 0 119 | } 120 | 121 | // Private 122 | 123 | Side.prototype._processRefillQueue = function _processRefillQueue () { 124 | // Prevent recursion 125 | if (this._refilling) { 126 | return 127 | } 128 | this._refilling = true 129 | 130 | while (this._refillQueue.length > 0) { 131 | var item = this._refillQueue[0] 132 | 133 | if (this.isEmpty()) { 134 | break 135 | } 136 | 137 | this.window.debug('id=%d side=%s refilled for size=%d', 138 | this.window.id, 139 | this.name, 140 | -item.size) 141 | 142 | this._refillQueue.shift() 143 | this.update(item.size, item.callback) 144 | } 145 | 146 | this._refilling = false 147 | } 148 | 149 | function Window (options) { 150 | this.id = options.id 151 | this.isServer = options.isServer 152 | this.debug = this.isServer ? debug.server : debug.client 153 | 154 | this.recv = new Side(this, 'recv', options.recv) 155 | this.send = new Side(this, 'send', options.send) 156 | } 157 | module.exports = Window 158 | 159 | Window.prototype.clone = function clone (id) { 160 | return new Window({ 161 | id: id, 162 | isServer: this.isServer, 163 | recv: { 164 | size: this.recv.max, 165 | max: this.recv.limit, 166 | lowWaterMark: this.recv.lowWaterMark 167 | }, 168 | send: { 169 | size: this.send.max, 170 | max: this.send.limit, 171 | lowWaterMark: this.send.lowWaterMark 172 | } 173 | }) 174 | } 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spdy-transport", 3 | "version": "3.0.0", 4 | "main": "lib/spdy-transport", 5 | "files": [ 6 | "lib" 7 | ], 8 | "description": "SPDY v2, v3, v3.1 and HTTP2 transport", 9 | "license": "MIT", 10 | "keywords": [ 11 | "spdy", 12 | "http2", 13 | "transport" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/spdy-http2/spdy-transport.git" 18 | }, 19 | "homepage": "https://github.com/spdy-http2/spdy-transport", 20 | "author": "Fedor Indutny ", 21 | "dependencies": { 22 | "debug": "^4.1.0", 23 | "detect-node": "^2.0.4", 24 | "hpack.js": "^2.1.6", 25 | "obuf": "^1.1.2", 26 | "readable-stream": "^3.0.6", 27 | "wbuf": "^1.7.3" 28 | }, 29 | "devDependencies": { 30 | "async": "^2.6.1", 31 | "istanbul": "^0.4.5", 32 | "mocha": "^5.2.0", 33 | "pre-commit": "^1.2.2", 34 | "standard": "^12.0.1", 35 | "stream-pair": "^1.0.3" 36 | }, 37 | "scripts": { 38 | "lint": "standard", 39 | "test": "mocha --reporter=spec test/**/*-test.js test/**/**/*-test.js", 40 | "coverage": "istanbul cover node_modules/.bin/_mocha -- --reporter=spec test/**/*-test.js test/**/**/*-test.js" 41 | }, 42 | "pre-commit": [ 43 | "lint", 44 | "test" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /test/base/priority-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | 5 | var transport = require('../../') 6 | 7 | describe('Stream Priority tree', function () { 8 | var tree 9 | beforeEach(function () { 10 | tree = transport.Priority.create({}) 11 | }) 12 | 13 | it('should create basic tree', function () { 14 | // 0 15 | // [1 p=2] [2 p=4] [3 p=2] 16 | // [4 p=2] [5 p=2] 17 | 18 | tree.add({ id: 1, parent: 0, weight: 2 }) 19 | tree.add({ id: 5, parent: 1, weight: 2 }) 20 | tree.add({ id: 2, parent: 0, weight: 4 }) 21 | tree.add({ id: 3, parent: 0, weight: 2 }) 22 | tree.add({ id: 4, parent: 1, weight: 2 }) 23 | 24 | assert.deepStrictEqual([ 1, 2, 3, 4, 5 ].map(function (id) { 25 | return tree.get(id).priority 26 | }), [ 0.25, 0.5, 0.25, 0.125, 0.125 ]) 27 | 28 | // Ranges 29 | assert.deepStrictEqual([ 1, 2, 3, 4, 5 ].map(function (id) { 30 | return tree.get(id).getPriorityRange() 31 | }), [ 32 | // First level 33 | { from: 0.0, to: 0.25 }, 34 | { from: 0.5, to: 1.0 }, 35 | { from: 0.25, to: 0.5 }, 36 | 37 | // Second level 38 | { from: 0, to: 0.125 }, 39 | { from: 0.125, to: 0.25 } 40 | ]) 41 | }) 42 | 43 | it('should create default node on error', function () { 44 | var node1 = tree.add({ id: 1, parent: 1 }) 45 | assert.strictEqual(node1.parent.id, 0) 46 | assert.strictEqual(node1.weight, tree.defaultWeight) 47 | 48 | var node2 = tree.add({ id: 1, parent: 3 }) 49 | assert.strictEqual(node2.parent.id, 0) 50 | assert.strictEqual(node2.weight, tree.defaultWeight) 51 | }) 52 | 53 | it('should remove empty node', function () { 54 | var node = tree.add({ id: 1, parent: 0, weight: 1 }) 55 | assert(tree.get(1) !== undefined) 56 | assert.strictEqual(tree.count, 2) 57 | node.remove() 58 | assert(tree.get(1) === undefined) 59 | assert.strictEqual(tree.count, 1) 60 | }) 61 | 62 | it('should move children to parent node on removal', function () { 63 | // Tree from the first test 64 | var one = tree.add({ id: 1, parent: 0, weight: 2 }) 65 | tree.add({ id: 5, parent: 1, weight: 2 }) 66 | tree.add({ id: 2, parent: 0, weight: 4 }) 67 | tree.add({ id: 3, parent: 0, weight: 2 }) 68 | tree.add({ id: 4, parent: 1, weight: 2 }) 69 | 70 | assert.strictEqual(tree.count, 6) 71 | one.remove() 72 | assert(tree.get(1) === undefined) 73 | assert.strictEqual(tree.count, 5) 74 | 75 | assert.deepStrictEqual([ 2, 3, 4, 5 ].map(function (id) { 76 | return tree.get(id).priority 77 | }), [ 0.4, 0.2, 0.2, 0.19999999999999996 ]) 78 | }) 79 | 80 | it('should move children on exclusive addition', function () { 81 | // 0 82 | // / \ 83 | // 1 2 84 | // / | \ 85 | // 3 4 5 86 | tree.add({ id: 1, parent: 0, weight: 2 }) 87 | tree.add({ id: 2, parent: 0, weight: 2 }) 88 | tree.add({ id: 3, parent: 1, weight: 4 }) 89 | tree.add({ id: 4, parent: 1, weight: 2 }) 90 | tree.add({ id: 5, parent: 1, weight: 2 }) 91 | 92 | assert.deepStrictEqual([ 1, 2, 3, 4, 5 ].map(function (id) { 93 | return tree.get(id).priority 94 | }), [ 0.5, 0.5, 0.25, 0.125, 0.125 ]) 95 | 96 | // 0 97 | // / \ 98 | // 1 2 99 | // | 100 | // 6 101 | // / | \ 102 | // 3 4 5 103 | tree.add({ id: 6, parent: 1, exclusive: true, weight: 2 }) 104 | 105 | assert.deepStrictEqual([ 1, 2, 3, 4, 5, 6 ].map(function (id) { 106 | return tree.get(id).priority 107 | }), [ 0.5, 0.5, 0.25, 0.125, 0.125, 0.5 ]) 108 | }) 109 | 110 | it('should remove excessive nodes on hitting maximum', function () { 111 | tree = transport.Priority.create({ 112 | maxCount: 6 113 | }) 114 | 115 | // 0 116 | // / \ 117 | // 1 2 118 | // / | \ 119 | // 3 4 5 120 | tree.add({ id: 1, parent: 0, weight: 2 }) 121 | tree.add({ id: 2, parent: 0, weight: 2 }) 122 | tree.add({ id: 3, parent: 1, weight: 4 }) 123 | tree.add({ id: 4, parent: 1, weight: 2 }) 124 | tree.add({ id: 5, parent: 1, weight: 2 }) 125 | 126 | // 0 127 | // / \ 128 | // 6 2 129 | // / | \ 130 | // 3 4 5 131 | tree.add({ id: 6, parent: 1, exclusive: true, weight: 2 }) 132 | 133 | assert.strictEqual(tree.get(1), undefined) 134 | assert.deepStrictEqual([ 2, 3, 4, 5, 6 ].map(function (id) { 135 | return tree.get(id).priority 136 | }), [ 0.5, 0.25, 0.125, 0.125, 0.5 ]) 137 | 138 | // This should not throw when removing ex-child of node swapped by 139 | // exclusive one 140 | tree.add({ id: 7, parent: 5, exclusive: false, weight: 2 }) 141 | tree.add({ id: 8, parent: 5, exclusive: false, weight: 2 }) 142 | }) 143 | 144 | it('should use default weight', function () { 145 | tree.add({ id: 1, parent: 0 }) 146 | 147 | assert.strictEqual(tree.get(1).weight, 16) 148 | }) 149 | 150 | it('should create default node', function () { 151 | tree.addDefault(1) 152 | 153 | assert.strictEqual(tree.get(1).weight, 16) 154 | }) 155 | 156 | it('Removing a node should remove it from the tree\'s list', function () { 157 | tree.addDefault(1) 158 | 159 | tree.get(1).remove() 160 | 161 | assert.strictEqual(tree.list[0], undefined) 162 | }) 163 | }) 164 | -------------------------------------------------------------------------------- /test/base/scheduler-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | 5 | var transport = require('../../') 6 | var base = transport.protocol.base 7 | 8 | describe('Frame Scheduler', function () { 9 | var scheduler 10 | beforeEach(function () { 11 | scheduler = base.Scheduler.create() 12 | }) 13 | 14 | function chunk (stream, priority, chunks, callback) { 15 | return { 16 | stream: stream, 17 | priority: priority, 18 | chunks: chunks, 19 | callback: callback 20 | } 21 | } 22 | 23 | function expect (string, done) { 24 | var actual = '' 25 | var pending = scheduler.count 26 | var got = 0 27 | scheduler.on('data', function (chunk) { 28 | actual += chunk 29 | if (++got !== pending) { 30 | return 31 | } 32 | 33 | assert.strictEqual(actual, string) 34 | done() 35 | }) 36 | } 37 | 38 | it('should schedule and emit one frame', function (done) { 39 | scheduler.schedule(chunk(0, 0, [ 'hello', ' ', 'world' ])) 40 | 41 | expect('hello world', done) 42 | }) 43 | 44 | it('should schedule and emit two frames', function (done) { 45 | scheduler.schedule(chunk(0, 0, [ 'hello', ' ' ])) 46 | scheduler.schedule(chunk(0, 0, [ 'world' ])) 47 | 48 | expect('hello world', done) 49 | }) 50 | 51 | it('should interleave between two streams', function (done) { 52 | scheduler.schedule(chunk(0, 0, [ 'hello ' ])) 53 | scheduler.schedule(chunk(0, 0, [ ' hello ' ])) 54 | scheduler.schedule(chunk(1, 0, [ 'world!' ])) 55 | scheduler.schedule(chunk(1, 0, [ 'world' ])) 56 | 57 | expect('hello world! hello world', done) 58 | }) 59 | 60 | it('should interleave between two shuffled streams', function (done) { 61 | scheduler.schedule(chunk(0, 0, [ 'hello ' ])) 62 | scheduler.schedule(chunk(1, 0, [ 'world!' ])) 63 | scheduler.schedule(chunk(1, 0, [ 'world' ])) 64 | scheduler.schedule(chunk(0, 0, [ ' hello ' ])) 65 | 66 | expect('hello world! hello world', done) 67 | }) 68 | 69 | it('should interleave between three streams', function (done) { 70 | scheduler.schedule(chunk(0, 0, [ 'hello ' ])) 71 | scheduler.schedule(chunk(1, 0, [ 'world!' ])) 72 | scheduler.schedule(chunk(1, 0, [ 'world' ])) 73 | scheduler.schedule(chunk(0, 0, [ ' hello ' ])) 74 | scheduler.schedule(chunk(2, 0, [ ' (yes)' ])) 75 | 76 | expect('hello world! (yes) hello world', done) 77 | }) 78 | 79 | it('should respect priority window', function (done) { 80 | scheduler.schedule(chunk(0, 0.5, [ 'a' ])) 81 | scheduler.schedule(chunk(1, 0.4, [ 'b' ])) 82 | scheduler.schedule(chunk(2, 0.3, [ 'c' ])) 83 | scheduler.schedule(chunk(3, 0.2, [ 'd' ])) 84 | scheduler.schedule(chunk(4, 0.1, [ 'f' ])) 85 | scheduler.schedule(chunk(0, 0.5, [ 'A' ])) 86 | scheduler.schedule(chunk(1, 0.4, [ 'B' ])) 87 | scheduler.schedule(chunk(2, 0.3, [ 'C' ])) 88 | 89 | expect('abcABCdf', done) 90 | }) 91 | 92 | it('should not interleave sync data', function (done) { 93 | scheduler.schedule(chunk(0, false, [ 'hello ' ])) 94 | scheduler.schedule(chunk(1, false, [ 'world!' ])) 95 | scheduler.schedule(chunk(1, false, [ 'world' ])) 96 | scheduler.schedule(chunk(0, false, [ ' hello ' ])) 97 | scheduler.schedule(chunk(2, false, [ 'someone\'s ' ])) 98 | 99 | expect('hello world!world hello someone\'s ', done) 100 | }) 101 | 102 | it('should not fail on big gap in priorities', function (done) { 103 | scheduler.schedule(chunk(255, false, [ 'hello' ])) 104 | 105 | expect('hello', done) 106 | }) 107 | 108 | it('should invoke callback on push', function (done) { 109 | scheduler.schedule(chunk(0, 0, [ 'hello ' ], function () { 110 | assert.strictEqual(scheduler.read().toString(), 'hello ') 111 | done() 112 | })) 113 | }) 114 | 115 | it('should synchronously dump data', function () { 116 | scheduler.schedule(chunk(0, false, [ 'hello' ])) 117 | 118 | scheduler.dump() 119 | 120 | assert.strictEqual(scheduler.read() + '', 'hello') 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /test/base/utils-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | 5 | var transport = require('../../') 6 | var utils = transport.utils 7 | 8 | describe('utils', function () { 9 | function compare (a, b) { 10 | return a - b 11 | } 12 | 13 | describe('binaryInsert', function () { 14 | var binaryInsert = utils.binaryInsert 15 | it('should properly insert items in sequential order', function () { 16 | var list = [] 17 | binaryInsert(list, 1, compare) 18 | binaryInsert(list, 2, compare) 19 | binaryInsert(list, 3, compare) 20 | binaryInsert(list, 4, compare) 21 | 22 | assert.deepStrictEqual(list, [ 1, 2, 3, 4 ]) 23 | }) 24 | 25 | it('should properly insert items in reverse order', function () { 26 | var list = [] 27 | binaryInsert(list, 4, compare) 28 | binaryInsert(list, 3, compare) 29 | binaryInsert(list, 2, compare) 30 | binaryInsert(list, 1, compare) 31 | 32 | assert.deepStrictEqual(list, [ 1, 2, 3, 4 ]) 33 | }) 34 | 35 | it('should properly insert items in random order', function () { 36 | var list = [] 37 | binaryInsert(list, 3, compare) 38 | binaryInsert(list, 2, compare) 39 | binaryInsert(list, 4, compare) 40 | binaryInsert(list, 1, compare) 41 | 42 | assert.deepStrictEqual(list, [ 1, 2, 3, 4 ]) 43 | }) 44 | }) 45 | 46 | describe('binarySearch', function () { 47 | var binarySearch = utils.binarySearch 48 | 49 | it('should return the index of the value', function () { 50 | var list = [ 1, 2, 3, 4, 5, 6, 7 ] 51 | for (var i = 0; i < list.length; i++) { 52 | assert.strictEqual(binarySearch(list, list[i], compare), i) 53 | } 54 | }) 55 | 56 | it('should return -1 when value is not present in list', function () { 57 | var list = [ 1, 2, 3, 5, 6, 7 ] 58 | assert.strictEqual(binarySearch(list, 4, compare), -1) 59 | assert.strictEqual(binarySearch(list, 0, compare), -1) 60 | assert.strictEqual(binarySearch(list, 8, compare), -1) 61 | }) 62 | }) 63 | 64 | describe('priority to weight', function () { 65 | var utils = transport.protocol.base.utils 66 | 67 | var toWeight = utils.priorityToWeight 68 | var toPriority = utils.weightToPriority 69 | 70 | it('should preserve weight=16', function () { 71 | var priority = toPriority(16) 72 | assert.strictEqual(priority, 3) 73 | assert.strictEqual(toWeight(priority), 16) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /test/both/transport/connection-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | var async = require('async') 5 | var streamPair = require('stream-pair') 6 | var fixtures = require('./fixtures') 7 | 8 | var expectData = fixtures.expectData 9 | var everyProtocol = fixtures.everyProtocol 10 | 11 | var transport = require('../../../') 12 | 13 | describe('Transport/Connection', function () { 14 | everyProtocol(function (name, version) { 15 | var server 16 | var client 17 | var pair 18 | 19 | beforeEach(function () { 20 | server = fixtures.server 21 | client = fixtures.client 22 | pair = fixtures.pair 23 | }) 24 | 25 | it('should send SETTINGS frame on both ends', function (done) { 26 | async.map([ server, client ], function (side, callback) { 27 | side.on('frame', function (frame) { 28 | if (frame.type !== 'SETTINGS') { 29 | return 30 | } 31 | 32 | callback() 33 | }) 34 | }, done) 35 | }) 36 | 37 | it('should emit `close` after GOAWAY', function (done) { 38 | client.request({ 39 | path: '/hello-split' 40 | }, function (err, stream) { 41 | assert(!err) 42 | 43 | stream.resume() 44 | stream.end() 45 | }) 46 | 47 | var once = false 48 | server.on('stream', function (stream) { 49 | assert(!once) 50 | once = true 51 | 52 | stream.respond(200, {}) 53 | stream.resume() 54 | stream.end() 55 | 56 | var waiting = 2 57 | function next () { 58 | if (--waiting === 0) { 59 | done() 60 | } 61 | } 62 | 63 | pair.destroySoon = next 64 | server.once('close', next) 65 | server.end() 66 | }) 67 | }) 68 | 69 | it('should dump data on GOAWAY', function (done) { 70 | client.request({ 71 | path: '/hello-split' 72 | }, function (err, stream) { 73 | assert(!err) 74 | 75 | stream.resume() 76 | stream.end() 77 | }) 78 | 79 | var once = false 80 | server.on('stream', function (stream) { 81 | assert(!once) 82 | once = true 83 | 84 | stream.respond(200, {}) 85 | stream.resume() 86 | stream.end() 87 | 88 | pair.destroySoon = function () { 89 | pair.end() 90 | server.ping() 91 | 92 | setTimeout(done, 10) 93 | } 94 | server.end() 95 | }) 96 | }) 97 | 98 | it('should kill late streams on GOAWAY', function (done) { 99 | client.request({ 100 | path: '/hello-split' 101 | }, function (err, stream) { 102 | assert(!err) 103 | 104 | stream.resume() 105 | stream.end() 106 | 107 | client.request({ 108 | path: '/late' 109 | }, function (err, stream) { 110 | assert(!err) 111 | 112 | stream.on('error', function () { 113 | done() 114 | }) 115 | }) 116 | }) 117 | 118 | var once = false 119 | server.on('stream', function (stream) { 120 | assert(!once) 121 | once = true 122 | 123 | stream.respond(200, {}) 124 | stream.resume() 125 | stream.end() 126 | 127 | server.end() 128 | }) 129 | }) 130 | 131 | it('should send and receive ping', function (done) { 132 | client.ping(function () { 133 | server.ping(done) 134 | }) 135 | }) 136 | 137 | it('should ignore request after GOAWAY', function (done) { 138 | client.request({ 139 | path: '/hello-split' 140 | }, function (err, stream) { 141 | assert(!err) 142 | 143 | client.request({ 144 | path: '/second' 145 | }, function (err, stream) { 146 | assert(!err) 147 | 148 | stream.on('error', function () { 149 | // Ignore 150 | }) 151 | }) 152 | }) 153 | 154 | var once = false 155 | server.on('stream', function (stream) { 156 | assert(!once) 157 | once = true 158 | 159 | // Send GOAWAY 160 | server.end() 161 | }) 162 | 163 | var waiting = 2 164 | server.on('frame', function (frame) { 165 | if (frame.type === 'HEADERS' && --waiting === 0) { setTimeout(done, 10) } 166 | }) 167 | }) 168 | 169 | it('should return Stream after GOAWAY', function (done) { 170 | client.end(function () { 171 | var stream = client.request({ 172 | path: '/hello-split' 173 | }) 174 | assert(stream) 175 | 176 | stream.once('error', function () { 177 | done() 178 | }) 179 | }) 180 | }) 181 | 182 | it('should timeout when sending request', function (done) { 183 | server.setTimeout(50, function () { 184 | server.end() 185 | setTimeout(done, 50) 186 | }) 187 | 188 | setTimeout(function () { 189 | client.request({ 190 | path: '/hello-with-data' 191 | }, function (err, stream) { 192 | assert(err) 193 | }) 194 | }, 100) 195 | 196 | server.on('stream', function (stream) { 197 | assert(false) 198 | }) 199 | }) 200 | 201 | it('should not timeout when sending request', function (done) { 202 | server.setTimeout(100, function () { 203 | assert(false) 204 | }) 205 | 206 | setTimeout(function () { 207 | client.request({ 208 | path: '/hello-with-data' 209 | }, function (err, stream) { 210 | assert(!err) 211 | 212 | stream.end('ok') 213 | setTimeout(second, 50) 214 | }) 215 | }, 50) 216 | 217 | function second () { 218 | client.request({ 219 | path: '/hello-with-data' 220 | }, function (err, stream) { 221 | assert(!err) 222 | 223 | stream.end('ok') 224 | setTimeout(third, 50) 225 | }) 226 | } 227 | 228 | function third () { 229 | client.ping(function () { 230 | server.end() 231 | setTimeout(done, 50) 232 | }) 233 | } 234 | 235 | server.on('stream', function (stream) { 236 | stream.respond(200, {}) 237 | stream.end() 238 | expectData(stream, 'ok', function () {}) 239 | }) 240 | }) 241 | 242 | it('should ignore request without `stream` listener', function (done) { 243 | client.request({ 244 | path: '/hello-split' 245 | }, function (err, stream) { 246 | assert(!err) 247 | 248 | stream.on('close', function (err) { 249 | assert(err) 250 | done() 251 | }) 252 | }) 253 | }) 254 | 255 | it('should ignore HEADERS frame after FIN', function (done) { 256 | function sendHeaders () { 257 | client._spdyState.framer.requestFrame({ 258 | id: 1, 259 | method: 'GET', 260 | path: '/', 261 | priority: null, 262 | headers: {}, 263 | fin: true 264 | }, function (err) { 265 | assert(!err) 266 | }) 267 | } 268 | 269 | client.request({ 270 | path: '/hello' 271 | }, function (err, stream) { 272 | assert(!err) 273 | 274 | stream.resume() 275 | stream.once('end', function () { 276 | stream.end(sendHeaders) 277 | }) 278 | }) 279 | 280 | var incoming = 0 281 | server.on('stream', function (stream) { 282 | incoming++ 283 | assert(incoming <= 1) 284 | 285 | stream.resume() 286 | stream.end() 287 | }) 288 | 289 | var waiting = 2 290 | server.on('frame', function (frame) { 291 | if (frame.type === 'HEADERS' && --waiting === 0) { 292 | process.nextTick(done) 293 | } 294 | }) 295 | }) 296 | 297 | it('should use last received id when killing streams', function (done) { 298 | var waiting = 2 299 | function next () { 300 | if (--waiting === 0) { 301 | return done() 302 | } 303 | } 304 | client.once('stream', next) 305 | server.once('stream', next) 306 | 307 | server.request({ 308 | path: '/hello' 309 | }, function () { 310 | client.request({ 311 | path: '/hello' 312 | }) 313 | }) 314 | }) 315 | 316 | it('should kill stream on wrong id', function (done) { 317 | client._spdyState.stream.nextId = 2 318 | 319 | var stream = client.request({ 320 | path: '/hello' 321 | }) 322 | stream.once('error', function (err) { 323 | assert(err) 324 | done() 325 | }) 326 | }) 327 | 328 | it('should handle SETTINGS', function (done) { 329 | client._spdyState.framer.settingsFrame({ 330 | max_frame_size: 100000, 331 | max_header_list_size: 1000, 332 | header_table_size: 32, 333 | enable_push: true 334 | }, function (err) { 335 | assert(!err) 336 | }) 337 | client._spdyState.parser.setMaxFrameSize(100000) 338 | 339 | client.request({ 340 | path: '/hello' 341 | }, function (err, stream) { 342 | assert(!err) 343 | 344 | stream.on('data', function (chunk) { 345 | assert(chunk.length > 16384 || version < 4) 346 | }) 347 | 348 | stream.once('end', done) 349 | }) 350 | 351 | var incoming = 0 352 | server.on('stream', function (stream) { 353 | incoming++ 354 | assert(incoming <= 1) 355 | 356 | stream.resume() 357 | server._spdyState.framer.dataFrame({ 358 | id: stream.id, 359 | priority: stream._spdyState.priority.getPriority(), 360 | fin: true, 361 | data: Buffer.alloc(32000) 362 | }) 363 | }) 364 | }) 365 | 366 | it('should handle SETTINGS.initial_window_size=0', function (done) { 367 | var pair = streamPair.create() 368 | 369 | var client = transport.connection.create(pair.other, { 370 | protocol: name, 371 | windowSize: 256, 372 | isServer: false 373 | }) 374 | client.start(version) 375 | 376 | var proto = transport.protocol[name] 377 | 378 | var framer = proto.framer.create({ 379 | window: new transport.Window({ 380 | id: 0, 381 | isServer: false, 382 | recv: { size: 1024 * 1024 }, 383 | send: { size: 1024 * 1024 } 384 | }) 385 | }) 386 | var parser = proto.parser.create({ 387 | window: new transport.Window({ 388 | id: 0, 389 | isServer: false, 390 | recv: { size: 1024 * 1024 }, 391 | send: { size: 1024 * 1024 } 392 | }) 393 | }) 394 | 395 | framer.setVersion(version) 396 | parser.setVersion(version) 397 | 398 | var pool = proto.compressionPool.create() 399 | var comp = pool.get(version) 400 | framer.setCompression(comp) 401 | parser.setCompression(comp) 402 | 403 | framer.pipe(pair) 404 | pair.pipe(parser) 405 | 406 | framer.settingsFrame({ 407 | initial_window_size: 0 408 | }, function (err) { 409 | assert(!err) 410 | }) 411 | 412 | client.on('frame', function (frame) { 413 | if (frame.type !== 'SETTINGS') { 414 | return 415 | } 416 | 417 | client.request({ 418 | path: '/hello' 419 | }, function (err, stream) { 420 | assert(!err) 421 | 422 | // Attempt to get data through 423 | setTimeout(done, 100) 424 | }).end('hello') 425 | }) 426 | 427 | parser.on('data', function (frame) { 428 | assert.notStrictEqual(frame.type, 'DATA') 429 | }) 430 | }) 431 | 432 | if (version >= 4) { 433 | it('should ignore too large HPACK table in SETTINGS', function (done) { 434 | var limit = 0xffffffff 435 | server._spdyState.framer.settingsFrame({ 436 | header_table_size: limit 437 | }, function (err) { 438 | assert(!err) 439 | }) 440 | 441 | var headers = {} 442 | for (var i = 0; i < 2048; i++) { 443 | headers['h' + i] = (i % 250).toString() 444 | } 445 | 446 | client.on('frame', function (frame) { 447 | if (frame.type !== 'SETTINGS' || 448 | frame.settings.header_table_size !== 0xffffffff) { 449 | return 450 | } 451 | 452 | // Time for request! 453 | var one = client.request({ 454 | headers: headers, 455 | path: '/hello' 456 | }) 457 | one.end() 458 | one.resume() 459 | }) 460 | 461 | server.on('frame', function (frame) { 462 | if (frame.type === 'SETTINGS') { 463 | // Emulate bigger table on server-side 464 | server._spdyState.pair.decompress._table.protocolMaxSize = limit 465 | server._spdyState.pair.decompress._table.maxSize = limit 466 | } 467 | 468 | if (frame.type !== 'HEADERS') { return } 469 | 470 | assert.strictEqual(server._spdyState.pair.decompress._table.size, 4062) 471 | assert.strictEqual(client._spdyState.pair.compress._table.size, 4062) 472 | assert.strictEqual(client._spdyState.pair.compress._table.maxSize, 473 | client._spdyState.constants.HEADER_TABLE_SIZE) 474 | done() 475 | }) 476 | }) 477 | 478 | it('should allow receiving PRIORITY on idle stream', function (done) { 479 | client._spdyState.framer.priorityFrame({ 480 | id: 5, 481 | priority: { 482 | exclusive: false, 483 | parent: 3, 484 | weight: 10 485 | } 486 | }, function () { 487 | }) 488 | 489 | server.on('frame', function (frame) { 490 | if (frame.type === 'PRIORITY') { 491 | setImmediate(done) 492 | } 493 | }) 494 | 495 | client.on('frame', function (frame) { 496 | assert.notStrictEqual(frame.type, 'GOAWAY') 497 | }) 498 | }) 499 | 500 | it('should allow receiving PRIORITY on small-id stream', function (done) { 501 | server.on('stream', function (stream) { 502 | stream.end() 503 | }) 504 | 505 | client._spdyState.stream.nextId = 3 506 | 507 | var one = client.request({ 508 | path: '/hello' 509 | }) 510 | one.end() 511 | one.resume() 512 | 513 | one.on('close', function () { 514 | client._spdyState.framer.priorityFrame({ 515 | id: 1, 516 | priority: { 517 | exclusive: false, 518 | parent: 3, 519 | weight: 10 520 | } 521 | }, function () { 522 | }) 523 | }) 524 | 525 | server.on('frame', function (frame) { 526 | if (frame.type === 'PRIORITY' && frame.id === 1) { 527 | setImmediate(done) 528 | } 529 | }) 530 | 531 | client.removeAllListeners('frame') 532 | client.on('frame', function (frame) { 533 | assert.notStrictEqual(frame.type, 'GOAWAY') 534 | }) 535 | }) 536 | 537 | it('should allow receiving PRIORITY on even-id stream', function (done) { 538 | client._spdyState.framer.priorityFrame({ 539 | id: 2, 540 | priority: { 541 | exclusive: false, 542 | parent: 3, 543 | weight: 10 544 | } 545 | }, function () { 546 | }) 547 | 548 | server.on('frame', function (frame) { 549 | if (frame.type === 'PRIORITY' && frame.id === 2) { 550 | setImmediate(done) 551 | } 552 | }) 553 | 554 | client.removeAllListeners('frame') 555 | client.on('frame', function (frame) { 556 | assert.notStrictEqual(frame.type, 'GOAWAY') 557 | }) 558 | }) 559 | } 560 | 561 | it('should send X_FORWARDED_FOR', function (done) { 562 | client.sendXForwardedFor('1.2.3.4') 563 | 564 | client.request({ 565 | path: '/hello' 566 | }, function (err, stream) { 567 | assert(!err) 568 | 569 | stream.resume() 570 | stream.once('end', done) 571 | }) 572 | 573 | server.on('stream', function (stream) { 574 | assert.strictEqual(server.getXForwardedFor(), '1.2.3.4') 575 | 576 | stream.resume() 577 | stream.end() 578 | }) 579 | }) 580 | }) 581 | }) 582 | -------------------------------------------------------------------------------- /test/both/transport/fixtures.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | var streamPair = require('stream-pair') 5 | 6 | var transport = require('../../../') 7 | 8 | exports.pair = null 9 | exports.server = null 10 | exports.client = null 11 | 12 | function expectData (stream, expected, callback) { 13 | var actual = '' 14 | 15 | stream.on('data', function (chunk) { 16 | actual += chunk 17 | }) 18 | stream.on('end', function () { 19 | assert.strictEqual(actual, expected.toString()) 20 | callback() 21 | }) 22 | } 23 | exports.expectData = expectData 24 | 25 | function protocol (name, version, body) { 26 | describe(name + ' (v' + version + ')', function () { 27 | beforeEach(function () { 28 | exports.pair = streamPair.create() 29 | 30 | exports.server = transport.connection.create(exports.pair, { 31 | protocol: name, 32 | windowSize: 256, 33 | isServer: true 34 | }) 35 | exports.client = transport.connection.create(exports.pair.other, { 36 | protocol: name, 37 | windowSize: 256, 38 | isServer: false 39 | }) 40 | 41 | exports.client.start(version) 42 | }) 43 | 44 | body(name, version) 45 | }) 46 | } 47 | exports.protocol = protocol 48 | 49 | function everyProtocol (body) { 50 | protocol('http2', 4, body) 51 | protocol('spdy', 2, body) 52 | protocol('spdy', 3, body) 53 | protocol('spdy', 3.1, body) 54 | } 55 | exports.everyProtocol = everyProtocol 56 | -------------------------------------------------------------------------------- /test/both/transport/push-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | var fixtures = require('./fixtures') 5 | 6 | var expectData = fixtures.expectData 7 | var everyProtocol = fixtures.everyProtocol 8 | 9 | describe('Transport/Push', function () { 10 | everyProtocol(function (name, version) { 11 | var server 12 | var client 13 | 14 | beforeEach(function () { 15 | server = fixtures.server 16 | client = fixtures.client 17 | // var pair = fixtures.pair 18 | }) 19 | 20 | it('should create PUSH_PROMISE', function (done) { 21 | client.request({ 22 | path: '/parent' 23 | }, function (err, stream) { 24 | assert(!err) 25 | 26 | stream.on('pushPromise', function (push) { 27 | assert.strictEqual(push.path, '/push') 28 | assert.strictEqual(client.getCounter('push'), 1) 29 | push.on('response', function (status, headers) { 30 | assert.strictEqual(status, 201) 31 | done() 32 | }) 33 | }) 34 | }) 35 | 36 | server.on('stream', function (stream) { 37 | assert.strictEqual(stream.path, '/parent') 38 | 39 | stream.respond(200, {}) 40 | stream.pushPromise({ 41 | path: '/push', 42 | status: 201, 43 | priority: { 44 | parent: 0, 45 | exclusive: false, 46 | weight: 42 47 | } 48 | }, function (err, stream) { 49 | assert(!err) 50 | }) 51 | }) 52 | }) 53 | 54 | it('should send HEADERS on PUSH_PROMISE', function (done) { 55 | client.request({ 56 | path: '/parent' 57 | }, function (err, stream) { 58 | assert(!err) 59 | 60 | stream.on('pushPromise', function (push) { 61 | push.on('headers', function (headers) { 62 | assert.deepStrictEqual(headers, { a: 'b' }) 63 | done() 64 | }) 65 | }) 66 | }) 67 | 68 | server.on('stream', function (stream) { 69 | assert.strictEqual(stream.path, '/parent') 70 | 71 | stream.respond(200, {}) 72 | stream.pushPromise({ 73 | path: '/push', 74 | priority: { 75 | parent: 0, 76 | exclusive: false, 77 | weight: 42 78 | } 79 | }, function (err, stream) { 80 | assert(!err) 81 | 82 | stream.sendHeaders({ a: 'b' }) 83 | }) 84 | }) 85 | }) 86 | 87 | if (version >= 4) { 88 | it('should not send HEADERS on PUSH_PROMISE if disabled', function (done) { 89 | client.request({ 90 | path: '/parent' 91 | }, function (err, stream) { 92 | assert(!err) 93 | 94 | stream.on('pushPromise', function (push) { 95 | push.on('response', function (status, headers) { 96 | assert.strictEqual(status, 201) 97 | }) 98 | push.on('headers', function (headers) { 99 | assert(false) 100 | }) 101 | push.on('close', function (err) { 102 | assert(!err) 103 | done() 104 | }) 105 | push.resume() 106 | }) 107 | }) 108 | 109 | server.on('stream', function (stream) { 110 | assert.strictEqual(stream.path, '/parent') 111 | 112 | stream.respond(200, {}) 113 | stream.pushPromise({ 114 | path: '/push', 115 | response: false 116 | }, function (err, stream) { 117 | assert(!err) 118 | 119 | stream.respond(201) 120 | stream.end() 121 | }) 122 | }) 123 | }) 124 | 125 | it('should send PUSH_PROMISE+HEADERS and HEADERS concurrently', 126 | function (done) { 127 | var seq = [] 128 | 129 | client.request({ 130 | path: '/parent' 131 | }, function (err, stream) { 132 | assert(!err) 133 | 134 | stream.on('pushPromise', function (push) { 135 | assert.strictEqual(push.path, '/push') 136 | assert.strictEqual(client.getCounter('push'), 1) 137 | push.on('response', function (status) { 138 | assert.strictEqual(status, 201) 139 | assert.deepStrictEqual(seq, [ 0, 1, 2 ]) 140 | done() 141 | }) 142 | }) 143 | }) 144 | 145 | client.on('frame', function (frame) { 146 | if (frame.type === 'HEADERS' || frame.type === 'PUSH_PROMISE') { 147 | seq.push(frame.headers.seq | 0) 148 | } 149 | }) 150 | 151 | server.on('stream', function (stream) { 152 | assert.strictEqual(stream.path, '/parent') 153 | 154 | stream.pushPromise({ 155 | path: '/push', 156 | status: 201, 157 | priority: { 158 | parent: 0, 159 | exclusive: false, 160 | weight: 42 161 | }, 162 | headers: { 163 | 'seq': '0' 164 | }, 165 | response: { 166 | 'seq': '2' 167 | } 168 | }, function (err, stream) { 169 | assert(!err) 170 | }) 171 | process.nextTick(function () { 172 | stream.respond(200, { 173 | 'seq': '1' 174 | }) 175 | }) 176 | }) 177 | }) 178 | } 179 | 180 | it('should create PUSH_PROMISE and end parent req', function (done) { 181 | client.request({ 182 | path: '/parent' 183 | }, function (err, stream) { 184 | assert(!err) 185 | 186 | stream.resume() 187 | stream.end() 188 | stream.on('pushPromise', function (push) { 189 | assert.strictEqual(push.path, '/push') 190 | done() 191 | }) 192 | }) 193 | 194 | server.on('stream', function (stream) { 195 | assert.strictEqual(stream.path, '/parent') 196 | 197 | stream.respond(200, {}) 198 | stream.resume() 199 | stream.on('end', function () { 200 | stream.pushPromise({ 201 | path: '/push', 202 | priority: { 203 | parent: 0, 204 | exclusive: false, 205 | weight: 42 206 | } 207 | }, function (err, stream) { 208 | assert(!err) 209 | }) 210 | stream.end() 211 | }) 212 | }) 213 | }) 214 | 215 | it('should cork PUSH_PROMISE on write', function (done) { 216 | client.request({ 217 | path: '/parent' 218 | }, function (err, stream) { 219 | assert(!err) 220 | 221 | stream.on('pushPromise', function (push) { 222 | assert.strictEqual(push.path, '/push') 223 | expectData(push, 'ok', done) 224 | }) 225 | }) 226 | 227 | server.on('stream', function (stream) { 228 | assert.strictEqual(stream.path, '/parent') 229 | 230 | stream.respond(200, {}) 231 | var push = stream.pushPromise({ 232 | path: '/push', 233 | priority: { 234 | parent: 0, 235 | exclusive: false, 236 | weight: 42 237 | } 238 | }, function (err, stream) { 239 | assert(!err) 240 | }) 241 | 242 | push.end('ok') 243 | }) 244 | }) 245 | 246 | it('should emit `close` on PUSH_PROMISE', function (done) { 247 | client.request({ 248 | path: '/parent' 249 | }, function (err, stream) { 250 | assert(!err) 251 | 252 | stream.on('pushPromise', function (push) { 253 | assert.strictEqual(push.path, '/push') 254 | 255 | push.on('close', next) 256 | push.resume() 257 | }) 258 | }) 259 | 260 | server.on('stream', function (stream) { 261 | assert.strictEqual(stream.path, '/parent') 262 | 263 | stream.respond(200, {}) 264 | stream.pushPromise({ 265 | path: '/push', 266 | priority: { 267 | parent: 0, 268 | exclusive: false, 269 | weight: 42 270 | } 271 | }, function (err, stream) { 272 | assert(!err) 273 | stream.on('close', next) 274 | stream.end('ohai') 275 | }) 276 | }) 277 | 278 | var waiting = 2 279 | function next () { 280 | if (--waiting === 0) { 281 | return done() 282 | } 283 | } 284 | }) 285 | 286 | it('should ignore PUSH_PROMISE', function (done) { 287 | client.request({ 288 | path: '/parent' 289 | }, function (err, stream) { 290 | assert(!err) 291 | }) 292 | 293 | server.on('stream', function (stream) { 294 | assert.strictEqual(stream.path, '/parent') 295 | 296 | stream.respond(200, {}) 297 | stream.pushPromise({ 298 | path: '/push', 299 | priority: { 300 | parent: 0, 301 | exclusive: false, 302 | weight: 42 303 | } 304 | }, function (err, stream) { 305 | assert(!err) 306 | stream.once('close', function (err) { 307 | assert(err) 308 | done() 309 | }) 310 | }) 311 | }) 312 | }) 313 | 314 | it('should fail on server-disabled PUSH_PROMISE', function (done) { 315 | client.request({ 316 | path: '/parent' 317 | }, function (err, stream) { 318 | assert(!err) 319 | 320 | stream._spdyState.framer.enablePush(true) 321 | stream.pushPromise({ 322 | path: '/push', 323 | priority: { 324 | parent: 0, 325 | exclusive: false, 326 | weight: 42 327 | } 328 | }, function (err, stream) { 329 | assert(!err) 330 | stream.on('error', function (err) { 331 | assert(err) 332 | }) 333 | }) 334 | 335 | client.on('close', function (err) { 336 | assert(err) 337 | done() 338 | }) 339 | }) 340 | 341 | server.on('stream', function (stream) { 342 | assert.strictEqual(stream.path, '/parent') 343 | 344 | stream.respond(200, {}) 345 | stream.on('pushPromise', function () { 346 | assert(false) 347 | }) 348 | }) 349 | }) 350 | 351 | it('should fail on client-disabled PUSH_PROMISE', function (done) { 352 | client.request({ 353 | path: '/parent' 354 | }, function (err, stream) { 355 | assert(!err) 356 | 357 | stream._spdyState.framer.enablePush(false) 358 | var push = stream.pushPromise({ 359 | path: '/push', 360 | priority: { 361 | parent: 0, 362 | exclusive: false, 363 | weight: 42 364 | } 365 | }, function (err, stream) { 366 | assert(err) 367 | setTimeout(function () { 368 | done() 369 | }, 50) 370 | }) 371 | push.write('hello') 372 | }) 373 | 374 | // The PUSH data should not be sent 375 | server.on('frame', function (frame) { 376 | assert.notStrictEqual(frame.type, 'DATA') 377 | }) 378 | 379 | server.on('stream', function (stream) { 380 | assert.strictEqual(stream.path, '/parent') 381 | 382 | stream.respond(200, {}) 383 | stream.on('pushPromise', function () { 384 | assert(false) 385 | }) 386 | }) 387 | }) 388 | 389 | it('should get error on disabled PUSH_PROMISE', function (done) { 390 | client.request({ 391 | path: '/parent' 392 | }, function (err, stream) { 393 | assert(!err) 394 | 395 | stream.pushPromise({ 396 | path: '/push', 397 | priority: { 398 | parent: 0, 399 | exclusive: false, 400 | weight: 42 401 | } 402 | }, function (err, stream) { 403 | assert(err) 404 | done() 405 | }) 406 | }) 407 | 408 | server.on('stream', function (stream) { 409 | assert.strictEqual(stream.path, '/parent') 410 | 411 | stream.respond(200, {}) 412 | 413 | stream.on('pushPromise', function () { 414 | assert(false) 415 | }) 416 | }) 417 | }) 418 | 419 | it('should not error on extra PRIORITY frame', function (done) { 420 | client.request({ 421 | path: '/parent' 422 | }, function (err, stream) { 423 | assert(!err) 424 | 425 | stream.on('pushPromise', function (push) { 426 | push.on('response', function () { 427 | // .abort() does this only on next tick 428 | push.emit('close') 429 | 430 | stream.end('ok') 431 | }) 432 | }) 433 | }) 434 | 435 | server.on('stream', function (stream) { 436 | assert.strictEqual(stream.path, '/parent') 437 | 438 | stream.respond(200, {}) 439 | stream.pushPromise({ 440 | path: '/push', 441 | priority: { 442 | parent: 0, 443 | exclusive: false, 444 | weight: 42 445 | } 446 | }, function (err, stream) { 447 | assert(!err) 448 | stream.on('error', function () { 449 | assert(false) 450 | }) 451 | }) 452 | 453 | expectData(stream, 'ok', done) 454 | }) 455 | }) 456 | 457 | it('should not fail on aborting PUSH_PROMISE frame', function (done) { 458 | client.request({ 459 | path: '/parent' 460 | }, function (err, stream) { 461 | assert(!err) 462 | 463 | stream.on('pushPromise', function (push) { 464 | push.abort() 465 | stream.end('ok') 466 | }) 467 | }) 468 | 469 | server.on('stream', function (stream) { 470 | assert.strictEqual(stream.path, '/parent') 471 | 472 | stream.respond(200, {}) 473 | stream.pushPromise({ 474 | path: '/push', 475 | priority: { 476 | parent: 0, 477 | exclusive: false, 478 | weight: 42 479 | } 480 | }, function (err, stream) { 481 | assert(!err) 482 | stream.on('error', function () { 483 | assert(false) 484 | }) 485 | }) 486 | 487 | expectData(stream, 'ok', done) 488 | }) 489 | }) 490 | }) 491 | }) 492 | -------------------------------------------------------------------------------- /test/http2/parser-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | 5 | var transport = require('../../') 6 | var http2 = transport.protocol.http2 7 | 8 | describe('HTTP2 Parser', function () { 9 | var parser 10 | var window 11 | 12 | beforeEach(function () { 13 | window = new transport.Window({ 14 | id: 0, 15 | isServer: true, 16 | recv: { size: 1024 * 1024 }, 17 | send: { size: 1024 * 1024 } 18 | }) 19 | 20 | var pool = http2.compressionPool.create() 21 | parser = http2.parser.create({ 22 | window: window 23 | }) 24 | var comp = pool.get() 25 | parser.setCompression(comp) 26 | parser.skipPreface() 27 | }) 28 | 29 | function pass (data, expected, done) { 30 | parser.write(Buffer.from(data, 'hex')) 31 | 32 | parser.once('data', function (frame) { 33 | assert.deepStrictEqual(frame, expected) 34 | assert.strictEqual(parser.buffer.size, 0) 35 | done() 36 | }) 37 | } 38 | 39 | function fail (data, code, re, done) { 40 | parser.write(Buffer.from(data, 'hex'), function (err) { 41 | assert(err) 42 | assert(err instanceof transport.protocol.base.utils.ProtocolError) 43 | assert.strictEqual(err.code, http2.constants.error[code]) 44 | assert(re.test(err.message), err.message) 45 | 46 | done() 47 | }) 48 | parser.once('error', assert) 49 | } 50 | 51 | describe('SETTINGS', function () { 52 | it('should parse regular frame', function (done) { 53 | pass('00000c0400000000000003000003e8000400a00000', { 54 | type: 'SETTINGS', 55 | settings: { 56 | initial_window_size: 10485760, 57 | max_concurrent_streams: 1000 58 | } 59 | }, done) 60 | }) 61 | 62 | it('should parse ACK', function (done) { 63 | pass('000000040100000000', { 64 | type: 'ACK_SETTINGS' 65 | }, done) 66 | }) 67 | 68 | it('should fail on non-empty ACK', function (done) { 69 | fail('000001040100000000ff', 'FRAME_SIZE_ERROR', /ACK.*non-zero/i, done) 70 | }) 71 | 72 | it('should fail on non-aligned frame', function (done) { 73 | fail('000001040000000000ff', 'FRAME_SIZE_ERROR', /multiple/i, done) 74 | }) 75 | 76 | it('should fail on non-zero stream id frame', function (done) { 77 | fail('000000040000000001', 'PROTOCOL_ERROR', /stream id/i, done) 78 | }) 79 | }) 80 | 81 | describe('WINDOW_UPDATE', function () { 82 | it('should parse regular frame', function (done) { 83 | pass('000004080000000000009f0001', { 84 | type: 'WINDOW_UPDATE', 85 | id: 0, 86 | delta: 10420225 87 | }, done) 88 | }) 89 | 90 | it('should parse frame with negative window', function (done) { 91 | pass('000004080000000000ffffffff', { 92 | type: 'WINDOW_UPDATE', 93 | id: 0, 94 | delta: -1 95 | }, done) 96 | }) 97 | 98 | it('should fail on bigger frame', function (done) { 99 | fail('000005080000000000009f000102', 'FRAME_SIZE_ERROR', /length/i, done) 100 | }) 101 | 102 | it('should fail on smaller frame', function (done) { 103 | fail('000003080000000000009f00', 'FRAME_SIZE_ERROR', /length/i, done) 104 | }) 105 | }) 106 | 107 | describe('HEADERS', function () { 108 | it('should parse regular frame', function (done) { 109 | var hex = '000155012500000001' 110 | hex += '00000000ff418a089d5c0b8170dc644c8b82848753b8497ca589d34d1f43ae' + 111 | 'ba0c41a4c7a98f33a69a3fdf9a68fa1d75d0620d263d4c79a68fbed00177fe' + 112 | '8d48e62b1e0b1d7f5f2c7cfdf6800bbd508e9bd9abfa5242cb40d25fa51121' + 113 | '27519cb2d5b6f0fab2dfbed00177be8b52dc377df6800bb9f45abefb4005da' + 114 | '5887a47e561cc5801f60ac8a2b5348e07dc7df10190ae171e782ebe2684b85' + 115 | 'a0bce36cbecb8b85a642eb8f81d12e1699640f819782bbbf60bb8a2b534fb8' + 116 | '1f71f7c40642b85a0bce36cbecb8b8570af6a69222c83fa90d61489feffe5a' + 117 | '9a484aa0fea43585227fbff96a69253241fd547a8bfdff4003646e7401317a' + 118 | 'dcd07f66a281b0dae053fad0321aa49d13fda992a49685340c8a6adca7e281' + 119 | '0441044cff6a435d74179163cc64b0db2eaecb8a7f59b1efd19fe94a0dd4aa' + 120 | '62293a9ffb52f4f61e92b0d32b817132dbab844d29b8728ec330db2eaecb9f' 121 | 122 | pass(hex, { 123 | type: 'HEADERS', 124 | id: 1, 125 | priority: { 126 | parent: 0, 127 | exclusive: false, 128 | weight: 256 129 | }, 130 | fin: true, 131 | writable: true, 132 | path: '/', 133 | headers: { 134 | ':authority': '127.0.0.1:3232', 135 | ':method': 'GET', 136 | ':path': '/', 137 | ':scheme': 'https', 138 | accept: 'text/html,' + 139 | 'application/xhtml+xml,' + 140 | 'application/xml;q=0.9,' + 141 | 'image/webp,*/*;q=0.8', 142 | 'accept-encoding': 'gzip, deflate, sdch', 143 | 'accept-language': 'ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4', 144 | 'cache-control': 'max-age=0', 145 | cookie: '__utma=96992031.1688179242.1418653936.' + 146 | '1431769072.1433090381.7; ' + 147 | '__utmz=96992031.1418653936.1.1.utmcsr=(direct)|' + 148 | 'utmccn=(direct)|utmcmd=(none)', 149 | dnt: '1', 150 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) ' + 151 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 152 | 'Chrome/43.0.2357.124 Safari/537.36' 153 | } 154 | }, done) 155 | }) 156 | 157 | it('should unpad data', function (done) { 158 | var hello = '40849cb4507f84f07b2893' 159 | 160 | var first = '000011010c0000000105' + hello + 'ABCDEF1234' 161 | 162 | pass(first, { 163 | type: 'HEADERS', 164 | id: 1, 165 | priority: { 166 | parent: 0, 167 | exclusive: false, 168 | weight: 16 169 | }, 170 | fin: false, 171 | writable: true, 172 | path: undefined, 173 | headers: { 174 | hello: 'world' 175 | } 176 | }, done) 177 | }) 178 | 179 | it('should parse headers with continuation', function (done) { 180 | var hello = '40849cb4507f84f07b2893' 181 | var how = '40839cfe3f871d8553d1edff3f' 182 | 183 | var first = '00000b010000000001' + hello 184 | var second = '00000d090400000001' + how 185 | 186 | pass(first + second, { 187 | type: 'HEADERS', 188 | id: 1, 189 | priority: { 190 | parent: 0, 191 | exclusive: false, 192 | weight: 16 193 | }, 194 | fin: false, 195 | writable: true, 196 | path: undefined, 197 | headers: { 198 | hello: 'world', 199 | how: 'are you?' 200 | } 201 | }, done) 202 | }) 203 | 204 | it('should fail on zero stream id', function (done) { 205 | var first = '000000010000000000' 206 | fail(first, 'PROTOCOL_ERROR', /stream id/i, done) 207 | }) 208 | 209 | it('should fail to unpad too big data', function (done) { 210 | var hello = '40849cb4507f84f07b2893' 211 | 212 | var first = '000011010c00000001ff' + hello + 'ABCDEF1234' 213 | 214 | fail(first, 'PROTOCOL_ERROR', /invalid padding/i, done) 215 | }) 216 | 217 | it('should fail to unpad too small data', function (done) { 218 | var first = '000000010c00000001' 219 | 220 | fail(first, 'FRAME_SIZE_ERROR', /not enough space/i, done) 221 | }) 222 | 223 | it('should fail on just continuation', function (done) { 224 | var how = '40839cfe3f871d8553d1edff3f' 225 | 226 | var second = '00000d090400000001' + how 227 | 228 | fail(second, 'PROTOCOL_ERROR', /no matching stream/i, done) 229 | }) 230 | 231 | it('should fail on way double END_HEADERS continuations', function (done) { 232 | var hello = '40849cb4507f84f07b2893' 233 | var how = '40839cfe3f871d8553d1edff3f' 234 | 235 | var first = '00000b010000000001' + hello 236 | var second = '00000d090400000001' + how 237 | 238 | fail(first + second + second, 'PROTOCOL_ERROR', /no matching/i, done) 239 | }) 240 | 241 | it('should fail on way too many continuations', function (done) { 242 | var hello = '40849cb4507f84f07b2893' 243 | var how = '40839cfe3f871d8553d1edff3f' 244 | 245 | var first = '00000b010000000001' + hello 246 | var second = '00000d090000000001' + how 247 | 248 | var msg = first + new Array(200).join(second) 249 | 250 | parser.setMaxHeaderListSize(1000) 251 | fail(msg, 'PROTOCOL_ERROR', /list is too large/i, done) 252 | }) 253 | }) 254 | 255 | describe('RST_STREAM', function () { 256 | it('should parse general frame', function (done) { 257 | pass('0000040300000000010000000a', { 258 | type: 'RST', 259 | id: 1, 260 | code: 'CONNECT_ERROR' 261 | }, done) 262 | }) 263 | 264 | it('should fail on 0-stream', function (done) { 265 | fail('0000040300000000000000000a', 'PROTOCOL_ERROR', /stream id/i, done) 266 | }) 267 | 268 | it('should fail on empty frame', function (done) { 269 | fail('000000030000000001', 'FRAME_SIZE_ERROR', /length not 4/i, done) 270 | }) 271 | 272 | it('should fail on bigger frame', function (done) { 273 | fail('0000050300000000010102030405', 274 | 'FRAME_SIZE_ERROR', 275 | /length not 4/i, 276 | done) 277 | }) 278 | }) 279 | 280 | describe('DATA', function () { 281 | it('should parse general frame', function (done) { 282 | pass('000004000000000001abbadead', { 283 | type: 'DATA', 284 | id: 1, 285 | fin: false, 286 | data: Buffer.from('abbadead', 'hex') 287 | }, done) 288 | }) 289 | 290 | it('should parse partial frame', function (done) { 291 | pass('000004000000000001abbade', { 292 | type: 'DATA', 293 | id: 1, 294 | fin: false, 295 | data: Buffer.from('abbade', 'hex') 296 | }, function () { 297 | assert.strictEqual(parser.waiting, 1) 298 | pass('ff', { 299 | type: 'DATA', 300 | id: 1, 301 | fin: false, 302 | data: Buffer.from('ff', 'hex') 303 | }, function () { 304 | assert.strictEqual(window.recv.current, 1048572) 305 | done() 306 | }) 307 | }) 308 | }) 309 | 310 | it('should parse END_STREAM frame', function (done) { 311 | pass('000004000100000001deadbeef', { 312 | type: 'DATA', 313 | id: 1, 314 | fin: true, 315 | data: Buffer.from('deadbeef', 'hex') 316 | }, done) 317 | }) 318 | 319 | it('should parse partial END_STREAM frame', function (done) { 320 | pass('000004000100000001abbade', { 321 | type: 'DATA', 322 | id: 1, 323 | fin: false, 324 | data: Buffer.from('abbade', 'hex') 325 | }, function () { 326 | assert.strictEqual(parser.waiting, 1) 327 | pass('ff', { 328 | type: 'DATA', 329 | id: 1, 330 | fin: true, 331 | data: Buffer.from('ff', 'hex') 332 | }, done) 333 | }) 334 | }) 335 | 336 | it('should parse padded frame', function (done) { 337 | pass('0000070008000000010212345678ffff', { 338 | type: 'DATA', 339 | id: 1, 340 | fin: false, 341 | data: Buffer.from('12345678', 'hex') 342 | }, done) 343 | }) 344 | 345 | it('should not parse partial padded frame', function (done) { 346 | pass('0000070008000000010212345678ff', { 347 | type: 'DATA', 348 | id: 1, 349 | fin: false, 350 | data: Buffer.from('12345678', 'hex') 351 | }, function () { 352 | assert(false) 353 | }) 354 | setTimeout(done, 50) 355 | }) 356 | 357 | it('should fail on incorrectly padded frame', function (done) { 358 | fail('000007000800000001ff0000000affff', 359 | 'PROTOCOL_ERROR', 360 | /padding size/i, 361 | done) 362 | }) 363 | }) 364 | 365 | describe('PUSH_PROMISE', function () { 366 | it('should parse general frame', function (done) { 367 | var hello = '40849cb4507f84f07b2893' 368 | 369 | pass('00000f05040000000100000002' + hello, { 370 | type: 'PUSH_PROMISE', 371 | id: 1, 372 | promisedId: 2, 373 | fin: false, 374 | headers: { 375 | hello: 'world' 376 | }, 377 | path: undefined 378 | }, done) 379 | }) 380 | 381 | it('should parse padded frame', function (done) { 382 | var hello = '40849cb4507f84f07b2893' 383 | 384 | pass('000011050c000000010100000002' + hello + 'ff', { 385 | type: 'PUSH_PROMISE', 386 | id: 1, 387 | promisedId: 2, 388 | fin: false, 389 | headers: { 390 | hello: 'world' 391 | }, 392 | path: undefined 393 | }, done) 394 | }) 395 | 396 | it('should fail on incorreclty padded frame', function (done) { 397 | var hello = '40849cb4507f84f07b2893' 398 | 399 | fail('000011050c00000001ff00000002' + hello + 'ff', 400 | 'PROTOCOL_ERROR', 401 | /padding size/i, 402 | done) 403 | }) 404 | 405 | it('should fail on empty frame', function (done) { 406 | fail('000000050400000001', 'FRAME_SIZE_ERROR', /length less than/i, done) 407 | }) 408 | 409 | it('should fail on 0-stream id', function (done) { 410 | var hello = '40849cb4507f84f07b2893' 411 | 412 | fail('00000f05040000000000000002' + hello, 413 | 'PROTOCOL_ERROR', 414 | /stream id/i, 415 | done) 416 | }) 417 | }) 418 | 419 | describe('PING', function () { 420 | it('should parse general frame', function (done) { 421 | pass('0000080600000000000102030405060708', { 422 | type: 'PING', 423 | ack: false, 424 | opaque: Buffer.from('0102030405060708', 'hex') 425 | }, done) 426 | }) 427 | 428 | it('should parse ack frame', function (done) { 429 | pass('0000080601000000000102030405060708', { 430 | type: 'PING', 431 | ack: true, 432 | opaque: Buffer.from('0102030405060708', 'hex') 433 | }, done) 434 | }) 435 | 436 | it('should fail on empty frame', function (done) { 437 | fail('000000060100000000', 'FRAME_SIZE_ERROR', /length !=/i, done) 438 | }) 439 | 440 | it('should fail too big frame', function (done) { 441 | fail('000009060100000000010203040506070809', 442 | 'FRAME_SIZE_ERROR', 443 | /length !=/i, 444 | done) 445 | }) 446 | 447 | it('should fail on non-zero stream id', function (done) { 448 | fail('0000080601000000010102030405060708', 449 | 'PROTOCOL_ERROR', 450 | /invalid stream id/i, 451 | done) 452 | }) 453 | }) 454 | 455 | describe('GOAWAY', function () { 456 | it('should parse general frame', function (done) { 457 | pass('0000080700000000000000000100000002', { 458 | type: 'GOAWAY', 459 | lastId: 1, 460 | code: 'INTERNAL_ERROR' 461 | }, done) 462 | }) 463 | 464 | it('should parse frame with debug data', function (done) { 465 | pass('00000a0700000000000000000100000002dead', { 466 | type: 'GOAWAY', 467 | lastId: 1, 468 | code: 'INTERNAL_ERROR', 469 | debug: Buffer.from('dead', 'hex') 470 | }, done) 471 | }) 472 | 473 | it('should fail on empty frame', function (done) { 474 | fail('000000070000000000', 'FRAME_SIZE_ERROR', /length < 8/i, done) 475 | }) 476 | 477 | it('should fail on non-zero stream id', function (done) { 478 | fail('0000080700000001000000000100000002', 479 | 'PROTOCOL_ERROR', 480 | /invalid stream/i, 481 | done) 482 | }) 483 | }) 484 | 485 | describe('PRIORITY', function () { 486 | it('should parse general frame', function (done) { 487 | pass('00000502000000000100000002ab', { 488 | type: 'PRIORITY', 489 | id: 1, 490 | priority: { 491 | exclusive: false, 492 | parent: 2, 493 | weight: 0xac 494 | } 495 | }, done) 496 | }) 497 | 498 | it('should parse exclusive frame', function (done) { 499 | pass('00000502000000000180000002ab', { 500 | type: 'PRIORITY', 501 | id: 1, 502 | priority: { 503 | exclusive: true, 504 | parent: 2, 505 | weight: 0xac 506 | } 507 | }, done) 508 | }) 509 | 510 | it('should fail on empty frame', function (done) { 511 | fail('000000020000000001', 'FRAME_SIZE_ERROR', /length != 5/i, done) 512 | }) 513 | 514 | it('should fail on too big frame', function (done) { 515 | fail('000006020000000001010203040506', 516 | 'FRAME_SIZE_ERROR', 517 | /length != 5/i, 518 | done) 519 | }) 520 | 521 | it('should fail on too big frame', function (done) { 522 | fail('000006020000000001010203040506', 523 | 'FRAME_SIZE_ERROR', 524 | /length != 5/i, 525 | done) 526 | }) 527 | 528 | it('should fail on zero stream id', function (done) { 529 | fail('0000050200000000000102030405', 530 | 'PROTOCOL_ERROR', 531 | /invalid stream id/i, 532 | done) 533 | }) 534 | }) 535 | 536 | describe('X_FORWARDED_FOR', function () { 537 | it('should parse general frame', function (done) { 538 | pass('000004de00000000006f686169', { 539 | type: 'X_FORWARDED_FOR', 540 | host: 'ohai' 541 | }, done) 542 | }) 543 | }) 544 | }) 545 | -------------------------------------------------------------------------------- /test/spdy/transport-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | var streamPair = require('stream-pair') 5 | 6 | var transport = require('../../') 7 | 8 | describe('SPDY Transport', function () { 9 | var server = null 10 | var client = null 11 | 12 | beforeEach(function () { 13 | var pair = streamPair.create() 14 | 15 | server = transport.connection.create(pair, { 16 | protocol: 'spdy', 17 | windowSize: 256, 18 | isServer: true, 19 | autoSpdy31: true 20 | }) 21 | 22 | client = transport.connection.create(pair.other, { 23 | protocol: 'spdy', 24 | windowSize: 256, 25 | isServer: false, 26 | autoSpdy31: true 27 | }) 28 | }) 29 | 30 | describe('autoSpdy31', function () { 31 | it('should automatically switch on server', function (done) { 32 | server.start(3) 33 | assert.strictEqual(server.getVersion(), 3) 34 | 35 | client.start(3.1) 36 | 37 | server.on('version', function () { 38 | assert.strictEqual(server.getVersion(), 3.1) 39 | done() 40 | }) 41 | }) 42 | }) 43 | 44 | describe('version detection', function () { 45 | it('should detect v2 on server', function (done) { 46 | client.start(2) 47 | 48 | server.on('version', function () { 49 | assert.strictEqual(server.getVersion(), 2) 50 | done() 51 | }) 52 | }) 53 | 54 | it('should detect v3 on server', function (done) { 55 | client.start(3) 56 | 57 | server.on('version', function () { 58 | assert.strictEqual(server.getVersion(), 3) 59 | done() 60 | }) 61 | }) 62 | }) 63 | 64 | it('it should not wait for id=0 WINDOW_UPDATE on v3', function (done) { 65 | client.start(3) 66 | 67 | var buf = Buffer.alloc(64 * 1024) 68 | buf.fill('x') 69 | 70 | client.request({ 71 | method: 'POST', 72 | path: '/', 73 | headers: {} 74 | }, function (err, stream) { 75 | assert(!err) 76 | 77 | stream.write(buf) 78 | stream.write(buf) 79 | stream.write(buf) 80 | stream.end(buf) 81 | }) 82 | 83 | server.on('stream', function (stream) { 84 | stream.respond(200, {}) 85 | 86 | var received = 0 87 | stream.on('data', function (chunk) { 88 | received += chunk.length 89 | }) 90 | 91 | stream.on('end', function () { 92 | assert.strictEqual(received, buf.length * 4) 93 | done() 94 | }) 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /test/spdy/v3-parser-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | 5 | var transport = require('../../') 6 | var spdy = transport.protocol.spdy 7 | 8 | describe('SPDY Parser (v3)', function () { 9 | var parser 10 | var window 11 | 12 | beforeEach(function () { 13 | window = new transport.Window({ 14 | id: 0, 15 | isServer: true, 16 | recv: { size: 1024 * 1024 }, 17 | send: { size: 1024 * 1024 } 18 | }) 19 | 20 | var pool = spdy.compressionPool.create() 21 | parser = spdy.parser.create({ window: window }) 22 | var comp = pool.get('3.1') 23 | parser.setCompression(comp) 24 | parser.skipPreface() 25 | }) 26 | 27 | function pass (data, expected, done) { 28 | parser.write(Buffer.from(data, 'hex')) 29 | 30 | if (!Array.isArray(expected)) { 31 | expected = [ expected ] 32 | } 33 | 34 | parser.on('data', function (frame) { 35 | if (expected.length === 0) { 36 | return 37 | } 38 | 39 | assert.deepStrictEqual(frame, expected.shift()) 40 | 41 | if (expected.length === 0) { 42 | assert.strictEqual(parser.buffer.size, 0) 43 | done() 44 | } 45 | }) 46 | } 47 | 48 | function fail (data, code, re, done) { 49 | parser.write(Buffer.from(data, 'hex'), function (err) { 50 | assert(err) 51 | assert(err instanceof transport.protocol.base.utils.ProtocolError) 52 | assert.strictEqual(err.code, spdy.constants.error[code]) 53 | assert(re.test(err.message), err.message) 54 | 55 | done() 56 | }) 57 | parser.once('error', assert) 58 | } 59 | 60 | describe('SYN_STREAM', function () { 61 | it('should parse frame with http header', function (done) { 62 | var hexFrame = '800300010000002c0000000100000000000078' + 63 | 'f9e3c6a7c202e50e507ab442a45a77d7105006' + 64 | 'b32a4804974d8cfa00000000ffff' 65 | 66 | pass(hexFrame, { 67 | fin: false, 68 | headers: { 69 | ':method': 'GET', 70 | ':path': '/' 71 | }, 72 | id: 1, 73 | path: '/', 74 | priority: { 75 | exclusive: false, 76 | parent: 0, 77 | weight: 1.0 78 | }, 79 | type: 'HEADERS', // by spec 'SYN_STREAM' 80 | writable: true 81 | }, done) 82 | }) 83 | 84 | it('should fail on stream ID 0', function (done) { 85 | var hexFrame = '800300010000002c0000000000000000000078' + 86 | 'f9e3c6a7c202e50e507ab442a45a77d7105006' + 87 | 'b32a4804974d8cfa00000000ffff' 88 | 89 | fail(hexFrame, 'PROTOCOL_ERROR', /Invalid*/i, done) 90 | }) 91 | 92 | it('should parse frame with http header and FIN flag', function (done) { 93 | var hexFrame = '800300010100002c0000000100000000000078' + 94 | 'f9e3c6a7c202e50e507ab442a45a77d7105006' + 95 | 'b32a4804974d8cfa00000000ffff' 96 | 97 | pass(hexFrame, { 98 | fin: true, 99 | headers: { 100 | ':method': 'GET', 101 | ':path': '/' 102 | }, 103 | id: 1, 104 | path: '/', 105 | priority: { 106 | exclusive: false, 107 | parent: 0, 108 | weight: 1.0 109 | }, 110 | type: 'HEADERS', // by spec 'SYN_STREAM' 111 | writable: true 112 | }, done) 113 | }) 114 | 115 | it('should parse frame with unidirectional flag', function (done) { 116 | var hexFrame = '800300010200002c0000000100000000000078' + 117 | 'f9e3c6a7c202e50e507ab442a45a77d7105006' + 118 | 'b32a4804974d8cfa00000000ffff' 119 | 120 | pass(hexFrame, { 121 | fin: false, 122 | headers: { 123 | ':method': 'GET', 124 | ':path': '/' 125 | }, 126 | id: 1, 127 | path: '/', 128 | priority: { 129 | exclusive: false, 130 | parent: 0, 131 | weight: 1.0 132 | }, 133 | type: 'HEADERS', // by spec 'SYN_STREAM' 134 | writable: false 135 | }, done) 136 | }) 137 | }) 138 | 139 | describe('SYN_REPLY', function () { 140 | it('should parse a frame without headers', function (done) { 141 | var hexFrame = '80030002000000140000000178f9e3c6a7c202a6230600000000ffff' 142 | 143 | pass(hexFrame, { 144 | fin: false, 145 | headers: {}, 146 | id: 1, 147 | path: undefined, 148 | priority: { 149 | exclusive: false, 150 | parent: 0, 151 | weight: 16 152 | }, 153 | type: 'HEADERS', // by spec SYN_REPLY 154 | writable: true 155 | }, done) 156 | }) 157 | 158 | it('should parse a frame with headers', function (done) { 159 | var hexFrame = '8003000200000057000000013830e3c6a7c2004300bcff' + 160 | '00000003000000057468657265000000057468657265000000073a737' + 161 | '46174757300000006323030204f4b000000083a76657273696f6e0000' + 162 | '0008485454502f312e31000000ffff' 163 | 164 | pass(hexFrame, { 165 | fin: false, 166 | headers: { 167 | ':status': '200', 168 | there: 'there' 169 | }, 170 | id: 1, 171 | path: undefined, 172 | priority: { 173 | exclusive: false, 174 | parent: 0, 175 | weight: 16 176 | }, 177 | type: 'HEADERS', // by spec SYN_REPLY 178 | writable: true 179 | }, done) 180 | }) 181 | 182 | it('should parse frame with FIN_FLAG', function (done) { 183 | var hexFrame = '80030002010000140000000178f9e3c6a7c202a6230600000000ffff' 184 | 185 | pass(hexFrame, { 186 | fin: true, 187 | headers: {}, 188 | id: 1, 189 | path: undefined, 190 | priority: { 191 | exclusive: false, 192 | parent: 0, 193 | weight: 16 194 | }, 195 | type: 'HEADERS', // by spec SYN_REPLY 196 | writable: true 197 | }, done) 198 | }) 199 | }) 200 | 201 | describe('DATA_FRAME', function () { 202 | it('should parse frame with no flags', function (done) { 203 | var hexFrame = '000000010000001157726974696e6720746f2073747265616d' 204 | 205 | pass(hexFrame, { 206 | data: Buffer.from('57726974696e6720746f2073747265616d', 'hex'), 207 | fin: false, 208 | id: 1, 209 | type: 'DATA' 210 | }, done) 211 | }) 212 | 213 | it('should parse partial frame with no flags', function (done) { 214 | var hexFrame = '000000010000001157726974696e6720746f207374726561' 215 | 216 | pass(hexFrame, { 217 | data: Buffer.from('57726974696e6720746f207374726561', 'hex'), 218 | fin: false, 219 | id: 1, 220 | type: 'DATA' 221 | }, function () { 222 | assert.strictEqual(parser.waiting, 1) 223 | pass('ff', { 224 | data: Buffer.from('ff', 'hex'), 225 | fin: false, 226 | id: 1, 227 | type: 'DATA' 228 | }, function () { 229 | assert.strictEqual(window.recv.current, 1048559) 230 | done() 231 | }) 232 | }) 233 | }) 234 | 235 | it('should parse frame with FLAG_FIN', function (done) { 236 | var hexFrame = '000000010100001157726974696e6720746f2073747265616d' 237 | 238 | pass(hexFrame, { 239 | data: Buffer.from('57726974696e6720746f2073747265616d', 'hex'), 240 | fin: true, 241 | id: 1, 242 | type: 'DATA' 243 | }, done) 244 | }) 245 | 246 | it('should parse partial frame with FLAG_FIN', function (done) { 247 | var hexFrame = '000000010100001157726974696e6720746f207374726561' 248 | 249 | pass(hexFrame, { 250 | data: Buffer.from('57726974696e6720746f207374726561', 'hex'), 251 | fin: false, 252 | id: 1, 253 | type: 'DATA' 254 | }, function () { 255 | assert.strictEqual(parser.waiting, 1) 256 | pass('ff', { 257 | data: Buffer.from('ff', 'hex'), 258 | fin: true, 259 | id: 1, 260 | type: 'DATA' 261 | }, done) 262 | }) 263 | }) 264 | }) 265 | 266 | describe('RST_STREAM', function () { 267 | it('should parse w/ status code PROTOCOL_ERROR', function (done) { 268 | var hexFrame = '80030003000000080000000100000001' 269 | 270 | pass(hexFrame, { 271 | code: 'PROTOCOL_ERROR', 272 | id: 1, 273 | type: 'RST' // RST_STREAM by spec 274 | }, done) 275 | }) 276 | 277 | it('should parse w/ status code INVALID_STREAM', function (done) { 278 | var hexFrame = '80030003000000080000000100000002' 279 | 280 | pass(hexFrame, { 281 | code: 'INVALID_STREAM', 282 | id: 1, 283 | type: 'RST' // RST_STREAM by spec 284 | }, done) 285 | }) 286 | 287 | it('should parse w/ status code REFUSED_STREAN', function (done) { 288 | var hexFrame = '80030003000000080000000100000003' 289 | 290 | pass(hexFrame, { 291 | code: 'REFUSED_STREAM', 292 | id: 1, 293 | type: 'RST' // RST_STREAM by spec 294 | }, done) 295 | }) 296 | 297 | it('should parse w/ status code UNSUPPORTED_VERSION', function (done) { 298 | var hexFrame = '80030003000000080000000100000004' 299 | 300 | pass(hexFrame, { 301 | code: 'UNSUPPORTED_VERSION', 302 | id: 1, 303 | type: 'RST' // RST_STREAM by spec 304 | }, done) 305 | }) 306 | 307 | it('should parse w/ status code CANCEL', function (done) { 308 | var hexFrame = '80030003000000080000000100000005' 309 | 310 | pass(hexFrame, { 311 | code: 'CANCEL', 312 | id: 1, 313 | type: 'RST' // RST_STREAM by spec 314 | }, done) 315 | }) 316 | 317 | it('should parse w/ status code INTERNAL_ERROR', function (done) { 318 | var hexFrame = '80030003000000080000000100000006' 319 | 320 | pass(hexFrame, { 321 | code: 'INTERNAL_ERROR', 322 | id: 1, 323 | type: 'RST' // RST_STREAM by spec 324 | }, done) 325 | }) 326 | 327 | it('should parse w/ status code FLOW_CONTROL_ERROR', function (done) { 328 | var hexFrame = '80030003000000080000000100000007' 329 | 330 | pass(hexFrame, { 331 | code: 'FLOW_CONTROL_ERROR', 332 | id: 1, 333 | type: 'RST' // RST_STREAM by spec 334 | }, done) 335 | }) 336 | 337 | it('should parse w/ status code STREAM_IN_USE', function (done) { 338 | var hexFrame = '80030003000000080000000100000008' 339 | 340 | pass(hexFrame, { 341 | code: 'STREAM_IN_USE', 342 | id: 1, 343 | type: 'RST' // RST_STREAM by spec 344 | }, done) 345 | }) 346 | 347 | it('should parse w/ status code STREAM_ALREADY_CLOSED', function (done) { 348 | var hexFrame = '80030003000000080000000100000009' 349 | 350 | pass(hexFrame, { 351 | code: 'STREAM_CLOSED', // STREAM_ALREADY_CLOSED by spec 352 | id: 1, 353 | type: 'RST' // RST_STREAM by spec 354 | }, done) 355 | }) 356 | 357 | it('should parse w/ status code FRAME_TOO_LARGE', function (done) { 358 | var hexframe = '8003000300000008000000010000000b' 359 | 360 | pass(hexframe, { 361 | code: 'FRAME_TOO_LARGE', 362 | id: 1, 363 | type: 'RST' // RST_STREAM by spec 364 | }, done) 365 | }) 366 | }) 367 | 368 | describe('SETTINGS', function () { 369 | it('should parse frame', function (done) { 370 | var hexFrame = '800300040000000c000000010100000700000100' 371 | 372 | pass(hexFrame, { 373 | settings: { 374 | initial_window_size: 256 375 | }, 376 | type: 'SETTINGS' 377 | }, done) 378 | }) 379 | }) 380 | 381 | describe('PING', function () { 382 | it('should parse ACK frame', function (done) { 383 | var hexFrame = '800300060000000400000001' 384 | 385 | pass(hexFrame, { 386 | ack: true, 387 | opaque: Buffer.from('00000001', 'hex'), 388 | type: 'PING' 389 | }, done) 390 | }) 391 | }) 392 | 393 | describe('PING', function () { 394 | it('should parse not ACK frame', function (done) { 395 | var hexFrame = '800300060000000400000002' 396 | 397 | pass(hexFrame, { 398 | ack: false, 399 | opaque: Buffer.from('00000002', 'hex'), 400 | type: 'PING' 401 | }, done) 402 | }) 403 | }) 404 | 405 | describe('GOAWAY', function () { 406 | it('should parse frame with status code OK', function (done) { 407 | var hexframe = '80030007000000080000000100000000' 408 | 409 | pass(hexframe, { 410 | code: 'OK', 411 | lastId: 1, 412 | type: 'GOAWAY' 413 | }, done) 414 | }) 415 | 416 | it('should parse frame with status code PROTOCOL_ERROR', function (done) { 417 | var hexframe = '80030007000000080000000100000001' 418 | 419 | pass(hexframe, { 420 | code: 'PROTOCOL_ERROR', 421 | lastId: 1, 422 | type: 'GOAWAY' 423 | }, done) 424 | }) 425 | 426 | it('should parse frame with status code INTERNAL_ERROR', function (done) { 427 | var hexframe = '80030007000000080000000100000002' 428 | 429 | pass(hexframe, { 430 | code: 'INTERNAL_ERROR', 431 | lastId: 1, 432 | type: 'GOAWAY' 433 | }, done) 434 | }) 435 | }) 436 | 437 | describe('HEADERS', function () { 438 | it('should parse frame', function (done) { 439 | var context = '8003000200000057000000013830e3c6a7c2004300bcff00000003' + 440 | '000000057468657265000000057468657265000000073a737461747573' + 441 | '00000006323030204f4b000000083a76657273696f6e00000008485454' + 442 | '502f312e31000000ffff' 443 | 444 | var frame = '800300080000002500000001001700e8ff00000001000000046d6f7265' + 445 | '0000000768656164657273000000ffff' 446 | 447 | pass(context, { 448 | fin: false, 449 | headers: { 450 | ':status': '200', 451 | there: 'there' 452 | }, 453 | id: 1, 454 | path: undefined, 455 | priority: { 456 | exclusive: false, 457 | parent: 0, 458 | weight: 16 459 | }, 460 | type: 'HEADERS', 461 | writable: true 462 | }, function () { 463 | pass(frame, { 464 | fin: false, 465 | headers: { 466 | more: 'headers' 467 | }, 468 | id: 1, 469 | path: undefined, 470 | priority: { 471 | exclusive: false, 472 | parent: 0, 473 | weight: 16 474 | }, 475 | type: 'HEADERS', 476 | writable: true 477 | }, done) 478 | }) 479 | }) 480 | 481 | it('should parse two consecutive frames', function (done) { 482 | var data = '8003000800000022000000043830e3c6a7c2000e00f1ff00' + 483 | '00000100000001610000000162000000ffff800300080000' + 484 | '001c00000004000e00f1ff00000001000000016100000001' + 485 | '62000000ffff' 486 | 487 | pass(data, [ { 488 | fin: false, 489 | headers: { 490 | a: 'b' 491 | }, 492 | id: 4, 493 | path: undefined, 494 | priority: { 495 | exclusive: false, 496 | parent: 0, 497 | weight: 16 498 | }, 499 | type: 'HEADERS', 500 | writable: true 501 | }, { 502 | fin: false, 503 | headers: { 504 | a: 'b' 505 | }, 506 | id: 4, 507 | path: undefined, 508 | priority: { 509 | exclusive: false, 510 | parent: 0, 511 | weight: 16 512 | }, 513 | type: 'HEADERS', 514 | writable: true 515 | } ], done) 516 | }) 517 | }) 518 | 519 | describe('WINDOW_UPDATE', function () { 520 | it('should parse frame', function (done) { 521 | var hexframe = '8003000900000008000000010000abca' 522 | 523 | pass(hexframe, { 524 | delta: 43978, 525 | id: 1, 526 | type: 'WINDOW_UPDATE' 527 | }, done) 528 | }) 529 | }) 530 | }) 531 | --------------------------------------------------------------------------------