Initial commit.

Seems to work.
This commit is contained in:
tastytea 2020-11-03 22:53:20 +01:00
commit 0173093d3d
Signed by: tastytea
GPG Key ID: CFC39497F1B26E07
9 changed files with 656 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build/

18
CMakeLists.txt Normal file
View File

@ -0,0 +1,18 @@
# Support version 3.9 and above, but use policy settings up to 3.17.
cmake_minimum_required(VERSION 3.9...3.17)
# Ranges are supported from 3.12, set policy to current for < 3.12.
if(${CMAKE_VERSION} VERSION_LESS 3.12)
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif()
project(curl_wrapper
DESCRIPTION "Simple curl wrapper."
LANGUAGES CXX)
find_package(CURL 7.52 REQUIRED)
add_subdirectory(src)
if(WITH_TESTS)
add_subdirectory(tests)
endif()

22
src/CMakeLists.txt Normal file
View File

@ -0,0 +1,22 @@
file(GLOB sources *.cpp)
file(GLOB headers *.hpp)
add_library(${PROJECT_NAME} STATIC ${sources} ${headers})
unset(sources)
unset(headers)
set_target_properties(${PROJECT_NAME}
PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF)
# FindCURL provides an IMPORTED target since CMake 3.12.
if(NOT ${CMAKE_VERSION} VERSION_LESS 3.12)
target_link_libraries(${PROJECT_NAME} PUBLIC CURL::libcurl)
else()
target_include_directories(${PROJECT_NAME} PUBLIC ${CURL_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} PUBLIC ${CURL_LIBRARIES})
endif()
target_include_directories(${PROJECT_NAME}
PUBLIC "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>")

216
src/curl_wrapper.cpp Normal file
View File

