UNPKG

h2cli

Version:

A command line interface for HTTP/2

318 lines (297 loc) 10.5 kB
var util = require('util'); var events = require('events'); var h2connection = require('./connection'); var h2frame = require('./frame'); var h2util = require('./util'); var HTTP2_PREFACE = new Buffer('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n', 'ascii'); var handleFrame = function (stream, frame) { var handler = frameHandlers[frame.type]; if (handler) { handler.call(this, stream, frame); } else if (handler === null) { // ignore the frame } else { console.error('no handler: %d', frame.type); } }; var handleDataFrame = function (stream, frame) { var data = frame.getData(); stream.emit('data', data); }; var handleHeadersFrame = function (stream, frames) { var i, headerBlocks = []; for (i = 0; i < frames.length; i++) { headerBlocks.push(frames[i].getBlock()); } var headers = this.connection.hpack.decode(Buffer.concat(headerBlocks)); stream.emit('header', headers); }; var handleSettingsFrame = function (stream, frame) { var p, i, params, nParam, ackFrame; if (frame.flags & h2frame.Http2SettingsFrame.FLAG_ACK) { params = this._settingQueue.shift(); if (params) { nParam = params.length; for (i = 0; i < nParam; i++) { p = params[i]; this.connection.setLocalSetting(p.id, p.value); } } } else { nParam = frame.getParamCount(); for (i = 0; i < nParam; i++) { p = frame.getParamByIndex(i); this.connection.setRemoteSetting(p.id, p.value); } if (this.config['http2.auto_ack']) { ackFrame = new h2frame.Http2SettingsFrame(); ackFrame.flags |= h2frame.Http2SettingsFrame.FLAG_ACK; this.connection.streams[0].send(ackFrame); } } }; var handlePushPromiseFrame = function (stream, frame) { var self = this; var id = stream.connection.newStream(frame.promisedStreamId); var newStream = this.connection.streams[id]; newStream.on('close', function () {}); newStream.on('header', function () {}); newStream.on('data', function (data) { if (data.length) { var frame = new h2frame.Http2WindowUpdateFrame(); frame.windowSizeIncrement = data.length; this.send(frame); self.connection.streams[0].send(frame); } }); }; var handlePingFrame = function (stream, frame) { if (!frame.flags & h2frame.Http2PingFrame.FLAG_ACK) { var ackFrame = new h2frame.Http2PingFrame(); ackFrame.payload = frame.payload; ackFrame.flags |= h2frame.Http2PingFrame.FLAG_ACK; this.connection.streams[0].send(ackFrame); } }; var handleGoawayFrame = function (stream, frame) { this.connection.close(); }; var frameHandlers = [ handleDataFrame, null, void(0), void(0), handleSettingsFrame, handlePushPromiseFrame, handlePingFrame, handleGoawayFrame, void(0), null, void(0), void(0), void(0), ]; var H2 = function (config) { this.config = { 'http2.auto_preface': 1, 'http2.auto_ack': 1, 'http2.use_padding': 0, 'http2.use_upgrade': 0, 'log.state_change': 0, 'log.frame_sent': 0, 'log.frame_received': 0, 'log.verbose': 1, 'hpack.use_huffman': 1 }; var self = this; if (config) { Object.keys(config).forEach(function (k) { self.config[k] = config[k]; }); } }; util.inherits(H2, events.EventEmitter); H2.prototype.connect = function (hostname, port, options, callback) { var self = this; if (!options.secure && typeof options.useUpgrade === 'undefined') { options.useUpgrade = self.config['http2.use_upgrade']; } this.connection = new h2connection.Connection(hostname, port, options); this.connection.hpack.useHuffman(self.config['hpack.use_huffman']); this.connection.on('connect', function (stream) { self.connection.newStream(); var settingsFrame = new h2frame.Http2SettingsFrame(); if (!options.useUpgrade) { if (self.config['http2.auto_preface']) { self.connection.send(HTTP2_PREFACE); self.connection.streams[0].send(settingsFrame); } } else { var frameB64 = settingsFrame.getBuffer().slice(9).toString('base64').replace(/=*$/, ''); self.connection.newStream(1); self.connection.send("GET / HTTP/1.1\r\n"); self.connection.send("Host: " + hostname + "\r\n"); self.connection.send("Connection: Upgrade\r\n"); self.connection.send("Upgrade: h2c\r\n"); self.connection.send("HTTP2-Settings: " + frameB64 + "\r\n"); self.connection.send("\r\n"); } callback(stream); }); this.connection.on('close', function () { self.connection = void(0); }); this.connection.on('error', function (e) { callback(null, e); }); this.connection.on('newStream', function (stream) { stream.on('frame', function (frame) { if (self.config['log.frame_received']) { console.log('RECV[%d]: ' + h2util.printFrame(frame, self.config['log.verbose']), this.id); } handleFrame.call(self, this, frame); }); stream.on('headerFrames', function (frames) { handleHeadersFrame.call(self, this, frames); }); stream.on('send', function (frame) { if (self.config['log.frame_sent']) { console.log("SEND[%d]: " + h2util.printFrame(frame, self.config['log.verbose']), this.id); } }); stream.on('stateChange', function (oldState, newState) { if (self.config['log.state_change']) { console.log('STATE CHANGE[%d]: %s -> %s', this.id, oldState, newState); } }); }); this.connection.on('frame', function (data) { var frame = h2frame.Http2FrameFactory.createFrame(data); var stream = self.connection.streams[frame.streamId]; if (!stream) { console.log('Unexpected streamId: ' + frame.streamId); } stream.consumeFrame(frame); }); this._settingQueue = []; }; H2.prototype.close = function (callback) { if (this.connection) { this.connection.close(callback); } else { if (callback) { callback(); } } }; H2.prototype.send = function (frame, streamId, callback) { if (!this.connection) { console.error('Not connected yet.'); arguments[arguments.length - 1](); return; } if (arguments[0] instanceof h2frame.Http2Frame) { if (typeof arguments[1] === 'number') { this.connection.newStream(streamId); this.connection.streams[streamId].send(frame, arguments[2]); } else { this.connection.streams[0].send(frame, arguments[1]); } } else { this.connection.send(frame, callback); } }; H2.prototype.request = function (/* authority, path, method, data, callbacks */) { var self = this, authority, path, method, data, callbacks, pos, i, f; pos = 0; if (arguments[0][0] === '/') { authority = self.connection ? self.connection.hostname : ''; } else { authority = arguments[pos++]; } if (self.connection && ((self.connection.port !== 443 || !self.connection.secure) && (self.connection.port !== 80 || self.connection.secure))) { authority += ':' + self.connection.port; } path = arguments[pos++]; method = arguments[pos++]; if (typeof arguments[pos] === 'string') { data = new Buffer(arguments[pos++]); callbacks = arguments[pos]; } else { callbacks = arguments[pos]; } if (!self.connection) { console.error('Not connected yet.'); callbacks.error(); return; } var headers = [ [':method', method], [':scheme', self.connection.secure ? 'https' : 'http'], [':authority', authority], [':path', path], ['user-agent', 'h2cli/0.19.0'], ]; if (data) { headers.push(['content-length', '' + data.length]); } var id = this.connection.newStream(); var stream = this.connection.streams[id]; stream.on('close', callbacks.close); stream.on('header', callbacks.header); stream.on('data', function (data) { callbacks.data(data); if (data.length) { var frame = new h2frame.Http2WindowUpdateFrame(); frame.windowSizeIncrement = data.length; this.send(frame); self.connection.streams[0].send(frame); } }); var headersFrames = h2frame.Http2FrameFactory.createRequestFrames(this.connection.hpack, headers, this.config['http2.use_padding']); if (data) { stream.send(headersFrames); var dataFrames = h2frame.Http2FrameFactory.createDataFrames(data, this.config['http2.use_padding']); dataFrames[dataFrames.length - 1].flags |= h2frame.Http2DataFrame.FLAG_END_STREAM; stream.send(dataFrames); } else { if (headersFrames.length > 1) { var dataFrame = new h2frame.Http2DataFrame(); dataFrame.flags |= h2frame.Http2DataFrame.FLAG_END_STREAM; headersFrames.push(dataFrame); } else { headersFrames[0].flags |= h2frame.Http2HeadersFrame.FLAG_END_STREAM; } stream.send(headersFrames); } }; H2.prototype.setSetting = function (name, value, callback) { var frame = new h2frame.Http2SettingsFrame(); var paramId = h2frame.Http2SettingsFrame['PARAM_SETTINGS_' + name]; if (paramId) { frame.setParam(paramId, value); this._settingQueue.push([{'id': paramId, 'value': value}]); this.connection.streams[0].send(frame); } callback(); }; H2.prototype.setConfig = function (name, value) { this.config[name] = parseInt(value, 10); if (name === 'hpack.use_huffman') { if (this.connection) { this.connection.hpack.useHuffman(this.config['hpack.use_huffman']); } } }; module.exports = { 'HTTP2_PREFACE': HTTP2_PREFACE, 'frame': h2frame, 'createClient': function (config) { return new H2(config); }, 'registerFrameHandler': function (type, handler) { frameHandlers[type] = handler; }, };