node/lib/internal/http2/compat.js
Anatoli Papirovski a4e923f5c1 http2: fix subsequent end calls to not throw
Calling Http2ServerResponse.end multiple times should never
cause the code to throw an error, subsequent calls should
instead return false. Fix behaviour to match http1.

Fixes: https://github.com/nodejs/node/issues/15385
PR-URL: https://github.com/nodejs/node/pull/15414
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
2017-09-18 12:28:24 +02:00

587 lines
14 KiB
JavaScript

'use strict';
const Stream = require('stream');
const Readable = Stream.Readable;
const binding = process.binding('http2');
const constants = binding.constants;
const errors = require('internal/errors');
const kFinish = Symbol('finish');
const kBeginSend = Symbol('begin-send');
const kState = Symbol('state');
const kStream = Symbol('stream');
const kRequest = Symbol('request');
const kResponse = Symbol('response');
const kHeaders = Symbol('headers');
const kRawHeaders = Symbol('rawHeaders');
const kTrailers = Symbol('trailers');
const kRawTrailers = Symbol('rawTrailers');
const {
NGHTTP2_NO_ERROR,
HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_PATH,
HTTP2_HEADER_SCHEME,
HTTP2_HEADER_STATUS,
HTTP_STATUS_CONTINUE,
HTTP_STATUS_EXPECTATION_FAILED,
HTTP_STATUS_METHOD_NOT_ALLOWED,
HTTP_STATUS_OK
} = constants;
let statusMessageWarned = false;
// Defines and implements an API compatibility layer on top of the core
// HTTP/2 implementation, intended to provide an interface that is as
// close as possible to the current require('http') API
function assertValidHeader(name, value) {
if (name === '')
throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Header name', name);
if (isPseudoHeader(name))
throw new errors.Error('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED');
if (value === undefined || value === null)
throw new errors.TypeError('ERR_HTTP2_INVALID_HEADER_VALUE');
}
function isPseudoHeader(name) {
switch (name) {
case HTTP2_HEADER_STATUS: // :status
case HTTP2_HEADER_METHOD: // :method
case HTTP2_HEADER_PATH: // :path
case HTTP2_HEADER_AUTHORITY: // :authority
case HTTP2_HEADER_SCHEME: // :scheme
return true;
default:
return false;
}
}
function statusMessageWarn() {
if (statusMessageWarned === false) {
process.emitWarning(
'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)',
'UnsupportedWarning'
);
statusMessageWarned = true;
}
}
function onStreamData(chunk) {
if (!this[kRequest].push(chunk))
this.pause();
}
function onStreamTrailers(trailers, flags, rawTrailers) {
const request = this[kRequest];
Object.assign(request[kTrailers], trailers);
request[kRawTrailers].push(...rawTrailers);
}
function onStreamEnd() {
// Cause the request stream to end as well.
this[kRequest].push(null);
}
function onStreamError(error) {
// this is purposefully left blank
//
// errors in compatibility mode are
// not forwarded to the request
// and response objects. However,
// they are forwarded to 'clientError'
// on the server by Http2Stream
}
function onRequestPause() {
this[kStream].pause();
}
function onRequestResume() {
this[kStream].resume();
}
function onRequestDrain() {
if (this.isPaused())
this.resume();
}
function onStreamResponseDrain() {
this[kResponse].emit('drain');
}
function onStreamClosedRequest() {
this[kRequest].push(null);
}
function onStreamClosedResponse() {
const res = this[kResponse];
res.writable = false;
res.emit('finish');
}
function onAborted(hadError, code) {
if ((this.writable) ||
(this._readableState && !this._readableState.ended)) {
this.emit('aborted', hadError, code);
}
}
class Http2ServerRequest extends Readable {
constructor(stream, headers, options, rawHeaders) {
super(options);
this[kState] = {
closed: false,
closedCode: NGHTTP2_NO_ERROR
};
this[kHeaders] = headers;
this[kRawHeaders] = rawHeaders;
this[kTrailers] = {};
this[kRawTrailers] = [];
this[kStream] = stream;
stream[kRequest] = this;
// Pause the stream..
stream.pause();
stream.on('data', onStreamData);
stream.on('trailers', onStreamTrailers);
stream.on('end', onStreamEnd);
stream.on('error', onStreamError);
stream.on('close', onStreamClosedRequest);
stream.on('aborted', onAborted.bind(this));
const onfinish = this[kFinish].bind(this);
stream.on('streamClosed', onfinish);
stream.on('finish', onfinish);
this.on('pause', onRequestPause);
this.on('resume', onRequestResume);
this.on('drain', onRequestDrain);
}
get closed() {
return this[kState].closed;
}
get code() {
return this[kState].closedCode;
}
get stream() {
return this[kStream];
}
get headers() {
return this[kHeaders];
}
get rawHeaders() {
return this[kRawHeaders];
}
get trailers() {
return this[kTrailers];
}
get rawTrailers() {
return this[kRawTrailers];
}
get httpVersionMajor() {
return 2;
}
get httpVersionMinor() {
return 0;
}
get httpVersion() {
return '2.0';
}
get socket() {
const stream = this[kStream];
if (stream === undefined)
return;
return stream.session.socket;
}
get connection() {
return this.socket;
}
_read(nread) {
const stream = this[kStream];
if (stream !== undefined) {
stream.resume();
} else {
this.emit('error', new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
}
}
get method() {
return this[kHeaders][HTTP2_HEADER_METHOD];
}
get authority() {
return this[kHeaders][HTTP2_HEADER_AUTHORITY];
}
get scheme() {
return this[kHeaders][HTTP2_HEADER_SCHEME];
}
get url() {
return this[kHeaders][HTTP2_HEADER_PATH];
}
set url(url) {
this[kHeaders][HTTP2_HEADER_PATH] = url;
}
setTimeout(msecs, callback) {
const stream = this[kStream];
if (stream === undefined) return;
stream.setTimeout(msecs, callback);
}
[kFinish](code) {
const state = this[kState];
if (state.closed)
return;
if (code !== undefined)
state.closedCode = Number(code);
state.closed = true;
this.push(null);
process.nextTick(() => (this[kStream] = undefined));
}
}
class Http2ServerResponse extends Stream {
constructor(stream, options) {
super(options);
this[kState] = {
sendDate: true,
statusCode: HTTP_STATUS_OK,
closed: false,
closedCode: NGHTTP2_NO_ERROR
};
this[kHeaders] = Object.create(null);
this[kTrailers] = Object.create(null);
this[kStream] = stream;
stream[kResponse] = this;
this.writable = true;
stream.on('drain', onStreamResponseDrain);
stream.on('close', onStreamClosedResponse);
const onfinish = this[kFinish].bind(this);
stream.on('streamClosed', onfinish);
stream.on('finish', onfinish);
}
get finished() {
const stream = this[kStream];
return stream === undefined || stream._writableState.ended;
}
get closed() {
return this[kState].closed;
}
get code() {
return this[kState].closedCode;
}
get stream() {
return this[kStream];
}
get headersSent() {
const stream = this[kStream];
return stream !== undefined ? stream.headersSent : this[kState].headersSent;
}
get sendDate() {
return this[kState].sendDate;
}
set sendDate(bool) {
this[kState].sendDate = Boolean(bool);
}
get statusCode() {
return this[kState].statusCode;
}
set statusCode(code) {
code |= 0;
if (code >= 100 && code < 200)
throw new errors.RangeError('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED');
if (code < 100 || code > 599)
throw new errors.RangeError('ERR_HTTP2_STATUS_INVALID', code);
this[kState].statusCode = code;
}
setTrailer(name, value) {
if (typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string');
name = name.trim().toLowerCase();
assertValidHeader(name, value);
this[kTrailers][name] = String(value);
}
addTrailers(headers) {
const keys = Object.keys(headers);
let key = '';
for (var i = 0; i < keys.length; i++) {
key = keys[i];
this.setTrailer(key, headers[key]);
}
}
getHeader(name) {
if (typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string');
name = name.trim().toLowerCase();
return this[kHeaders][name];
}
getHeaderNames() {
return Object.keys(this[kHeaders]);
}
getHeaders() {
return Object.assign({}, this[kHeaders]);
}
hasHeader(name) {
if (typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string');
name = name.trim().toLowerCase();
return Object.prototype.hasOwnProperty.call(this[kHeaders], name);
}
removeHeader(name) {
if (typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string');
name = name.trim().toLowerCase();
delete this[kHeaders][name];
}
setHeader(name, value) {
if (typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string');
name = name.trim().toLowerCase();
assertValidHeader(name, value);
this[kHeaders][name] = String(value);
}
get statusMessage() {
statusMessageWarn();
return '';
}
set statusMessage(msg) {
statusMessageWarn();
}
flushHeaders() {
const stream = this[kStream];
if (stream !== undefined && stream.headersSent === false)
this[kBeginSend]();
}
writeHead(statusCode, statusMessage, headers) {
if (typeof statusMessage === 'string') {
statusMessageWarn();
}
if (headers === undefined && typeof statusMessage === 'object') {
headers = statusMessage;
}
const stream = this[kStream];
if (stream === undefined) {
throw new errors.Error('ERR_HTTP2_STREAM_CLOSED');
}
if (stream.headersSent === true) {
throw new errors.Error('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND');
}
if (typeof headers === 'object') {
const keys = Object.keys(headers);
let key = '';
for (var i = 0; i < keys.length; i++) {
key = keys[i];
this.setHeader(key, headers[key]);
}
}
this.statusCode = statusCode;
this[kBeginSend]();
}
write(chunk, encoding, cb) {
const stream = this[kStream];
if (typeof encoding === 'function') {
cb = encoding;
encoding = 'utf8';
}
if (stream === undefined) {
const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
if (cb)
process.nextTick(cb, err);
else
throw err;
return;
}
this[kBeginSend]();
return stream.write(chunk, encoding, cb);
}
end(chunk, encoding, cb) {
const stream = this[kStream];
if (typeof chunk === 'function') {
cb = chunk;
chunk = null;
encoding = 'utf8';
} else if (typeof encoding === 'function') {
cb = encoding;
encoding = 'utf8';
}
if (stream === undefined || stream.finished === true) {
return false;
}
if (chunk !== null && chunk !== undefined) {
this.write(chunk, encoding);
}
if (typeof cb === 'function') {
stream.once('finish', cb);
}
this[kBeginSend]({ endStream: true });
stream.end();
}
destroy(err) {
const stream = this[kStream];
if (stream === undefined) {
// nothing to do, already closed
return;
}
stream.destroy(err);
}
setTimeout(msecs, callback) {
const stream = this[kStream];
if (stream === undefined) return;
stream.setTimeout(msecs, callback);
}
createPushResponse(headers, callback) {
const stream = this[kStream];
if (stream === undefined) {
process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
return;
}
stream.pushStream(headers, {}, function(stream, headers, options) {
const response = new Http2ServerResponse(stream);
callback(null, response);
});
}
[kBeginSend](options) {
const stream = this[kStream];
if (stream !== undefined &&
stream.destroyed === false &&
stream.headersSent === false) {
options = options || Object.create(null);
const state = this[kState];
const headers = this[kHeaders];
headers[HTTP2_HEADER_STATUS] = state.statusCode;
if (stream.finished === true)
options.endStream = true;
options.getTrailers = (trailers) => {
Object.assign(trailers, this[kTrailers]);
};
stream.respond(headers, options);
}
}
[kFinish](code) {
const state = this[kState];
if (state.closed)
return;
if (code !== undefined)
state.closedCode = Number(code);
state.closed = true;
state.headersSent = this[kStream].headersSent;
this.end();
process.nextTick(() => (this[kStream] = undefined));
this.emit('finish');
}
// TODO doesn't support callbacks
writeContinue() {
const stream = this[kStream];
if (stream === undefined) return false;
this[kStream].additionalHeaders({
[HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE
});
return true;
}
}
function onServerStream(stream, headers, flags, rawHeaders) {
const server = this;
const request = new Http2ServerRequest(stream, headers, undefined,
rawHeaders);
const response = new Http2ServerResponse(stream);
// Check for the CONNECT method
const method = headers[HTTP2_HEADER_METHOD];
if (method === 'CONNECT') {
if (!server.emit('connect', request, response)) {
response.statusCode = HTTP_STATUS_METHOD_NOT_ALLOWED;
response.end();
}
return;
}
// Check for Expectations
if (headers.expect !== undefined) {
if (headers.expect === '100-continue') {
if (server.listenerCount('checkContinue')) {
server.emit('checkContinue', request, response);
} else {
response.writeContinue();
server.emit('request', request, response);
}
} else if (server.listenerCount('checkExpectation')) {
server.emit('checkExpectation', request, response);
} else {
response.statusCode = HTTP_STATUS_EXPECTATION_FAILED;
response.end();
}
return;
}
server.emit('request', request, response);
}
module.exports = {
onServerStream,
Http2ServerRequest,
Http2ServerResponse,
};