@ -0,0 +1,216 @@
/* This file is part of curl_wrapper.
* Copyright © 2020 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "curl_wrapper.hpp"
#include <curl/curl.h>
#include <atomic>
#include <cstdint>
#include <exception>
#include <stdexcept>
#include <string>
namespace curl_wrapper
{
static std::atomic<std::uint64_t> curlwrapper_instances{0};
void CURLWrapper::init()
{
if (curlwrapper_instances == 0)
{
// NOLINTNEXTLINE(hicpp-signed-bitwise)
check(curl_global_init(CURL_GLOBAL_ALL));
}
++curlwrapper_instances;
_connection = curl_easy_init();
if (_connection == nullptr)
{
throw std::runtime_error{"Failed to initialize curl."};
}
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
curl_easy_setopt(_connection, CURLOPT_ERRORBUFFER, _buffer_error);
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
curl_easy_setopt(_connection, CURLOPT_WRITEFUNCTION, writer_body_wrapper);
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
curl_easy_setopt(_connection, CURLOPT_WRITEDATA, this);
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
curl_easy_setopt(_connection, CURLOPT_HEADERFUNCTION,
writer_headers_wrapper);
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
curl_easy_setopt(_connection, CURLOPT_HEADERDATA, this);
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
check(curl_easy_setopt(_connection, CURLOPT_FOLLOWLOCATION, 1L));
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
curl_easy_setopt(_connection, CURLOPT_MAXREDIRS, 5L);
}
CURLWrapper::CURLWrapper()
: _connection{nullptr}
{
init();
}
CURLWrapper::CURLWrapper(const CURLWrapper &)
: _connection{nullptr}
{
init();
}
CURLWrapper::~CURLWrapper() noexcept
{
curl_easy_cleanup(_connection);
--curlwrapper_instances;
if (curlwrapper_instances == 0)
{
curl_global_cleanup();
}
}
void CURLWrapper::set_useragent(string_view useragent)
{
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
check(curl_easy_setopt(_connection, CURLOPT_USERAGENT, useragent.data()));
}
string CURLWrapper::make_http_request(http_method method, string_view uri)
{
_buffer_headers.clear();
_buffer_body.clear();
switch (method)
{
case http_method::DELETE:
{
// NOTE: Use CURLOPT_MIMEPOST, then set to DELETE to send data.
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
check(curl_easy_setopt(_connection, CURLOPT_CUSTOMREQUEST, "DELETE"));
break;
}
case http_method::GET:
{
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
curl_easy_setopt(_connection, CURLOPT_HTTPGET, 1L);
break;
}
case http_method::PATCH:
{
// NOTE: Use CURLOPT_MIMEPOST, then set to PATCH to send data.
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
check(curl_easy_setopt(_connection, CURLOPT_CUSTOMREQUEST, "PATCH"));
break;
}
case http_method::POST:
{
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
curl_easy_setopt(_connection, CURLOPT_POST, 1L);
// NOTE: Use CURLOPT_MIMEPOST to send data.
break;
}
case http_method::PUT:
{
// NOTE: Use CURLOPT_MIMEPOST, then set to PUT to send data.
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
check(curl_easy_setopt(_connection, CURLOPT_CUSTOMREQUEST, "PUT"));
break;
}
}
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
check(curl_easy_setopt(_connection, CURLOPT_URL, uri.data()));
try
{
check(curl_easy_perform(_connection));
}
catch (const CURLException &e)
{
// PARTIAL_FILE error seems to be normal for HEAD requests.
if (!(method == http_method::HEAD
&& e.error_code == CURLE_PARTIAL_FILE))
{
std::rethrow_exception(std::current_exception());
}
}
long http_status; // NOLINT(google-runtime-int)
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
curl_easy_getinfo(_connection, CURLINFO_RESPONSE_CODE, &http_status);
// status code is in http_status, headers are in _buffer_headers, body is in
// _buffer_body.
// TODO: Communicate the above.
return _buffer_body;
}
size_t CURLWrapper::writer_body(char *data, size_t size, size_t nmemb)
{
if (data == nullptr)
{
return 0;
}
_buffer_body.append(data, size * nmemb);
return size * nmemb;
}
size_t CURLWrapper::writer_headers(char *data, size_t size, size_t nmemb)
{
if (data == nullptr)
{
return 0;
}
_buffer_headers.append(data, size * nmemb);
return size * nmemb;
}
void CURLWrapper::check(const CURLcode code)
{
if (code != CURLE_OK)
{
throw CURLException{code, _buffer_error};
}
}
const char *CURLException::what() noexcept
{
if (!_error_message.empty())
{
_error_message = ": " + _error_message;
}
_error_message = "libcurl_error: " + std::to_string(error_code)
+ _error_message;
return _error_message.c_str();
}
} // namespace curl_wrapper

256
src/curl_wrapper.hpp Normal file
View File

@ -0,0 +1,256 @@
/* This file is part of curl_wrapper.
* Copyright © 2020 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CURL_WRAPPER_HPP
#define CURL_WRAPPER_HPP
#include "curl/curl.h"
#include <exception>
#include <string>
#include <string_view>
namespace curl_wrapper
{
using std::string;
using std::string_view;
enum class http_method
{
DELETE,
GET,
HEAD,
PATCH,
POST,
PUT
};
class CURLWrapper
{
public:
/*!
* @brief Initializes curl and sets up connection.
*
* The first time an instance of CURLWrapper is created, it calls
* `curl_global_init`, which is not thread-safe. For more information
* consult [curl_global_init(3)]
* (https://curl.haxx.se/libcurl/c/curl_global_init.html).
*
* May throw CURLException or std::runtime_error.
*
* @since INSERT_VERSION
*/
CURLWrapper();
/*!
* @brief Copy constructor. Does the same as the Constructor.
*
* @since INSERT_VERSION
*/
CURLWrapper(const CURLWrapper &);
//! Move constructor
CURLWrapper(CURLWrapper &&other) noexcept = delete;
/*!
* @brief Cleans up curl and connection.
*
* May call `curl_global_cleanup`, which is not thread-safe. For more
* information consult [curl_global_cleanup(3)]
* (https://curl.haxx.se/libcurl/c/curl_global_cleanup.html).
*
* @since INSERT_VERSION
*/
virtual ~CURLWrapper() noexcept;
//! Copy assignment operator
CURLWrapper &operator=(const CURLWrapper &other) = delete;
//! Move assignment operator
CURLWrapper &operator=(CURLWrapper &&other) noexcept = delete;
/*!
* @brief Returns pointer to the CURL easy handle.
*
* You can use this handle to set or modify curl options. For more
* information consult [curl_easy_setopt(3)]
* (https://curl.haxx.se/libcurl/c/curl_easy_setopt.html).
*
* @since INSERT_VERSION
*/
inline CURL *get_curl_easy_handle()
{
return _connection;
}
/*!
* @brief URL encodes the given string.
*
* For more information consult [curl_easy_escape(3)]
* (https://curl.haxx.se/libcurl/c/curl_easy_escape.html).
*
* @param url String to escape.
*
* @return The escaped string or {} if it failed.
*
* @since INSERT_VERSION
*/
[[nodiscard]] inline string escape_url(const string_view url) const
{
char *cbuf{curl_easy_escape(_connection, url.data(),
static_cast<int>(url.size()))};
string sbuf{cbuf};
curl_free(cbuf);
return sbuf;
}
/*!
* @brief URL decodes the given string.
*
* For more information consult [curl_easy_unescape(3)]
* (https://curl.haxx.se/libcurl/c/curl_easy_unescape.html).
*
* @param url String to unescape.
*
* @return The unescaped string or {} if it failed.
*
* @since INSERT_VERSION
*/
[[nodiscard]] inline string unescape_url(const string_view url) const
{
char *cbuf{curl_easy_unescape(_connection, url.data(),
static_cast<int>(url.size()), nullptr)};
string sbuf{cbuf};
curl_free(cbuf);
return sbuf;
}
/*!
* @brief Set the User-Agent.
*
* May throw CURLException.
*
* @since INSERT_VERSION
*/
void set_useragent(string_view useragent);
/*!
* @brief Make a HTTP request.
*
* May throw CURLException.
*
* @param method The HTTP method.
* @param uri The full URI.
*
* @return The body of the page.
*
* @since INSERT_VERSION
*/
[[nodiscard]] string make_http_request(http_method method, string_view uri);
private:
CURL *_connection;
char _buffer_error[CURL_ERROR_SIZE]{};
string _buffer_headers;
string _buffer_body;
/*!
* @brief Initializes curl and sets up connection.
*
* @since INSERT_VERSION
*/
void init();
/*!
* @brief libcurl write callback function.
*
* @since INSERT_VERSION
*/
size_t writer_body(char *data, size_t size, size_t nmemb);
/*!
* @brief Wrapper for curl, because it can only call static member
* functions.
*
* <https://curl.haxx.se/docs/faq.html#Using_C_non_static_functions_f>
*
* @since INSERT_VERSION
*/
static inline size_t writer_body_wrapper(char *data, size_t sz,
size_t nmemb, void *f)
{
return static_cast<CURLWrapper *>(f)->writer_body(data, sz, nmemb);
}
//! @copydoc writer_body
size_t writer_headers(char *data, size_t size, size_t nmemb);
//! @copydoc writer_body_wrapper
static inline size_t writer_headers_wrapper(char *data, size_t sz,
size_t nmemb, void *f)
{
return static_cast<CURLWrapper *>(f)->writer_headers(data, sz, nmemb);
}
/*!
* @brief Throw runtime error if command doesn't return CURLE_OK.
*
* @since INSERT_VERSION
*/
void check(CURLcode code);
};
/*!
* @brief Exception for libcurl errors.
*
* @since INSERT_VERSION
*
* @headerfile exceptions.hpp mastodonpp/exceptions.hpp
*/
class CURLException : public std::exception
{
public:
/*!
* @brief Constructor with error code.
*
* @since INSERT_VERSION
*/
explicit CURLException(const CURLcode code)
: error_code{code}
{}
/*!
* @brief Constructor with error code and error buffer.
*
* @since INSERT_VERSION
*/
explicit CURLException(const CURLcode code, string_view error_buffer)
: error_code{code}
, _error_message{error_buffer}
{}
const CURLcode error_code;
[[nodiscard]] const char *what() noexcept;
private:
string _error_message;
};
} // namespace curl_wrapper
#endif // CURL_WRAPPER_HPP

