//////////////////////////////////////////////////////////////////////////////// /// DISCLAIMER /// /// Copyright 2014-2016 ArangoDB GmbH, Cologne, Germany /// Copyright 2004-2014 triAGENS GmbH, Cologne, Germany /// /// 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. /// /// Copyright holder is ArangoDB GmbH, Cologne, Germany /// /// @author Dr. Frank Celler /// @author Achim Brandt //////////////////////////////////////////////////////////////////////////////// #include "HttpResponse.h" #include #include #include #include #include "Basics/Exceptions.h" #include "Basics/StringBuffer.h" #include "Basics/StringUtils.h" #include "Basics/VPackStringBufferAdapter.h" #include "Basics/VelocyPackDumper.h" #include "Basics/tri-strings.h" #include "Rest/GeneralRequest.h" using namespace arangodb; using namespace arangodb::basics; bool HttpResponse::HIDE_PRODUCT_HEADER = false; HttpResponse::HttpResponse(ResponseCode code) : GeneralResponse(code), _connectionType(CONNECTION_KEEP_ALIVE), _contentType(CONTENT_TYPE_TEXT), _isHeadResponse(false), _body(TRI_UNKNOWN_MEM_ZONE, false), _bodySize(0) { if (_body.c_str() == nullptr) { // no buffer could be reserved. out of memory! THROW_ARANGO_EXCEPTION(TRI_ERROR_OUT_OF_MEMORY); } } void HttpResponse::setCookie(std::string const& name, std::string const& value, int lifeTimeSeconds, std::string const& path, std::string const& domain, bool secure, bool httpOnly) { std::unique_ptr buffer = std::make_unique(TRI_UNKNOWN_MEM_ZONE); std::string tmp = StringUtils::trim(name); buffer->appendText(tmp); buffer->appendChar('='); tmp = StringUtils::urlEncode(value); buffer->appendText(tmp); if (lifeTimeSeconds != 0) { time_t rawtime; time(&rawtime); if (lifeTimeSeconds > 0) { rawtime += lifeTimeSeconds; } else { rawtime = 1; } if (rawtime > 0) { struct tm* timeinfo; char buffer2[80]; timeinfo = gmtime(&rawtime); strftime(buffer2, 80, "%a, %d-%b-%Y %H:%M:%S %Z", timeinfo); buffer->appendText(TRI_CHAR_LENGTH_PAIR("; expires=")); buffer->appendText(buffer2); } } if (!path.empty()) { buffer->appendText(TRI_CHAR_LENGTH_PAIR("; path=")); buffer->appendText(path); } if (!domain.empty()) { buffer->appendText(TRI_CHAR_LENGTH_PAIR("; domain=")); buffer->appendText(domain); } if (secure) { buffer->appendText(TRI_CHAR_LENGTH_PAIR("; secure")); } if (httpOnly) { buffer->appendText(TRI_CHAR_LENGTH_PAIR("; HttpOnly")); } _cookies.emplace_back(buffer->c_str()); } void HttpResponse::headResponse(size_t size) { _body.clear(); _isHeadResponse = true; _bodySize = size; } size_t HttpResponse::bodySize() const { if (_isHeadResponse) { return _bodySize; } return _body.length(); } void HttpResponse::writeHeader(StringBuffer* output) { output->appendText(TRI_CHAR_LENGTH_PAIR("HTTP/1.1 ")); output->appendText(responseString(_responseCode)); output->appendText("\r\n", 2); bool seenServerHeader = false; bool seenConnectionHeader = false; bool seenTransferEncodingHeader = false; std::string transferEncoding; for (auto const& it : _headers) { std::string const& key = it.first; size_t const keyLength = key.size(); // ignore content-length if (keyLength == 14 && key[0] == 'c' && memcmp(key.c_str(), "content-length", keyLength) == 0) { continue; } // save transfer encoding if (keyLength == 17 && key[0] == 't' && memcmp(key.c_str(), "transfer-encoding", keyLength) == 0) { seenTransferEncodingHeader = true; transferEncoding = it.second; continue; } if (keyLength == 6 && key[0] == 's' && memcmp(key.c_str(), "server", keyLength) == 0) { // this ensures we don't print two "Server" headers seenServerHeader = true; // go on and use the user-defined "Server" header value } else if (keyLength == 10 && key[0] == 'c' && memcmp(key.c_str(), "connection", keyLength) == 0) { // this ensures we don't print two "Connection" headers seenConnectionHeader = true; // go on and use the user-defined "Connection" header value } // reserve enough space for header name + ": " + value + "\r\n" if (output->reserve(keyLength + 2 + it.second.size() + 2) != TRI_ERROR_NO_ERROR) { THROW_ARANGO_EXCEPTION(TRI_ERROR_OUT_OF_MEMORY); } char const* p = key.c_str(); char const* end = p + keyLength; int capState = 1; while (p < end) { if (capState == 1) { // upper case output->appendCharUnsafe(::toupper(*p)); capState = 0; } else if (capState == 0) { // normal case output->appendCharUnsafe(::tolower(*p)); if (*p == '-') { capState = 1; } else if (*p == ':') { capState = 2; } } else { // output as is output->appendCharUnsafe(*p); } ++p; } output->appendTextUnsafe(": ", 2); output->appendTextUnsafe(it.second); output->appendTextUnsafe("\r\n", 2); } // add "Server" response header if (!seenServerHeader && !HIDE_PRODUCT_HEADER) { output->appendText("Server: ArangoDB\r\n"); } // add "Connection" response header if (!seenConnectionHeader) { switch (_connectionType) { case CONNECTION_KEEP_ALIVE: output->appendText(TRI_CHAR_LENGTH_PAIR("Connection: Keep-Alive\r\n")); break; case CONNECTION_CLOSE: output->appendText(TRI_CHAR_LENGTH_PAIR("Connection: Close\r\n")); break; case CONNECTION_NONE: output->appendText(TRI_CHAR_LENGTH_PAIR("Connection: \r\n")); break; } } // add "Content-Type" header switch (_contentType) { case CONTENT_TYPE_JSON: output->appendText(TRI_CHAR_LENGTH_PAIR( "Content-Type: application/json; charset=utf-8\r\n")); break; case CONTENT_TYPE_VPACK: output->appendText( TRI_CHAR_LENGTH_PAIR("Content-Type: application/x-velocypack\r\n")); break; case CONTENT_TYPE_TEXT: output->appendText( TRI_CHAR_LENGTH_PAIR("Content-Type: text/plain; charset=utf-8\r\n")); break; case CONTENT_TYPE_HTML: output->appendText( TRI_CHAR_LENGTH_PAIR("Content-Type: text/html; charset=utf-8\r\n")); break; case CONTENT_TYPE_DUMP: output->appendText(TRI_CHAR_LENGTH_PAIR( "Content-Type: application/x-arango-dump; charset=utf-8\r\n")); break; case CONTENT_TYPE_CUSTOM: { // intentionally don't print anything here // the header should have been in _headers already, and should have been // handled above } } for (auto const& it : _cookies) { output->appendText(TRI_CHAR_LENGTH_PAIR("Set-Cookie: ")); output->appendText(it); output->appendText("\r\n", 2); } if (seenTransferEncodingHeader && transferEncoding == "chunked") { output->appendText( TRI_CHAR_LENGTH_PAIR("Transfer-Encoding: chunked\r\n\r\n")); } else { if (seenTransferEncodingHeader) { output->appendText(TRI_CHAR_LENGTH_PAIR("Transfer-Encoding: ")); output->appendText(transferEncoding); output->appendText("\r\n", 2); } output->appendText(TRI_CHAR_LENGTH_PAIR("Content-Length: ")); if (_isHeadResponse) { // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13 // // 14.13 Content-Length // // The Content-Length entity-header field indicates the size of the // entity-body, // in decimal number of OCTETs, sent to the recipient or, in the case of // the HEAD method, // the size of the entity-body that would have been sent had the request // been a GET. output->appendInteger(_bodySize); } else { output->appendInteger(_body.length()); } output->appendText("\r\n\r\n", 4); } // end of header, body to follow } void HttpResponse::fillBody(GeneralRequest const* request, arangodb::velocypack::Slice const& slice, bool generateBody, VPackOptions const& options) { // VELOCYPACK if (request->velocyPackResponse()) { setContentType(HttpResponse::CONTENT_TYPE_VPACK); size_t length = static_cast(slice.byteSize()); if (generateBody) { _body.appendText(slice.startAs(), length); } else { headResponse(length); } } // JSON else { setContentType(HttpResponse::CONTENT_TYPE_JSON); if (generateBody) { arangodb::basics::VelocyPackDumper dumper(&_body, &options); dumper.dumpValue(slice); } else { // TODO can we optimize this? // Just dump some where else to find real length StringBuffer tmp(TRI_UNKNOWN_MEM_ZONE, false); // convert object to string VPackStringBufferAdapter buffer(tmp.stringBuffer()); // usual dumping - but not to the response body VPackDumper dumper(&buffer, &options); dumper.dump(slice); headResponse(tmp.length()); } } }