Adding curl and nohttp transport options (#54)

* http client
This commit is contained in:
Victor Vazquez 2020-04-02 11:05:26 -07:00 committed by GitHub
parent 9631e5978a
commit 61b8cdc3d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 460 additions and 240 deletions

View File

@ -37,3 +37,6 @@ include(global_compile_options)
# sub-projects
add_subdirectory(sdk/core/azure-core)
add_subdirectory(sdk/platform/http_client/curl) # will work only if BUILD_CURL_TRANSPORT=ON
add_subdirectory(sdk/samples/http_client/curl) # will work only if BUILD_CURL_TRANSPORT=ON
add_subdirectory(sdk/platform/http_client/nohttp) # will work only if !BUILD_CURL_TRANSPORT=ON

View File

@ -12,6 +12,7 @@ add_library (
src/credentials/credentials
src/http/http
src/http/request
src/http/response
)
target_include_directories (azure-core PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc> $<INSTALL_INTERFACE:include/az_core>)

View File

@ -4,8 +4,11 @@
#pragma once
#include <algorithm>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include <internal/contract.hpp>
@ -63,6 +66,177 @@ enum class HttpMethod
PATCH,
};
class Request
{
private:
// query needs to be first or at least before url, since url might update it
std::map<std::string, std::string> m_queryParameters;
HttpMethod _method;
std::string _url;
std::map<std::string, std::string> m_headers;
std::map<std::string, std::string> m_retryHeaders;
std::map<std::string, std::string> m_retryQueryParameters;
// Request can contain no body, or either of next bodies (_bodyBuffer plus size or bodyStream)
BodyStream* m_bodyStream;
BodyBuffer* m_bodyBuffer;
// flag to know where to insert header
bool m_retryModeEnabled;
// returns left map plus all items in right
// when duplicates, left items are preferred
static std::map<std::string, std::string> mergeMaps(
std::map<std::string, std::string> left,
std::map<std::string, std::string> const& right)
{
left.insert(right.begin(), right.end());
return left;
}
/**
* Will check if there are any query parameter in url looking for symbol '?'
* If it is found, it will insert query parameters to m_queryParameters internal field
* and remove it from url
*/
const std::string parseUrl(std::string const& url)
{
const auto firstPosition = std::find(url.begin(), url.end(), '?');
if (firstPosition == url.end())
{
return url; // not query parameters
}
auto position = firstPosition; // position of symbol ?
while (position != url.end())
{
++position; // skip over the ? or &
const auto nextPosition = std::find(position, url.end(), '&');
const auto equalChar = std::find(position, nextPosition, '=');
auto valueStart = equalChar;
if (valueStart != nextPosition)
{
++valueStart; // skip = symbol
}
// Note: if there is another = symbol before nextPosition, it will be part of the paramenter
// value. And if there is not a ? symbol, we add empty string as value
m_queryParameters.insert(std::pair<std::string, std::string>(
std::string(position, equalChar), std::string(valueStart, nextPosition)));
position = nextPosition;
}
return std::string(url.begin(), firstPosition);
}
Request(
HttpMethod httpMethod,
std::string const& url,
BodyStream* bodyStream,
BodyBuffer* bodyBuffer)
: _method(std::move(httpMethod)), _url(parseUrl(url)), m_bodyStream(bodyStream),
m_bodyBuffer(bodyBuffer), m_retryModeEnabled(false)
{
// TODO: parse url
}
public:
Request(HttpMethod httpMethod, std::string const& url)
: Request(httpMethod, url, BodyStream::null, BodyBuffer::null)
{
}
Request(HttpMethod httpMethod, std::string const& url, BodyBuffer* bodyBuffer)
: Request(httpMethod, url, BodyStream::null, bodyBuffer)
{
}
Request(HttpMethod httpMethod, std::string const& url, BodyStream* bodyStream)
: Request(httpMethod, url, bodyStream, BodyBuffer::null)
{
}
// Methods used to build HTTP request
void addPath(std::string const& path);
void addQueryParameter(std::string const& name, std::string const& value);
void addHeader(std::string const& name, std::string const& value);
void startRetry(); // only called by retry policy
// Methods used by transport layer (and logger) to send request
HttpMethod getMethod();
std::string getEncodedUrl(); // should return encoded url
std::map<std::string, std::string> getHeaders();
BodyStream* getBodyStream();
BodyBuffer* getBodyBuffer();
};
/*
* Response exceptions
*/
struct CouldNotResolveHostException : public std::exception
{
const char* what() const throw() { return "couldnt resolve host"; }
};
struct ErrorWhileWrittingResponse : public std::exception
{
const char* what() const throw() { return "couldnt write response"; }
};
// Any other excpetion from transport layer without an specific exception defined above
struct TransportException : public std::exception
{
const char* what() const throw() { return "Error on transport layer while sending request"; }
};
class Response
{
private:
uint16_t m_statusCode;
std::string m_reasonPhrase;
std::map<std::string, std::string> m_headers;
// Response can contain no body, or either of next bodies (_bodyBuffer plus size or bodyStream)
http::BodyBuffer* m_bodyBuffer;
http::BodyStream* m_bodyStream;
Response(
uint16_t statusCode,
std::string const& reasonPhrase,
BodyBuffer* const bodyBuffer,
BodyStream* const BodyStream)
: m_statusCode(statusCode), m_reasonPhrase(reasonPhrase), m_bodyBuffer(bodyBuffer),
m_bodyStream(BodyStream)
{
}
public:
Response(uint16_t statusCode, std::string const& reasonPhrase)
: Response(statusCode, reasonPhrase, http::BodyBuffer::null, http::BodyStream::null)
{
}
// Methods used to build HTTP response
void addHeader(std::string const& name, std::string const& value);
void setBody(BodyBuffer* bodyBuffer);
void setBody(BodyStream* bodyStream);
// Methods used by transport layer (and logger) to send response
uint16_t getStatusCode();
std::string const& getReasonPhrase();
std::map<std::string, std::string> const& getHeaders();
http::BodyStream* getBodyStream();
http::BodyBuffer* getBodyBuffer();
};
class Client
{
public:
static Response send(Request& request);
};
} // namespace http
} // namespace core
} // namespace azure

