1
0
Fork 0
arangodb/lib/Rest/HttpRequest.cpp

941 lines
25 KiB
C++

////////////////////////////////////////////////////////////////////////////////
/// 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 "HttpRequest.h"
#include "Basics/NumberUtils.h"
#include <velocypack/Builder.h>
#include <velocypack/Options.h>
#include <velocypack/Parser.h>
#include <velocypack/Validator.h>
#include <velocypack/velocypack-aliases.h>
#include "Basics/StaticStrings.h"
#include "Basics/StringUtils.h"
#include "Basics/conversions.h"
#include "Basics/debugging.h"
#include "Basics/tri-strings.h"
#include "Logger/Logger.h"
using namespace arangodb;
using namespace arangodb::basics;
HttpRequest::HttpRequest(ConnectionInfo const& connectionInfo, char const* header,
size_t length, bool allowMethodOverride)
: GeneralRequest(connectionInfo),
_contentLength(0),
_vpackBuilder(nullptr),
_version(ProtocolVersion::UNKNOWN),
_allowMethodOverride(allowMethodOverride) {
_contentType = ContentType::UNSET;
_contentTypeResponse = ContentType::JSON;
if (0 < length) {
auto buff = std::make_unique<char[]>(length + 1);
memcpy(buff.get(), header, length);
(buff.get())[length] = 0;
parseHeader(buff.get(), length);
}
}
// HACK HACK HACK
// This should only be called by createFakeRequest in ClusterComm
// as the Request is not fully constructed. This 2nd constructor
// avoids the need of a additional FakeRequest class.
HttpRequest::HttpRequest(ContentType contentType, char const* body, int64_t contentLength,
std::unordered_map<std::string, std::string> const& headers)
: GeneralRequest(ConnectionInfo()),
_contentLength(contentLength),
_vpackBuilder(nullptr),
_version(ProtocolVersion::UNKNOWN),
_allowMethodOverride(false) {
_contentType = contentType;
_contentTypeResponse = contentType;
_body.append(body, contentLength);
GeneralRequest::_headers = headers;
}
void HttpRequest::parseHeader(char* start, size_t length) {
char* end = start + length;
size_t const versionLength = strlen("http/1.x");
// current line number
int lineNum = 0;
// begin and end of current line
char* lineBegin = nullptr;
// split request
while (start < end) {
lineBegin = start;
// .............................................................................
// FIRST LINE
// .............................................................................
// check for request type (GET/POST in line 0), path and parameters
if (lineNum == 0) {
// split line at space
char* e = lineBegin;
for (; e < end && *e != ' ' && *e != '\n'; ++e) {
*e = ::StringUtils::tolower(*e);
}
// store key and value
char* keyBegin = lineBegin;
char* keyEnd = e;
char* valueBegin = nullptr;
char* valueEnd = nullptr;
// we found a space (*end is '\0')
if (*e == ' ') {
// extract the value
keyEnd = e;
// trim value from the start
while (e < end && *e == ' ') {
++e;
}
*keyEnd = '\0';
// there is no value at all
if (e == end) {
valueBegin = valueEnd = keyEnd;
start = end;
}
// there is only a NL
else if (*e == '\n') {
valueBegin = valueEnd = keyEnd;
start = e + 1;
}
// find space
else {
valueBegin = e;
while (e < end && *e != '\n' && *e != ' ') {
++e;
}
if (e == end) {
valueEnd = e;
start = end;
} else {
if (*e == '\n') {
valueEnd = e;
start = e + 1;
// skip \r
if (valueBegin < valueEnd && valueEnd[-1] == '\r') {
--valueEnd;
}
} else {
valueEnd = e;
// HTTP protocol version is expected next
// trim value
while (e < end && *e == ' ') {
++e;
}
if ((size_t)(end - e) > versionLength) {
if ((e[0] == 'h' || e[0] == 'H') && (e[1] == 't' || e[1] == 'T') &&
(e[2] == 't' || e[2] == 'T') && (e[3] == 'p' || e[3] == 'P') &&
e[4] == '/' && e[5] == '1' && e[6] == '.') {
if (e[7] == '1') {
_version = ProtocolVersion::HTTP_1_1;
} else if (e[7] == '0') {
_version = ProtocolVersion::HTTP_1_0;
} else {
_version = ProtocolVersion::UNKNOWN;
}
e += versionLength;
}
}
// go on until eol
while (e < end && *e != '\n') {
++e;
}
if (e == end) {
start = end;
} else {
start = e + 1;
}
}
*valueEnd = '\0';
}
}
// check the key
_type = findRequestType(keyBegin, keyEnd - keyBegin);
// extract the path and decode the url and parameters
if (_type != RequestType::ILLEGAL) {
char* pathBegin = valueBegin;
char* pathEnd = nullptr;
char* paramBegin = nullptr;
char* paramEnd = nullptr;
// find a question mark or space
char* f = pathBegin;
// get ride of "//"
char* g = f;
// do NOT url-decode the path, we need to distinguish between
// "/document/a/b" and "/document/a%2fb"
while (f < valueEnd && *f != '?' && *f != ' ' && *f != '\n') {
*g++ = *f;
if (*f == '/') {
while (f < valueEnd && *f == '/') {
++f;
}
} else {
++f;
}
}
pathEnd = g;
// look for database name in URL
if (pathEnd - pathBegin >= 5) {
char* q = pathBegin;
// check if the prefix is "_db"
if (q[0] == '/' && q[1] == '_' && q[2] == 'd' && q[3] == 'b' && q[4] == '/') {
// request contains database name
q += 5;
pathBegin = q;
// read until end of database name
while (*q != '\0') {
if (*q == '/' || *q == '?' || *q == ' ' || *q == '\n' || *q == '\r') {
break;
}
++q;
}
_databaseName = std::string(pathBegin, q - pathBegin);
pathBegin = q;
}
}
// no space, question mark or end-of-line
if (f == valueEnd) {
*pathEnd = '\0';
paramEnd = paramBegin = pathEnd;
// set full url = complete path
_fullUrl = std::string(pathBegin, pathEnd - pathBegin);
}
// no question mark
else if (*f == ' ' || *f == '\n') {
*pathEnd = '\0';
paramEnd = paramBegin = f;
// set full url = complete path
_fullUrl = std::string(pathBegin, pathEnd - pathBegin);
}
// found a question mark
else {
paramBegin = g + 1;
paramEnd = f + 1;
*g++ = '?';
while (paramEnd < valueEnd && *paramEnd != ' ' && *paramEnd != '\n') {
*g++ = *paramEnd++;
}
*g = '\0';
paramEnd = g;
// set full url = complete path + query parameters
_fullUrl = std::string(pathBegin, paramEnd - pathBegin);
// now that the full url was saved, we can insert the null bytes
*pathEnd = '\0';
}
if (pathBegin < pathEnd) {
_requestPath = std::string(pathBegin, pathEnd - pathBegin);
}
if (paramBegin < paramEnd) {
setValues(paramBegin, paramEnd);
}
}
}
// no space, either eol or newline
else {
if (e < end) {
*keyEnd = '\0';
start = e + 1;
} else {
start = end;
}
// check the key
_type = findRequestType(keyBegin, keyEnd - keyBegin);
}
}
// .............................................................................
// OTHER LINES
// .............................................................................
else {
// split line at colon
char* e = lineBegin;
for (; e < end && *e != ':' && *e != '\n'; ++e) {
*e = StringUtils::tolower(*e);
}
// store key and value
char* keyBegin = lineBegin;
char* keyEnd = e;
// we found a colon (*end is '\0')
if (*e == ':') {
char* valueBegin = nullptr;
char* valueEnd = nullptr;
// extract the value
keyEnd = e++;
while (e < end && *e == ' ') {
++e;
}
while (keyBegin < keyEnd && keyEnd[-1] == ' ') {
--keyEnd;
}
*keyEnd = '\0';
// there is no value at all
if (e == end) {
valueBegin = valueEnd = keyEnd;
start = end;
} else if (*e == '\n') {
valueBegin = valueEnd = keyEnd;
start = e + 1;
}
// find \n
else {
valueBegin = e;
while (e < end && *e != '\n') {
++e;
}
if (e == end) {
valueEnd = e;
start = end;
} else {
valueEnd = e;
start = e + 1;
}
// skip \r
if (valueBegin < valueEnd && valueEnd[-1] == '\r') {
--valueEnd;
}
// skip trailing spaces
while (valueBegin < valueEnd && valueEnd[-1] == ' ') {
--valueEnd;
}
*valueEnd = '\0';
}
if (keyBegin < keyEnd) {
setHeader(keyBegin, keyEnd - keyBegin, valueBegin, valueEnd - valueBegin);
}
}
// we found no colon, either eol or newline. Take the whole line as key
else {
if (e < end) {
*keyEnd = '\0';
start = e + 1;
} else {
start = end;
}
// skip \r
if (keyBegin < keyEnd && keyEnd[-1] == '\r') {
--keyEnd;
}
// use empty value
if (keyBegin < keyEnd) {
setHeader(keyBegin, keyEnd - keyBegin);
}
}
}
lineNum++;
}
}
namespace {
std::string url_decode(const char* begin, const char* end) {
std::string out;
out.reserve(static_cast<size_t>(end - begin));
for (const char* i = begin; i != end; ++i) {
std::string::value_type c = (*i);
if (c == '%') {
if (i + 2 < end) {
int h = StringUtils::hex2int(i[1], 0) << 4;
h += StringUtils::hex2int(i[2], 0);
out.push_back(static_cast<char>(h & 0x7F));
i += 2;
}
} else if (c == '+') {
out.push_back(' ');
} else {
out.push_back(c);
}
}
return out;
}
}
void HttpRequest::parseUrl(const char* path, size_t length) {
std::string tmp;
tmp.reserve(length);
// get rid of '//'
for (size_t i = 0; i < length; ++i) {
tmp.push_back(path[i]);
if (path[i] == '/') {
while (i + 1 < length && path[i+1] == '/') {
++i;
}
}
}
const char* start = tmp.data();
const char* end = start + tmp.size();
// look for database name in URL
if (end - start >= 5) {
char const* q = start;
// check if the prefix is "_db"
if (q[0] == '/' && q[1] == '_' && q[2] == 'd' && q[3] == 'b' && q[4] == '/') {
// request contains database name
q += 5;
start = q;
// read until end of database name
while (q < end) {
if (*q == '/' || *q == '?' || *q == ' ' || *q == '\n' || *q == '\r') {
break;
}
++q;
}
TRI_ASSERT(q >= start);
_databaseName = std::string(start, q - start);
_fullUrl.assign(q, end - q);
start = q;
} else {
_fullUrl.assign(start, end - start);
}
} else {
_fullUrl.assign(start, end - start);
}
TRI_ASSERT(!_fullUrl.empty());
char const* q = start;
while (q != end && *q != '?') {
++q;
}
if (q == end || *q == '?') {
_requestPath.assign(start, q - start);
}
if (q == end) {
return;
}
bool keyPhase = true;
const char* keyBegin = ++q;
const char* keyEnd = keyBegin;
const char* valueBegin = nullptr;
while (q != end) {
if (keyPhase) {
keyEnd = q;
if (*q == '=') {
keyPhase = false;
valueBegin = q + 1;
}
++q;
continue;
}
if (q + 1 == end || *(q + 1) == '&') {
++q; // skip ahead
std::string val = ::url_decode(valueBegin, q);
if (keyEnd - keyBegin > 2 && *(keyEnd - 2) == '[' && *(keyEnd - 1) == ']') {
// found parameter xxx[]
_arrayValues[::url_decode(keyBegin, keyEnd - 2)]
.emplace_back(std::move(val));
} else {
_values[::url_decode(keyBegin, keyEnd)] = std::move(val);
}
keyPhase = true;
keyBegin = q + 1;
continue;
}
++q;
}
}
void HttpRequest::setHeaderV2(std::string&& key, std::string&& value) {
StringUtils::tolowerInPlace(key); // always lowercase key
if (key == StaticStrings::ContentLength) {
_contentLength = NumberUtils::atoi_zero<int64_t>(value.c_str(), value.c_str() + value.size());
// do not store this header
return;
}
if (key == StaticStrings::Accept && value == StaticStrings::MimeTypeVPack) {
_contentTypeResponse = ContentType::VPACK;
} else if ((_contentType == ContentType::UNSET) && (key == StaticStrings::ContentTypeHeader)) {
if (value == StaticStrings::MimeTypeVPack) {
_contentType = ContentType::VPACK; // don't insert this header!!
return;
}
else if ((value.length() >= StaticStrings::MimeTypeJsonNoEncoding.length()) &&
(memcmp(value.c_str(),
StaticStrings::MimeTypeJsonNoEncoding.c_str(),
StaticStrings::MimeTypeJsonNoEncoding.length()) == 0)) {
// ignore encoding etc.
_contentType = ContentType::JSON;
return;
}
} else if (key == StaticStrings::AcceptEncoding) {
// This can be much more elaborated as the can specify weights on encodings
// However, for now just toggle on deflate if deflate is requested
if (StaticStrings::EncodingDeflate == value) {
// FXIME: cannot use substring search, Java driver chokes on deflated response
//if (value.find(StaticStrings::EncodingDeflate) != std::string::npos) {
_acceptEncoding = EncodingType::DEFLATE;
}
}
if (key == "cookie") {
parseCookies(value.c_str(), value.size());
return;
}
if (_allowMethodOverride && key.size() >= 13 && key[0] == 'x' && key[1] == '-') {
// handle x-... headers
// override HTTP method?
if (key == "x-http-method" ||
key == "x-method-override" ||
key == "x-http-method-override") {
StringUtils::tolowerInPlace(value);
_type = findRequestType(value.c_str(), value.size());
// don't insert this header!!
return;
}
}
_headers[std::move(key)] = std::move(value);
}
void HttpRequest::setArrayValue(char const* key, size_t length, char const* value) {
TRI_ASSERT(key != nullptr);
TRI_ASSERT(value != nullptr);
_arrayValues[std::string(key, length)].emplace_back(value);
}
void HttpRequest::setValues(char* buffer, char* end) {
char* keyBegin = nullptr;
char* key = nullptr;
char* valueBegin = nullptr;
char* value = nullptr;
enum { KEY, VALUE } phase = KEY;
enum { NORMAL, HEX1, HEX2 } reader = NORMAL;
int hex = 0;
char const AMB = '&';
char const EQUAL = '=';
char const PERCENT = '%';
char const PLUS = '+';
for (keyBegin = key = buffer; buffer < end; buffer++) {
char next = *buffer;
if (phase == KEY && next == EQUAL) {
phase = VALUE;
valueBegin = value = buffer + 1;
continue;
} else if (next == AMB) {
phase = KEY;
*key = '\0';
// check for missing value phase
if (valueBegin == nullptr) {
valueBegin = value = key;
} else {
*value = '\0';
}
if (key - keyBegin > 2 && (*(key - 2)) == '[' && (*(key - 1)) == ']') {
// found parameter xxx[]
*(key - 2) = '\0';
setArrayValue(keyBegin, key - keyBegin - 2, valueBegin);
} else {
_values[std::string(keyBegin, key - keyBegin)] =
std::string(valueBegin, value - valueBegin);
}
keyBegin = key = buffer + 1;
valueBegin = value = nullptr;
continue;
} else if (next == PERCENT) {
reader = HEX1;
continue;
} else if (reader == HEX1) {
int h1 = StringUtils::hex2int(next, -1);
if (h1 == -1) {
reader = NORMAL;
--buffer;
continue;
}
hex = h1 * 16;
reader = HEX2;
continue;
} else if (reader == HEX2) {
int h1 = StringUtils::hex2int(next, -1);
if (h1 == -1) {
--buffer;
} else {
hex += h1;
}
reader = NORMAL;
next = static_cast<char>(hex);
} else if (next == PLUS) {
next = ' ';
}
if (phase == KEY) {
*key++ = next;
} else {
*value++ = next;
}
}
if (keyBegin != key) {
*key = '\0';
// check for missing value phase
if (valueBegin == nullptr) {
valueBegin = value = key;
} else {
*value = '\0';
}
if (key - keyBegin > 2 && (*(key - 2)) == '[' && (*(key - 1)) == ']') {
// found parameter xxx[]
*(key - 2) = '\0';
setArrayValue(keyBegin, key - keyBegin - 2, valueBegin);
} else {
_values[std::string(keyBegin, key - keyBegin)] =
std::string(valueBegin, value - valueBegin);
}
}
}
/// @brief sets a key/value header
void HttpRequest::setHeader(char const* key, size_t keyLength,
char const* value, size_t valueLength) {
TRI_ASSERT(key != nullptr);
TRI_ASSERT(value != nullptr);
if (keyLength == StaticStrings::ContentLength.size() &&
memcmp(key, StaticStrings::ContentLength.c_str(), keyLength) == 0) { // 14 = strlen("content-length")
_contentLength = NumberUtils::atoi_zero<int64_t>(value, value + valueLength);
// do not store this header
return;
}
if (keyLength == StaticStrings::Accept.size() &&
valueLength == StaticStrings::MimeTypeVPack.size() &&
memcmp(key, StaticStrings::Accept.c_str(), keyLength) == 0 &&
memcmp(value, StaticStrings::MimeTypeVPack.c_str(), valueLength) == 0) {
_contentTypeResponse = ContentType::VPACK;
} else if (keyLength == StaticStrings::AcceptEncoding.size() &&
valueLength == StaticStrings::EncodingDeflate.size() &&
memcmp(key, StaticStrings::AcceptEncoding.c_str(), keyLength) == 0 &&
memcmp(value, StaticStrings::EncodingDeflate.c_str(), valueLength) == 0) {
// This can be much more elaborated as the can specify weights on encodings
// However, for now just toggle on deflate if deflate is requested
_acceptEncoding = EncodingType::DEFLATE;
} else if ((_contentType == ContentType::UNSET) &&
(keyLength == StaticStrings::ContentTypeHeader.size()) &&
(memcmp(key, StaticStrings::ContentTypeHeader.c_str(), keyLength) == 0)) {
if (valueLength == StaticStrings::MimeTypeVPack.size() &&
memcmp(value, StaticStrings::MimeTypeVPack.c_str(), valueLength) == 0) {
_contentType = ContentType::VPACK;
// don't insert this header!!
return;
}
if (valueLength >= StaticStrings::MimeTypeJsonNoEncoding.size() &&
memcmp(value, StaticStrings::MimeTypeJsonNoEncoding.c_str(),
StaticStrings::MimeTypeJsonNoEncoding.length()) == 0) {
_contentType = ContentType::JSON;
// don't insert this header!!
return;
}
}
if (keyLength == 6 && memcmp(key, "cookie", keyLength) == 0) { // 6 = strlen("cookie")
parseCookies(value, valueLength);
return;
}
if (_allowMethodOverride && keyLength >= 13 && *key == 'x' && *(key + 1) == '-') {
// handle x-... headers
// override HTTP method?
if ((keyLength == 13 && memcmp(key, "x-http-method", keyLength) == 0) ||
(keyLength == 17 && memcmp(key, "x-method-override", keyLength) == 0) ||
(keyLength == 22 && memcmp(key, "x-http-method-override", keyLength) == 0)) {
std::string overriddenType(value, valueLength);
StringUtils::tolowerInPlace(overriddenType);
_type = findRequestType(overriddenType.c_str(), overriddenType.size());
// don't insert this header!!
return;
}
}
_headers[std::string(key, keyLength)] = std::string(value, valueLength);
}
/// @brief sets a key-only header
void HttpRequest::setHeader(char const* key, size_t keyLength) {
_headers[std::string(key, keyLength)] = StaticStrings::Empty;
}
void HttpRequest::setCookie(char* key, size_t length, char const* value) {
_cookies[std::string(key, length)] = value;
}
void HttpRequest::parseCookies(char const* buffer, size_t length) {
char* keyBegin = nullptr;
char* key = nullptr;
char* valueBegin = nullptr;
char* value = nullptr;
enum { KEY, VALUE } phase = KEY;
enum { NORMAL, HEX1, HEX2 } reader = NORMAL;
int hex = 0;
char const AMB = ';';
char const EQUAL = '=';
char const PERCENT = '%';
char const SPACE = ' ';
char* buffer2 = (char*)buffer;
char* end = buffer2 + length;
for (keyBegin = key = buffer2; buffer2 < end; buffer2++) {
char next = *buffer2;
if (phase == KEY && next == EQUAL) {
phase = VALUE;
valueBegin = value = buffer2 + 1;
continue;
} else if (next == AMB) {
phase = KEY;
*key = '\0';
// check for missing value phase
if (valueBegin == nullptr) {
valueBegin = value = key;
} else {
*value = '\0';
}
setCookie(keyBegin, key - keyBegin, valueBegin);
// keyBegin = key = buffer2 + 1;
while (*(keyBegin = key = buffer2 + 1) == SPACE && buffer2 < end) {
buffer2++;
}
valueBegin = value = nullptr;
continue;
} else if (next == PERCENT) {
reader = HEX1;
continue;
} else if (reader == HEX1) {
int h1 = StringUtils::hex2int(next, -1);
if (h1 == -1) {
reader = NORMAL;
--buffer2;
continue;
}
hex = h1 * 16;
reader = HEX2;
continue;
} else if (reader == HEX2) {
int h1 = StringUtils::hex2int(next, -1);
if (h1 == -1) {
--buffer2;
} else {
hex += h1;
}
reader = NORMAL;
next = static_cast<char>(hex);
}
if (phase == KEY) {
*key++ = next;
} else {
*value++ = next;
}
}
if (keyBegin != key) {
*key = '\0';
// check for missing value phase
if (valueBegin == nullptr) {
valueBegin = value = key;
} else {
*value = '\0';
}
setCookie(keyBegin, key - keyBegin, valueBegin);
}
}
std::string const& HttpRequest::cookieValue(std::string const& key) const {
auto it = _cookies.find(key);
if (it == _cookies.end()) {
return StaticStrings::Empty;
}
return it->second;
}
std::string const& HttpRequest::cookieValue(std::string const& key, bool& found) const {
auto it = _cookies.find(key);
if (it == _cookies.end()) {
found = false;
return StaticStrings::Empty;
}
found = true;
return it->second;
}
VPackStringRef HttpRequest::rawPayload() const {
return VPackStringRef(reinterpret_cast<const char*>(_body.data()), _body.size());
};
VPackSlice HttpRequest::payload(VPackOptions const* options) {
TRI_ASSERT(options != nullptr);
if ((_contentType == ContentType::UNSET) || (_contentType == ContentType::JSON)) {
if (!_body.empty()) {
if (!_vpackBuilder) {
VPackParser parser(options);
parser.parse(_body.data(),
_body.size());
_vpackBuilder = parser.steal();
}
return VPackSlice(_vpackBuilder->slice());
}
return VPackSlice::noneSlice(); // no body
} else if (_contentType == ContentType::VPACK) {
VPackOptions validationOptions = *options; // intentional copy
validationOptions.validateUtf8Strings = true;
validationOptions.checkAttributeUniqueness = true;
validationOptions.disallowExternals = true;
validationOptions.disallowCustom = true;
VPackValidator validator(&validationOptions);
validator.validate(_body.data(), _body.length()); // throws on error
return VPackSlice(reinterpret_cast<uint8_t const*>(_body.data()));
}
return VPackSlice::noneSlice();
}
HttpRequest* HttpRequest::createHttpRequest(
ContentType contentType, char const* body, int64_t contentLength,
std::unordered_map<std::string, std::string> const& headers) {
return new HttpRequest(contentType, body, contentLength, headers);
}