37
tests/CMakeLists.txt Normal file
View File

@ -0,0 +1,37 @@
include(CTest)
file(GLOB sources_tests test_*.cpp)
find_package(Catch2 CONFIG)
if(Catch2_FOUND) # Catch 2.x
include(Catch)
add_executable(all_tests main.cpp ${sources_tests})
set_target_properties(all_tests
PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF)
target_link_libraries(all_tests
PRIVATE Catch2::Catch2 ${PROJECT_NAME})
target_include_directories(all_tests PRIVATE "/usr/include/catch2")
catch_discover_tests(all_tests EXTRA_ARGS "${EXTRA_TEST_ARGS}")
else() # Catch 1.x
if(EXISTS "/usr/include/catch.hpp")
message(STATUS "Catch 1.x found.")
foreach(src ${sources_tests})
get_filename_component(bin ${src} NAME_WE)
add_executable(${bin} main.cpp ${src})
set_target_properties(${bin}
PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF)
target_link_libraries(${bin}
PRIVATE ${PROJECT_NAME})
add_test(${bin} ${bin} "${EXTRA_TEST_ARGS}")
endforeach()
else()
message(FATAL_ERROR
"Neither Catch 2.x nor Catch 1.x could be found.")
endif()
endif()

3
tests/main.cpp Normal file
View File

