1 | // Copyright (C) 2024 The Qt Company Ltd. |
---|---|
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qhttpserverhttp1protocolhandler_p.h" |
5 | |
6 | #include <QtCore/qloggingcategory.h> |
7 | #include <QtCore/qmetaobject.h> |
8 | #include <QtCore/qthread.h> |
9 | #include <QtCore/qpointer.h> |
10 | #include <QtHttpServer/qabstracthttpserver.h> |
11 | #include <QtHttpServer/qhttpserverrequest.h> |
12 | #include <QtHttpServer/qhttpserverresponder.h> |
13 | #include <QtNetwork/qlocalsocket.h> |
14 | #include <QtNetwork/qtcpsocket.h> |
15 | |
16 | #include <private/qabstracthttpserver_p.h> |
17 | #include <private/qhttpserverliterals_p.h> |
18 | #include <private/qhttpserverrequest_p.h> |
19 | |
20 | QT_BEGIN_NAMESPACE |
21 | |
22 | Q_LOGGING_CATEGORY(lcHttpServerHttp1Handler, "qt.httpserver.http1handler") |
23 | |
24 | // https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html |
25 | static const std::map<QHttpServerResponder::StatusCode, QByteArray> statusString{ |
26 | #define XX(name, string) { QHttpServerResponder::StatusCode::name, QByteArrayLiteral(string) } |
27 | XX(Continue, "Continue"), |
28 | XX(SwitchingProtocols, "Switching Protocols"), |
29 | XX(Processing, "Processing"), |
30 | XX(Ok, "OK"), |
31 | XX(Created, "Created"), |
32 | XX(Accepted, "Accepted"), |
33 | XX(NonAuthoritativeInformation, "Non-Authoritative Information"), |
34 | XX(NoContent, "No Content"), |
35 | XX(ResetContent, "Reset Content"), |
36 | XX(PartialContent, "Partial Content"), |
37 | XX(MultiStatus, "Multi-Status"), |
38 | XX(AlreadyReported, "Already Reported"), |
39 | XX(IMUsed, "I'm Used"), |
40 | XX(MultipleChoices, "Multiple Choices"), |
41 | XX(MovedPermanently, "Moved Permanently"), |
42 | XX(Found, "Found"), |
43 | XX(SeeOther, "See Other"), |
44 | XX(NotModified, "Not Modified"), |
45 | XX(UseProxy, "Use Proxy"), |
46 | XX(TemporaryRedirect, "Temporary Redirect"), |
47 | XX(PermanentRedirect, "Permanent Redirect"), |
48 | XX(BadRequest, "Bad Request"), |
49 | XX(Unauthorized, "Unauthorized"), |
50 | XX(PaymentRequired, "Payment Required"), |
51 | XX(Forbidden, "Forbidden"), |
52 | XX(NotFound, "Not Found"), |
53 | XX(MethodNotAllowed, "Method Not Allowed"), |
54 | XX(NotAcceptable, "Not Acceptable"), |
55 | XX(ProxyAuthenticationRequired, "Proxy Authentication Required"), |
56 | XX(RequestTimeout, "Request Timeout"), |
57 | XX(Conflict, "Conflict"), |
58 | XX(Gone, "Gone"), |
59 | XX(LengthRequired, "Length Required"), |
60 | XX(PreconditionFailed, "Precondition Failed"), |
61 | XX(PayloadTooLarge, "Request Entity Too Large"), |
62 | XX(UriTooLong, "Request-URI Too Long"), |
63 | XX(UnsupportedMediaType, "Unsupported Media Type"), |
64 | XX(RequestRangeNotSatisfiable, "Requested Range Not Satisfiable"), |
65 | XX(ExpectationFailed, "Expectation Failed"), |
66 | XX(ImATeapot, "I'm a teapot"), |
67 | XX(MisdirectedRequest, "Misdirected Request"), |
68 | XX(UnprocessableEntity, "Unprocessable Entity"), |
69 | XX(Locked, "Locked"), |
70 | XX(FailedDependency, "Failed Dependency"), |
71 | XX(UpgradeRequired, "Upgrade Required"), |
72 | XX(PreconditionRequired, "Precondition Required"), |
73 | XX(TooManyRequests, "Too Many Requests"), |
74 | XX(RequestHeaderFieldsTooLarge, "Request Header Fields Too Large"), |
75 | XX(UnavailableForLegalReasons, "Unavailable For Legal Reasons"), |
76 | XX(InternalServerError, "Internal Server Error"), |
77 | XX(NotImplemented, "Not Implemented"), |
78 | XX(BadGateway, "Bad Gateway"), |
79 | XX(ServiceUnavailable, "Service Unavailable"), |
80 | XX(GatewayTimeout, "Gateway Timeout"), |
81 | XX(HttpVersionNotSupported, "HTTP Version Not Supported"), |
82 | XX(VariantAlsoNegotiates, "Variant Also Negotiates"), |
83 | XX(InsufficientStorage, "Insufficient Storage"), |
84 | XX(LoopDetected, "Loop Detected"), |
85 | XX(NotExtended, "Not Extended"), |
86 | XX(NetworkAuthenticationRequired, "Network Authentication Required"), |
87 | XX(NetworkConnectTimeoutError, "Network Connect Timeout Error"), |
88 | #undef XX |
89 | }; |
90 | |
91 | namespace { |
92 | |
93 | template <qint64 BUFFERSIZE = 128 * 1024> |
94 | struct IOChunkedTransfer |
95 | { |
96 | // TODO This is not the fastest implementation, as it does read & write |
97 | // in a sequential fashion, but these operation could potentially overlap. |
98 | // TODO Can we implement it without the buffer? Direct write to the target buffer |
99 | // would be great. |
100 | |
101 | static constexpr qint64 bufferSize = BUFFERSIZE; |
102 | static constexpr qint64 targetWriteBufferSaturation = bufferSize / 2; |
103 | char buffer[BUFFERSIZE]; |
104 | qint64 beginIndex = -1; |
105 | qint64 endIndex = -1; |
106 | QPointer<QIODevice> source; |
107 | const QPointer<QIODevice> sink; |
108 | const QMetaObject::Connection bytesWrittenConnection; |
109 | const QMetaObject::Connection readyReadConnection; |
110 | bool inRead = false; |
111 | |
112 | IOChunkedTransfer(QIODevice *input, QIODevice *output) : |
113 | source(input), |
114 | sink(output), |
115 | bytesWrittenConnection(connectToBytesWritten(that: this, device: output)), |
116 | readyReadConnection(QObject::connect(source.data(), &QIODevice::readyRead, source.data(), |
117 | [this]() { readFromInput(); })) |
118 | { |
119 | Q_ASSERT(!source->atEnd()); // TODO error out |
120 | QObject::connect(sink.data(), &QObject::destroyed, source.data(), &QObject::deleteLater); |
121 | QObject::connect(source.data(), &QObject::destroyed, source.data(), [this]() { |
122 | delete this; |
123 | }); |
124 | readFromInput(); |
125 | } |
126 | |
127 | ~IOChunkedTransfer() |
128 | { |
129 | QObject::disconnect(bytesWrittenConnection); |
130 | QObject::disconnect(readyReadConnection); |
131 | } |
132 | |
133 | static QMetaObject::Connection connectToBytesWritten(IOChunkedTransfer *that, QIODevice *device) |
134 | { |
135 | auto send = [that]() { that->writeToOutput(); }; |
136 | #if QT_CONFIG(ssl) |
137 | if (auto *sslSocket = qobject_cast<QSslSocket *>(object: device)) { |
138 | return QObject::connect(sslSocket, &QSslSocket::encryptedBytesWritten, sslSocket, |
139 | std::move(send)); |
140 | } |
141 | #endif |
142 | return QObject::connect(device, &QIODevice::bytesWritten, device, std::move(send)); |
143 | } |
144 | |
145 | inline bool isBufferEmpty() |
146 | { |
147 | Q_ASSERT(beginIndex <= endIndex); |
148 | return beginIndex == endIndex; |
149 | } |
150 | |
151 | void readFromInput() |
152 | { |
153 | if (inRead) |
154 | return; |
155 | if (source.isNull()) |
156 | return; |
157 | |
158 | if (!isBufferEmpty()) // We haven't consumed all the data yet. |
159 | return; |
160 | QScopedValueRollback inReadGuard(inRead, true); |
161 | |
162 | while (isBufferEmpty()) { |
163 | beginIndex = 0; |
164 | endIndex = source->read(buffer, bufferSize); |
165 | if (endIndex < 0) { |
166 | endIndex = beginIndex; // Mark the buffer as empty |
167 | qCWarning(lcHttpServerHttp1Handler, "Error reading chunk: %ls", |
168 | qUtf16Printable(source->errorString())); |
169 | break; |
170 | } |
171 | if (endIndex == 0) |
172 | break; |
173 | memset(buffer + endIndex, 0, sizeof(buffer) - std::size_t(endIndex)); |
174 | writeToOutput(); |
175 | } |
176 | } |
177 | |
178 | void writeToOutput() |
179 | { |
180 | if (sink.isNull() || source.isNull()) |
181 | return; |
182 | |
183 | if (isBufferEmpty()) |
184 | return; |
185 | |
186 | // If downstream has enough data to write already, |
187 | // don't bother writing more now. That would only lead to |
188 | // higher, unnecessary memory usage. |
189 | if (sink->bytesToWrite() >= targetWriteBufferSaturation) |
190 | return; |
191 | #if QT_CONFIG(ssl) |
192 | if (auto *sslSocket = qobject_cast<QSslSocket *>(object: sink.data())) { |
193 | const qint64 budget = targetWriteBufferSaturation - sink->bytesToWrite(); |
194 | if (sslSocket->encryptedBytesToWrite() >= budget) |
195 | return; |
196 | } |
197 | #endif |
198 | |
199 | const auto writtenBytes = sink->write(buffer + beginIndex, endIndex); |
200 | if (writtenBytes < 0) { |
201 | qCWarning(lcHttpServerHttp1Handler, "Error writing chunk: %ls", |
202 | qUtf16Printable(sink->errorString())); |
203 | return; |
204 | } |
205 | beginIndex += writtenBytes; |
206 | if (isBufferEmpty()) { |
207 | if (source->bytesAvailable() && !inRead) |
208 | readFromInput(); |
209 | else if (source->atEnd()) // Finishing |
210 | source->deleteLater(); |
211 | } |
212 | } |
213 | }; |
214 | |
215 | } // anonymous namespace |
216 | |
217 | |
218 | QHttpServerHttp1ProtocolHandler::QHttpServerHttp1ProtocolHandler(QAbstractHttpServer *server, |
219 | QIODevice *socket) |
220 | : QHttpServerStream(server), |
221 | server(server), |
222 | socket(socket), |
223 | tcpSocket(qobject_cast<QTcpSocket *>(object: socket)), |
224 | #if QT_CONFIG(localserver) |
225 | localSocket(qobject_cast<QLocalSocket*>(object: socket)), |
226 | #endif |
227 | request(initRequestFromSocket(socket: tcpSocket)) |
228 | { |
229 | socket->setParent(this); |
230 | |
231 | if (tcpSocket) { |
232 | qCDebug(lcHttpServerHttp1Handler) << "Connection from:"<< tcpSocket->peerAddress(); |
233 | connect(sender: socket, signal: &QTcpSocket::readyRead, |
234 | context: this, slot: &QHttpServerHttp1ProtocolHandler::handleReadyRead); |
235 | connect(sender: tcpSocket, signal: &QTcpSocket::disconnected, |
236 | context: this, slot: &QHttpServerHttp1ProtocolHandler::socketDisconnected); |
237 | #if QT_CONFIG(localserver) |
238 | } else if (localSocket) { |
239 | qCDebug(lcHttpServerHttp1Handler) << "Connection from:"<< localSocket->serverName(); |
240 | connect(sender: socket, signal: &QLocalSocket::readyRead, |
241 | context: this, slot: &QHttpServerHttp1ProtocolHandler::handleReadyRead); |
242 | connect(sender: localSocket, signal: &QLocalSocket::disconnected, |
243 | context: this, slot: &QHttpServerHttp1ProtocolHandler::socketDisconnected); |
244 | #endif |
245 | } |
246 | } |
247 | |
248 | void QHttpServerHttp1ProtocolHandler::responderDestroyed() |
249 | { |
250 | Q_ASSERT(QThread::currentThread() == thread()); |
251 | if (protocolChanged) { |
252 | deleteLater(); |
253 | return; |
254 | } |
255 | Q_ASSERT(handlingRequest); |
256 | handlingRequest = false; |
257 | |
258 | if (tcpSocket) { |
259 | if (tcpSocket->state() != QAbstractSocket::ConnectedState) { |
260 | deleteLater(); |
261 | } else { |
262 | connect(sender: tcpSocket, signal: &QTcpSocket::readyRead, |
263 | context: this, slot: &QHttpServerHttp1ProtocolHandler::handleReadyRead); |
264 | QMetaObject::invokeMethod(object: tcpSocket, function: &QTcpSocket::readyRead, type: Qt::QueuedConnection); |
265 | } |
266 | #if QT_CONFIG(localserver) |
267 | } else if (localSocket) { |
268 | if (localSocket->state() != QLocalSocket::ConnectedState) { |
269 | deleteLater(); |
270 | } else { |
271 | connect(sender: localSocket, signal: &QLocalSocket::readyRead, |
272 | context: this, slot: &QHttpServerHttp1ProtocolHandler::handleReadyRead); |
273 | QMetaObject::invokeMethod(object: localSocket, function: &QLocalSocket::readyRead, type: Qt::QueuedConnection); |
274 | } |
275 | #endif |
276 | } |
277 | } |
278 | |
279 | void QHttpServerHttp1ProtocolHandler::startHandlingRequest() |
280 | { |
281 | handlingRequest = true; |
282 | } |
283 | |
284 | void QHttpServerHttp1ProtocolHandler::socketDisconnected() |
285 | { |
286 | if (!handlingRequest) |
287 | deleteLater(); |
288 | } |
289 | |
290 | void QHttpServerHttp1ProtocolHandler::handleReadyRead() |
291 | { |
292 | if (handlingRequest) |
293 | return; |
294 | |
295 | if (!socket->isTransactionStarted()) |
296 | socket->startTransaction(); |
297 | |
298 | if (!request.d->parse(socket)) { |
299 | if (tcpSocket) |
300 | tcpSocket->disconnectFromHost(); |
301 | #if QT_CONFIG(localserver) |
302 | else if (localSocket) |
303 | localSocket->disconnectFromServer(); |
304 | #endif |
305 | return; |
306 | } |
307 | |
308 | if (request.d->state != QHttpServerRequestPrivate::State::AllDone) |
309 | return; // Partial read |
310 | |
311 | qCDebug(lcHttpServerHttp1Handler) << "Request:"<< request; |
312 | |
313 | QHttpServerResponder responder(this); |
314 | |
315 | #if defined(QT_WEBSOCKETS_LIB) |
316 | if (auto *tcpSocket = qobject_cast<QTcpSocket*>(socket)) { |
317 | if (request.d->upgrade) { // Upgrade |
318 | const auto &upgradeValue = request.value(QByteArrayLiteral("upgrade")); |
319 | if (upgradeValue.compare(QByteArrayLiteral("websocket"), Qt::CaseInsensitive) == 0) { |
320 | const auto upgradeResponse = server->verifyWebSocketUpgrade(request); |
321 | static const auto signal = |
322 | QMetaMethod::fromSignal(&QAbstractHttpServer::newWebSocketConnection); |
323 | if (server->isSignalConnected(signal) |
324 | && upgradeResponse.type() |
325 | != QHttpServerWebSocketUpgradeResponse::ResponseType::PassToNext) { |
326 | if (upgradeResponse.type() |
327 | == QHttpServerWebSocketUpgradeResponse::ResponseType::Accept) { |
328 | // Socket will now be managed by websocketServer |
329 | protocolChanged = true; |
330 | socket->disconnect(); |
331 | socket->rollbackTransaction(); |
332 | socket->setParent(nullptr); |
333 | server->d_func()->websocketServer.handleConnection(tcpSocket); |
334 | Q_EMIT socket->readyRead(); |
335 | } else { |
336 | qCDebug(lcHttpServerHttp1Handler, "WebSocket upgrade denied: %ls", |
337 | qUtf16Printable(QLatin1StringView(upgradeResponse.denyMessage()))); |
338 | QByteArray buffer; |
339 | buffer.append("HTTP/1.1 "); |
340 | buffer.append(QByteArray::number(quint32(upgradeResponse.denyStatus()))); |
341 | buffer.append(" "); |
342 | buffer.append(upgradeResponse.denyMessage()); |
343 | buffer.append("\r\n\r\n"); |
344 | tcpSocket->write(buffer); |
345 | } |
346 | } else { |
347 | if (!server->isSignalConnected(signal)) { |
348 | qCWarning(lcHttpServerHttp1Handler, |
349 | "WebSocket received but no slots connected to " |
350 | "QWebSocketServer::newConnection"); |
351 | } |
352 | server->missingHandler(request, responder); |
353 | tcpSocket->disconnectFromHost(); |
354 | } |
355 | return; |
356 | } |
357 | } |
358 | } |
359 | #endif // QT_WEBSOCKETS_LIB |
360 | |
361 | socket->commitTransaction(); |
362 | |
363 | if (!server->handleRequest(request, responder)) |
364 | server->missingHandler(request, responder); |
365 | |
366 | if (handlingRequest) |
367 | disconnect(sender: socket, signal: &QIODevice::readyRead, receiver: this, slot: &QHttpServerHttp1ProtocolHandler::handleReadyRead); |
368 | else if (socket->bytesAvailable() > 0) |
369 | QMetaObject::invokeMethod(object: socket, function: &QIODevice::readyRead, type: Qt::QueuedConnection); |
370 | } |
371 | |
372 | void QHttpServerHttp1ProtocolHandler::write(const QByteArray &body, const QHttpHeaders &headers, |
373 | QHttpServerResponder::StatusCode status, quint32 streamId) |
374 | { |
375 | Q_UNUSED(streamId); |
376 | Q_ASSERT(state == TransferState::Ready); |
377 | writeStatusAndHeaders(status, headers); |
378 | write(data: body); |
379 | state = TransferState::Ready; |
380 | } |
381 | |
382 | void QHttpServerHttp1ProtocolHandler::write(QHttpServerResponder::StatusCode status, quint32 streamId) |
383 | { |
384 | Q_UNUSED(streamId); |
385 | Q_ASSERT(state == TransferState::Ready); |
386 | QHttpHeaders headers; |
387 | headers.append(name: QHttpHeaders::WellKnownHeader::ContentType, |
388 | value: QHttpServerLiterals::contentTypeXEmpty()); |
389 | headers.append(name: QHttpHeaders::WellKnownHeader::ContentLength, value: "0"); |
390 | writeStatusAndHeaders(status, headers); |
391 | state = TransferState::Ready; |
392 | } |
393 | |
394 | void QHttpServerHttp1ProtocolHandler::write(QIODevice *data, const QHttpHeaders &headers, |
395 | QHttpServerResponder::StatusCode status, quint32 streamId) |
396 | { |
397 | Q_UNUSED(streamId); |
398 | Q_ASSERT(state == TransferState::Ready); |
399 | std::unique_ptr<QIODevice, QScopedPointerDeleteLater> input(data); |
400 | |
401 | input->setParent(nullptr); |
402 | if (!input->isOpen()) { |
403 | if (!input->open(mode: QIODevice::ReadOnly)) { |
404 | // TODO Add developer error handling |
405 | qCDebug(lcHttpServerHttp1Handler, "500: Could not open device %ls", |
406 | qUtf16Printable(input->errorString())); |
407 | write(status: QHttpServerResponder::StatusCode::InternalServerError, streamId: 0); |
408 | return; |
409 | } |
410 | } else if (!(input->openMode() & QIODevice::ReadOnly)) { |
411 | // TODO Add developer error handling |
412 | qCDebug(lcHttpServerHttp1Handler) << "500: Device is opened in a wrong mode" |
413 | << input->openMode(); |
414 | write(status: QHttpServerResponder::StatusCode::InternalServerError, streamId: 0); |
415 | return; |
416 | } |
417 | |
418 | QHttpHeaders allHeaders(headers); |
419 | if (!input->isSequential()) { // Non-sequential QIODevice should know its data size |
420 | allHeaders.append(name: QHttpHeaders::WellKnownHeader::ContentLength, |
421 | value: QByteArray::number(input->size())); |
422 | } |
423 | |
424 | writeStatusAndHeaders(status, headers: allHeaders); |
425 | |
426 | if (input->atEnd()) { |
427 | qCDebug(lcHttpServerHttp1Handler, "No more data available."); |
428 | return; |
429 | } |
430 | |
431 | // input takes ownership of the IOChunkedTransfer pointer inside his constructor |
432 | new IOChunkedTransfer<>(input.release(), socket); |
433 | state = TransferState::Ready; |
434 | } |
435 | |
436 | void QHttpServerHttp1ProtocolHandler::writeBeginChunked(const QHttpHeaders &headers, |
437 | QHttpServerResponder::StatusCode status, |
438 | quint32 streamId) |
439 | { |
440 | Q_UNUSED(streamId); |
441 | Q_ASSERT(state == TransferState::Ready); |
442 | QHttpHeaders allHeaders(headers); |
443 | allHeaders.append(name: QHttpHeaders::WellKnownHeader::TransferEncoding, value: "chunked"); |
444 | writeStatusAndHeaders(status, headers: allHeaders); |
445 | state = TransferState::ChunkedTransferBegun; |
446 | } |
447 | |
448 | void QHttpServerHttp1ProtocolHandler::writeChunk(const QByteArray &data, quint32 streamId) |
449 | { |
450 | Q_UNUSED(streamId); |
451 | Q_ASSERT(state == TransferState::ChunkedTransferBegun); |
452 | |
453 | if (data.length() == 0) { |
454 | qCWarning(lcHttpServerHttp1Handler, "Chunk must have length > 0"); |
455 | return; |
456 | } |
457 | |
458 | write(data: QByteArray::number(data.length(), base: 16)); |
459 | write(data: "\r\n"); |
460 | write(data); |
461 | write(data: "\r\n"); |
462 | } |
463 | |
464 | void QHttpServerHttp1ProtocolHandler::writeEndChunked(const QByteArray &data, |
465 | const QHttpHeaders &trailers, |
466 | quint32 streamId) |
467 | { |
468 | Q_UNUSED(streamId); |
469 | Q_ASSERT(state == TransferState::ChunkedTransferBegun); |
470 | writeChunk(data, streamId: 0); |
471 | write(data: "0\r\n"); |
472 | for (qsizetype i = 0; i < trailers.size(); ++i) { |
473 | const auto name = trailers.nameAt(i); |
474 | const auto value = trailers.valueAt(i); |
475 | writeHeader(key: { name.data(), name.size() }, value: value.toByteArray()); |
476 | } |
477 | write(data: "\r\n"); |
478 | state = TransferState::Ready; |
479 | } |
480 | |
481 | void QHttpServerHttp1ProtocolHandler::writeStatusAndHeaders(QHttpServerResponder::StatusCode status, |
482 | const QHttpHeaders &headers) |
483 | { |
484 | Q_ASSERT(state == TransferState::Ready); |
485 | QByteArray payload; |
486 | payload.append(s: "HTTP/1.1 "); |
487 | payload.append(a: QByteArray::number(quint32(status))); |
488 | const auto it = statusString.find(x: status); |
489 | if (it != statusString.end()) { |
490 | payload.append(s: " "); |
491 | payload.append(a: statusString.at(k: status)); |
492 | } |
493 | payload.append(s: "\r\n"); |
494 | |
495 | for (qsizetype i = 0; i < headers.size(); ++i) { |
496 | const auto name = headers.nameAt(i); |
497 | payload.append(a: QByteArrayView(name.data(), name.size()) + ": " |
498 | + headers.valueAt(i).toByteArray() + "\r\n"); |
499 | } |
500 | payload.append(s: "\r\n"); |
501 | write(data: payload); |
502 | state = TransferState::HeadersSent; |
503 | } |
504 | |
505 | void QHttpServerHttp1ProtocolHandler::writeHeader(const QByteArray &key, const QByteArray &value) |
506 | { |
507 | write(data: key + ": "+ value + "\r\n"); |
508 | } |
509 | |
510 | void QHttpServerHttp1ProtocolHandler::write(const QByteArray &ba) |
511 | { |
512 | Q_ASSERT(QThread::currentThread() == thread()); |
513 | socket->write(data: ba); |
514 | } |
515 | |
516 | void QHttpServerHttp1ProtocolHandler::write(const char *body, qint64 size) |
517 | { |
518 | Q_ASSERT(QThread::currentThread() == thread()); |
519 | socket->write(data: body, len: size); |
520 | } |
521 | |
522 | QT_END_NAMESPACE |
523 |
Definitions
- lcHttpServerHttp1Handler
- statusString
- IOChunkedTransfer
- bufferSize
- targetWriteBufferSaturation
- IOChunkedTransfer
- ~IOChunkedTransfer
- connectToBytesWritten
- isBufferEmpty
- readFromInput
- writeToOutput
- QHttpServerHttp1ProtocolHandler
- responderDestroyed
- startHandlingRequest
- socketDisconnected
- handleReadyRead
- write
- write
- write
- writeBeginChunked
- writeChunk
- writeEndChunked
- writeStatusAndHeaders
- writeHeader
- write
Learn Advanced QML with KDAB
Find out more