diff options
Diffstat (limited to 'src/network/access/http2')
-rw-r--r-- | src/network/access/http2/http2frames.cpp | 6 | ||||
-rw-r--r-- | src/network/access/http2/http2frames_p.h | 2 | ||||
-rw-r--r-- | src/network/access/http2/http2protocol.cpp | 148 | ||||
-rw-r--r-- | src/network/access/http2/http2protocol_p.h | 65 |
4 files changed, 216 insertions, 5 deletions
diff --git a/src/network/access/http2/http2frames.cpp b/src/network/access/http2/http2frames.cpp index 5a684c2f41..e695b4dd9e 100644 --- a/src/network/access/http2/http2frames.cpp +++ b/src/network/access/http2/http2frames.cpp @@ -361,6 +361,12 @@ FrameWriter::FrameWriter(FrameType type, FrameFlags flags, quint32 streamID) start(type, flags, streamID); } +void FrameWriter::setOutboundFrame(Frame &&newFrame) +{ + frame = std::move(newFrame); + updatePayloadSize(); +} + void FrameWriter::start(FrameType type, FrameFlags flags, quint32 streamID) { auto &buffer = frame.buffer; diff --git a/src/network/access/http2/http2frames_p.h b/src/network/access/http2/http2frames_p.h index e5f6d46c67..4bdc775806 100644 --- a/src/network/access/http2/http2frames_p.h +++ b/src/network/access/http2/http2frames_p.h @@ -129,6 +129,8 @@ public: return frame; } + void setOutboundFrame(Frame &&newFrame); + // Frame 'builders': void start(FrameType type, FrameFlags flags, quint32 streamID); void setPayloadSize(quint32 size); diff --git a/src/network/access/http2/http2protocol.cpp b/src/network/access/http2/http2protocol.cpp index 7f788a6f42..f51af4be5c 100644 --- a/src/network/access/http2/http2protocol.cpp +++ b/src/network/access/http2/http2protocol.cpp @@ -37,9 +37,14 @@ ** ****************************************************************************/ -#include <QtCore/qstring.h> - #include "http2protocol_p.h" +#include "http2frames_p.h" + +#include "private/qhttpnetworkrequest_p.h" +#include "private/qhttpnetworkreply_p.h" + +#include <QtCore/qbytearray.h> +#include <QtCore/qstring.h> QT_BEGIN_NAMESPACE @@ -57,6 +62,131 @@ const char Http2clientPreface[clientPrefaceLength] = 0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a}; +// TODO: (in 5.11) - remove it! +const char *http2ParametersPropertyName = "QT_HTTP2_PARAMETERS_PROPERTY"; + +ProtocolParameters::ProtocolParameters() +{ + 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)); + const char *src = reinterpret_cast<const char *>(frame.dataBegin()); + const QByteArray wrapper(QByteArray::fromRawData(src, int(frame.dataSize()))); + // 3.2.1 + // The content of the HTTP2-Settings header field is the payload + // of a SETTINGS frame (Section 6.5), encoded as a base64url string + // (that is, the URL- and filename-safe Base64 encoding described in + // Section 5 of [RFC4648], with any trailing '=' characters omitted). + return wrapper.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); +} + +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")); + // We _append_ 'Upgrade': + if (value.size()) + value += ", "; + + value += "Upgrade, HTTP2-Settings"; + request->setHeaderField("Connection", value); + // This we just (re)write. + request->setHeaderField("Upgrade", "h2c"); + // This we just (re)write. + request->setHeaderField("HTTP2-Settings", settingsFrameToBase64()); +} void qt_error(quint32 errorCode, QNetworkReply::NetworkError &error, QString &errorMessage) @@ -151,6 +281,20 @@ QNetworkReply::NetworkError qt_error(quint32 errorCode) return error; } +bool is_protocol_upgraded(const QHttpNetworkReply &reply) +{ + if (reply.statusCode() == 101) { + // Do some minimal checks here - we expect 'Upgrade: h2c' to be found. + const auto &header = reply.header(); + for (const QPair<QByteArray, QByteArray> &field : header) { + if (field.first.toLower() == "upgrade" && field.second.toLower() == "h2c") + return true; + } + } + + return false; } +} // namespace Http2 + QT_END_NAMESPACE diff --git a/src/network/access/http2/http2protocol_p.h b/src/network/access/http2/http2protocol_p.h index 5d730404bb..f7a89859d3 100644 --- a/src/network/access/http2/http2protocol_p.h +++ b/src/network/access/http2/http2protocol_p.h @@ -53,12 +53,16 @@ #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. QT_BEGIN_NAMESPACE +class QHttpNetworkRequest; +class QHttpNetworkReply; +class QByteArray; class QString; namespace Http2 @@ -127,10 +131,61 @@ enum Http2PredefinedParameters maxConcurrentStreams = 100 // HTTP/2, 6.5.2 }; -// It's int, it has internal linkage, it's ok to have it in headers - -// no ODR violation is possible. +// These are ints, const, they have internal linkage, it's ok to have them in +// headers - no ODR violation. const quint32 lastValidStreamID((quint32(1) << 31) - 1); // HTTP/2, 5.1.1 +// The default size of 64K is too small and limiting: if we use it, we end up +// 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 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]; enum class FrameStatus @@ -169,11 +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_protocol_upgraded(const QHttpNetworkReply &reply); -} +} // namespace Http2 Q_DECLARE_LOGGING_CATEGORY(QT_HTTP2) QT_END_NAMESPACE +Q_DECLARE_METATYPE(Http2::Settings) +Q_DECLARE_METATYPE(Http2::ProtocolParameters) + #endif |