@ -0,0 +1,3 @@
#define CATCH_CONFIG_MAIN
#include <catch.hpp>

View File

@ -0,0 +1,62 @@
#include "curl_wrapper.hpp"
#include <catch.hpp>
#include <exception>
#include <string>
namespace curl_wrapper
{
using std::string;
SCENARIO("URL encoding / decoding")
{
const string text_raw{"Hüpfburg am rande!"};
const string text_escaped("H%C3%BCpfburg%20am%20rande%21");
bool exception = false;
string answer;
WHEN("Encoding " + text_raw)
{
try
{
CURLWrapper curl;
answer = curl.escape_url(text_raw);
}
catch (const std::exception &e)
{
exception = true;
}
THEN("No exception is thrown")
AND_THEN("The text is successfully encoded")
{
REQUIRE_FALSE(exception);
REQUIRE(answer == text_escaped);
}
}
WHEN("Decoding " + text_escaped)
{
try
{
CURLWrapper curl;
answer = curl.unescape_url(text_escaped);
}
catch (const std::exception &e)
{
exception = true;
}
THEN("No exception is thrown")
AND_THEN("The text is successfully decoded")
{
REQUIRE_FALSE(exception);
REQUIRE(answer == text_raw);
}
}
}
} // namespace curl_wrapper

41
tests/test_get.cpp Normal file
View File

@ -0,0 +1,41 @@
#include "curl_wrapper.hpp"
#include <catch.hpp>
#include <exception>
#include <string>
namespace curl_wrapper
{
using std::string;
SCENARIO("HTTP GET", "[http]")
{
const string uri{"https://schlomp.space/api/v1/version"};
bool exception = false;
string answer;
WHEN("GETing " + uri)
{
try
{
CURLWrapper curl;
answer = curl.make_http_request(http_method::GET, uri);
}
catch (const std::exception &e)
{
exception = true;
}
THEN("No exception is thrown")
AND_THEN("We get the right answer")
{
REQUIRE_FALSE(exception);
REQUIRE(answer.substr(0, 11) == R"({"version":)");
}
}
}
} // namespace curl_wrapper