View File

@ -1,147 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#pragma once
#include <algorithm>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include <internal/contract.hpp>
namespace azure
{
namespace core
{
namespace http
{
class Request
{
private:
Request(Request const&) = delete;
void operator=(Request const&) = delete;
// query needs to be first or at least before url, since url might update it
std::map<std::string, std::string> _queryParameters;
HttpMethod _method;
std::string _url;
std::map<std::string, std::string> _headers;
std::map<std::string, std::string> _retryHeaders;
std::map<std::string, std::string> _retryQueryParameters;
// Request can contain no body, or either of next bodies (_bodyBuffer plus size or bodyStream)
BodyStream* _bodyStream;
BodyBuffer* _bodyBuffer;
// flag to know where to insert header
bool _retryModeEnabled;
// returns left map plus all items in right
// when duplicates, left items are preferred
static std::map<std::string, std::string> mergeMaps(
std::map<std::string, std::string> const& left,
std::map<std::string, std::string> const& right)
{
auto result = std::map<std::string, std::string>(left);
std::for_each(right.begin(), right.end(), [&](auto const& pair) { result.insert(pair); });
return result;
}
/**
* Create an stream from source string. Then use getline and separator to get as many tokens as
* possible and insert each one to result vector
*/
static std::vector<std::string> split(std::string source, char separator)
{
auto result = std::vector<std::string>();
auto stringAsStream = std::stringstream(source);
auto token = std::string();
while (std::getline(stringAsStream, token, separator))
{
result.push_back(token);
}
return result;
}
/**
* Will check if there are any query parameter in url looking for symbol '?'
* If it is found, it will insert query parameters to _queryParameters internal field
* and remove it from url
*/
std::string parseUrl(std::string url)
{
auto position = url.find('?');
if (position == std::string::npos)
{
return url; // no query parameters. Nothing else to do
}
// Get query parameters string and update
auto queryParameters = url.substr(position + 1);
url = url.substr(0, position);
// Split all query parameters (should be separated by &)
auto queryParametersVector = split(queryParameters, '&');
// insert each query parameter to internal field
std::for_each(queryParametersVector.begin(), queryParametersVector.end(), [&](auto query) {
auto parameter = split(query, '=');
// Will throw if parameter in query is not valid (i.e. arg:1)
_queryParameters.insert(std::pair<std::string, std::string>(parameter[0], parameter[1]));
});
return url;
}
Request(
HttpMethod httpMethod,
std::string const& url,
BodyStream* bodyStream,
BodyBuffer* bodyBuffer)
: _method(std::move(httpMethod)), _url(parseUrl(std::move(url))), _bodyStream(bodyStream),
_bodyBuffer(bodyBuffer), _retryModeEnabled(false)
{
// TODO: parse url
}
public:
Request(HttpMethod httpMethod, std::string const& url)
: Request(httpMethod, url, BodyStream::null, BodyBuffer::null)
{
}
Request(HttpMethod httpMethod, std::string const& url, BodyBuffer* bodyBuffer)
: Request(httpMethod, url, BodyStream::null, bodyBuffer)
{
}
Request(HttpMethod httpMethod, std::string const& url, BodyStream* bodyStream)
: Request(httpMethod, url, bodyStream, BodyBuffer::null)
{
}
// Methods used to build HTTP request
void addPath(std::string const& path);
void addQueryParameter(std::string const& name, std::string const& value);
void addHeader(std::string const& name, std::string const& value);
void startRetry(); // only called by retry policy
// Methods used by transport layer (and logger) to send request
HttpMethod getMethod();
std::string getEncodedUrl(); // should return encoded url
std::map<std::string, std::string> getHeaders();
BodyStream* getBodyStream();
BodyBuffer* getBodyBuffer();
};
} // namespace http
} // namespace core
} // namespace azure

