//////////////////////////////////////////////////////////////////////////////// /// @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 #include #include #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& 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& 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 >::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::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; } } }