HTTP/2 - make protocol settings configurable
1. Recently we have updated our receive window size to a larger value. Unfortunately, this also results in auto-test pumping through more data, which probably takes more time on CI. At the moment we do not have any public API on QNAM's level to customize HTTP/2 parameters (aka 5.10/FF and so on). So we use the fact that QNAM is QObject and we can set a property on it. This property is our Http2::ProtocolParameters object that allows us to configure: - HPACK parameters (in 5.10 - noop) - session receive window size - different SETTINGS as described by RFC 7540, 6.5.2. 2. Undocumented environment variable to set ENABLE_PUSH is not needed anymore. 3. In 5.11 Http2::ProtocolParameter will become a public API and we'll introduce a new setter in QNAM. Change-Id: If08fd5e09e7c0b61cf9700b426b60b5837b6b2e6 Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io> Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>bb10
parent
f174d31667
commit
76a6b32942
|
|
@ -62,9 +62,88 @@ const char Http2clientPreface[clientPrefaceLength] =
|
|||
0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a,
|
||||
0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a};
|
||||
|
||||
QByteArray default_SETTINGS_to_Base64()
|
||||
// TODO: (in 5.11) - remove it!
|
||||
const char *http2ParametersPropertyName = "QT_HTTP2_PARAMETERS_PROPERTY";
|
||||
|
||||
ProtocolParameters::ProtocolParameters()
|
||||
{
|
||||
Frame frame(default_SETTINGS_frame());
|
||||
settingsFrameData[Settings::INITIAL_WINDOW_SIZE_ID] = qtDefaultStreamReceiveWindowSize;
|
||||
settingsFrameData[Settings::ENABLE_PUSH_ID] = 0;
|
||||
}
|
||||
|
||||
bool ProtocolParameters::validate() const
|
||||
{
|
||||
// 0. Huffman/indexing: any values are valid and allowed.
|
||||
|
||||
// 1. Session receive window size (client side): HTTP/2 starts from the
|
||||
// default value of 64Kb, if a client code tries to set lesser value,
|
||||
// the delta would become negative, but this is not allowed.
|
||||
if (maxSessionReceiveWindowSize < qint32(defaultSessionWindowSize)) {
|
||||
qCWarning(QT_HTTP2, "Session receive window must be at least 65535 bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. HEADER_TABLE_SIZE: we do not validate HEADER_TABLE_SIZE, considering
|
||||
// all values as valid. RFC 7540 and 7541 do not provide any lower/upper
|
||||
// limits. If it's 0 - we do not index anything, if it's too huge - a user
|
||||
// who provided such a value can potentially have a huge memory footprint,
|
||||
// up to them to decide.
|
||||
|
||||
// 3. SETTINGS_ENABLE_PUSH: RFC 7540, 6.5.2, a value other than 0 or 1 will
|
||||
// be treated by our peer as a PROTOCOL_ERROR.
|
||||
if (settingsFrameData.contains(Settings::ENABLE_PUSH_ID)
|
||||
&& settingsFrameData[Settings::ENABLE_PUSH_ID] > 1) {
|
||||
qCWarning(QT_HTTP2, "SETTINGS_ENABLE_PUSH can be only 0 or 1");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. SETTINGS_MAX_CONCURRENT_STREAMS : RFC 7540 recommends 100 as the lower
|
||||
// limit, says nothing about the upper limit. The RFC allows 0, but this makes
|
||||
// no sense to us at all: there is no way a user can change this later and
|
||||
// we'll not be able to get any responses on such a connection.
|
||||
if (settingsFrameData.contains(Settings::MAX_CONCURRENT_STREAMS_ID)
|
||||
&& !settingsFrameData[Settings::MAX_CONCURRENT_STREAMS_ID]) {
|
||||
qCWarning(QT_HTTP2, "MAX_CONCURRENT_STREAMS must be a positive number");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. SETTINGS_INITIAL_WINDOW_SIZE.
|
||||
if (settingsFrameData.contains(Settings::INITIAL_WINDOW_SIZE_ID)) {
|
||||
const quint32 value = settingsFrameData[Settings::INITIAL_WINDOW_SIZE_ID];
|
||||
// RFC 7540, 6.5.2 (the upper limit). The lower limit is our own - we send
|
||||
// SETTINGS frame only once and will not be able to change this 0, thus
|
||||
// we'll suspend all streams.
|
||||
if (!value || value > quint32(maxSessionReceiveWindowSize)) {
|
||||
qCWarning(QT_HTTP2, "INITIAL_WINDOW_SIZE must be in the range "
|
||||
"(0, 2^31-1]");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. SETTINGS_MAX_FRAME_SIZE: RFC 7540, 6.5.2, a value outside of the range
|
||||
// [2^14-1, 2^24-1] will be treated by our peer as a PROTOCOL_ERROR.
|
||||
if (settingsFrameData.contains(Settings::MAX_FRAME_SIZE_ID)) {
|
||||
const quint32 value = settingsFrameData[Settings::INITIAL_WINDOW_SIZE_ID];
|
||||
if (value < maxFrameSize || value > maxPayloadSize) {
|
||||
qCWarning(QT_HTTP2, "MAX_FRAME_SIZE must be in the range [2^14, 2^24-1]");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// For SETTINGS_MAX_HEADER_LIST_SIZE RFC 7540 does not provide any specific
|
||||
// numbers. It's clear, if a value is too small, no header can ever be sent
|
||||
// by our peer at all. The default value is unlimited and we normally do not
|
||||
// change this.
|
||||
//
|
||||
// Note: the size is calculated as the length of uncompressed (no HPACK)
|
||||
// name + value + 32 bytes.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QByteArray ProtocolParameters::settingsFrameToBase64() const
|
||||
{
|
||||
Frame frame(settingsFrame());
|
||||
// SETTINGS frame's payload consists of pairs:
|
||||
// 2-byte-identifier | 4-byte-value == multiple of 6.
|
||||
Q_ASSERT(frame.payloadSize() && !(frame.payloadSize() % 6));
|
||||
|
|
@ -78,21 +157,35 @@ QByteArray default_SETTINGS_to_Base64()
|
|||
return wrapper.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
}
|
||||
|
||||
void prepare_for_protocol_upgrade(QHttpNetworkRequest &request)
|
||||
Frame ProtocolParameters::settingsFrame() const
|
||||
{
|
||||
// 6.5 SETTINGS
|
||||
FrameWriter builder(FrameType::SETTINGS, FrameFlag::EMPTY, connectionStreamID);
|
||||
for (auto it = settingsFrameData.cbegin(), end = settingsFrameData.cend();
|
||||
it != end; ++it) {
|
||||
builder.append(it.key());
|
||||
builder.append(it.value());
|
||||
}
|
||||
|
||||
return builder.outboundFrame();
|
||||
}
|
||||
|
||||
void ProtocolParameters::addProtocolUpgradeHeaders(QHttpNetworkRequest *request) const
|
||||
{
|
||||
Q_ASSERT(request);
|
||||
// RFC 2616, 14.10
|
||||
// RFC 7540, 3.2
|
||||
QByteArray value(request.headerField("Connection"));
|
||||
QByteArray value(request->headerField("Connection"));
|
||||
// We _append_ 'Upgrade':
|
||||
if (value.size())
|
||||
value += ", ";
|
||||
|
||||
value += "Upgrade, HTTP2-Settings";
|
||||
request.setHeaderField("Connection", value);
|
||||
request->setHeaderField("Connection", value);
|
||||
// This we just (re)write.
|
||||
request.setHeaderField("Upgrade", "h2c");
|
||||
request->setHeaderField("Upgrade", "h2c");
|
||||
// This we just (re)write.
|
||||
request.setHeaderField("HTTP2-Settings", default_SETTINGS_to_Base64());
|
||||
request->setHeaderField("HTTP2-Settings", settingsFrameToBase64());
|
||||
}
|
||||
|
||||
void qt_error(quint32 errorCode, QNetworkReply::NetworkError &error,
|
||||
|
|
@ -188,13 +281,6 @@ QNetworkReply::NetworkError qt_error(quint32 errorCode)
|
|||
return error;
|
||||
}
|
||||
|
||||
bool is_PUSH_PROMISE_enabled()
|
||||
{
|
||||
bool ok = false;
|
||||
const int env = qEnvironmentVariableIntValue("QT_HTTP2_ENABLE_PUSH_PROMISE", &ok);
|
||||
return ok && env;
|
||||
}
|
||||
|
||||
bool is_protocol_upgraded(const QHttpNetworkReply &reply)
|
||||
{
|
||||
if (reply.statusCode() == 101) {
|
||||
|
|
@ -209,21 +295,6 @@ bool is_protocol_upgraded(const QHttpNetworkReply &reply)
|
|||
return false;
|
||||
}
|
||||
|
||||
Frame default_SETTINGS_frame()
|
||||
{
|
||||
// 6.5 SETTINGS
|
||||
FrameWriter builder(FrameType::SETTINGS, FrameFlag::EMPTY, connectionStreamID);
|
||||
// MAX frame size (16 kb), disable/enable PUSH_PROMISE
|
||||
builder.append(Settings::MAX_FRAME_SIZE_ID);
|
||||
builder.append(quint32(maxFrameSize));
|
||||
builder.append(Settings::INITIAL_WINDOW_SIZE_ID);
|
||||
builder.append(initialStreamReceiveWindowSize);
|
||||
builder.append(Settings::ENABLE_PUSH_ID);
|
||||
builder.append(quint32(is_PUSH_PROMISE_enabled()));
|
||||
|
||||
return builder.outboundFrame();
|
||||
}
|
||||
|
||||
} // namespace Http2
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
|
||||
#include <QtNetwork/qnetworkreply.h>
|
||||
#include <QtCore/qloggingcategory.h>
|
||||
#include <QtCore/qmetatype.h>
|
||||
#include <QtCore/qglobal.h>
|
||||
|
||||
// Different HTTP/2 constants/values as defined by RFC 7540.
|
||||
|
|
@ -61,6 +62,7 @@ QT_BEGIN_NAMESPACE
|
|||
|
||||
class QHttpNetworkRequest;
|
||||
class QHttpNetworkReply;
|
||||
class QByteArray;
|
||||
class QString;
|
||||
|
||||
namespace Http2
|
||||
|
|
@ -137,14 +139,54 @@ const quint32 lastValidStreamID((quint32(1) << 31) - 1); // HTTP/2, 5.1.1
|
|||
// sending WINDOW_UPDATE frames on a stream/session all the time, for each
|
||||
// 2 DATE frames of size 16K (also default) we'll send a WINDOW_UPDATE frame
|
||||
// for a given stream and have a download speed order of magnitude lower than
|
||||
// our own HTTP/1.1 protocol handler. We choose a bigger window size (normally,
|
||||
// HTTP/2 servers are not afraid to immediately set it to possible max anyway)
|
||||
// and split this window size between our concurrent streams.
|
||||
const qint32 initialSessionReceiveWindowSize = defaultSessionWindowSize * 10000;
|
||||
const qint32 initialStreamReceiveWindowSize = initialSessionReceiveWindowSize / maxConcurrentStreams;
|
||||
// our own HTTP/1.1 protocol handler. We choose a bigger window size: normally,
|
||||
// HTTP/2 servers are not afraid to immediately set it to the possible max,
|
||||
// we do the same and split this window size between our concurrent streams.
|
||||
const qint32 maxSessionReceiveWindowSize((quint32(1) << 31) - 1);
|
||||
const qint32 qtDefaultStreamReceiveWindowSize = maxSessionReceiveWindowSize / maxConcurrentStreams;
|
||||
|
||||
// The class ProtocolParameters allows client code to customize HTTP/2 protocol
|
||||
// handler, if needed. Normally, we use our own default parameters (see below).
|
||||
// In 5.10 we can also use setProperty/property on a QNAM object to pass the
|
||||
// non-default values to the protocol handler. In 5.11 this will probably become
|
||||
// a public API.
|
||||
|
||||
using RawSettings = QMap<Settings, quint32>;
|
||||
|
||||
struct Q_AUTOTEST_EXPORT ProtocolParameters
|
||||
{
|
||||
ProtocolParameters();
|
||||
|
||||
bool validate() const;
|
||||
QByteArray settingsFrameToBase64() const;
|
||||
struct Frame settingsFrame() const;
|
||||
void addProtocolUpgradeHeaders(QHttpNetworkRequest *request) const;
|
||||
|
||||
// HPACK:
|
||||
// TODO: for now we ignore them (fix it for 5.11, would require changes in HPACK)
|
||||
bool useHuffman = true;
|
||||
bool indexStrings = true;
|
||||
|
||||
// This parameter is not negotiated via SETTINGS frames, so we have it
|
||||
// as a member and will convey it to our peer as a WINDOW_UPDATE frame:
|
||||
qint32 maxSessionReceiveWindowSize = Http2::maxSessionReceiveWindowSize;
|
||||
|
||||
// This is our default SETTINGS frame:
|
||||
//
|
||||
// SETTINGS_INITIAL_WINDOW_SIZE: (2^31 - 1) / 100
|
||||
// SETTINGS_ENABLE_PUSH: 0.
|
||||
//
|
||||
// Note, whenever we skip some value in our SETTINGS frame, our peer
|
||||
// will assume the defaults recommended by RFC 7540, which in general
|
||||
// are good enough, although we (and most browsers) prefer to work
|
||||
// with larger window sizes.
|
||||
RawSettings settingsFrameData;
|
||||
};
|
||||
|
||||
// TODO: remove in 5.11
|
||||
extern const Q_AUTOTEST_EXPORT char *http2ParametersPropertyName;
|
||||
|
||||
extern const Q_AUTOTEST_EXPORT char Http2clientPreface[clientPrefaceLength];
|
||||
void prepare_for_protocol_upgrade(QHttpNetworkRequest &request);
|
||||
|
||||
enum class FrameStatus
|
||||
{
|
||||
|
|
@ -182,14 +224,15 @@ enum Http2Error
|
|||
void qt_error(quint32 errorCode, QNetworkReply::NetworkError &error, QString &errorString);
|
||||
QString qt_error_string(quint32 errorCode);
|
||||
QNetworkReply::NetworkError qt_error(quint32 errorCode);
|
||||
bool is_PUSH_PROMISE_enabled();
|
||||
bool is_protocol_upgraded(const QHttpNetworkReply &reply);
|
||||
struct Frame default_SETTINGS_frame();
|
||||
|
||||
}
|
||||
} // namespace Http2
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(QT_HTTP2)
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
Q_DECLARE_METATYPE(Http2::Settings)
|
||||
Q_DECLARE_METATYPE(Http2::ProtocolParameters)
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -168,9 +168,34 @@ QHttp2ProtocolHandler::QHttp2ProtocolHandler(QHttpNetworkConnectionChannel *chan
|
|||
decoder(HPack::FieldLookupTable::DefaultSize),
|
||||
encoder(HPack::FieldLookupTable::DefaultSize, true)
|
||||
{
|
||||
Q_ASSERT(channel);
|
||||
Q_ASSERT(channel && m_connection);
|
||||
|
||||
continuedFrames.reserve(20);
|
||||
pushPromiseEnabled = is_PUSH_PROMISE_enabled();
|
||||
|
||||
const ProtocolParameters params(m_connection->http2Parameters());
|
||||
Q_ASSERT(params.validate());
|
||||
|
||||
maxSessionReceiveWindowSize = params.maxSessionReceiveWindowSize;
|
||||
|
||||
const RawSettings &data = params.settingsFrameData;
|
||||
for (auto param = data.cbegin(), end = data.cend(); param != end; ++param) {
|
||||
switch (param.key()) {
|
||||
case Settings::INITIAL_WINDOW_SIZE_ID:
|
||||
streamInitialReceiveWindowSize = param.value();
|
||||
break;
|
||||
case Settings::ENABLE_PUSH_ID:
|
||||
pushPromiseEnabled = param.value();
|
||||
break;
|
||||
case Settings::HEADER_TABLE_SIZE_ID:
|
||||
case Settings::MAX_CONCURRENT_STREAMS_ID:
|
||||
case Settings::MAX_FRAME_SIZE_ID:
|
||||
case Settings::MAX_HEADER_LIST_SIZE_ID:
|
||||
// These other settings are just recommendations to our peer. We
|
||||
// only check they are not crazy in ProtocolParameters::validate().
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!channel->ssl) {
|
||||
// We upgraded from HTTP/1.1 to HTTP/2. channel->request was already sent
|
||||
|
|
@ -360,20 +385,25 @@ bool QHttp2ProtocolHandler::sendClientPreface()
|
|||
if (prefaceSent)
|
||||
return true;
|
||||
|
||||
const qint64 written = m_socket->write(Http2clientPreface,
|
||||
const qint64 written = m_socket->write(Http2::Http2clientPreface,
|
||||
Http2::clientPrefaceLength);
|
||||
if (written != Http2::clientPrefaceLength)
|
||||
return false;
|
||||
|
||||
// 6.5 SETTINGS
|
||||
frameWriter.setOutboundFrame(default_SETTINGS_frame());
|
||||
const ProtocolParameters params(m_connection->http2Parameters());
|
||||
Q_ASSERT(params.validate());
|
||||
frameWriter.setOutboundFrame(params.settingsFrame());
|
||||
Q_ASSERT(frameWriter.outboundFrame().payloadSize());
|
||||
|
||||
if (!frameWriter.write(*m_socket))
|
||||
return false;
|
||||
|
||||
sessionRecvWindowSize = Http2::initialSessionReceiveWindowSize;
|
||||
const auto delta = Http2::initialSessionReceiveWindowSize - Http2::defaultSessionWindowSize;
|
||||
sessionReceiveWindowSize = maxSessionReceiveWindowSize;
|
||||
// ProtocolParameters::validate does not allow maxSessionReceiveWindowSize
|
||||
// to be smaller than defaultSessionWindowSize, so everything is OK here with
|
||||
// 'delta':
|
||||
const auto delta = maxSessionReceiveWindowSize - Http2::defaultSessionWindowSize;
|
||||
if (!sendWINDOW_UPDATE(Http2::connectionStreamID, delta))
|
||||
return false;
|
||||
|
||||
|
|
@ -523,10 +553,10 @@ void QHttp2ProtocolHandler::handleDATA()
|
|||
if (!activeStreams.contains(streamID) && !streamWasReset(streamID))
|
||||
return connectionError(ENHANCE_YOUR_CALM, "DATA on invalid stream");
|
||||
|
||||
if (qint32(inboundFrame.payloadSize()) > sessionRecvWindowSize)
|
||||
if (qint32(inboundFrame.payloadSize()) > sessionReceiveWindowSize)
|
||||
return connectionError(FLOW_CONTROL_ERROR, "Flow control error");
|
||||
|
||||
sessionRecvWindowSize -= inboundFrame.payloadSize();
|
||||
sessionReceiveWindowSize -= inboundFrame.payloadSize();
|
||||
|
||||
if (activeStreams.contains(streamID)) {
|
||||
auto &stream = activeStreams[streamID];
|
||||
|
|
@ -545,20 +575,20 @@ void QHttp2ProtocolHandler::handleDATA()
|
|||
if (inboundFrame.flags().testFlag(FrameFlag::END_STREAM)) {
|
||||
finishStream(stream);
|
||||
deleteActiveStream(stream.streamID);
|
||||
} else if (stream.recvWindow < Http2::initialStreamReceiveWindowSize / 2) {
|
||||
} else if (stream.recvWindow < streamInitialReceiveWindowSize / 2) {
|
||||
QMetaObject::invokeMethod(this, "sendWINDOW_UPDATE", Qt::QueuedConnection,
|
||||
Q_ARG(quint32, stream.streamID),
|
||||
Q_ARG(quint32, Http2::initialStreamReceiveWindowSize - stream.recvWindow));
|
||||
stream.recvWindow = Http2::initialStreamReceiveWindowSize;
|
||||
Q_ARG(quint32, streamInitialReceiveWindowSize - stream.recvWindow));
|
||||
stream.recvWindow = streamInitialReceiveWindowSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionRecvWindowSize < Http2::initialSessionReceiveWindowSize / 2) {
|
||||
if (sessionReceiveWindowSize < maxSessionReceiveWindowSize / 2) {
|
||||
QMetaObject::invokeMethod(this, "sendWINDOW_UPDATE", Qt::QueuedConnection,
|
||||
Q_ARG(quint32, connectionStreamID),
|
||||
Q_ARG(quint32, Http2::initialSessionReceiveWindowSize - sessionRecvWindowSize));
|
||||
sessionRecvWindowSize = Http2::initialSessionReceiveWindowSize;
|
||||
Q_ARG(quint32, maxSessionReceiveWindowSize - sessionReceiveWindowSize));
|
||||
sessionReceiveWindowSize = maxSessionReceiveWindowSize;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1199,7 +1229,7 @@ quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message, b
|
|||
|
||||
const Stream newStream(message, newStreamID,
|
||||
streamInitialSendWindowSize,
|
||||
Http2::initialStreamReceiveWindowSize);
|
||||
streamInitialReceiveWindowSize);
|
||||
|
||||
if (!uploadDone) {
|
||||
if (auto src = newStream.data()) {
|
||||
|
|
@ -1394,8 +1424,7 @@ bool QHttp2ProtocolHandler::tryReserveStream(const Http2::Frame &pushPromiseFram
|
|||
promise.reservedID = reservedID;
|
||||
promise.pushHeader = requestHeader;
|
||||
|
||||
activeStreams.insert(reservedID, Stream(urlKey, reservedID,
|
||||
Http2::initialStreamReceiveWindowSize));
|
||||
activeStreams.insert(reservedID, Stream(urlKey, reservedID, streamInitialReceiveWindowSize));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1429,7 +1458,7 @@ void QHttp2ProtocolHandler::initReplyFromPushPromise(const HttpMessagePair &mess
|
|||
// Let's pretent we're sending a request now:
|
||||
Stream closedStream(message, promise.reservedID,
|
||||
streamInitialSendWindowSize,
|
||||
Http2::initialStreamReceiveWindowSize);
|
||||
streamInitialReceiveWindowSize);
|
||||
closedStream.state = Stream::halfClosedLocal;
|
||||
activeStreams.insert(promise.reservedID, closedStream);
|
||||
promisedStream = &activeStreams[promise.reservedID];
|
||||
|
|
|
|||
|
|
@ -172,20 +172,38 @@ private:
|
|||
bool continuationExpected = false;
|
||||
std::vector<Http2::Frame> continuedFrames;
|
||||
|
||||
// Peer's max number of streams ...
|
||||
quint32 maxConcurrentStreams = Http2::maxConcurrentStreams;
|
||||
|
||||
// Control flow:
|
||||
|
||||
// Signed integer, it can become negative (it's still a valid window size):
|
||||
qint32 sessionRecvWindowSize = Http2::initialSessionReceiveWindowSize;
|
||||
// This is how many concurrent streams our peer expects from us:
|
||||
// 100 is the default value, can be updated by the server's SETTINGS
|
||||
// frame(s):
|
||||
quint32 maxConcurrentStreams = Http2::maxConcurrentStreams;
|
||||
// While we allow sending SETTTINGS_MAX_CONCURRENT_STREAMS to limit our peer,
|
||||
// it's just a hint and we do not actually enforce it (and we can continue
|
||||
// sending requests and creating streams while maxConcurrentStreams allows).
|
||||
|
||||
// Updated by SETTINGS and WINDOW_UPDATE.
|
||||
// This is the max value, we set it in a ctor from Http2::ProtocolParameters,
|
||||
// it does not change after that.
|
||||
qint32 maxSessionReceiveWindowSize = Http2::defaultSessionWindowSize;
|
||||
|
||||
// Our session receive window size, default is 64Kb. We'll update it from QNAM's
|
||||
// Http2::ProtocolParameters. Signed integer since it can become negative
|
||||
// (it's still a valid window size).
|
||||
qint32 sessionReceiveWindowSize = Http2::defaultSessionWindowSize;
|
||||
// Our per-stream receive window size, default is 64 Kb, will be updated
|
||||
// from QNAM's Http2::ProtocolParameters. Again, signed - can become negative.
|
||||
qint32 streamInitialReceiveWindowSize = Http2::defaultSessionWindowSize;
|
||||
|
||||
// These are our peer's receive window sizes, they will be updated by the
|
||||
// peer's SETTINGS and WINDOW_UPDATE frames.
|
||||
qint32 sessionSendWindowSize = Http2::defaultSessionWindowSize;
|
||||
qint32 streamInitialSendWindowSize = Http2::defaultSessionWindowSize;
|
||||
|
||||
// It's unlimited by default, but can be changed via SETTINGS.
|
||||
// Our peer's header size limitations. It's unlimited by default, but can
|
||||
// be changed via peer's SETTINGS frame.
|
||||
quint32 maxHeaderListSize = (std::numeric_limits<quint32>::max)();
|
||||
// While we can send SETTINGS_MAX_HEADER_LIST_SIZE value (our limit on
|
||||
// the headers size), we never enforce it, it's just a hint to our peer.
|
||||
|
||||
Q_INVOKABLE void resumeSuspendedStreams();
|
||||
// Our stream IDs (all odd), the first valid will be 1.
|
||||
|
|
|
|||
|
|
@ -1437,6 +1437,23 @@ void QHttpNetworkConnection::setConnectionType(ConnectionType type)
|
|||
d->connectionType = type;
|
||||
}
|
||||
|
||||
Http2::ProtocolParameters QHttpNetworkConnection::http2Parameters() const
|
||||
{
|
||||
Q_D(const QHttpNetworkConnection);
|
||||
return d->http2Parameters;
|
||||
}
|
||||
|
||||
void QHttpNetworkConnection::setHttp2Parameters(const Http2::ProtocolParameters ¶ms)
|
||||
{
|
||||
Q_D(QHttpNetworkConnection);
|
||||
if (params.validate()) {
|
||||
d->http2Parameters = params;
|
||||
} else {
|
||||
qCWarning(QT_HTTP2)
|
||||
<< "invalid HTTP/2 parameters, falling back to defaults instead";
|
||||
}
|
||||
}
|
||||
|
||||
// SSL support below
|
||||
#ifndef QT_NO_SSL
|
||||
void QHttpNetworkConnection::setSslConfiguration(const QSslConfiguration &config)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
#include <private/qhttpnetworkheader_p.h>
|
||||
#include <private/qhttpnetworkrequest_p.h>
|
||||
#include <private/qhttpnetworkreply_p.h>
|
||||
#include <private/http2protocol_p.h>
|
||||
|
||||
#include <private/qhttpnetworkconnectionchannel_p.h>
|
||||
|
||||
|
|
@ -139,6 +140,9 @@ public:
|
|||
ConnectionType connectionType();
|
||||
void setConnectionType(ConnectionType type);
|
||||
|
||||
Http2::ProtocolParameters http2Parameters() const;
|
||||
void setHttp2Parameters(const Http2::ProtocolParameters ¶ms);
|
||||
|
||||
#ifndef QT_NO_SSL
|
||||
void setSslConfiguration(const QSslConfiguration &config);
|
||||
void ignoreSslErrors(int channel = -1);
|
||||
|
|
@ -282,6 +286,8 @@ public:
|
|||
QSharedPointer<QNetworkSession> networkSession;
|
||||
#endif
|
||||
|
||||
Http2::ProtocolParameters http2Parameters;
|
||||
|
||||
friend class QHttpNetworkConnectionChannel;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -907,7 +907,9 @@ void QHttpNetworkConnectionChannel::_q_connected()
|
|||
if (tryProtocolUpgrade) {
|
||||
// Let's augment our request with some magic headers and try to
|
||||
// switch to HTTP/2.
|
||||
Http2::prepare_for_protocol_upgrade(request);
|
||||
const Http2::ProtocolParameters params(connection->http2Parameters());
|
||||
Q_ASSERT(params.validate());
|
||||
params.addProtocolUpgradeHeaders(&request);
|
||||
}
|
||||
sendRequest();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,7 +292,6 @@ void QHttpThreadDelegate::startRequest()
|
|||
QHttpNetworkConnection::ConnectionType connectionType
|
||||
= httpRequest.isHTTP2Allowed() ? QHttpNetworkConnection::ConnectionTypeHTTP2
|
||||
: QHttpNetworkConnection::ConnectionTypeHTTP;
|
||||
|
||||
#ifndef QT_NO_SSL
|
||||
if (ssl && !incomingSslConfiguration.data())
|
||||
incomingSslConfiguration.reset(new QSslConfiguration);
|
||||
|
|
@ -334,7 +333,11 @@ void QHttpThreadDelegate::startRequest()
|
|||
httpConnection = new QNetworkAccessCachedHttpConnection(urlCopy.host(), urlCopy.port(), ssl,
|
||||
connectionType,
|
||||
networkSession);
|
||||
#endif
|
||||
#endif // QT_NO_BEARERMANAGEMENT
|
||||
if (connectionType == QHttpNetworkConnection::ConnectionTypeHTTP2
|
||||
&& http2Parameters.validate()) {
|
||||
httpConnection->setHttp2Parameters(http2Parameters);
|
||||
} // else we ignore invalid parameters and use our own defaults.
|
||||
#ifndef QT_NO_SSL
|
||||
// Set the QSslConfiguration from this QNetworkRequest.
|
||||
if (ssl)
|
||||
|
|
@ -360,7 +363,6 @@ void QHttpThreadDelegate::startRequest()
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Send the request to the connection
|
||||
httpReply = httpConnection->sendRequest(httpRequest);
|
||||
httpReply->setParent(this);
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@
|
|||
#include <QScopedPointer>
|
||||
#include "private/qnoncontiguousbytedevice_p.h"
|
||||
#include "qnetworkaccessauthenticationmanager_p.h"
|
||||
#include <QtNetwork/private/http2protocol_p.h>
|
||||
|
||||
#ifndef QT_NO_HTTP
|
||||
|
||||
|
|
@ -115,6 +116,7 @@ public:
|
|||
qint64 removedContentLength;
|
||||
QNetworkReply::NetworkError incomingErrorCode;
|
||||
QString incomingErrorDetail;
|
||||
Http2::ProtocolParameters http2Parameters;
|
||||
#ifndef QT_NO_BEARERMANAGEMENT
|
||||
QSharedPointer<QNetworkSession> networkSession;
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -783,6 +783,10 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq
|
|||
|
||||
// Create the HTTP thread delegate
|
||||
QHttpThreadDelegate *delegate = new QHttpThreadDelegate;
|
||||
// Propagate Http/2 settings if any
|
||||
const QVariant blob(manager->property(Http2::http2ParametersPropertyName));
|
||||
if (blob.isValid() && blob.canConvert<Http2::ProtocolParameters>())
|
||||
delegate->http2Parameters = blob.value<Http2::ProtocolParameters>();
|
||||
#ifndef QT_NO_BEARERMANAGEMENT
|
||||
delegate->networkSession = managerPrivate->getNetworkSession();
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -76,13 +76,11 @@ void fill_push_header(const HttpHeader &originalRequest, HttpHeader &promisedReq
|
|||
|
||||
}
|
||||
|
||||
Http2Server::Http2Server(bool h2c, const Http2Settings &ss, const Http2Settings &cs)
|
||||
Http2Server::Http2Server(bool h2c, const Http2::RawSettings &ss, const Http2::RawSettings &cs)
|
||||
: serverSettings(ss),
|
||||
expectedClientSettings(cs),
|
||||
clearTextHTTP2(h2c)
|
||||
{
|
||||
for (const auto &s : cs)
|
||||
expectedClientSettings[quint16(s.identifier)] = s.value;
|
||||
|
||||
responseBody = "<html>\n"
|
||||
"<head>\n"
|
||||
"<title>Sample \"Hello, World\" Application</title>\n"
|
||||
|
|
@ -159,11 +157,11 @@ void Http2Server::sendServerSettings()
|
|||
return;
|
||||
|
||||
writer.start(FrameType::SETTINGS, FrameFlag::EMPTY, connectionStreamID);
|
||||
for (const auto &s : serverSettings) {
|
||||
writer.append(s.identifier);
|
||||
writer.append(s.value);
|
||||
if (s.identifier == Settings::INITIAL_WINDOW_SIZE_ID)
|
||||
streamRecvWindowSize = s.value;
|
||||
for (auto it = serverSettings.cbegin(); it != serverSettings.cend(); ++it) {
|
||||
writer.append(it.key());
|
||||
writer.append(it.value());
|
||||
if (it.key() == Settings::INITIAL_WINDOW_SIZE_ID)
|
||||
streamRecvWindowSize = it.value();
|
||||
}
|
||||
writer.write(*socket);
|
||||
// Now, let's update our peer on a session recv window size:
|
||||
|
|
@ -285,9 +283,9 @@ void Http2Server::incomingConnection(qintptr socketDescriptor)
|
|||
|
||||
quint32 Http2Server::clientSetting(Http2::Settings identifier, quint32 defaultValue)
|
||||
{
|
||||
const auto it = expectedClientSettings.find(quint16(identifier));
|
||||
const auto it = expectedClientSettings.find(identifier);
|
||||
if (it != expectedClientSettings.end())
|
||||
return it->second;
|
||||
return it.value();
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
|
|
@ -623,7 +621,7 @@ void Http2Server::handleSETTINGS()
|
|||
const auto notFound = expectedClientSettings.end();
|
||||
|
||||
while (src != end) {
|
||||
const auto id = qFromBigEndian<quint16>(src);
|
||||
const auto id = Http2::Settings(qFromBigEndian<quint16>(src));
|
||||
const auto value = qFromBigEndian<quint32>(src + 2);
|
||||
if (expectedClientSettings.find(id) == notFound ||
|
||||
expectedClientSettings[id] != value) {
|
||||
|
|
|
|||
|
|
@ -48,19 +48,6 @@
|
|||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
struct Http2Setting
|
||||
{
|
||||
Http2::Settings identifier;
|
||||
quint32 value = 0;
|
||||
|
||||
Http2Setting(Http2::Settings ident, quint32 v)
|
||||
: identifier(ident),
|
||||
value(v)
|
||||
{}
|
||||
};
|
||||
|
||||
using Http2Settings = std::vector<Http2Setting>;
|
||||
|
||||
// At the moment we do not have any public API parsing HTTP headers. Even worse -
|
||||
// the code that can do this exists only in QHttpNetworkReplyPrivate class.
|
||||
// To be able to access reply's d_func() we have these classes:
|
||||
|
|
@ -78,8 +65,8 @@ class Http2Server : public QTcpServer
|
|||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Http2Server(bool clearText, const Http2Settings &serverSettings,
|
||||
const Http2Settings &clientSettings);
|
||||
Http2Server(bool clearText, const Http2::RawSettings &serverSettings,
|
||||
const Http2::RawSettings &clientSettings);
|
||||
|
||||
~Http2Server();
|
||||
|
||||
|
|
@ -144,8 +131,8 @@ private:
|
|||
bool settingsSent = false;
|
||||
bool waitingClientAck = false;
|
||||
|
||||
Http2Settings serverSettings;
|
||||
std::map<quint16, quint32> expectedClientSettings;
|
||||
Http2::RawSettings serverSettings;
|
||||
Http2::RawSettings expectedClientSettings;
|
||||
|
||||
bool connectionError = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
#include "http2srv.h"
|
||||
|
||||
#include <QtNetwork/private/http2protocol_p.h>
|
||||
#include <QtNetwork/qnetworkaccessmanager.h>
|
||||
#include <QtNetwork/qnetworkrequest.h>
|
||||
#include <QtNetwork/qnetworkreply.h>
|
||||
|
|
@ -96,8 +97,8 @@ private:
|
|||
// small payload.
|
||||
void runEventLoop(int ms = 5000);
|
||||
void stopEventLoop();
|
||||
Http2Server *newServer(const Http2Settings &serverSettings,
|
||||
const Http2Settings &clientSettings = defaultClientSettings);
|
||||
Http2Server *newServer(const Http2::RawSettings &serverSettings,
|
||||
const Http2::ProtocolParameters &clientSettings = {});
|
||||
// Send a get or post request, depending on a payload (empty or not).
|
||||
void sendRequest(int streamNumber,
|
||||
QNetworkRequest::Priority priority = QNetworkRequest::NormalPriority,
|
||||
|
|
@ -118,14 +119,10 @@ private:
|
|||
bool prefaceOK = false;
|
||||
bool serverGotSettingsACK = false;
|
||||
|
||||
static const Http2Settings defaultServerSettings;
|
||||
static const Http2Settings defaultClientSettings;
|
||||
static const Http2::RawSettings defaultServerSettings;
|
||||
};
|
||||
|
||||
const Http2Settings tst_Http2::defaultServerSettings{{Http2::Settings::MAX_CONCURRENT_STREAMS_ID, 100}};
|
||||
const Http2Settings tst_Http2::defaultClientSettings{{Http2::Settings::MAX_FRAME_SIZE_ID, quint32(Http2::maxFrameSize)},
|
||||
{Http2::Settings::INITIAL_WINDOW_SIZE_ID, quint32(Http2::initialStreamReceiveWindowSize)},
|
||||
{Http2::Settings::ENABLE_PUSH_ID, quint32(0)}};
|
||||
const Http2::RawSettings tst_Http2::defaultServerSettings{{Http2::Settings::MAX_CONCURRENT_STREAMS_ID, 100}};
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -142,27 +139,6 @@ struct ServerDeleter
|
|||
|
||||
using ServerPtr = QScopedPointer<Http2Server, ServerDeleter>;
|
||||
|
||||
struct EnvVarGuard
|
||||
{
|
||||
EnvVarGuard(const char *name, const QByteArray &value)
|
||||
: varName(name),
|
||||
prevValue(qgetenv(name))
|
||||
{
|
||||
Q_ASSERT(name);
|
||||
qputenv(name, value);
|
||||
}
|
||||
~EnvVarGuard()
|
||||
{
|
||||
if (prevValue.size())
|
||||
qputenv(varName.c_str(), prevValue);
|
||||
else
|
||||
qunsetenv(varName.c_str());
|
||||
}
|
||||
|
||||
const std::string varName;
|
||||
const QByteArray prevValue;
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
tst_Http2::tst_Http2()
|
||||
|
|
@ -241,9 +217,11 @@ void tst_Http2::multipleRequests()
|
|||
|
||||
// Just to make the order a bit more interesting
|
||||
// we'll index this randomly:
|
||||
QNetworkRequest::Priority priorities[] = {QNetworkRequest::HighPriority,
|
||||
QNetworkRequest::NormalPriority,
|
||||
QNetworkRequest::LowPriority};
|
||||
const QNetworkRequest::Priority priorities[] = {
|
||||
QNetworkRequest::HighPriority,
|
||||
QNetworkRequest::NormalPriority,
|
||||
QNetworkRequest::LowPriority
|
||||
};
|
||||
|
||||
for (int i = 0; i < nRequests; ++i)
|
||||
sendRequest(i, priorities[std::rand() % 3]);
|
||||
|
|
@ -258,11 +236,11 @@ void tst_Http2::multipleRequests()
|
|||
void tst_Http2::flowControlClientSide()
|
||||
{
|
||||
// Create a server but impose limits:
|
||||
// 1. Small MAX frame size, so we test CONTINUATION frames.
|
||||
// 2. Small client windows so server responses cause client streams
|
||||
// to suspend and server sends WINDOW_UPDATE frames.
|
||||
// 3. Few concurrent streams, to test protocol handler can resume
|
||||
// suspended requests.
|
||||
// 1. Small client receive windows so server's responses cause client
|
||||
// streams to suspend and protocol handler has to send WINDOW_UPDATE
|
||||
// frames.
|
||||
// 2. Few concurrent streams supported by the server, to test protocol
|
||||
// handler in the client can suspend and then resume streams.
|
||||
using namespace Http2;
|
||||
|
||||
clearHTTP2State();
|
||||
|
|
@ -271,11 +249,20 @@ void tst_Http2::flowControlClientSide()
|
|||
nRequests = 10;
|
||||
windowUpdates = 0;
|
||||
|
||||
const Http2Settings serverSettings = {{Settings::MAX_CONCURRENT_STREAMS_ID, 3}};
|
||||
Http2::ProtocolParameters params;
|
||||
// A small window size for a session, and even a smaller one per stream -
|
||||
// this will result in WINDOW_UPDATE frames both on connection stream and
|
||||
// per stream.
|
||||
params.maxSessionReceiveWindowSize = Http2::defaultSessionWindowSize * 5;
|
||||
params.settingsFrameData[Settings::INITIAL_WINDOW_SIZE_ID] = Http2::defaultSessionWindowSize;
|
||||
// Inform our manager about non-default settings:
|
||||
manager.setProperty(Http2::http2ParametersPropertyName, QVariant::fromValue(params));
|
||||
|
||||
ServerPtr srv(newServer(serverSettings));
|
||||
const Http2::RawSettings serverSettings = {{Settings::MAX_CONCURRENT_STREAMS_ID, quint32(3)}};
|
||||
ServerPtr srv(newServer(serverSettings, params));
|
||||
|
||||
const QByteArray respond(int(Http2::initialStreamReceiveWindowSize * 5), 'x');
|
||||
|
||||
const QByteArray respond(int(Http2::defaultSessionWindowSize * 10), 'x');
|
||||
srv->setResponseBody(respond);
|
||||
|
||||
QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection);
|
||||
|
|
@ -309,7 +296,7 @@ void tst_Http2::flowControlServerSide()
|
|||
serverPort = 0;
|
||||
nRequests = 30;
|
||||
|
||||
const Http2Settings serverSettings = {{Settings::MAX_CONCURRENT_STREAMS_ID, 7}};
|
||||
const Http2::RawSettings serverSettings = {{Settings::MAX_CONCURRENT_STREAMS_ID, 7}};
|
||||
|
||||
ServerPtr srv(newServer(serverSettings));
|
||||
|
||||
|
|
@ -341,12 +328,12 @@ void tst_Http2::pushPromise()
|
|||
serverPort = 0;
|
||||
nRequests = 1;
|
||||
|
||||
const EnvVarGuard env("QT_HTTP2_ENABLE_PUSH_PROMISE", "1");
|
||||
const Http2Settings clientSettings{{Settings::MAX_FRAME_SIZE_ID, quint32(Http2::maxFrameSize)},
|
||||
{Http2::Settings::INITIAL_WINDOW_SIZE_ID, quint32(Http2::initialStreamReceiveWindowSize)},
|
||||
{Settings::ENABLE_PUSH_ID, quint32(1)}};
|
||||
Http2::ProtocolParameters params;
|
||||
// Defaults are good, except ENABLE_PUSH:
|
||||
params.settingsFrameData[Settings::ENABLE_PUSH_ID] = 1;
|
||||
manager.setProperty(Http2::http2ParametersPropertyName, QVariant::fromValue(params));
|
||||
|
||||
ServerPtr srv(newServer(defaultServerSettings, clientSettings));
|
||||
ServerPtr srv(newServer(defaultServerSettings, params));
|
||||
srv->enablePushPromise(true, QByteArray("/script.js"));
|
||||
|
||||
QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection);
|
||||
|
|
@ -420,7 +407,7 @@ void tst_Http2::goaway()
|
|||
serverPort = 0;
|
||||
nRequests = 3;
|
||||
|
||||
ServerPtr srv(newServer(defaultServerSettings, defaultClientSettings));
|
||||
ServerPtr srv(newServer(defaultServerSettings));
|
||||
srv->emulateGOAWAY(responseTimeoutMS);
|
||||
QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection);
|
||||
runEventLoop();
|
||||
|
|
@ -463,6 +450,7 @@ void tst_Http2::clearHTTP2State()
|
|||
windowUpdates = 0;
|
||||
prefaceOK = false;
|
||||
serverGotSettingsACK = false;
|
||||
manager.setProperty(Http2::http2ParametersPropertyName, QVariant());
|
||||
}
|
||||
|
||||
void tst_Http2::runEventLoop(int ms)
|
||||
|
|
@ -478,11 +466,12 @@ void tst_Http2::stopEventLoop()
|
|||
eventLoop.quit();
|
||||
}
|
||||
|
||||
Http2Server *tst_Http2::newServer(const Http2Settings &serverSettings,
|
||||
const Http2Settings &clientSettings)
|
||||
Http2Server *tst_Http2::newServer(const Http2::RawSettings &serverSettings,
|
||||
const Http2::ProtocolParameters &clientSettings)
|
||||
{
|
||||
using namespace Http2;
|
||||
auto srv = new Http2Server(clearTextHTTP2, serverSettings, clientSettings);
|
||||
auto srv = new Http2Server(clearTextHTTP2, serverSettings,
|
||||
clientSettings.settingsFrameData);
|
||||
|
||||
using Srv = Http2Server;
|
||||
using Cl = tst_Http2;
|
||||
|
|
|
|||
Loading…
Reference in New Issue