View File

@ -1,65 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#pragma once
#include <algorithm>
#include <map>
#include <string>
#include <internal/contract.hpp>
namespace azure
{
namespace core
{
namespace http
{
class Response
{
private:
Response(Response const&) = delete;
void operator=(Response const&) = delete;
uint16_t _statusCode;
std::string _reasonPhrase;
std::map<std::string, std::string> _headers;
// Response can contain no body, or either of next bodies (_bodyBuffer plus size or bodyStream)
BodyBuffer& _bodyBuffer;
BodyStream& _bodyStream;
Response(
uint16_t statusCode,
std::string reasonPhrase,
BodyBuffer& const bodyBuffer,
BodyStream& const BodyStream)
: _statusCode(statusCode), _reasonPhrase(reasonPhrase), _bodyBuffer(bodyBuffer),
_bodyStream(BodyStream)
{
}
public:
Response(uint16_t statusCode, std::string reasonPhrase)
: Response(statusCode, reasonPhrase, BodyBuffer::null, BodyStream::null)
{
}
// Methods used to build HTTP response
void addHeader(std::string const& name, std::string const& value);
void setBody(BodyBuffer& const bodyBuffer);
void setBody(BodyStream& const bodyStream);
// Methods used by transport layer (and logger) to send response
uint16_t const& getStatusCode();
std::string const& getReasonPhrase();
std::map<std::string, std::string> const& getHeaders();
http::BodyStream& getBodyStream();
http::BodyBuffer& getBodyBuffer();
};
} // namespace http
} // namespace core
} // namespace azure

View File

