mirror of https://gitee.com/bigwinds/arangodb
556 lines
16 KiB
C++
556 lines
16 KiB
C++
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief simple http client
|
|
///
|
|
/// @file
|
|
///
|
|
/// DISCLAIMER
|
|
///
|
|
/// Copyright 2004-2013 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 triAGENS GmbH, Cologne, Germany
|
|
///
|
|
/// @author Dr. Frank Celler
|
|
/// @author Achim Brandt
|
|
/// @author Copyright 2009-2013, triAGENS GmbH, Cologne, Germany
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "SimpleHttpClient.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string>
|
|
#include <errno.h>
|
|
|
|
#include "Basics/StringUtils.h"
|
|
#include "BasicsC/logging.h"
|
|
|
|
#include "GeneralClientConnection.h"
|
|
#include "SimpleHttpResult.h"
|
|
|
|
using namespace triagens::basics;
|
|
using namespace triagens::rest;
|
|
using namespace std;
|
|
|
|
namespace triagens {
|
|
namespace httpclient {
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// constructors and destructors
|
|
// -----------------------------------------------------------------------------
|
|
|
|
SimpleHttpClient::SimpleHttpClient (GeneralClientConnection* connection,
|
|
double requestTimeout,
|
|
bool warn) :
|
|
_connection(connection),
|
|
_keepConnectionOnDestruction(false),
|
|
_writeBuffer(TRI_UNKNOWN_MEM_ZONE),
|
|
_readBuffer(TRI_UNKNOWN_MEM_ZONE),
|
|
_requestTimeout(requestTimeout),
|
|
_warn(warn),
|
|
_locationRewriter(),
|
|
_nextChunkedSize(0),
|
|
_result(0),
|
|
_maxPacketSize(128 * 1024 * 1024),
|
|
_keepAlive(true) {
|
|
|
|
// waiting for C++11...
|
|
_locationRewriter.func = 0;
|
|
_locationRewriter.data = 0;
|
|
|
|
_errorMessage = "";
|
|
_written = 0;
|
|
_state = IN_CONNECT;
|
|
if (_connection->isConnected()) {
|
|
_state = FINISHED;
|
|
}
|
|
}
|
|
|
|
SimpleHttpClient::~SimpleHttpClient () {
|
|
if (!_keepConnectionOnDestruction) {
|
|
_connection->disconnect();
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// public methods
|
|
// -----------------------------------------------------------------------------
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief close connection
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
bool SimpleHttpClient::close () {
|
|
_connection->disconnect();
|
|
_state = IN_CONNECT;
|
|
|
|
reset();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
SimpleHttpResult* SimpleHttpClient::request (rest::HttpRequest::HttpRequestType method,
|
|
const string& location,
|
|
const char* body,
|
|
size_t bodyLength,
|
|
const map<string, string>& headerFields) {
|
|
|
|
assert(_result == 0);
|
|
|
|
_result = new SimpleHttpResult;
|
|
_errorMessage = "";
|
|
|
|
// set body
|
|
setRequest(method, rewriteLocation(location), body, bodyLength, headerFields);
|
|
|
|
double endTime = now() + _requestTimeout;
|
|
double remainingTime = _requestTimeout;
|
|
|
|
while (isWorking() && remainingTime > 0.0) {
|
|
switch (_state) {
|
|
case (IN_CONNECT): {
|
|
handleConnect();
|
|
break;
|
|
}
|
|
|
|
case (IN_WRITE): {
|
|
size_t bytesWritten = 0;
|
|
|
|
TRI_set_errno(TRI_ERROR_NO_ERROR);
|
|
if (! _connection->handleWrite(remainingTime, (void*) (_writeBuffer.c_str() + _written), _writeBuffer.length() - _written, &bytesWritten)) {
|
|
setErrorMessage(TRI_last_error(), false);
|
|
this->close();
|
|
}
|
|
else {
|
|
_written += bytesWritten;
|
|
if (_written == _writeBuffer.length()) {
|
|
_state = IN_READ_HEADER;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case (IN_READ_HEADER):
|
|
case (IN_READ_BODY):
|
|
case (IN_READ_CHUNKED_HEADER):
|
|
case (IN_READ_CHUNKED_BODY): {
|
|
TRI_set_errno(TRI_ERROR_NO_ERROR);
|
|
|
|
if (_connection->handleRead(remainingTime, _readBuffer)) {
|
|
switch (_state) {
|
|
case (IN_READ_HEADER):
|
|
readHeader();
|
|
break;
|
|
case (IN_READ_BODY):
|
|
readBody();
|
|
break;
|
|
case (IN_READ_CHUNKED_HEADER):
|
|
readChunkedHeader();
|
|
break;
|
|
case (IN_READ_CHUNKED_BODY):
|
|
readChunkedBody();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
if (! _result->hasContentLength() &&
|
|
! _connection->isConnected() &&
|
|
_state == IN_READ_BODY) {
|
|
// no content-length header in response, now set the length
|
|
_result->setContentLength(_readBuffer.length());
|
|
readBody();
|
|
break;
|
|
}
|
|
|
|
setErrorMessage(TRI_last_error(), false);
|
|
this->close();
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
remainingTime = endTime - now();
|
|
}
|
|
|
|
if (isWorking() && _errorMessage == "" ) {
|
|
setErrorMessage("Request timeout reached");
|
|
}
|
|
|
|
// set result type in getResult()
|
|
SimpleHttpResult* result = getResult();
|
|
|
|
_result = 0;
|
|
|
|
return result;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// private methods
|
|
// -----------------------------------------------------------------------------
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief initialise the connection
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void SimpleHttpClient::handleConnect () {
|
|
if (! _connection->connect()) {
|
|
setErrorMessage("Could not connect to '" + _connection->getEndpoint()->getSpecification() + "'", errno);
|
|
_state = DEAD;
|
|
}
|
|
else {
|
|
// can write now
|
|
_state = IN_WRITE;
|
|
_written = 0;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief reset state
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void SimpleHttpClient::reset () {
|
|
_readBuffer.clear();
|
|
|
|
if (_result) {
|
|
_result->clear();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief sets username and password
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void SimpleHttpClient::setUserNamePassword (const string& prefix,
|
|
const string& username,
|
|
const string& password) {
|
|
|
|
string value = triagens::basics::StringUtils::encodeBase64(username + ":" + password);
|
|
|
|
_pathToBasicAuth.push_back(make_pair(prefix, value));
|
|
}
|
|
|
|
SimpleHttpResult* SimpleHttpClient::getResult () {
|
|
switch (_state) {
|
|
case (IN_CONNECT):
|
|
_result->setResultType(SimpleHttpResult::COULD_NOT_CONNECT);
|
|
break;
|
|
|
|
case (IN_WRITE):
|
|
_result->setResultType(SimpleHttpResult::WRITE_ERROR);
|
|
break;
|
|
|
|
case (IN_READ_HEADER):
|
|
case (IN_READ_BODY):
|
|
case (IN_READ_CHUNKED_HEADER):
|
|
case (IN_READ_CHUNKED_BODY):
|
|
_result->setResultType(SimpleHttpResult::READ_ERROR);
|
|
break;
|
|
|
|
case (FINISHED):
|
|
_result->setResultType(SimpleHttpResult::COMPLETE);
|
|
break;
|
|
|
|
default :
|
|
_result->setResultType(SimpleHttpResult::COULD_NOT_CONNECT);
|
|
}
|
|
|
|
return _result;
|
|
}
|
|
|
|
void SimpleHttpClient::setRequest (rest::HttpRequest::HttpRequestType method,
|
|
const string& location,
|
|
const char* body,
|
|
size_t bodyLength,
|
|
const map<string, string>& headerFields) {
|
|
|
|
_method = method;
|
|
|
|
if (_state == DEAD) {
|
|
_connection->resetNumConnectRetries();
|
|
}
|
|
|
|
///////////////////// fill the write buffer //////////////////////////////
|
|
_writeBuffer.clear();
|
|
|
|
HttpRequest::appendMethod(method, &_writeBuffer);
|
|
|
|
string l = location;
|
|
if (location.length() == 0 || location[0] != '/') {
|
|
l = "/" + location;
|
|
}
|
|
|
|
_writeBuffer.appendText(l);
|
|
_writeBuffer.appendText(" HTTP/1.1\r\n");
|
|
|
|
string hostname = _connection->getEndpoint()->getHostString();
|
|
if (hostname.find(':') != string::npos) {
|
|
hostname = hostname.substr(0, hostname.find(':'));
|
|
}
|
|
|
|
_writeBuffer.appendText("Host: ");
|
|
_writeBuffer.appendText(hostname);
|
|
_writeBuffer.appendText("\r\n");
|
|
|
|
if (_keepAlive) {
|
|
_writeBuffer.appendText("Connection: Keep-Alive\r\n");
|
|
}
|
|
else {
|
|
_writeBuffer.appendText("Connection: Close\r\n");
|
|
}
|
|
_writeBuffer.appendText("User-Agent: ArangoDB\r\n");
|
|
_writeBuffer.appendText("X-Arango-Version: 1.5\r\n");
|
|
_writeBuffer.appendText("Accept-Encoding: deflate\r\n");
|
|
|
|
// do basic authorization
|
|
if (! _pathToBasicAuth.empty()) {
|
|
string foundPrefix;
|
|
string foundValue;
|
|
std::vector< std::pair<std::string, std::string> >::iterator i = _pathToBasicAuth.begin();
|
|
for (; i != _pathToBasicAuth.end(); ++i) {
|
|
string f = i->first;
|
|
if (l.find(f) == 0) {
|
|
// f is prefix of l
|
|
if (f.length() > foundPrefix.length()) {
|
|
foundPrefix = f;
|
|
foundValue = i->second;
|
|
}
|
|
}
|
|
}
|
|
if (foundValue.length() > 0) {
|
|
_writeBuffer.appendText("Authorization: Basic ");
|
|
_writeBuffer.appendText(foundValue);
|
|
_writeBuffer.appendText("\r\n");
|
|
}
|
|
}
|
|
|
|
for (map<string, string>::const_iterator i = headerFields.begin(); i != headerFields.end(); ++i) {
|
|
// TODO: check Header name and value
|
|
_writeBuffer.appendText(i->first);
|
|
_writeBuffer.appendText(": ");
|
|
_writeBuffer.appendText(i->second);
|
|
_writeBuffer.appendText("\r\n");
|
|
}
|
|
|
|
if (method != HttpRequest::HTTP_REQUEST_GET) {
|
|
_writeBuffer.appendText("Content-Length: ");
|
|
_writeBuffer.appendInteger(bodyLength);
|
|
_writeBuffer.appendText("\r\n\r\n");
|
|
}
|
|
else {
|
|
_writeBuffer.appendText("\r\n");
|
|
}
|
|
|
|
if (body != 0) {
|
|
_writeBuffer.appendText(body, bodyLength);
|
|
}
|
|
|
|
LOG_TRACE("Request: %s", _writeBuffer.c_str());
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
if (_state != FINISHED) {
|
|
// close connection to reset all read and write buffers
|
|
this->close();
|
|
}
|
|
|
|
if (_connection->isConnected()) {
|
|
// we are connected, start with writing
|
|
_state = IN_WRITE;
|
|
_written = 0;
|
|
}
|
|
else {
|
|
// connect to server
|
|
_state = IN_CONNECT;
|
|
}
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// private methods
|
|
// -----------------------------------------------------------------------------
|
|
|
|
bool SimpleHttpClient::readHeader () {
|
|
char* pos = (char*) memchr(_readBuffer.c_str(), '\n', _readBuffer.length());
|
|
|
|
while (pos) {
|
|
// size_t is unsigned, should never get < 0
|
|
assert(pos >= _readBuffer.c_str());
|
|
|
|
size_t len = pos - _readBuffer.c_str();
|
|
string line(_readBuffer.c_str(), len);
|
|
_readBuffer.erase_front(len + 1);
|
|
|
|
//printf("found header line %s\n", line.c_str());
|
|
|
|
if (line == "\r" || line == "") {
|
|
// end of header found
|
|
if (_result->isChunked()) {
|
|
_state = IN_READ_CHUNKED_HEADER;
|
|
return readChunkedHeader();
|
|
}
|
|
else if (! _result->hasContentLength()) {
|
|
// no content-length header in response
|
|
_state = IN_READ_BODY;
|
|
return readBody();
|
|
}
|
|
else if (_result->hasContentLength() && _result->getContentLength() > 0) {
|
|
// found content-length header in response
|
|
if (_result->getContentLength() > _maxPacketSize) {
|
|
setErrorMessage("Content-Length > max packet size found", true);
|
|
|
|
// reset connection
|
|
this->close();
|
|
_state = DEAD;
|
|
|
|
return false;
|
|
}
|
|
|
|
_state = IN_READ_BODY;
|
|
return readBody();
|
|
}
|
|
else {
|
|
_result->setResultType(SimpleHttpResult::COMPLETE);
|
|
_state = FINISHED;
|
|
|
|
if (! _keepAlive) {
|
|
_connection->disconnect();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
else {
|
|
_result->addHeaderField(line);
|
|
}
|
|
|
|
pos = (char*) memchr(_readBuffer.c_str(), '\n', _readBuffer.length());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SimpleHttpClient::readBody () {
|
|
if (_method == HttpRequest::HTTP_REQUEST_HEAD) {
|
|
// HEAD requests may be responded to without a body...
|
|
_result->setResultType(SimpleHttpResult::COMPLETE);
|
|
_state = FINISHED;
|
|
if (! _keepAlive) {
|
|
_connection->disconnect();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (_result->hasContentLength() && _readBuffer.length() >= _result->getContentLength()) {
|
|
if (_result->isDeflated()) {
|
|
// body is compressed using deflate. inflate it
|
|
_readBuffer.inflate(_result->getBody());
|
|
}
|
|
else {
|
|
// body is not compressed
|
|
_result->getBody().write(_readBuffer.c_str(), _result->getContentLength());
|
|
}
|
|
|
|
_readBuffer.erase_front(_result->getContentLength());
|
|
_result->setResultType(SimpleHttpResult::COMPLETE);
|
|
_state = FINISHED;
|
|
if (! _keepAlive) {
|
|
_connection->disconnect();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SimpleHttpClient::readChunkedHeader () {
|
|
char* pos = (char*) memchr(_readBuffer.c_str(), '\n', _readBuffer.length());
|
|
|
|
while (pos) {
|
|
// got a line
|
|
size_t len = pos - _readBuffer.c_str();
|
|
string line(_readBuffer.c_str(), len);
|
|
_readBuffer.erase_front(len + 1);
|
|
|
|
string trimmed = StringUtils::trim(line);
|
|
|
|
if (trimmed == "\r" || trimmed == "") {
|
|
// ignore empty lines
|
|
pos = (char*) memchr(_readBuffer.c_str(), '\n', _readBuffer.length());
|
|
continue;
|
|
}
|
|
|
|
uint32_t contentLength;
|
|
sscanf(trimmed.c_str(), "%x", &contentLength);
|
|
|
|
if (contentLength == 0) {
|
|
// OK: last content length found
|
|
|
|
_result->setResultType(SimpleHttpResult::COMPLETE);
|
|
|
|
_state = FINISHED;
|
|
if (! _keepAlive) {
|
|
_connection->disconnect();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (contentLength > _maxPacketSize) {
|
|
// failed: too many bytes
|
|
|
|
setErrorMessage("Content-Length > max packet size found!", true);
|
|
// reset connection
|
|
this->close();
|
|
_state = DEAD;
|
|
|
|
return false;
|
|
}
|
|
|
|
_state = IN_READ_CHUNKED_BODY;
|
|
_nextChunkedSize = contentLength;
|
|
|
|
return readChunkedBody();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SimpleHttpClient::readChunkedBody () {
|
|
if (_method == HttpRequest::HTTP_REQUEST_HEAD) {
|
|
// HEAD requests may be responded to without a body...
|
|
_result->setResultType(SimpleHttpResult::COMPLETE);
|
|
_state = FINISHED;
|
|
if (! _keepAlive) {
|
|
_connection->disconnect();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (_readBuffer.length() >= _nextChunkedSize) {
|
|
|
|
_result->getBody().write(_readBuffer.c_str(), (size_t) _nextChunkedSize);
|
|
_readBuffer.erase_front((size_t) _nextChunkedSize);
|
|
_state = IN_READ_CHUNKED_HEADER;
|
|
return readChunkedHeader();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
}
|