/* * Authored by Alex Hultman, 2018-2020. * Intellectual property of third-party. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef UWS_HTTPRESPONSE_H #define UWS_HTTPRESPONSE_H /* An HttpResponse is the channel on which you send back a response */ #include "AsyncSocket.h" #include "HttpResponseData.h" #include "HttpContextData.h" #include "Utilities.h" #include "WebSocketExtensions.h" #include "WebSocketHandshake.h" #include "WebSocket.h" #include "WebSocketContextData.h" #include "HttpContext.h" #include "f2/function2.hpp" /* todo: tryWrite is missing currently, only send smaller segments with write */ namespace uWS { /* Some pre-defined status constants to use with writeStatus */ static const char *HTTP_200_OK = "200 OK"; /* The general timeout for HTTP sockets */ static const int HTTP_TIMEOUT_S = 10; template struct HttpResponse : public AsyncSocket { /* Solely used for getHttpResponseData() */ template friend struct TemplatedApp; typedef AsyncSocket Super; private: HttpResponseData *getHttpResponseData() { return (HttpResponseData *) Super::getAsyncSocketData(); } /* Write an unsigned 32-bit integer in hex */ void writeUnsignedHex(unsigned int value) { char buf[10]; int length = utils::u32toaHex(value, buf); /* For now we do this copy */ Super::write(buf, length); } /* Write an unsigned 32-bit integer */ void writeUnsigned(unsigned int value) { char buf[10]; int length = utils::u32toa(value, buf); /* For now we do this copy */ Super::write(buf, length); } /* When we are done with a response we mark it like so */ void markDone(HttpResponseData *httpResponseData) { httpResponseData->onAborted = nullptr; /* Also remove onWritable so that we do not emit when draining behind the scenes. */ httpResponseData->onWritable = nullptr; /* We are done with this request */ httpResponseData->state &= ~HttpResponseData::HTTP_RESPONSE_PENDING; } /* Called only once per request */ void writeMark() { #ifndef UWS_HTTPRESPONSE_NO_WRITEMARK /* We only expose major version */ writeHeader("uWebSockets", "18"); #endif } /* Returns true on success, indicating that it might be feasible to write more data. * Will start timeout if stream reaches totalSize or write failure. */ bool internalEnd(std::string_view data, int totalSize, bool optional, bool allowContentLength = true) { /* Write status if not already done */ writeStatus(HTTP_200_OK); /* If no total size given then assume this chunk is everything */ if (!totalSize) { totalSize = (int) data.length(); } HttpResponseData *httpResponseData = getHttpResponseData(); if (httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED) { /* We do not have tryWrite-like functionalities, so ignore optional in this path */ /* Do not allow sending 0 chunk here */ if (data.length()) { Super::write("\r\n", 2); writeUnsignedHex((unsigned int) data.length()); Super::write("\r\n", 2); /* Ignoring optional for now */ Super::write(data.data(), (int) data.length()); } /* Terminating 0 chunk */ Super::write("\r\n0\r\n\r\n", 7); markDone(httpResponseData); /* tryEnd can never fail when in chunked mode, since we do not have tryWrite (yet), only write */ Super::timeout(HTTP_TIMEOUT_S); return true; } else { /* Write content-length on first call */ if (!(httpResponseData->state & HttpResponseData::HTTP_END_CALLED)) { /* Write mark, this propagates to WebSockets too */ writeMark(); /* WebSocket upgrades does not allow content-length */ if (allowContentLength) { /* Even zero is a valid content-length */ Super::write("Content-Length: ", 16); writeUnsigned(totalSize); Super::write("\r\n\r\n", 4); } else { Super::write("\r\n", 2); } /* Mark end called */ httpResponseData->state |= HttpResponseData::HTTP_END_CALLED; } /* Even if we supply no new data to write, its failed boolean is useful to know * if it failed to drain any prior failed header writes */ /* Write as much as possible without causing backpressure */ auto [written, failed] = Super::write(data.data(), (int) data.length(), optional); httpResponseData->offset += written; /* Success is when we wrote the entire thing without any failures */ bool success = (unsigned int) written == data.length() && !failed; /* If we are now at the end, start a timeout. Also start a timeout if we failed. */ if (!success || httpResponseData->offset == totalSize) { Super::timeout(HTTP_TIMEOUT_S); } /* Remove onAborted function if we reach the end */ if (httpResponseData->offset == totalSize) { markDone(httpResponseData); } return success; } } public: /* This call is identical to end, but will never write content-length and is thus suitable for upgrades */ template void upgrade(UserData &&userData, std::string_view secWebSocketKey, std::string_view secWebSocketProtocol, std::string_view secWebSocketExtensions, struct us_socket_context_t *webSocketContext) { /* Extract needed parameters from WebSocketContextData */ WebSocketContextData *webSocketContextData = (WebSocketContextData *) us_socket_context_ext(SSL, webSocketContext); int compression = webSocketContextData->compression; int idleTimeout = webSocketContextData->idleTimeout; /* Note: OpenSSL can be used here to speed this up somewhat */ char secWebSocketAccept[29] = {}; WebSocketHandshake::generate(secWebSocketKey.data(), secWebSocketAccept); writeStatus("101 Switching Protocols") ->writeHeader("Upgrade", "websocket") ->writeHeader("Connection", "Upgrade") ->writeHeader("Sec-WebSocket-Accept", secWebSocketAccept); /* Select first subprotocol if present */ if (secWebSocketProtocol.length()) { writeHeader("Sec-WebSocket-Protocol", secWebSocketProtocol.substr(0, secWebSocketProtocol.find(','))); } /* Negotiate compression, we may use a smaller compression window than we negotiate */ bool perMessageDeflate = false; /* We are always allowed to share compressor, if perMessageDeflate */ int compressOptions = /*behavior.compression*/ compression & SHARED_COMPRESSOR; if (/*behavior.compression*/ compression != DISABLED) { //std::string_view extensions = req->getHeader("sec-websocket-extensions"); if (secWebSocketExtensions.length()) { /* We never support client context takeover (the client cannot compress with a sliding window). */ int wantedOptions = PERMESSAGE_DEFLATE | CLIENT_NO_CONTEXT_TAKEOVER; /* Shared compressor is the default */ if (/*behavior.compression*/ compression == SHARED_COMPRESSOR) { /* Disable per-socket compressor */ wantedOptions |= SERVER_NO_CONTEXT_TAKEOVER; } /* isServer = true */ ExtensionsNegotiator extensionsNegotiator(wantedOptions); extensionsNegotiator.readOffer(secWebSocketExtensions); /* Todo: remove these mid string copies */ std::string offer = extensionsNegotiator.generateOffer(); if (offer.length()) { writeHeader("Sec-WebSocket-Extensions", offer); } /* Did we negotiate permessage-deflate? */ if (extensionsNegotiator.getNegotiatedOptions() & PERMESSAGE_DEFLATE) { perMessageDeflate = true; } /* Is the server allowed to compress with a sliding window? */ if (!(extensionsNegotiator.getNegotiatedOptions() & SERVER_NO_CONTEXT_TAKEOVER)) { compressOptions = /*behavior.*/compression; } } } internalEnd({nullptr, 0}, 0, false, false); /* Grab the httpContext from res */ HttpContext *httpContext = (HttpContext *) us_socket_context(SSL, (struct us_socket_t *) this); /* Move any backpressure */ std::string backpressure(std::move(((AsyncSocketData *) getHttpResponseData())->buffer)); /* Keep any fallback buffer alive until we returned from open event, keeping req valid */ std::string fallback(std::move(getHttpResponseData()->salvageFallbackBuffer())); /* Destroy HttpResponseData */ getHttpResponseData()->~HttpResponseData(); /* Adopting a socket invalidates it, do not rely on it directly to carry any data */ WebSocket *webSocket = (WebSocket *) us_socket_context_adopt_socket(SSL, (us_socket_context_t *) webSocketContext, (us_socket_t *) this, sizeof(WebSocketData) + sizeof(UserData)); /* Update corked socket in case we got a new one (assuming we always are corked in handlers). */ webSocket->AsyncSocket::cork(); /* Initialize websocket with any moved backpressure intact */ /* Todo: this is the only use of HttpContext! Move that code in here! */ /* We should not depend on the HttpContext.h! */ httpContext->upgradeToWebSocket( webSocket->init(perMessageDeflate, compressOptions, std::move(backpressure)) ); /* Arm idleTimeout */ us_socket_timeout(SSL, (us_socket_t *) webSocket, /*behavior.*/idleTimeout); /* Default construct the UserData right before calling open handler */ new (webSocket->getUserData()) UserData(std::move(userData)); /* Emit open event and start the timeout */ if (webSocketContextData->openHandler) { webSocketContextData->openHandler(webSocket); } // if we weren't corked then uncork here! otherwise we were called from httpContext! } /* Immediately terminate this Http response */ using Super::close; using Super::getRemoteAddress; /* Manually upgrade to WebSocket, called in upgrade handler of a WebSocket route */ /*void upgrade() { }*/ /* Note: Headers are not checked in regards to timeout. * We only check when you actively push data or end the request */ /* Write 100 Continue, can be done any amount of times */ HttpResponse *writeContinue() { Super::write("HTTP/1.1 100 Continue\r\n\r\n", 25); return this; } /* Write the HTTP status */ HttpResponse *writeStatus(std::string_view status) { HttpResponseData *httpResponseData = getHttpResponseData(); /* Do not allow writing more than one status */ if (httpResponseData->state & HttpResponseData::HTTP_STATUS_CALLED) { return this; } /* Update status */ httpResponseData->state |= HttpResponseData::HTTP_STATUS_CALLED; Super::write("HTTP/1.1 ", 9); Super::write(status.data(), (int) status.length()); Super::write("\r\n", 2); return this; } /* Write an HTTP header with string value */ HttpResponse *writeHeader(std::string_view key, std::string_view value) { writeStatus(HTTP_200_OK); Super::write(key.data(), (int) key.length()); Super::write(": ", 2); Super::write(value.data(), (int) value.length()); Super::write("\r\n", 2); return this; } /* Write an HTTP header with unsigned int value */ HttpResponse *writeHeader(std::string_view key, unsigned int value) { Super::write(key.data(), (int) key.length()); Super::write(": ", 2); writeUnsigned(value); Super::write("\r\n", 2); return this; } /* End the response with an optional data chunk. Always starts a timeout. */ void end(std::string_view data = {}) { internalEnd(data, (int) data.length(), false); } /* Try and end the response. Returns [true, true] on success. * Starts a timeout in some cases. Returns [ok, hasResponded] */ std::pair tryEnd(std::string_view data, int totalSize = 0) { return {internalEnd(data, totalSize, true), hasResponded()}; } /* Write parts of the response in chunking fashion. Starts timeout if failed. */ bool write(std::string_view data) { writeStatus(HTTP_200_OK); /* Do not allow sending 0 chunks, they mark end of response */ if (!data.length()) { /* If you called us, then according to you it was fine to call us so it's fine to still call us */ return true; } HttpResponseData *httpResponseData = getHttpResponseData(); if (!(httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED)) { /* Write mark on first call to write */ writeMark(); writeHeader("Transfer-Encoding", "chunked"); httpResponseData->state |= HttpResponseData::HTTP_WRITE_CALLED; } Super::write("\r\n", 2); writeUnsignedHex((unsigned int) data.length()); Super::write("\r\n", 2); auto [written, failed] = Super::write(data.data(), (int) data.length()); if (failed) { Super::timeout(HTTP_TIMEOUT_S); } /* If we did not fail the write, accept more */ return !failed; } /* Get the current byte write offset for this Http response */ int getWriteOffset() { HttpResponseData *httpResponseData = getHttpResponseData(); return httpResponseData->offset; } /* Checking if we have fully responded and are ready for another request */ bool hasResponded() { HttpResponseData *httpResponseData = getHttpResponseData(); return !(httpResponseData->state & HttpResponseData::HTTP_RESPONSE_PENDING); } /* Corks the response if possible. Leaves already corked socket be. */ HttpResponse *cork(fu2::unique_function &&handler) { if (!Super::isCorked() && Super::canCork()) { Super::cork(); handler(); /* Timeout on uncork failure, since most writes will succeed while corked */ auto [written, failed] = Super::uncork(); if (failed) { /* For now we only have one single timeout so let's use it */ /* This behavior should equal the behavior in HttpContext when uncorking fails */ Super::timeout(HTTP_TIMEOUT_S); } } else { /* We are already corked, or can't cork so let's just call the handler */ handler(); } return this; } /* Attach handler for writable HTTP response */ HttpResponse *onWritable(fu2::unique_function &&handler) { HttpResponseData *httpResponseData = getHttpResponseData(); httpResponseData->onWritable = std::move(handler); return this; } /* Attach handler for aborted HTTP request */ HttpResponse *onAborted(fu2::unique_function &&handler) { HttpResponseData *httpResponseData = getHttpResponseData(); httpResponseData->onAborted = std::move(handler); return this; } /* Attach a read handler for data sent. Will be called with FIN set true if last segment. */ void onData(fu2::unique_function &&handler) { HttpResponseData *data = getHttpResponseData(); data->inStream = std::move(handler); } }; } #endif // UWS_HTTPRESPONSE_H