@ -6,7 +6,6 @@
#include <vector>
#include <http/http.hpp>
#include <http/request.hpp>
using namespace azure::core::http;
@ -14,47 +13,47 @@ void Request::addPath(std::string const& path) { this->_url += "/" + path; }
void Request::addQueryParameter(std::string const& name, std::string const& value)
{
if (this->_retryModeEnabled)
if (this->m_retryModeEnabled)
{
// When retry mode is ON, any new value must override previous
this->_retryQueryParameters[name] = value;
this->m_retryQueryParameters[name] = value;
}
else
{
this->_queryParameters.insert(std::pair<std::string, std::string>(name, value));
this->m_queryParameters.insert(std::pair<std::string, std::string>(name, value));
}
}
void Request::addHeader(std::string const& name, std::string const& value)
{
if (this->_retryModeEnabled)
if (this->m_retryModeEnabled)
{
// When retry mode is ON, any new value must override previous
this->_retryHeaders[name] = value;
this->m_retryHeaders[name] = value;
}
else
{
this->_headers.insert(std::pair<std::string, std::string>(name, value));
this->m_headers.insert(std::pair<std::string, std::string>(name, value));
}
}
void Request::startRetry()
{
this->_retryModeEnabled = true;
this->_retryHeaders.clear();
this->m_retryModeEnabled = true;
this->m_retryHeaders.clear();
}
HttpMethod Request::getMethod() { return this->_method; }
std::string Request::getEncodedUrl()
{
if (this->_queryParameters.size() == 0 && this->_retryQueryParameters.size() == 0)
if (this->m_queryParameters.size() == 0 && this->m_retryQueryParameters.size() == 0)
{
return _url; // no query parameters to add
}
// remove query duplicates
auto queryParameters = Request::mergeMaps(this->_retryQueryParameters, this->_queryParameters);
auto queryParameters = Request::mergeMaps(this->m_retryQueryParameters, this->m_queryParameters);
// build url
auto queryString = std::string("");
for (auto pair : queryParameters)
@ -69,9 +68,9 @@ std::map<std::string, std::string> Request::getHeaders()
{
// create map with retry headers witch are the most important and we don't want
// to override them with any duplicate header
return Request::mergeMaps(this->_retryHeaders, this->_headers);
return Request::mergeMaps(this->m_retryHeaders, this->m_headers);
}
BodyStream* Request::getBodyStream() { return _bodyStream; }
BodyStream* Request::getBodyStream() { return m_bodyStream; }
BodyBuffer* Request::getBodyBuffer() { return _bodyBuffer; }
BodyBuffer* Request::getBodyBuffer() { return m_bodyBuffer; }

View File

@ -6,19 +6,15 @@
#include <vector>
#include <http/http.hpp>
#include <http/response.hpp>
using namespace azure::core::http;
BodyStream& BodyStream::null = *(BodyStream*)NULL;
BodyBuffer& BodyBuffer::null = *(BodyBuffer*)NULL;
uint16_t Response::getStatusCode() { return m_statusCode; }
uint16_t const& Response::getStatusCode() { return _statusCode; }
std::string const& Response::getReasonPhrase() { return m_reasonPhrase; }
std::string const& Response::getReasonPhrase() { return _reasonPhrase; }
std::map<std::string, std::string> const& Response::getHeaders() { return this->m_headers; }
std::map<std::string, std::string> const& Response::getHeaders() { return this->_headers; }
BodyStream* Response::getBodyStream() { return m_bodyStream; }
BodyStream& Response::getBodyStream() { return _bodyStream; }
BodyBuffer& Response::getBodyBuffer() { return _bodyBuffer; }
BodyBuffer* Response::getBodyBuffer() { return m_bodyBuffer; }

View File

@ -5,15 +5,17 @@ cmake_minimum_required (VERSION 3.12)
include(AddGoogleTest)
project (azure-core-test LANGUAGES CXX)
set(TARGET_NAME "azure-core-test")
project (${TARGET_NAME} LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_executable (
azure-core-test
${TARGET_NAME}
main.cpp
)
target_link_libraries(azure-core-test PRIVATE azure-core)
add_gtest(azure-core-test)
target_link_libraries(${TARGET_NAME} PRIVATE azure-core)
add_gtest(${TARGET_NAME})

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: MIT
#include <http/http.hpp>
#include <http/request.hpp>
#include <internal/credentials_internal.hpp>
#include "gtest/gtest.h"

View File

@ -0,0 +1,32 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
if (BUILD_CURL_TRANSPORT)
cmake_minimum_required (VERSION 3.12)
set(TARGET_NAME "azure-transport-curl")
project(${TARGET_NAME} LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)
find_package(CURL CONFIG)
if(NOT CURL_FOUND)
find_package(CURL REQUIRED)
endif()
add_library (
${TARGET_NAME} STATIC
src/azure_transport_curl
src/curl_client
)
target_include_directories (${TARGET_NAME} PRIVATE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc> $<INSTALL_INTERFACE:include/azure_curl>)
# make sure that users can consume the project as a library.
add_library (azure::curl ALIAS ${TARGET_NAME})
target_include_directories(${TARGET_NAME} PUBLIC ${CURL_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} PRIVATE azure-core CURL::libcurl)
endif()

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#pragma once
#include <curl/curl.h>
#include <http/http.hpp>
class CurlClient
{
private:
azure::core::http::Request& m_request;
CURL* m_p_curl;
// setHeaders()
CURLcode setUrl()
{
return curl_easy_setopt(m_p_curl, CURLOPT_URL, this->m_request.getEncodedUrl().c_str());
}
CURLcode perform()
{
auto settingUp = setUrl();
if (settingUp != CURLE_OK)
{
return settingUp;
}
return curl_easy_perform(m_p_curl);
}
public:
CurlClient(azure::core::http::Request& request) : m_request(request)
{
m_p_curl = curl_easy_init();
}
// client curl struct on destruct
~CurlClient() { curl_easy_cleanup(m_p_curl); }
azure::core::http::Response send();
};

View File

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include <curl_client.hpp>
#include <http/http.hpp>
using namespace azure::core::http;
// implement send method
Response Client::send(Request& request)
{
CurlClient client(request);
// return request response
return client.send();
}

View File

@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include <curl_client.hpp>
#include <http/http.hpp>
#include <iostream>
using namespace azure::core::http;
using namespace std;
Response CurlClient::send()
{
auto performing = perform();
if (performing != CURLE_OK)
{
switch (performing)
{
case CURLE_COULDNT_RESOLVE_HOST:
{
throw azure::core::http::CouldNotResolveHostException();
}
case CURLE_WRITE_ERROR:
{
throw azure::core::http::ErrorWhileWrittingResponse();
}
default:
{
throw azure::core::http::TransportException();
}
}
}
return Response(200, "OK\n");
}

View File

@ -0,0 +1,23 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
if (NOT BUILD_CURL_TRANSPORT)
cmake_minimum_required (VERSION 3.12)
set(TARGET_NAME "azure-transport-nothhp")
project(${TARGET_NAME} LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_library (
${TARGET_NAME} STATIC
src/azure_transport_nohttp.cpp
)
target_link_libraries(${TARGET_NAME} PRIVATE azure-core)
# make sure that users can consume the project as a library.
add_library (azure::nohttp ALIAS ${TARGET_NAME})
endif()

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include <http/http.hpp>
using namespace azure::core::http;
// implement send method
Response Client::send(Request& request)
{
(void)request;
throw;
}

View File

@ -0,0 +1,20 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
if (BUILD_CURL_TRANSPORT)
cmake_minimum_required (VERSION 3.12)
set(TARGET_NAME "azure_core_with_curl")
project(${TARGET_NAME} LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_executable (
${TARGET_NAME}
src/azure_core_with_curl
)
target_link_libraries(${TARGET_NAME} PRIVATE azure-core azure-transport-curl)
endif()

View File

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
/**
* @brief Simulates customer application that is linked with azure-core and azure-transport-curl
*
*/
#include <http/http.hpp>
#include <iostream>
using namespace azure::core;
using namespace std;
int main()
{
string host("https://httpbin.org/get");
cout << "testing curl from transport" << endl << "Host: " << host << endl;
auto request = http::Request(http::HttpMethod::GET, host);
try
{
auto response = http::Client::send(request);
cout << response.getReasonPhrase();
}
catch (http::CouldNotResolveHostException& e)
{
cout << e.what() << endl;
}
catch (http::TransportException& e)
{
cout << e.what() << endl;
}
return 0;
}