maskit/h2cli

View on GitHub
lib/h2.js

Summary

Maintainability
D
2 days
Test Coverage
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;
    },
};