HTTP/2 - implement the proper 'h2c' (protocol upgrade)
Without TLS (and thus ALPN/NPN negotiation) HTTP/2 requires a protocol upgrade procedure, as described in RFC 7540, 3.2. We start as HTTP/1.1 (and thus we create QHttpProtocolHandler first), augmenting the headers we send with 'Upgrade: h2c'. In case we receive HTTP/1.1 response with status code 101 ('Switching Protocols'), we continue as HTTP/2 session, creating QHttp2ProtocolHandler and pretending the first request we sent was HTTP/2 request on a real HTTP/2 stream. If the first response is something different from 101, we continue as HTTP/1.1. This change also required auto-test update: our toy-server now has to respond to the initial HTTP/1.1 request on a platform without ALPN/NPN. As a bonus a subtle flakyness in 'goaway' auto-test went away (well, it was fixed). [ChangeLog][QtNetwork][HTTP/2] In case of clear text HTTP/2 we now initiate a required protocol upgrade procedure instead of 'H2Direct' connection. Task-number: QTBUG-61397 Change-Id: I573fa304fdaf661490159037dc47775d97c8ea5b Reviewed-by: Edward Welbourne <edward.welbourne@qt.io> Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
This commit is contained in:
parent
fabedd399e
commit
53357f0156
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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,38 @@ const char Http2clientPreface[clientPrefaceLength] =
|
||||
0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a,
|
||||
0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a};
|
||||
|
||||
QByteArray default_SETTINGS_to_Base64()
|
||||
{
|
||||
Frame frame(default_SETTINGS_frame());
|
||||
// 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);
|
||||
}
|
||||
|
||||
void prepare_for_protocol_upgrade(QHttpNetworkRequest &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", default_SETTINGS_to_Base64());
|
||||
}
|
||||
|
||||
void qt_error(quint32 errorCode, QNetworkReply::NetworkError &error,
|
||||
QString &errorMessage)
|
||||
@ -151,6 +188,40 @@ 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) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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::ENABLE_PUSH_ID);
|
||||
builder.append(quint32(is_PUSH_PROMISE_enabled()));
|
||||
|
||||
return builder.outboundFrame();
|
||||
}
|
||||
|
||||
} // namespace Http2
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
@ -59,6 +59,8 @@
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class QHttpNetworkRequest;
|
||||
class QHttpNetworkReply;
|
||||
class QString;
|
||||
|
||||
namespace Http2
|
||||
@ -132,6 +134,7 @@ enum Http2PredefinedParameters
|
||||
const quint32 lastValidStreamID((quint32(1) << 31) - 1); // HTTP/2, 5.1.1
|
||||
|
||||
extern const Q_AUTOTEST_EXPORT char Http2clientPreface[clientPrefaceLength];
|
||||
void prepare_for_protocol_upgrade(QHttpNetworkRequest &request);
|
||||
|
||||
enum class FrameStatus
|
||||
{
|
||||
@ -169,6 +172,9 @@ 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();
|
||||
|
||||
}
|
||||
|
||||
|
@ -170,10 +170,22 @@ QHttp2ProtocolHandler::QHttp2ProtocolHandler(QHttpNetworkConnectionChannel *chan
|
||||
decoder(HPack::FieldLookupTable::DefaultSize),
|
||||
encoder(HPack::FieldLookupTable::DefaultSize, true)
|
||||
{
|
||||
Q_ASSERT(channel);
|
||||
continuedFrames.reserve(20);
|
||||
bool ok = false;
|
||||
const int env = qEnvironmentVariableIntValue("QT_HTTP2_ENABLE_PUSH_PROMISE", &ok);
|
||||
pushPromiseEnabled = ok && env;
|
||||
pushPromiseEnabled = is_PUSH_PROMISE_enabled();
|
||||
|
||||
if (!channel->ssl) {
|
||||
// We upgraded from HTTP/1.1 to HTTP/2. channel->request was already sent
|
||||
// as HTTP/1.1 request. The response with status code 101 triggered
|
||||
// protocol switch and now we are waiting for the real response, sent
|
||||
// as HTTP/2 frames.
|
||||
Q_ASSERT(channel->reply);
|
||||
const quint32 initialStreamID = createNewStream(HttpMessagePair(channel->request, channel->reply),
|
||||
true /* uploaded by HTTP/1.1 */);
|
||||
Q_ASSERT(initialStreamID == 1);
|
||||
Stream &stream = activeStreams[initialStreamID];
|
||||
stream.state = Stream::halfClosedLocal;
|
||||
}
|
||||
}
|
||||
|
||||
void QHttp2ProtocolHandler::_q_uploadDataReadyRead()
|
||||
@ -356,12 +368,8 @@ bool QHttp2ProtocolHandler::sendClientPreface()
|
||||
return false;
|
||||
|
||||
// 6.5 SETTINGS
|
||||
frameWriter.start(FrameType::SETTINGS, FrameFlag::EMPTY, Http2::connectionStreamID);
|
||||
// MAX frame size (16 kb), enable/disable PUSH
|
||||
frameWriter.append(Settings::MAX_FRAME_SIZE_ID);
|
||||
frameWriter.append(quint32(Http2::maxFrameSize));
|
||||
frameWriter.append(Settings::ENABLE_PUSH_ID);
|
||||
frameWriter.append(quint32(pushPromiseEnabled));
|
||||
frameWriter.setOutboundFrame(default_SETTINGS_frame());
|
||||
Q_ASSERT(frameWriter.outboundFrame().payloadSize());
|
||||
|
||||
if (!frameWriter.write(*m_socket))
|
||||
return false;
|
||||
@ -1157,7 +1165,7 @@ void QHttp2ProtocolHandler::finishStreamWithError(Stream &stream, QNetworkReply:
|
||||
<< "finished with error:" << message;
|
||||
}
|
||||
|
||||
quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message)
|
||||
quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message, bool uploadDone)
|
||||
{
|
||||
const qint32 newStreamID = allocateStreamID();
|
||||
if (!newStreamID)
|
||||
@ -1178,10 +1186,12 @@ quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message)
|
||||
streamInitialSendWindowSize,
|
||||
streamInitialRecvWindowSize);
|
||||
|
||||
if (auto src = newStream.data()) {
|
||||
connect(src, SIGNAL(readyRead()), this,
|
||||
SLOT(_q_uploadDataReadyRead()), Qt::QueuedConnection);
|
||||
src->setProperty("HTTP2StreamID", newStreamID);
|
||||
if (!uploadDone) {
|
||||
if (auto src = newStream.data()) {
|
||||
connect(src, SIGNAL(readyRead()), this,
|
||||
SLOT(_q_uploadDataReadyRead()), Qt::QueuedConnection);
|
||||
src->setProperty("HTTP2StreamID", newStreamID);
|
||||
}
|
||||
}
|
||||
|
||||
activeStreams.insert(newStreamID, newStream);
|
||||
|
@ -98,7 +98,7 @@ private:
|
||||
using Stream = Http2::Stream;
|
||||
|
||||
void _q_readyRead() override;
|
||||
void _q_receiveReply() override;
|
||||
Q_INVOKABLE void _q_receiveReply() override;
|
||||
Q_INVOKABLE bool sendRequest() override;
|
||||
|
||||
bool sendClientPreface();
|
||||
@ -136,7 +136,7 @@ private:
|
||||
const QString &message);
|
||||
|
||||
// Stream's lifecycle management:
|
||||
quint32 createNewStream(const HttpMessagePair &message);
|
||||
quint32 createNewStream(const HttpMessagePair &message, bool uploadDone = false);
|
||||
void addToSuspended(Stream &stream);
|
||||
void markAsReset(quint32 streamID);
|
||||
quint32 popStreamToResume();
|
||||
|
@ -627,7 +627,8 @@ QHttpNetworkReply* QHttpNetworkConnectionPrivate::queueRequest(const QHttpNetwor
|
||||
if (request.isPreConnect())
|
||||
preConnectRequests++;
|
||||
|
||||
if (connectionType == QHttpNetworkConnection::ConnectionTypeHTTP) {
|
||||
if (connectionType == QHttpNetworkConnection::ConnectionTypeHTTP
|
||||
|| (!encrypt && connectionType == QHttpNetworkConnection::ConnectionTypeHTTP2 && !channels[0].switchedToHttp2)) {
|
||||
switch (request.priority()) {
|
||||
case QHttpNetworkRequest::HighPriority:
|
||||
highPriorityQueue.prepend(pair);
|
||||
@ -638,7 +639,7 @@ QHttpNetworkReply* QHttpNetworkConnectionPrivate::queueRequest(const QHttpNetwor
|
||||
break;
|
||||
}
|
||||
}
|
||||
else { // SPDY, HTTP/2
|
||||
else { // SPDY, HTTP/2 ('h2' mode)
|
||||
if (!pair.second->d_func()->requestIsPrepared)
|
||||
prepareRequest(pair);
|
||||
channels[0].spdyRequestsToSend.insertMulti(request.priority(), pair);
|
||||
@ -672,6 +673,25 @@ QHttpNetworkReply* QHttpNetworkConnectionPrivate::queueRequest(const QHttpNetwor
|
||||
return reply;
|
||||
}
|
||||
|
||||
void QHttpNetworkConnectionPrivate::fillHttp2Queue()
|
||||
{
|
||||
for (auto &pair : highPriorityQueue) {
|
||||
if (!pair.second->d_func()->requestIsPrepared)
|
||||
prepareRequest(pair);
|
||||
channels[0].spdyRequestsToSend.insertMulti(QHttpNetworkRequest::HighPriority, pair);
|
||||
}
|
||||
|
||||
highPriorityQueue.clear();
|
||||
|
||||
for (auto &pair : lowPriorityQueue) {
|
||||
if (!pair.second->d_func()->requestIsPrepared)
|
||||
prepareRequest(pair);
|
||||
channels[0].spdyRequestsToSend.insertMulti(pair.first.priority(), pair);
|
||||
}
|
||||
|
||||
lowPriorityQueue.clear();
|
||||
}
|
||||
|
||||
void QHttpNetworkConnectionPrivate::requeueRequest(const HttpMessagePair &pair)
|
||||
{
|
||||
Q_Q(QHttpNetworkConnection);
|
||||
@ -1047,8 +1067,7 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest()
|
||||
}
|
||||
case QHttpNetworkConnection::ConnectionTypeHTTP2:
|
||||
case QHttpNetworkConnection::ConnectionTypeSPDY: {
|
||||
|
||||
if (channels[0].spdyRequestsToSend.isEmpty())
|
||||
if (channels[0].spdyRequestsToSend.isEmpty() && channels[0].switchedToHttp2)
|
||||
return;
|
||||
|
||||
if (networkLayerState == IPv4)
|
||||
@ -1057,7 +1076,7 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest()
|
||||
channels[0].networkLayerPreference = QAbstractSocket::IPv6Protocol;
|
||||
channels[0].ensureConnection();
|
||||
if (channels[0].socket && channels[0].socket->state() == QAbstractSocket::ConnectedState
|
||||
&& !channels[0].pendingEncrypt)
|
||||
&& !channels[0].pendingEncrypt && channels[0].spdyRequestsToSend.size())
|
||||
channels[0].sendRequest();
|
||||
break;
|
||||
}
|
||||
@ -1355,6 +1374,12 @@ QHttpNetworkReply* QHttpNetworkConnection::sendRequest(const QHttpNetworkRequest
|
||||
return d->queueRequest(request);
|
||||
}
|
||||
|
||||
void QHttpNetworkConnection::fillHttp2Queue()
|
||||
{
|
||||
Q_D(QHttpNetworkConnection);
|
||||
d->fillHttp2Queue();
|
||||
}
|
||||
|
||||
bool QHttpNetworkConnection::isSsl() const
|
||||
{
|
||||
Q_D(const QHttpNetworkConnection);
|
||||
|
@ -122,6 +122,7 @@ public:
|
||||
|
||||
//add a new HTTP request through this connection
|
||||
QHttpNetworkReply* sendRequest(const QHttpNetworkRequest &request);
|
||||
void fillHttp2Queue();
|
||||
|
||||
#ifndef QT_NO_NETWORKPROXY
|
||||
//set the proxy for this connection
|
||||
@ -208,6 +209,7 @@ public:
|
||||
|
||||
QHttpNetworkReply *queueRequest(const QHttpNetworkRequest &request);
|
||||
void requeueRequest(const HttpMessagePair &pair); // e.g. after pipeline broke
|
||||
void fillHttp2Queue();
|
||||
bool dequeueRequest(QAbstractSocket *socket);
|
||||
void prepareRequest(HttpMessagePair &request);
|
||||
void updateChannel(int i, const HttpMessagePair &messagePair);
|
||||
|
@ -63,6 +63,20 @@
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class ProtocolHandlerDeleter : public QObject
|
||||
{
|
||||
public:
|
||||
explicit ProtocolHandlerDeleter(QAbstractProtocolHandler *h) : handler(h) {}
|
||||
~ProtocolHandlerDeleter() { delete handler; }
|
||||
private:
|
||||
QAbstractProtocolHandler *handler = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// TODO: Put channel specific stuff here so it does not polute qhttpnetworkconnection.cpp
|
||||
|
||||
// Because in-flight when sending a request, the server might close our connection (because the persistent HTTP
|
||||
@ -424,6 +438,40 @@ void QHttpNetworkConnectionChannel::allDone()
|
||||
return;
|
||||
}
|
||||
|
||||
if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2
|
||||
&& !ssl && !switchedToHttp2) {
|
||||
if (Http2::is_protocol_upgraded(*reply)) {
|
||||
switchedToHttp2 = true;
|
||||
protocolHandler->setReply(nullptr);
|
||||
|
||||
// As allDone() gets called from the protocol handler, it's not yet
|
||||
// safe to delete it. There is no 'deleteLater', since
|
||||
// QAbstractProtocolHandler is not a QObject. Instead we do this
|
||||
// trick with ProtocolHandlerDeleter, a QObject-derived class.
|
||||
// These dances below just make it somewhat exception-safe.
|
||||
// 1. Create a new owner:
|
||||
QAbstractProtocolHandler *oldHandler = protocolHandler.data();
|
||||
QScopedPointer<ProtocolHandlerDeleter> deleter(new ProtocolHandlerDeleter(oldHandler));
|
||||
// 2. Retire the old one:
|
||||
protocolHandler.take();
|
||||
// 3. Call 'deleteLater':
|
||||
deleter->deleteLater();
|
||||
// 3. Give up the ownerthip:
|
||||
deleter.take();
|
||||
|
||||
connection->fillHttp2Queue();
|
||||
protocolHandler.reset(new QHttp2ProtocolHandler(this));
|
||||
QHttp2ProtocolHandler *h2c = static_cast<QHttp2ProtocolHandler *>(protocolHandler.data());
|
||||
QMetaObject::invokeMethod(h2c, "_q_receiveReply", Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(connection, "_q_startNextRequest", Qt::QueuedConnection);
|
||||
return;
|
||||
} else {
|
||||
// Ok, whatever happened, we do not try HTTP/2 anymore ...
|
||||
connection->setConnectionType(QHttpNetworkConnection::ConnectionTypeHTTP);
|
||||
connection->d_func()->activeChannelCount = connection->d_func()->channelCount;
|
||||
}
|
||||
}
|
||||
|
||||
// while handling 401 & 407, we might reset the status code, so save this.
|
||||
bool emitFinished = reply->d_func()->shouldEmitSignals();
|
||||
bool connectionCloseEnabled = reply->d_func()->isConnectionCloseEnabled();
|
||||
@ -838,19 +886,23 @@ void QHttpNetworkConnectionChannel::_q_connected()
|
||||
#endif
|
||||
} else {
|
||||
state = QHttpNetworkConnectionChannel::IdleState;
|
||||
if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2) {
|
||||
// We have to reset QHttp2ProtocolHandler's state machine, it's a new
|
||||
// connection and the handler's state is unique per connection.
|
||||
protocolHandler.reset(new QHttp2ProtocolHandler(this));
|
||||
if (spdyRequestsToSend.count() > 0) {
|
||||
// wait for data from the server first (e.g. initial window, max concurrent requests)
|
||||
QMetaObject::invokeMethod(connection, "_q_startNextRequest", Qt::QueuedConnection);
|
||||
const bool tryProtocolUpgrade = connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2;
|
||||
if (tryProtocolUpgrade) {
|
||||
// For HTTP/1.1 it's already created and never reset.
|
||||
protocolHandler.reset(new QHttpProtocolHandler(this));
|
||||
}
|
||||
switchedToHttp2 = false;
|
||||
|
||||
if (!reply)
|
||||
connection->d_func()->dequeueRequest(socket);
|
||||
|
||||
if (reply) {
|
||||
if (tryProtocolUpgrade) {
|
||||
// Let's augment our request with some magic headers and try to
|
||||
// switch to HTTP/2.
|
||||
Http2::prepare_for_protocol_upgrade(request);
|
||||
}
|
||||
} else {
|
||||
if (!reply)
|
||||
connection->d_func()->dequeueRequest(socket);
|
||||
if (reply)
|
||||
sendRequest();
|
||||
sendRequest();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1078,6 +1130,7 @@ void QHttpNetworkConnectionChannel::_q_encrypted()
|
||||
// has gone to the SPDY queue already
|
||||
break;
|
||||
} else if (nextProtocol == QSslConfiguration::ALPNProtocolHTTP2) {
|
||||
switchedToHttp2 = true;
|
||||
protocolHandler.reset(new QHttp2ProtocolHandler(this));
|
||||
connection->setConnectionType(QHttpNetworkConnection::ConnectionTypeHTTP2);
|
||||
break;
|
||||
|
@ -127,6 +127,7 @@ public:
|
||||
// HTTP/2 can be cleartext also, that's why it's
|
||||
// outside of QT_NO_SSL section. Sorted by priority:
|
||||
QMultiMap<int, HttpMessagePair> spdyRequestsToSend;
|
||||
bool switchedToHttp2 = false;
|
||||
#ifndef QT_NO_SSL
|
||||
bool ignoreAllSslErrors;
|
||||
QList<QSslError> ignoreSslErrorsList;
|
||||
|
@ -78,7 +78,7 @@ public:
|
||||
virtual void setHeaderField(const QByteArray &name, const QByteArray &data) = 0;
|
||||
};
|
||||
|
||||
class QHttpNetworkHeaderPrivate : public QSharedData
|
||||
class Q_AUTOTEST_EXPORT QHttpNetworkHeaderPrivate : public QSharedData
|
||||
{
|
||||
public:
|
||||
QUrl url;
|
||||
|
@ -132,8 +132,23 @@ void Http2Server::startServer()
|
||||
if (!clearTextHTTP2)
|
||||
return;
|
||||
#endif
|
||||
if (listen())
|
||||
if (listen()) {
|
||||
if (clearTextHTTP2)
|
||||
authority = QStringLiteral("127.0.0.1:%1").arg(serverPort()).toLatin1();
|
||||
emit serverStarted(serverPort());
|
||||
}
|
||||
}
|
||||
|
||||
bool Http2Server::sendProtocolSwitchReply()
|
||||
{
|
||||
Q_ASSERT(socket);
|
||||
Q_ASSERT(clearTextHTTP2 && upgradeProtocol);
|
||||
// The first and the last HTTP/1.1 response we send:
|
||||
const char response[] = "HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Upgrade: h2c\r\n\r\n";
|
||||
const qint64 size = sizeof response - 1;
|
||||
return socket->write(response, size) == size;
|
||||
}
|
||||
|
||||
void Http2Server::sendServerSettings()
|
||||
@ -232,6 +247,7 @@ void Http2Server::incomingConnection(qintptr socketDescriptor)
|
||||
Q_ASSERT(set);
|
||||
// Stop listening:
|
||||
close();
|
||||
upgradeProtocol = true;
|
||||
QMetaObject::invokeMethod(this, "connectionEstablished",
|
||||
Qt::QueuedConnection);
|
||||
} else {
|
||||
@ -275,19 +291,77 @@ quint32 Http2Server::clientSetting(Http2::Settings identifier, quint32 defaultVa
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool Http2Server::readMethodLine()
|
||||
{
|
||||
// We know for sure that Qt did the right thing sending us the correct
|
||||
// Request-line with CRLF at the end ...
|
||||
// We're overly simplistic here but all we need to know - the method.
|
||||
while (socket->bytesAvailable()) {
|
||||
char c = 0;
|
||||
if (socket->read(&c, 1) != 1)
|
||||
return false;
|
||||
if (c == '\n' && requestLine.endsWith('\r')) {
|
||||
if (requestLine.startsWith("GET"))
|
||||
requestType = QHttpNetworkRequest::Get;
|
||||
else if (requestLine.startsWith("POST"))
|
||||
requestType = QHttpNetworkRequest::Post;
|
||||
else
|
||||
requestType = QHttpNetworkRequest::Custom; // 'invalid'.
|
||||
requestLine.clear();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
requestLine.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Http2Server::verifyProtocolUpgradeRequest()
|
||||
{
|
||||
Q_ASSERT(protocolUpgradeHandler.data());
|
||||
|
||||
bool connectionOk = false;
|
||||
bool upgradeOk = false;
|
||||
bool settingsOk = false;
|
||||
|
||||
QHttpNetworkReplyPrivate *firstRequestReader = protocolUpgradeHandler->d_func();
|
||||
|
||||
// That's how we append them, that's what I expect to find:
|
||||
for (const auto &header : firstRequestReader->fields) {
|
||||
if (header.first == "Connection")
|
||||
connectionOk = header.second.contains("Upgrade, HTTP2-Settings");
|
||||
else if (header.first == "Upgrade")
|
||||
upgradeOk = header.second.contains("h2c");
|
||||
else if (header.first == "HTTP2-Settings")
|
||||
settingsOk = true;
|
||||
}
|
||||
|
||||
return connectionOk && upgradeOk && settingsOk;
|
||||
}
|
||||
|
||||
void Http2Server::triggerGOAWAYEmulation()
|
||||
{
|
||||
Q_ASSERT(testingGOAWAY);
|
||||
auto timer = new QTimer(this);
|
||||
timer->setSingleShot(true);
|
||||
connect(timer, &QTimer::timeout, [this]() {
|
||||
sendGOAWAY(quint32(connectionStreamID), quint32(INTERNAL_ERROR), 0);
|
||||
});
|
||||
timer->start(goawayTimeout);
|
||||
}
|
||||
|
||||
void Http2Server::connectionEstablished()
|
||||
{
|
||||
using namespace Http2;
|
||||
|
||||
if (testingGOAWAY) {
|
||||
auto timer = new QTimer(this);
|
||||
timer->setSingleShot(true);
|
||||
connect(timer, &QTimer::timeout, [this]() {
|
||||
sendGOAWAY(quint32(connectionStreamID), quint32(INTERNAL_ERROR), 0);
|
||||
});
|
||||
timer->start(goawayTimeout);
|
||||
return;
|
||||
}
|
||||
if (testingGOAWAY && !clearTextHTTP2)
|
||||
return triggerGOAWAYEmulation();
|
||||
|
||||
// For clearTextHTTP2 we first have to respond with 'protocol switch'
|
||||
// and then continue with whatever logic we have (testingGOAWAY or not),
|
||||
// otherwise our 'peer' cannot process HTTP/2 frames yet.
|
||||
|
||||
connect(socket.data(), SIGNAL(readyRead()),
|
||||
this, SLOT(readReady()));
|
||||
@ -296,9 +370,17 @@ void Http2Server::connectionEstablished()
|
||||
waitingClientAck = false;
|
||||
waitingClientSettings = false;
|
||||
settingsSent = false;
|
||||
// We immediately send our settings so that our client
|
||||
// can use flow control correctly.
|
||||
sendServerSettings();
|
||||
|
||||
if (clearTextHTTP2) {
|
||||
requestLine.clear();
|
||||
// Now we have to handle HTTP/1.1 request. We use Get/Post in our test,
|
||||
// so set requestType to something unsupported:
|
||||
requestType = QHttpNetworkRequest::Options;
|
||||
} else {
|
||||
// We immediately send our settings so that our client
|
||||
// can use flow control correctly.
|
||||
sendServerSettings();
|
||||
}
|
||||
|
||||
if (socket->bytesAvailable())
|
||||
readReady();
|
||||
@ -328,7 +410,9 @@ void Http2Server::readReady()
|
||||
if (connectionError)
|
||||
return;
|
||||
|
||||
if (waitingClientPreface) {
|
||||
if (upgradeProtocol) {
|
||||
handleProtocolUpgrade();
|
||||
} else if (waitingClientPreface) {
|
||||
handleConnectionPreface();
|
||||
} else {
|
||||
const auto status = reader.read(*socket);
|
||||
@ -348,6 +432,79 @@ void Http2Server::readReady()
|
||||
QMetaObject::invokeMethod(this, "readReady", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void Http2Server::handleProtocolUpgrade()
|
||||
{
|
||||
using ReplyPrivate = QHttpNetworkReplyPrivate;
|
||||
Q_ASSERT(upgradeProtocol);
|
||||
|
||||
if (!protocolUpgradeHandler.data())
|
||||
protocolUpgradeHandler.reset(new Http11Reply);
|
||||
|
||||
QHttpNetworkReplyPrivate *firstRequestReader = protocolUpgradeHandler->d_func();
|
||||
|
||||
// QHttpNetworkReplyPrivate parses ... reply. It will, unfortunately, fail
|
||||
// on the first line ... which is a part of request. So we read this line
|
||||
// and extract the method first.
|
||||
if (firstRequestReader->state == ReplyPrivate::NothingDoneState) {
|
||||
if (!readMethodLine())
|
||||
return;
|
||||
|
||||
if (requestType != QHttpNetworkRequest::Get && requestType != QHttpNetworkRequest::Post) {
|
||||
emit invalidRequest(1);
|
||||
return;
|
||||
}
|
||||
|
||||
firstRequestReader->state = ReplyPrivate::ReadingHeaderState;
|
||||
}
|
||||
|
||||
if (!socket->bytesAvailable())
|
||||
return;
|
||||
|
||||
if (firstRequestReader->state == ReplyPrivate::ReadingHeaderState)
|
||||
firstRequestReader->readHeader(socket.data());
|
||||
else if (firstRequestReader->state == ReplyPrivate::ReadingDataState)
|
||||
firstRequestReader->readBodyFast(socket.data(), &firstRequestReader->responseData);
|
||||
|
||||
switch (firstRequestReader->state) {
|
||||
case ReplyPrivate::ReadingHeaderState:
|
||||
return;
|
||||
case ReplyPrivate::ReadingDataState:
|
||||
if (requestType == QHttpNetworkRequest::Post)
|
||||
return;
|
||||
break;
|
||||
case ReplyPrivate::AllDoneState:
|
||||
break;
|
||||
default:
|
||||
socket->close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!verifyProtocolUpgradeRequest() || !sendProtocolSwitchReply()) {
|
||||
socket->close();
|
||||
return;
|
||||
}
|
||||
|
||||
upgradeProtocol = false;
|
||||
protocolUpgradeHandler.reset(nullptr);
|
||||
|
||||
if (testingGOAWAY)
|
||||
return triggerGOAWAYEmulation();
|
||||
|
||||
// HTTP/1.1 'fields' we have in firstRequestRead are useless (they are not
|
||||
// even allowed in HTTP/2 header). Let's pretend we have received
|
||||
// valid HTTP/2 headers and can extract fields we need:
|
||||
HttpHeader h2header;
|
||||
h2header.push_back(HeaderField(":scheme", "http")); // we are in clearTextHTTP2 mode.
|
||||
h2header.push_back(HeaderField(":authority", authority));
|
||||
activeRequests[1] = std::move(h2header);
|
||||
// After protocol switch we immediately send our SETTINGS.
|
||||
sendServerSettings();
|
||||
if (requestType == QHttpNetworkRequest::Get)
|
||||
emit receivedRequest(1);
|
||||
else
|
||||
emit receivedData(1);
|
||||
}
|
||||
|
||||
void Http2Server::handleConnectionPreface()
|
||||
{
|
||||
Q_ASSERT(waitingClientPreface);
|
||||
@ -382,6 +539,16 @@ void Http2Server::handleIncomingFrame()
|
||||
// 7. RST_STREAM
|
||||
// 8. GOAWAY
|
||||
|
||||
if (testingGOAWAY) {
|
||||
// GOAWAY test is simplistic for now: after HTTP/2 was
|
||||
// negotiated (via ALPN/NPN or a protocol switch), send
|
||||
// a GOAWAY frame after some (probably non-zero) timeout.
|
||||
// We do not handle any frames, but timeout gives QNAM
|
||||
// more time to initiate more streams and thus make the
|
||||
// test more interesting/complex (on a client side).
|
||||
return;
|
||||
}
|
||||
|
||||
inboundFrame = std::move(reader.inboundFrame());
|
||||
|
||||
if (continuedRequest.size()) {
|
||||
|
@ -29,11 +29,14 @@
|
||||
#ifndef HTTP2SRV_H
|
||||
#define HTTP2SRV_H
|
||||
|
||||
#include <QtNetwork/private/qhttpnetworkrequest_p.h>
|
||||
#include <QtNetwork/private/qhttpnetworkreply_p.h>
|
||||
#include <QtNetwork/private/http2protocol_p.h>
|
||||
#include <QtNetwork/private/http2frames_p.h>
|
||||
#include <QtNetwork/private/hpack_p.h>
|
||||
|
||||
#include <QtNetwork/qabstractsocket.h>
|
||||
#include <QtCore/qsharedpointer.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtNetwork/qtcpserver.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
@ -58,6 +61,19 @@ struct Http2Setting
|
||||
|
||||
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:
|
||||
class Http11ReplyPrivate : public QHttpNetworkReplyPrivate
|
||||
{
|
||||
};
|
||||
|
||||
class Http11Reply : public QHttpNetworkReply
|
||||
{
|
||||
public:
|
||||
Q_DECLARE_PRIVATE(Http11Reply)
|
||||
};
|
||||
|
||||
class Http2Server : public QTcpServer
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -75,6 +91,7 @@ public:
|
||||
// Invokables, since we can call them from the main thread,
|
||||
// but server (can) work on its own thread.
|
||||
Q_INVOKABLE void startServer();
|
||||
bool sendProtocolSwitchReply();
|
||||
Q_INVOKABLE void sendServerSettings();
|
||||
Q_INVOKABLE void sendGOAWAY(quint32 streamID, quint32 error,
|
||||
quint32 lastStreamID);
|
||||
@ -82,6 +99,7 @@ public:
|
||||
Q_INVOKABLE void sendDATA(quint32 streamID, quint32 windowSize);
|
||||
Q_INVOKABLE void sendWINDOW_UPDATE(quint32 streamID, quint32 delta);
|
||||
|
||||
Q_INVOKABLE void handleProtocolUpgrade();
|
||||
Q_INVOKABLE void handleConnectionPreface();
|
||||
Q_INVOKABLE void handleIncomingFrame();
|
||||
Q_INVOKABLE void handleSETTINGS();
|
||||
@ -114,6 +132,9 @@ private:
|
||||
void incomingConnection(qintptr socketDescriptor) Q_DECL_OVERRIDE;
|
||||
|
||||
quint32 clientSetting(Http2::Settings identifier, quint32 defaultValue);
|
||||
bool readMethodLine();
|
||||
bool verifyProtocolUpgradeRequest();
|
||||
void triggerGOAWAYEmulation();
|
||||
|
||||
QScopedPointer<QAbstractSocket> socket;
|
||||
|
||||
@ -166,6 +187,18 @@ private:
|
||||
bool testingGOAWAY = false;
|
||||
int goawayTimeout = 0;
|
||||
|
||||
// Clear text HTTP/2, we have to deal with the protocol upgrade request
|
||||
// from the initial HTTP/1.1 request.
|
||||
bool upgradeProtocol = false;
|
||||
QByteArray requestLine;
|
||||
QHttpNetworkRequest::Operation requestType;
|
||||
// We need QHttpNetworkReply (actually its private d-object) to handle the
|
||||
// first HTTP/1.1 request. QHttpNetworkReplyPrivate does parsing + in case
|
||||
// of POST it is also reading the body for us.
|
||||
QScopedPointer<Http11Reply> protocolUpgradeHandler;
|
||||
// We need it for PUSH_PROMISE, with the correct port number appended,
|
||||
// when replying to essentially 1.1 request.
|
||||
QByteArray authority;
|
||||
protected slots:
|
||||
void ignoreErrorSlot();
|
||||
};
|
||||
|
@ -47,10 +47,12 @@
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
// At the moment our HTTP/2 imlpementation requires ALPN and this means OpenSSL.
|
||||
#if !defined(QT_NO_OPENSSL) && OPENSSL_VERSION_NUMBER >= 0x10002000L && !defined(OPENSSL_NO_TLSEXT)
|
||||
// HTTP/2 over TLS requires ALPN/NPN to negotiate the protocol version.
|
||||
const bool clearTextHTTP2 = false;
|
||||
#else
|
||||
// No ALPN/NPN support to negotiate HTTP/2, we'll use cleartext 'h2c' with
|
||||
// a protocol upgrade procedure.
|
||||
const bool clearTextHTTP2 = true;
|
||||
#endif
|
||||
|
||||
@ -507,6 +509,7 @@ void tst_Http2::sendRequest(int streamNumber,
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, QVariant(true));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain"));
|
||||
request.setPriority(priority);
|
||||
|
||||
QNetworkReply *reply = nullptr;
|
||||
|
Loading…
Reference in New Issue
Block a user