├── .gitmodules ├── src ├── target.cmake ├── pch.cpp ├── logging.cpp ├── NoBodyReaderImpl.cpp ├── boost_compitability.cpp ├── ConnectionImpl.h ├── RequestBodyStringImpl.cpp ├── PlainReaderImpl.cpp ├── PlainWriterImpl.cpp ├── url_encode.cpp ├── IoWriterImpl.cpp ├── RequestBodyFileImpl.cpp ├── ReplyImpl.h ├── ChunkedWriterImpl.cpp ├── Url.cpp ├── IoReaderImpl.cpp ├── SocketImpl.h ├── TlsSocketImpl.h ├── ZipReaderImpl.cpp ├── ChunkedReaderImpl.cpp └── DataReaderStream.cpp ├── .gitattributes ├── tests ├── mock-nginx │ ├── htpasswd.bin │ ├── README.md │ ├── Dockerfile │ ├── build_and_run.sh │ └── proxy.conf.bin ├── run-unit-tests.sh ├── mock-squid │ ├── squid.conf.bin │ ├── build_and_run.sh │ └── Dockerfile ├── mock-json │ ├── build_and_run.sh │ ├── Dockerfile │ └── run.sh.bin ├── CMakeLists.txt ├── run-tests.sh ├── unit │ ├── RequestBuilder.cpp │ ├── AsyncSleepTests.cpp │ ├── CMakeLists.txt │ └── Iostream2JsonTests.cpp └── functional │ ├── CookieTests.cpp │ ├── PropertiesTests.cpp │ ├── AuthTest.cpp │ ├── UploadTests.cpp │ ├── ConnectionPoolInstancesTest.cpp │ ├── HttpsTest.cpp │ ├── RedirectTests.cpp │ ├── ProxyTests.cpp │ ├── InsertSerializerTest.cpp │ ├── CRUD_test.cpp │ ├── OwnIoserviceTests.cpp │ ├── ManyConnectionsTest.cpp │ ├── CMakeLists.txt │ └── ConnectionCacheTests.cpp ├── ci ├── mock-backends │ ├── nginx │ │ ├── htpasswd.bin │ │ ├── Dockerfile │ │ └── proxy.conf.bin │ ├── squid │ │ ├── squid.conf.bin │ │ └── Dockerfile │ ├── json │ │ ├── Dockerfile │ │ └── run.sh.bin │ └── docker-compose.yml ├── vcpkg │ └── vcpkg.json └── jenkins │ ├── Dockerfile.fedora │ ├── Dockefile.debian-bookworm │ ├── Dockefile.debian-bullseye │ ├── Dockefile.debian-testing │ ├── Dockerfile.debian-trixie │ ├── Dockefile.ubuntu-xenial │ ├── Dockerfile.ubuntu-noble │ ├── Dockerfile.ubuntu-plucky │ ├── Dockefile.ubuntu-jammy │ └── Dockerfile.centos7 ├── conan ├── conanfile.txt ├── build_gcc.sh ├── build_with_conan_vc2015.bat └── Readme.md ├── examples ├── logip │ ├── CMakeLists.txt │ └── logip.cpp ├── cmdline │ ├── build.sh │ ├── readme.md │ └── main.cpp ├── cmake_external_project │ ├── readme.md │ ├── CMakeLists.txt │ ├── cmake │ │ └── external-projects.cmake │ └── main.cpp └── cmake_normal │ ├── CMakeLists.txt │ ├── readme.md │ └── main.cpp ├── cmake_scripts ├── restc-cppConfig.cmake.in ├── doxygen.cmake ├── pch.cmake ├── install.cmake └── external-projects.cmake ├── doc ├── doxygen-mainpage.md ├── GettingStarted.md ├── Logging.md └── RunningTheTests.md ├── include └── restc-cpp │ ├── internals │ ├── helpers.h │ └── for_each_member.hpp │ ├── typename.h │ ├── url_encode.h │ ├── RequestBodyWriter.h │ ├── Connection.h │ ├── ConnectionPool.h │ ├── RapidJsonWriter.h │ ├── Url.h │ ├── test_helper.h │ ├── RequestBody.h │ ├── DataReader.h │ ├── helper.h │ ├── DataReaderStream.h │ ├── DataWriter.h │ ├── RapidJsonReader.h │ ├── Socket.h │ ├── error.h │ ├── IoTimer.h │ └── IteratorFromJsonSerializer.h ├── .gitignore ├── stop-containers.sh ├── config.h.template ├── create-and-run-containers.sh ├── LICENSE └── .github └── workflows └── ci.yaml /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/target.cmake: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bin binary 2 | -------------------------------------------------------------------------------- /tests/mock-nginx/htpasswd.bin: -------------------------------------------------------------------------------- 1 | alice:B4bt3LKEO3MsQ 2 | -------------------------------------------------------------------------------- /ci/mock-backends/nginx/htpasswd.bin: -------------------------------------------------------------------------------- 1 | alice:B4bt3LKEO3MsQ 2 | -------------------------------------------------------------------------------- /tests/run-unit-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./tests/run-tests.sh build/tests/unit 3 | -------------------------------------------------------------------------------- /tests/mock-squid/squid.conf.bin: -------------------------------------------------------------------------------- 1 | 2 | http_access allow localhost manager 3 | http_access deny manager 4 | http_access allow all 5 | http_port 3128 6 | -------------------------------------------------------------------------------- /ci/mock-backends/squid/squid.conf.bin: -------------------------------------------------------------------------------- 1 | 2 | http_access allow localhost manager 3 | http_access deny manager 4 | http_access allow all 5 | http_port 3128 6 | -------------------------------------------------------------------------------- /conan/conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | rapidjson/1.1.0 3 | boost/1.80.0 4 | openssl/1.1.1d 5 | zlib/1.2.12 6 | 7 | [generators] 8 | cmake 9 | 10 | [options] 11 | -------------------------------------------------------------------------------- /src/pch.cpp: -------------------------------------------------------------------------------- 1 | #include "restc-cpp/restc-cpp.h" 2 | #include "restc-cpp/IteratorFromJsonSerializer.h" 3 | #include "restc-cpp/RequestBuilder.h" 4 | 5 | namespace { 6 | void foo() {} 7 | } -------------------------------------------------------------------------------- /tests/mock-json/build_and_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker stop restc-cpp-json 4 | docker rm restc-cpp-json 5 | docker build . -t jgaa/restc-cpp-mock-json 6 | docker run --name restc-cpp-json -d -p 3000:80 jgaa/restc-cpp-mock-json 7 | -------------------------------------------------------------------------------- /tests/mock-nginx/README.md: -------------------------------------------------------------------------------- 1 | 2 | Docker container scripts for a nginx instance used by the tests 3 | in the project. 4 | 5 | Start the jsonserver docker instance (see ../mock-server), and then: 6 | ```sh 7 | $ ./build_and_run.sh 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/logip/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(examples) 2 | 3 | 4 | # ====================================== 5 | 6 | add_executable(logip logip.cpp) 7 | target_link_libraries(logip 8 | restc-cpp 9 | ${DEFAULT_LIBRARIES} 10 | ) 11 | SET_CPP_STANDARD(logip) 12 | -------------------------------------------------------------------------------- /tests/mock-nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | RUN rm /etc/nginx/conf.d/default.conf 3 | COPY proxy.conf.bin /etc/nginx/conf.d/proxy.conf 4 | COPY htpasswd.bin /etc/nginx/htpasswd 5 | RUN mkdir -p /etc/nginx/html/upload 6 | RUN chmod 777 /etc/nginx/html/upload 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/mock-nginx/build_and_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker stop restc-cpp-nginx 4 | docker rm restc-cpp-nginx 5 | docker build . -t jgaa/restc-cpp-mock-nginx 6 | docker run --name restc-cpp-nginx --link restc-cpp-json:api -d -p 3001:80 -t jgaa/restc-cpp-mock-nginx 7 | -------------------------------------------------------------------------------- /ci/mock-backends/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | RUN rm /etc/nginx/conf.d/default.conf 3 | COPY proxy.conf.bin /etc/nginx/conf.d/proxy.conf 4 | COPY htpasswd.bin /etc/nginx/htpasswd 5 | RUN mkdir -p /etc/nginx/html/upload 6 | RUN chmod 777 /etc/nginx/html/upload 7 | 8 | 9 | -------------------------------------------------------------------------------- /cmake_scripts/restc-cppConfig.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | include(${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@.cmake) 4 | 5 | check_required_components(@PROJECT_NAME@) 6 | 7 | set(RESTC_CPP_LIBRARIES restc-cpp) 8 | 9 | message(STATUS "@PROJECT_NAME@ found.") 10 | -------------------------------------------------------------------------------- /doc/doxygen-mainpage.md: -------------------------------------------------------------------------------- 1 | @mainpage 2 | 3 | # Taking the pain out of REST in C++ 4 | 5 | - @ref README.md 6 | - @ref doc/GettingStarted.md 7 | - @ref doc/Tutorial.md 8 | - @ref doc/RunningTheTests.md 9 | 10 | Home page: https://github.com/jgaa/restc-cpp 11 | 12 | -------------------------------------------------------------------------------- /tests/mock-squid/build_and_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker stop restc-cpp-squid 4 | docker rm restc-cpp-squid 5 | docker build . -t jgaa/restc-cpp-mock-squid 6 | docker run --name restc-cpp-squid --link restc-cpp-nginx:api.example.com -d -p 3003:3128 -t jgaa/restc-cpp-mock-squid 7 | 8 | 9 | -------------------------------------------------------------------------------- /include/restc-cpp/internals/helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifdef RESTC_CPP_THREADED_CTX 6 | # define LOCK_ std::lock_guard lock_{mutex_} 7 | #else 8 | # define LOCK_ 9 | #endif 10 | 11 | #define LOCK_ALWAYS_ std::lock_guard lock_{mutex_} 12 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(tests) 2 | 3 | if (NOT WIN32) 4 | add_definitions(-Wno-missing-braces) 5 | endif() 6 | 7 | if (RESTC_CPP_WITH_UNIT_TESTS) 8 | add_subdirectory(unit) 9 | endif() 10 | 11 | if (RESTC_CPP_WITH_FUNCTIONALT_TESTS) 12 | add_subdirectory(functional) 13 | endif() 14 | -------------------------------------------------------------------------------- /conan/build_gcc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | git clone https://github.com/jgaa/restc-cpp.git 4 | cd restc-cpp 5 | rm -rf build 6 | mkdir build 7 | cd build 8 | 9 | conan install ../conan/conanfile.txt -s build_type=Release --build 10 | cmake -G "Unix Makefiles" .. 11 | cmake --build . --config Release 12 | 13 | 14 | cd .. 15 | -------------------------------------------------------------------------------- /src/logging.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "restc-cpp/logging.h" 4 | 5 | using namespace std; 6 | 7 | namespace restc_cpp { 8 | 9 | Logger &Logger::Instance() noexcept 10 | { 11 | static Logger logger; 12 | return logger; 13 | } 14 | 15 | LogEvent::~LogEvent() 16 | { 17 | Logger::Instance().onEvent(level_, msg_.str()); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /examples/cmdline/build.sh: -------------------------------------------------------------------------------- 1 | g++ -DBOOST_COROUTINE_NO_DEPRECATION_WARNING main.cpp \ 2 | -lrestc-cpp -lz -lssl -lcrypto -lpthread \ 3 | -lboost_system -lboost_program_options \ 4 | -lboost_filesystem -lboost_date_time \ 5 | -lboost_context -lboost_coroutine \ 6 | -lboost_chrono -lboost_log -lboost_thread \ 7 | -lboost_log_setup -lboost_regex \ 8 | -lboost_atomic -lpthread 9 | -------------------------------------------------------------------------------- /include/restc-cpp/typename.h: -------------------------------------------------------------------------------- 1 | 2 | #include "restc-cpp/config.h" 3 | 4 | 5 | #ifdef RESTC_CPP_HAVE_BOOST_TYPEINDEX 6 | #include 7 | 8 | #define RESTC_CPP_TYPENAME(type) \ 9 | boost::typeindex::type_id().pretty_name() 10 | #else 11 | #define RESTC_CPP_TYPENAME(type) \ 12 | typeid(type).name() 13 | #endif 14 | 15 | -------------------------------------------------------------------------------- /include/restc-cpp/url_encode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef RESTC_CPP_URL_ENCODE_H_ 4 | #define RESTC_CPP_URL_ENCODE_H_ 5 | 6 | #include "restc-cpp.h" 7 | 8 | #include 9 | 10 | namespace restc_cpp { 11 | 12 | std::string url_encode(const boost::string_ref& src); 13 | 14 | } // namespace 15 | 16 | #endif // RESTC_CPP_URL_ENCODE_H_ 17 | -------------------------------------------------------------------------------- /tests/mock-json/Dockerfile: -------------------------------------------------------------------------------- 1 | # Original from https://github.com/clue/docker-json-server 2 | 3 | FROM node:latest 4 | MAINTAINER Jarle Aase 5 | 6 | RUN npm install -g json-server 7 | 8 | WORKDIR /data 9 | VOLUME /data 10 | 11 | COPY db.json.bin /data/db.json 12 | 13 | EXPOSE 80 14 | ADD run.sh.bin /run.sh 15 | ENTRYPOINT ["bash", "/run.sh"] 16 | CMD [] 17 | -------------------------------------------------------------------------------- /ci/mock-backends/json/Dockerfile: -------------------------------------------------------------------------------- 1 | # Original from https://github.com/clue/docker-json-server 2 | 3 | FROM node:latest 4 | MAINTAINER Jarle Aase 5 | 6 | RUN npm install -g json-server 7 | 8 | WORKDIR /data 9 | VOLUME /data 10 | 11 | COPY db.json.bin /data/db.json 12 | 13 | EXPOSE 80 14 | ADD run.sh.bin /run.sh 15 | ENTRYPOINT ["bash", "/run.sh"] 16 | CMD [] 17 | -------------------------------------------------------------------------------- /tests/mock-squid/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | MAINTAINER jarle@jgaa.com 3 | 4 | RUN apt-get update \ 5 | && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends upgrade \ 6 | && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install squid3 7 | COPY squid.conf.bin /etc/squid3/squid.conf 8 | 9 | EXPOSE 3128/tcp 10 | 11 | RUN cat /etc/hosts 12 | 13 | CMD squid3 -Nd 1 -f /etc/squid3/squid.conf 14 | -------------------------------------------------------------------------------- /tests/mock-json/run.sh.bin: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | args="$@" 4 | 5 | args="$@ -p 80" 6 | 7 | file=/data/db.json 8 | if [ -f $file ]; then 9 | echo "Found db.json, trying to open" 10 | args="$args db.json" 11 | fi 12 | 13 | file=/data/file.js 14 | if [ -f $file ]; then 15 | echo "Found file.js seed file, trying to open" 16 | args="$args file.js" 17 | fi 18 | 19 | echo "Args are: $args" 20 | 21 | json-server $args 22 | -------------------------------------------------------------------------------- /ci/mock-backends/squid/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:trixie 2 | MAINTAINER jarle@jgaa.com 3 | 4 | RUN apt-get update \ 5 | && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends upgrade \ 6 | && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install squid 7 | COPY squid.conf.bin /etc/squid/squid.conf 8 | 9 | EXPOSE 3128/tcp 10 | 11 | RUN cat /etc/hosts 12 | 13 | CMD squid -Nd 1 -f /etc/squid/squid.conf 14 | -------------------------------------------------------------------------------- /ci/mock-backends/json/run.sh.bin: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | args="$@" 4 | 5 | args="$@ -p 80 --host 0.0.0.0" 6 | 7 | file=/data/db.json 8 | if [ -f $file ]; then 9 | echo "Found db.json, trying to open" 10 | args="$args db.json" 11 | fi 12 | 13 | file=/data/file.js 14 | if [ -f $file ]; then 15 | echo "Found file.js seed file, trying to open" 16 | args="$args file.js" 17 | fi 18 | 19 | echo "Args are: $args" 20 | 21 | json-server $args 22 | -------------------------------------------------------------------------------- /ci/vcpkg/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restc-cpp", 3 | "license": "MIT", 4 | "dependencies": [ 5 | "boost-scope-exit", 6 | "boost-system", 7 | "boost-context", 8 | "boost-coroutine", 9 | "boost-filesystem", 10 | "boost-asio", 11 | "boost-chrono", 12 | "boost-date-time", 13 | "boost-log", 14 | "boost-uuid", 15 | "boost-program-options", 16 | "boost-functional", 17 | "zlib", 18 | "openssl", 19 | "gtest", 20 | "rapidjson" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /examples/cmake_external_project/readme.md: -------------------------------------------------------------------------------- 1 | # example with cmake and restc-cpp as an external project 2 | 3 | This example shows how to compile a program using restc-cpp from cmake, delegating the compilation of restc-cpp to cmake. 4 | 5 | This is probably the simplest way to use the library, but the external project dependencies does add some extra time each time you run make. 6 | 7 | Currently this example is only tested under Linux. 8 | 9 | ```sh 10 | ~$ rm -rf build/ 11 | ~$ mkdir build 12 | ~$ cd build/ 13 | ~$ cmake .. 14 | ~$ make 15 | ``` 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .kdev4 2 | build 3 | ?build 4 | /cmake-build-*/ 5 | generated-doc 6 | */*.rej 7 | include/restc-cpp/config.h 8 | lib 9 | *.kdev4 10 | /.idea/ 11 | /.vs/ 12 | 13 | 14 | 15 | # Compiled Object files 16 | *.slo 17 | *.lo 18 | *.o 19 | *.obj 20 | 21 | # Precompiled Headers 22 | *.gch 23 | *.pch 24 | 25 | # Compiled Dynamic libraries 26 | *.so 27 | *.dylib 28 | *.dll 29 | 30 | # Fortran module files 31 | *.mod 32 | 33 | # Compiled Static libraries 34 | *.lai 35 | *.la 36 | *.a 37 | *.lib 38 | 39 | # Executables 40 | *.exe 41 | *.out 42 | *.app 43 | 44 | -------------------------------------------------------------------------------- /conan/build_with_conan_vc2015.bat: -------------------------------------------------------------------------------- 1 | rem Please run from the root dir 2 | rem : .\conan\build_with_conan_vc2015.bat 3 | 4 | rmdir /S /Q build 5 | mkdir build 6 | cd build 7 | 8 | conan install -f conan/conanfile.txt .. -s compiler="Visual Studio" -s compiler.version="14" -s build_type=Debug --build 9 | cmake -G "Visual Studio 14 Win64" .. 10 | cmake --build . --config Debug 11 | 12 | rem conan install -f conan/conanfile.txt .. -s compiler="Visual Studio" -s compiler.version="14" -s build_type=Release --build 13 | rem cmake -G "Visual Studio 14 Win64" .. 14 | rem cmake --build . --config Release 15 | 16 | cd .. 17 | -------------------------------------------------------------------------------- /examples/cmdline/readme.md: -------------------------------------------------------------------------------- 1 | # example cmdline 2 | 3 | This example shows how to compile a program using restc-cpp from the command-line. 4 | 5 | We assume that the library has been built and installed. 6 | 7 | Currently this example is only tested under Linux. 8 | 9 | ```sh 10 | ~/src/restc-cpp/examples/cmdline$ ./build.sh 11 | ~/src/restc-cpp/examples/cmdline$ ./a.out 12 | Received post# 1, title: sunt aut facere repellat provident occaecati excepturi optio reprehenderit 13 | ~/src/restc-cpp/examples/cmdline$ 14 | ``` 15 | 16 | Please examine [build.sh](build.sh) to see the full list of libraries to link with. 17 | 18 | -------------------------------------------------------------------------------- /ci/mock-backends/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | json: 3 | build: json 4 | ports: 5 | - "3000:80" 6 | nginx: 7 | build: nginx 8 | ports: 9 | - "3001:80" 10 | links: 11 | - "json:api" 12 | squid: 13 | build: squid 14 | ports: 15 | - "3003:3128" 16 | links: 17 | - "nginx:api.example.com" 18 | 19 | socks: 20 | image: jgaafromnorth/shinysocks 21 | environment: 22 | - LOG_LEVEL=trace 23 | ports: 24 | - "3004:1080" 25 | links: 26 | - "nginx:api.example.com" 27 | -------------------------------------------------------------------------------- /src/NoBodyReaderImpl.cpp: -------------------------------------------------------------------------------- 1 | #include "restc-cpp/restc-cpp.h" 2 | #include "restc-cpp/DataReader.h" 3 | 4 | using namespace std; 5 | 6 | namespace restc_cpp { 7 | 8 | 9 | class NoBodyReaderImpl : public DataReader { 10 | public: 11 | NoBodyReaderImpl() = default; 12 | 13 | [[nodiscard]] bool IsEof() const override { return true; } 14 | 15 | void Finish() override { 16 | } 17 | 18 | ::restc_cpp::boost_const_buffer ReadSome() override { 19 | return {nullptr, 0}; 20 | } 21 | }; 22 | 23 | DataReader::ptr_t 24 | DataReader::CreateNoBodyReader() { 25 | return make_unique(); 26 | } 27 | 28 | 29 | } // namespace 30 | 31 | -------------------------------------------------------------------------------- /cmake_scripts/doxygen.cmake: -------------------------------------------------------------------------------- 1 | if (WITH_APIDOC) 2 | find_package(Doxygen) 3 | if (DOXYGEN_FOUND) 4 | 5 | set(DOXYGEN_INPUT Doxyfile) 6 | set(DOXYGEN_OUTPUT doc/html) 7 | 8 | add_custom_command( 9 | OUTPUT ${DOXYGEN_OUTPUT} 10 | COMMAND ${CMAKE_COMMAND} -E echo_append "Building API Documentation..." 11 | COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_INPUT} 12 | COMMAND ${CMAKE_COMMAND} -E echo "Done." 13 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 14 | DEPENDS ${DOXYGEN_INPUT} 15 | ) 16 | 17 | add_custom_target(apidoc ALL DEPENDS ${DOXYGEN_OUTPUT}) 18 | 19 | endif (DOXYGEN_FOUND) 20 | endif() 21 | -------------------------------------------------------------------------------- /ci/jenkins/Dockerfile.fedora: -------------------------------------------------------------------------------- 1 | FROM fedora:latest 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | RUN dnf -q update -y &&\ 6 | dnf -q upgrade -y &&\ 7 | dnf -q install -y openssh-server gcc-c++ git gnupg2 \ 8 | automake autoconf make \ 9 | zlib-devel gcc-c++ cmake boost-devel openssl-devel \ 10 | java-devel &&\ 11 | dnf -q autoremove -y &&\ 12 | dnf clean all &&\ 13 | ssh-keygen -A 14 | 15 | # Set user jenkins to the image 16 | RUN useradd -m -d /home/jenkins -s /bin/bash jenkins &&\ 17 | chmod 0777 /home/jenkins &&\ 18 | echo "jenkins:jenkins" | chpasswd &&\ 19 | mkdir -p /run/sshd 20 | 21 | # Standard SSH port 22 | EXPOSE 22 23 | 24 | # Default command 25 | CMD ["/usr/sbin/sshd", "-D"] 26 | -------------------------------------------------------------------------------- /tests/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TEST_DIR=${1:-dbuild/tests} 4 | 5 | case "$OSTYPE" in 6 | darwin*) 7 | echo "OSX" 8 | TESTS=`find $TEST_DIR -perm +0111 -type f` 9 | ;; 10 | linux*) 11 | echo "LINUX" 12 | TESTS=`find $TEST_DIR -type f -executable -print` 13 | ;; 14 | bsd*) 15 | echo "BSD" 16 | TESTS=`find $TEST_DIR -type f -executable -print` 17 | ;; 18 | msys*) 19 | echo "WINDOWS" 20 | TESTS=`find $TEST_DIR -name "*.exe" -print` 21 | ;; 22 | *) echo "unknown: $OSTYPE" ;; 23 | esac 24 | 25 | for f in $TESTS; 26 | do 27 | echo "testing $f" 28 | $f > /dev/null 29 | if [ $? -eq 0 ]; then 30 | echo "OK" 31 | else 32 | echo "$? tests FAILED" 33 | fi 34 | done 35 | -------------------------------------------------------------------------------- /include/restc-cpp/RequestBodyWriter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef RESTC_CPP_BODY_WRITER_H_ 4 | #define RESTC_CPP_BODY_WRITER_H_ 5 | 6 | #include "restc-cpp/DataWriter.h" 7 | #include "restc-cpp/RequestBody.h" 8 | #include "restc-cpp/error.h" 9 | 10 | namespace restc_cpp { 11 | 12 | /*! The body of the request. */ 13 | template 14 | class RequestBodyWriter : public RequestBody { 15 | public: 16 | RequestBodyWriter(fnT fn) 17 | : fn_{std::move(fn)} 18 | {} 19 | 20 | Type GetType() const noexcept override { 21 | return Type::CHUNKED_LAZY_PUSH; 22 | }; 23 | 24 | void PushData(DataWriter& writer) override { 25 | fn_(writer); 26 | } 27 | 28 | private: 29 | fnT fn_; 30 | }; 31 | 32 | } // restc_cpp 33 | 34 | #endif // RESTC_CPP_BODY_WRITER_H_ 35 | -------------------------------------------------------------------------------- /ci/jenkins/Dockefile.debian-bookworm: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | # In case you need proxy 6 | RUN DEBIAN_FRONTEND="noninteractive" apt-get -q update &&\ 7 | DEBIAN_FRONTEND="noninteractive" apt-get -y -q --no-install-recommends upgrade &&\ 8 | DEBIAN_FRONTEND="noninteractive" apt-get install -y -q\ 9 | openssh-server g++ git \ 10 | build-essential \ 11 | zlib1g-dev g++ cmake make libboost-all-dev libssl-dev \ 12 | default-jdk &&\ 13 | apt-get -y -q autoremove &&\ 14 | apt-get -y -q clean 15 | 16 | # Set user jenkins to the image 17 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 18 | echo "jenkins:jenkins" | chpasswd 19 | 20 | # Standard SSH port 21 | EXPOSE 22 22 | 23 | # Default command 24 | CMD ["/usr/sbin/sshd", "-D"] 25 | -------------------------------------------------------------------------------- /ci/jenkins/Dockefile.debian-bullseye: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | # In case you need proxy 6 | RUN DEBIAN_FRONTEND="noninteractive" apt-get -q update &&\ 7 | DEBIAN_FRONTEND="noninteractive" apt-get -y -q --no-install-recommends upgrade &&\ 8 | DEBIAN_FRONTEND="noninteractive" apt-get install -y -q\ 9 | openssh-server g++ git \ 10 | build-essential \ 11 | zlib1g-dev g++ cmake make libboost-all-dev libssl-dev \ 12 | default-jdk &&\ 13 | apt-get -y -q autoremove &&\ 14 | apt-get -y -q clean 15 | 16 | # Set user jenkins to the image 17 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 18 | echo "jenkins:jenkins" | chpasswd 19 | 20 | # Standard SSH port 21 | EXPOSE 22 22 | 23 | # Default command 24 | CMD ["/usr/sbin/sshd", "-D"] 25 | -------------------------------------------------------------------------------- /ci/jenkins/Dockefile.debian-testing: -------------------------------------------------------------------------------- 1 | FROM debian:testing 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | # In case you need proxy 6 | RUN DEBIAN_FRONTEND="noninteractive" apt-get -q update &&\ 7 | DEBIAN_FRONTEND="noninteractive" apt-get -y -q --no-install-recommends upgrade &&\ 8 | DEBIAN_FRONTEND="noninteractive" apt-get install -y -q\ 9 | openssh-server g++ git \ 10 | build-essential \ 11 | zlib1g-dev g++ cmake make libboost-all-dev libssl-dev \ 12 | default-jdk &&\ 13 | apt-get -y -q autoremove &&\ 14 | apt-get -y -q clean 15 | 16 | # Set user jenkins to the image 17 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 18 | echo "jenkins:jenkins" | chpasswd 19 | 20 | # Standard SSH port 21 | EXPOSE 22 22 | 23 | # Default command 24 | CMD ["/usr/sbin/sshd", "-D"] 25 | -------------------------------------------------------------------------------- /ci/jenkins/Dockerfile.debian-trixie: -------------------------------------------------------------------------------- 1 | FROM debian:trixie 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | # In case you need proxy 6 | RUN DEBIAN_FRONTEND="noninteractive" apt-get -q update &&\ 7 | DEBIAN_FRONTEND="noninteractive" apt-get -y -q --no-install-recommends upgrade &&\ 8 | DEBIAN_FRONTEND="noninteractive" apt-get install -y -q\ 9 | openssh-server g++ git \ 10 | build-essential \ 11 | zlib1g-dev g++ cmake make libboost-all-dev libssl-dev \ 12 | default-jdk &&\ 13 | apt-get -y -q autoremove &&\ 14 | apt-get -y -q clean 15 | 16 | # Set user jenkins to the image 17 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 18 | echo "jenkins:jenkins" | chpasswd 19 | 20 | # Standard SSH port 21 | EXPOSE 22 22 | 23 | # Default command 24 | CMD ["/usr/sbin/sshd", "-D"] 25 | -------------------------------------------------------------------------------- /ci/jenkins/Dockefile.ubuntu-xenial: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | # In case you need proxy 6 | #RUN echo 'Acquire::http::Proxy "http://127.0.0.1:8080";' >> /etc/apt/apt.conf 7 | 8 | RUN apt-get -q update &&\ 9 | apt-get -y -q --no-install-recommends upgrade &&\ 10 | apt-get -y -q install openssh-server g++ git \ 11 | automake autoconf build-essential \ 12 | zlib1g-dev g++ cmake make libboost-all-dev libssl-dev libgtest-dev \ 13 | openjdk-8-jdk &&\ 14 | apt-get -y -q autoremove &&\ 15 | apt-get -y -q clean 16 | 17 | # Set user jenkins to the image 18 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 19 | echo "jenkins:jenkins" | chpasswd 20 | 21 | # Standard SSH port 22 | EXPOSE 22 23 | 24 | # Default command 25 | CMD ["/usr/sbin/sshd", "-D"] 26 | -------------------------------------------------------------------------------- /ci/jenkins/Dockerfile.ubuntu-noble: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | RUN DEBIAN_FRONTEND="noninteractive" apt-get -q update &&\ 6 | DEBIAN_FRONTEND="noninteractive" apt-get -y -q --no-install-recommends upgrade &&\ 7 | DEBIAN_FRONTEND="noninteractive" apt-get install -y -q openssh-server g++ git gpgv \ 8 | automake autoconf build-essential rapidjson-dev \ 9 | zlib1g-dev g++ cmake make libboost-all-dev libssl-dev \ 10 | default-jdk &&\ 11 | apt-get -y -q autoremove &&\ 12 | apt-get -y -q clean 13 | 14 | # Set user jenkins to the image 15 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 16 | echo "jenkins:jenkins" | chpasswd &&\ 17 | mkdir -p /run/sshd 18 | 19 | # Standard SSH port 20 | EXPOSE 22 21 | 22 | # Default command 23 | CMD ["/usr/sbin/sshd", "-D"] 24 | -------------------------------------------------------------------------------- /ci/jenkins/Dockerfile.ubuntu-plucky: -------------------------------------------------------------------------------- 1 | FROM ubuntu:25.04 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | RUN DEBIAN_FRONTEND="noninteractive" apt-get -q update &&\ 6 | DEBIAN_FRONTEND="noninteractive" apt-get -y -q --no-install-recommends upgrade &&\ 7 | DEBIAN_FRONTEND="noninteractive" apt-get install -y -q openssh-server g++ git gpgv \ 8 | automake autoconf build-essential rapidjson-dev \ 9 | zlib1g-dev g++ cmake make libboost-all-dev libssl-dev \ 10 | default-jdk &&\ 11 | apt-get -y -q autoremove &&\ 12 | apt-get -y -q clean 13 | 14 | # Set user jenkins to the image 15 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 16 | echo "jenkins:jenkins" | chpasswd &&\ 17 | mkdir -p /run/sshd 18 | 19 | # Standard SSH port 20 | EXPOSE 22 21 | 22 | # Default command 23 | CMD ["/usr/sbin/sshd", "-D"] 24 | -------------------------------------------------------------------------------- /examples/cmake_external_project/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | project (example) 3 | 4 | find_package(ZLIB REQUIRED) 5 | find_package(OpenSSL REQUIRED) 6 | find_package(Boost REQUIRED COMPONENTS 7 | system 8 | program_options 9 | filesystem 10 | date_time 11 | context 12 | coroutine 13 | chrono 14 | log 15 | ) 16 | 17 | include(cmake/external-projects.cmake) 18 | 19 | add_executable(${PROJECT_NAME} main.cpp) 20 | add_dependencies(${PROJECT_NAME} externalRestcCpp) 21 | set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 14) 22 | target_link_libraries(${PROJECT_NAME} 23 | restc-cpp 24 | ${Boost_LIBRARIES} 25 | ${ZLIB_LIBRARIES} 26 | ${OPENSSL_LIBRARIES} 27 | ) 28 | 29 | add_definitions( 30 | -DBOOST_COROUTINE_NO_DEPRECATION_WARNING=1 31 | -DBOOST_ALL_DYN_LINK=1 32 | ) 33 | -------------------------------------------------------------------------------- /ci/jenkins/Dockefile.ubuntu-jammy: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | RUN DEBIAN_FRONTEND="noninteractive" apt-get -q update &&\ 6 | DEBIAN_FRONTEND="noninteractive" apt-get -y -q --no-install-recommends upgrade &&\ 7 | DEBIAN_FRONTEND="noninteractive" apt-get install -y -q openssh-server g++ git gpgv \ 8 | automake autoconf build-essential \ 9 | zlib1g-dev g++ cmake make libboost-all-dev libssl-dev \ 10 | default-jdk &&\ 11 | apt-get -y -q autoremove &&\ 12 | apt-get -y -q clean 13 | 14 | # Set user jenkins to the image 15 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 16 | echo "jenkins:jenkins" | chpasswd &&\ 17 | mkdir -p /home/jenkins/build/workspace/restc-staging &&\ 18 | mkdir -p /run/sshd 19 | 20 | # Standard SSH port 21 | EXPOSE 22 22 | 23 | # Default command 24 | CMD ["/usr/sbin/sshd", "-D"] 25 | -------------------------------------------------------------------------------- /include/restc-cpp/Connection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef RESTC_CPP_CONNECTION_H_ 3 | #define RESTC_CPP_CONNECTION_H_ 4 | 5 | #ifndef RESTC_CPP_H_ 6 | # error "Include restc-cpp.h first" 7 | #endif 8 | 9 | namespace restc_cpp { 10 | 11 | class Socket; 12 | 13 | class Connection { 14 | public: 15 | using ptr_t = std::shared_ptr; 16 | 17 | enum class Type { 18 | HTTP, 19 | HTTPS 20 | }; 21 | 22 | virtual ~Connection() = default; 23 | 24 | virtual boost::uuids::uuid GetId() const = 0; 25 | virtual Socket& GetSocket() = 0; 26 | virtual const Socket& GetSocket() const = 0; 27 | 28 | friend std::ostream& operator << (std::ostream& o, const Connection& v) { 29 | return v.Print(o); 30 | } 31 | 32 | private: 33 | std::ostream& Print(std::ostream& o) const; 34 | }; 35 | 36 | } // restc_cpp 37 | 38 | 39 | #endif // RESTC_CPP_CONNECTION_H_ 40 | -------------------------------------------------------------------------------- /stop-containers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if Docker is running 4 | docker ps > /dev/null 5 | if [ $? -ne 0 ]; then 6 | echo 7 | echo "Cannot run docker commands. " 8 | echo "Please install Docker or give this user access to run it" 9 | popd 10 | exit -1 11 | fi 12 | 13 | # Determine the correct Docker Compose command 14 | DOCKER_COMPOSE="docker compose" 15 | $DOCKER_COMPOSE version > /dev/null 2>&1 16 | 17 | if [ $? -ne 0 ]; then 18 | DOCKER_COMPOSE="docker-compose" 19 | $DOCKER_COMPOSE version > /dev/null 2>&1 20 | if [ $? -ne 0 ]; then 21 | echo "Neither 'docker compose' nor 'docker-compose' is available. Please install Docker Compose." 22 | popd 23 | exit -1 24 | fi 25 | fi 26 | 27 | echo "Using Docker Compose command: $DOCKER_COMPOSE" 28 | 29 | # Run Docker Compose commands 30 | pushd ci/mock-backends 31 | $DOCKER_COMPOSE down 32 | docker ps 33 | popd 34 | -------------------------------------------------------------------------------- /config.h.template: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* Edit the template file, not the generated header-file. 4 | */ 5 | 6 | #ifndef RESTC_CPP_CONFIG_H 7 | #define RESTC_CPP_CONFIG_H 8 | 9 | #ifndef BOOST_COROUTINES_NO_DEPRECATION_WARNING 10 | # define BOOST_COROUTINES_NO_DEPRECATION_WARNING 1 11 | #endif 12 | 13 | #cmakedefine RESTC_CPP_WITH_UNIT_TESTS 1 14 | #cmakedefine RESTC_CPP_WITH_TLS 1 15 | #cmakedefine RESTC_CPP_LOG_WITH_INTERNAL_LOG 1 16 | #cmakedefine RESTC_CPP_LOG_WITH_LOGFAULT 1 17 | #cmakedefine RESTC_CPP_LOG_WITH_BOOST_LOG 1 18 | #cmakedefine RESTC_CPP_LOG_WITH_CLOG 1 19 | #cmakedefine RESTC_CPP_WITH_ZLIB 1 20 | #cmakedefine RESTC_CPP_HAVE_BOOST_TYPEINDEX 1 21 | #cmakedefine RESTC_CPP_LOG_JSON_SERIALIZATION 1 22 | #cmakedefine RESTC_CPP_USE_CPP17 1 23 | #cmakedefine RESTC_CPP_THREADED_CTX 1 24 | 25 | #define RESTC_CPP_LOG_LEVEL ${RESTC_CPP_LOG_LEVEL} 26 | #define RESTC_CPP_MAX_INPUT_BUFFER_LENGTH ${RESTC_CPP_MAX_INPUT_BUFFER_LENGTH} 27 | 28 | #endif // RESTC_CPP_CONFIG_H 29 | -------------------------------------------------------------------------------- /create-and-run-containers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if Docker is running 4 | docker ps > /dev/null 5 | if [ $? -ne 0 ]; then 6 | echo 7 | echo "Cannot run docker commands. " 8 | echo "Please install Docker or give this user access to run it" 9 | popd 10 | exit -1 11 | fi 12 | 13 | # Determine the correct Docker Compose command 14 | DOCKER_COMPOSE="docker compose" 15 | $DOCKER_COMPOSE version > /dev/null 2>&1 16 | 17 | if [ $? -ne 0 ]; then 18 | DOCKER_COMPOSE="docker-compose" 19 | $DOCKER_COMPOSE version > /dev/null 2>&1 20 | if [ $? -ne 0 ]; then 21 | echo "Neither 'docker compose' nor 'docker-compose' is available. Please install Docker Compose." 22 | popd 23 | exit -1 24 | fi 25 | fi 26 | 27 | echo "Using Docker Compose command: $DOCKER_COMPOSE" 28 | 29 | # Run Docker Compose commands 30 | pushd ci/mock-backends 31 | $DOCKER_COMPOSE stop 32 | $DOCKER_COMPOSE build 33 | $DOCKER_COMPOSE up -d 34 | docker ps 35 | popd 36 | 37 | -------------------------------------------------------------------------------- /examples/cmake_external_project/cmake/external-projects.cmake: -------------------------------------------------------------------------------- 1 | # Here are registered all external projects 2 | # 3 | # Usage: 4 | # add_dependencies(TARGET externalProjectName) 5 | # target_link_libraries(TARGET PRIVATE ExternalLibraryName) 6 | 7 | set(EXTERNAL_PROJECTS_PREFIX ${CMAKE_BINARY_DIR}/external-projects) 8 | set(EXTERNAL_PROJECTS_INSTALL_PREFIX ${EXTERNAL_PROJECTS_PREFIX}/installed) 9 | 10 | include(GNUInstallDirs) 11 | 12 | # MUST be called before any add_executable() # https://stackoverflow.com/a/40554704/8766845 13 | link_directories(${EXTERNAL_PROJECTS_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}) 14 | include_directories($) 15 | 16 | include(ExternalProject) 17 | 18 | ExternalProject_Add(externalRestcCpp 19 | PREFIX "${EXTERNAL_PROJECTS_PREFIX}" 20 | GIT_REPOSITORY "https://github.com/jgaa/restc-cpp.git" 21 | GIT_TAG "master" 22 | CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${EXTERNAL_PROJECTS_INSTALL_PREFIX} 23 | ) 24 | -------------------------------------------------------------------------------- /examples/cmake_normal/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | project (example) 3 | 4 | find_package(restc-cpp REQUIRED) 5 | find_package(ZLIB REQUIRED) 6 | find_package(OpenSSL REQUIRED) 7 | find_package(Boost REQUIRED COMPONENTS 8 | system 9 | program_options 10 | filesystem 11 | date_time 12 | context 13 | coroutine 14 | chrono 15 | log 16 | ) 17 | 18 | add_executable(${PROJECT_NAME} main.cpp) 19 | set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 14) 20 | target_link_libraries(${PROJECT_NAME} 21 | ${RESTC_CPP_LIBRARIES} 22 | ${Boost_LIBRARIES} 23 | ${ZLIB_LIBRARIES} 24 | ${OPENSSL_LIBRARIES} 25 | ) 26 | 27 | add_definitions( 28 | -DBOOST_COROUTINE_NO_DEPRECATION_WARNING=1 29 | -DBOOST_ALL_DYN_LINK=1 30 | ) 31 | 32 | target_include_directories(${PROJECT_NAME} PUBLIC 33 | $ 34 | $ 35 | $ 36 | ) 37 | -------------------------------------------------------------------------------- /doc/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting started with restc-cpp 2 | 3 | Note that the restc-cpp wiki on github contains up to date 4 | build instructions for different platforms. The instructions 5 | below are generic. 6 | 7 | Clone the repository 8 | 9 | ```sh 10 | git clone https://github.com/jgaa/restc-cpp.git 11 | ``` 12 | 13 | Initialize the submodules 14 | ```sh 15 | cd restc-cpp 16 | git submodule init 17 | git submodule update 18 | ``` 19 | 20 | Compile the library and tests 21 | ```sh 22 | mkdir dbuild 23 | cd dbuild 24 | cmake .. 25 | make 26 | cd .. 27 | ``` 28 | 29 | At this point, you can start using the library in your own C++ projects. 30 | You need to specify to your project the paths to where you have the 31 | incluide/restc-cpp include directory, the externals/rapidjson/include 32 | and the library itself (./lib/librestc-cpp[D]. The 'D' is present in the 33 | library name if it is compiled for debugging. 34 | 35 | # Embedding restc-cpp's cmake file in your cmake project 36 | TBD 37 | -------------------------------------------------------------------------------- /tests/unit/RequestBuilder.cpp: -------------------------------------------------------------------------------- 1 | // Include before boost::log headers 2 | #include "restc-cpp/logging.h" 3 | 4 | #include 5 | 6 | #include "restc-cpp/restc-cpp.h" 7 | #include "restc-cpp/RequestBuilder.h" 8 | 9 | #include "gtest/gtest.h" 10 | #include "restc-cpp/test_helper.h" 11 | 12 | using namespace std; 13 | using namespace restc_cpp; 14 | 15 | TEST(RequestBuilder, DataWithCString) 16 | { 17 | RequestBuilder rb; 18 | rb.Data("{}"); 19 | EXPECT_EQ("{}", rb.GetData()); 20 | 21 | } 22 | 23 | TEST(RequestBuilder, DataWithCStringLen) 24 | { 25 | RequestBuilder rb; 26 | rb.Data("{}12345", 2); 27 | EXPECT_EQ("{}", rb.GetData()); 28 | 29 | } 30 | 31 | TEST(RequestBuilder, DataWithString) 32 | { 33 | 34 | const string data{"{}"}; 35 | 36 | RequestBuilder rb; 37 | rb.Data(data); 38 | EXPECT_EQ(data, rb.GetData()); 39 | 40 | } 41 | 42 | int main( int argc, char * argv[] ) 43 | { 44 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 45 | ::testing::InitGoogleTest(&argc, argv); 46 | return RUN_ALL_TESTS();; 47 | } 48 | -------------------------------------------------------------------------------- /include/restc-cpp/ConnectionPool.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | #ifndef RESTC_CPP_CONNECTION_POOL_H_ 4 | #define RESTC_CPP_CONNECTION_POOL_H_ 5 | 6 | #ifndef RESTC_CPP_H_ 7 | # error "Include restc-cpp.h first" 8 | #endif 9 | 10 | 11 | namespace restc_cpp { 12 | 13 | class ConnectionPool 14 | { 15 | public: 16 | using ptr_t = std::shared_ptr; 17 | virtual ~ConnectionPool() = default; 18 | 19 | virtual Connection::ptr_t GetConnection( 20 | const boost::asio::ip::tcp::endpoint ep, 21 | const Connection::Type connectionType, 22 | bool new_connection_please = false) = 0; 23 | 24 | virtual size_t GetIdleConnections() const = 0; 25 | static std::shared_ptr Create(RestClient& owner); 26 | 27 | /*! Close the connection-pool 28 | * 29 | * This is an internal method. 30 | * 31 | * This method is not thread-safe. It must be run by the 32 | * RestClients worker-thread. 33 | */ 34 | virtual void Close() = 0; 35 | }; 36 | 37 | } // restc_cpp 38 | 39 | 40 | #endif // RESTC_CPP_CONNECTION_POOL_H_ 41 | 42 | -------------------------------------------------------------------------------- /cmake_scripts/pch.cmake: -------------------------------------------------------------------------------- 1 | # Source: http://stackoverflow.com/questions/148570/using-pre-compiled-headers-with-cmake/2956392#2956392 2 | 3 | MACRO(ADD_MSVC_PRECOMPILED_HEADER PrecompiledHeader PrecompiledSource SourcesVar) 4 | IF(MSVC) 5 | GET_FILENAME_COMPONENT(PrecompiledBasename ${PrecompiledHeader} NAME_WE) 6 | SET(PrecompiledBinary "$(IntDir)/${PrecompiledBasename}.pch") 7 | SET(Sources ${${SourcesVar}}) 8 | 9 | SET_SOURCE_FILES_PROPERTIES(${PrecompiledSource} 10 | PROPERTIES COMPILE_FLAGS "/Yc\"${PrecompiledHeader}\" /Fp\"${PrecompiledBinary}\"" 11 | OBJECT_OUTPUTS "${PrecompiledBinary}") 12 | SET_SOURCE_FILES_PROPERTIES(${Sources} 13 | PROPERTIES COMPILE_FLAGS "/Yu\"${PrecompiledHeader}\" /FI\"${PrecompiledHeader}\" /Fp\"${PrecompiledBinary}\"" 14 | OBJECT_DEPENDS "${PrecompiledBinary}") 15 | # Add precompiled header to SourcesVar 16 | LIST(APPEND ${SourcesVar} ${PrecompiledSource}) 17 | ENDIF(MSVC) 18 | ENDMACRO(ADD_MSVC_PRECOMPILED_HEADER) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jarle Aase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/boost_compitability.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "restc-cpp/boost_compatibility.h" 3 | 4 | namespace restc_cpp { 5 | boost::asio::ip::tcp::endpoint boost_create_endpoint(const std::string& ip_address, unsigned short port) { 6 | #if BOOST_VERSION >= 106600 7 | // For Boost 1.66.0 and later 8 | return {boost::asio::ip::make_address(ip_address), port}; 9 | #else 10 | // For Boost versions earlier than 1.66.0 11 | return {boost::asio::ip::address::from_string(ip_address), port}; 12 | #endif 13 | } 14 | 15 | uint32_t boost_convert_ipv4_to_uint(const std::string& ip_address) { 16 | #if BOOST_VERSION >= 106600 17 | // For Boost 1.66.0 and later 18 | return boost::asio::ip::make_address_v4(ip_address).to_uint(); 19 | #else 20 | // For Boost versions earlier than 1.66.0 21 | return boost::asio::ip::address_v4::from_string(ip_address).to_ulong(); 22 | #endif 23 | } 24 | 25 | std::unique_ptr boost_make_work(boost_io_service& ioservice) { 26 | #if BOOST_VERSION >= 106600 27 | return std::make_unique(boost::asio::make_work_guard(ioservice)); 28 | #else 29 | return std::make_unique(ioservice); 30 | #endif 31 | } 32 | 33 | 34 | } // namespace restc_cpp 35 | -------------------------------------------------------------------------------- /include/restc-cpp/RapidJsonWriter.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "restc-cpp/DataWriter.h" 5 | 6 | namespace restc_cpp { 7 | 8 | /*! Wrapper from rapidjson (output) Stream concept to our DataWriter 9 | */ 10 | 11 | template 12 | class RapidJsonWriter { 13 | public: 14 | using Ch = chT; 15 | 16 | RapidJsonWriter(DataWriter& writer) 17 | : writer_{writer} {} 18 | 19 | Ch Peek() const { assert(false); return '\0'; } 20 | 21 | Ch Take() { assert(false); return '\0'; } 22 | 23 | size_t Tell() const { return 0; } 24 | 25 | Ch* PutBegin() { assert(false); return 0; } 26 | 27 | void Put(Ch c) { 28 | buffer_[bytes_] = c; 29 | if (++bytes_ == buffer_.size()) { 30 | Flush(); 31 | } 32 | } 33 | 34 | void Flush() { 35 | if (bytes_ == 0) { 36 | return; 37 | } 38 | 39 | writer_.Write({buffer_.data(), bytes_ * sizeof(Ch)}); 40 | bytes_ = 0; 41 | } 42 | 43 | size_t PutEnd(Ch*) { assert(false); return 0; } 44 | 45 | private: 46 | DataWriter& writer_; 47 | std::array buffer_; 48 | size_t bytes_ = 0; 49 | }; 50 | 51 | 52 | } // namespace 53 | 54 | -------------------------------------------------------------------------------- /include/restc-cpp/Url.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef RESTC_CPP_URL_H_ 4 | #define RESTC_CPP_URL_H_ 5 | 6 | #include 7 | 8 | namespace restc_cpp { 9 | 10 | 11 | class Url { 12 | public: 13 | enum class Protocol { 14 | UNKNOWN, 15 | HTTP, 16 | HTTPS 17 | }; 18 | 19 | Url(const char *url); 20 | 21 | Url& operator = (const char *url); 22 | 23 | boost::string_ref GetProtocolName() const { return protocol_name_; } 24 | boost::string_ref GetHost() const { return host_; } 25 | boost::string_ref GetPort() const { return port_; } 26 | boost::string_ref GetPath() const { return path_; } 27 | boost::string_ref GetArgs() const { return args_; } 28 | Protocol GetProtocol() const { return protocol_; } 29 | 30 | private: 31 | boost::string_ref protocol_name_; 32 | boost::string_ref host_; 33 | boost::string_ref port_; 34 | boost::string_ref path_ = "/"; 35 | boost::string_ref args_; 36 | Protocol protocol_ = Protocol::UNKNOWN; 37 | }; 38 | 39 | } // namespace restc_cpp 40 | 41 | std::ostream& operator <<(std::ostream& out, 42 | const restc_cpp::Url::Protocol& protocol); 43 | 44 | #endif // RESTC_CPP_URL_H_ 45 | -------------------------------------------------------------------------------- /tests/functional/CookieTests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Include before boost::log headers 4 | #include "restc-cpp/logging.h" 5 | 6 | #include "restc-cpp/restc-cpp.h" 7 | #include "restc-cpp/RequestBuilder.h" 8 | 9 | #include "gtest/gtest.h" 10 | #include "restc-cpp/test_helper.h" 11 | 12 | using namespace std; 13 | using namespace restc_cpp; 14 | 15 | 16 | static const string address = GetDockerUrl("http://localhost:3001/cookies/"); 17 | 18 | TEST(Cookies, HaveCookies) 19 | { 20 | auto rest_client = RestClient::Create(); 21 | 22 | rest_client->ProcessWithPromise([&](Context& ctx) { 23 | auto reply = RequestBuilder(ctx) 24 | .Get(address) 25 | .Execute(); 26 | 27 | auto cookies = reply->GetHeaders("Set-Cookie"); 28 | EXPECT_EQ(3, cookies.size()); 29 | 30 | set allowed = {"test1=yes", "test2=maybe", "test3=no"}; 31 | 32 | for (const auto &c : cookies) { 33 | EXPECT_EQ(true, allowed.find(c) != allowed.end()); 34 | allowed.erase(c); 35 | } 36 | 37 | EXPECT_EQ(0, allowed.size()); 38 | 39 | }).get(); 40 | } 41 | 42 | int main( int argc, char * argv[] ) 43 | { 44 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 45 | ::testing::InitGoogleTest(&argc, argv); 46 | return RUN_ALL_TESTS();; 47 | } 48 | -------------------------------------------------------------------------------- /tests/functional/PropertiesTests.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Include before boost::log headers 4 | #include "restc-cpp/restc-cpp.h" 5 | #include "restc-cpp/logging.h" 6 | #include "restc-cpp/RequestBuilder.h" 7 | 8 | #include "gtest/gtest.h" 9 | #include "restc-cpp/test_helper.h" 10 | 11 | using namespace std; 12 | using namespace restc_cpp; 13 | 14 | 15 | /* These url's points to a local Docker container with nginx, linked to 16 | * a jsonserver docker container with mock data. 17 | * The scripts to build and run these containers are in the ./tests directory. 18 | */ 19 | const string http_url = "http://localhost:3000/fail"; 20 | 21 | TEST(Properties, Res404) { 22 | 23 | Request::Properties properties; 24 | properties.throwOnHttpError = false; 25 | 26 | auto rest_client = RestClient::Create(properties); 27 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 28 | 29 | auto reply = RequestBuilder(ctx) 30 | .Get(GetDockerUrl(http_url)) // URL 31 | .Execute(); // Do it! 32 | 33 | EXPECT_EQ(404, reply->GetResponseCode()); 34 | 35 | }); 36 | 37 | EXPECT_NO_THROW(f.get()); 38 | } 39 | 40 | int main( int argc, char * argv[] ) 41 | { 42 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 43 | ::testing::InitGoogleTest(&argc, argv); 44 | return RUN_ALL_TESTS();; 45 | } 46 | -------------------------------------------------------------------------------- /src/ConnectionImpl.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include // generators 7 | 8 | #include "restc-cpp/restc-cpp.h" 9 | #include "restc-cpp/Socket.h" 10 | #include "restc-cpp/logging.h" 11 | 12 | using namespace std; 13 | 14 | namespace restc_cpp { 15 | 16 | std::ostream& Connection::Print(std::ostream& o) const { 17 | return o << "{Connection " 18 | << GetId() 19 | << ' ' 20 | << GetSocket() 21 | << '}'; 22 | } 23 | 24 | 25 | class ConnectionImpl : public Connection { 26 | public: 27 | 28 | ConnectionImpl(std::unique_ptr socket) 29 | : socket_{std::move(socket)} 30 | { 31 | RESTC_CPP_LOG_TRACE_(*this << " is constructed."); 32 | } 33 | 34 | ~ConnectionImpl() { 35 | RESTC_CPP_LOG_TRACE_(*this << " is dead."); 36 | } 37 | 38 | Socket& GetSocket() override { 39 | return *socket_; 40 | } 41 | 42 | const Socket& GetSocket() const override { 43 | return *socket_; 44 | } 45 | 46 | boost::uuids::uuid GetId() const override { 47 | return id_; 48 | } 49 | 50 | private: 51 | std::unique_ptr socket_; 52 | const boost::uuids::uuid id_ = boost::uuids::random_generator()(); 53 | }; 54 | 55 | } // restc_cpp 56 | 57 | -------------------------------------------------------------------------------- /tests/functional/AuthTest.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | // Include before boost::log headers 5 | #include "restc-cpp/logging.h" 6 | 7 | #include 8 | #include 9 | 10 | #include "restc-cpp/restc-cpp.h" 11 | #include "restc-cpp/error.h" 12 | #include "restc-cpp/RequestBuilder.h" 13 | 14 | #include "gtest/gtest.h" 15 | #include "restc-cpp/test_helper.h" 16 | 17 | 18 | using namespace std; 19 | using namespace restc_cpp; 20 | 21 | TEST(Auth, Failed) { 22 | auto rest_client = RestClient::Create(); 23 | rest_client->ProcessWithPromise([&](Context& ctx) { 24 | 25 | EXPECT_THROW(ctx.Get(GetDockerUrl("http://localhost:3001/restricted/posts/1")), 26 | HttpAuthenticationException); 27 | 28 | }).get(); 29 | } 30 | 31 | TEST(Auth, Success) { 32 | auto rest_client = RestClient::Create(); 33 | rest_client->ProcessWithPromise([&](Context& ctx) { 34 | 35 | auto reply = RequestBuilder(ctx) 36 | .Get(GetDockerUrl("http://localhost:3001/restricted/posts/1")) 37 | .BasicAuthentication("alice", "12345") 38 | .Execute(); 39 | 40 | EXPECT_EQ(200, reply->GetResponseCode()); 41 | 42 | }).get(); 43 | } 44 | 45 | 46 | int main( int argc, char * argv[] ) 47 | { 48 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 49 | ::testing::InitGoogleTest(&argc, argv); 50 | return RUN_ALL_TESTS(); 51 | } 52 | -------------------------------------------------------------------------------- /src/RequestBodyStringImpl.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include "restc-cpp/restc-cpp.h" 6 | #include "restc-cpp/RequestBody.h" 7 | #include "restc-cpp/DataWriter.h" 8 | 9 | using namespace std; 10 | 11 | 12 | namespace restc_cpp { 13 | namespace impl { 14 | 15 | class RequestBodyStringImpl : public RequestBody 16 | { 17 | public: 18 | explicit RequestBodyStringImpl(string body) 19 | : body_{std::move(body)} 20 | { 21 | } 22 | 23 | [[nodiscard]] Type GetType() const noexcept override { return Type::FIXED_SIZE; } 24 | 25 | [[nodiscard]] std::uint64_t GetFixedSize() const override { return body_.size(); } 26 | 27 | bool GetData(write_buffers_t & buffers) override { 28 | if (eof_) { 29 | return false; 30 | } 31 | 32 | buffers.emplace_back(body_.c_str(), body_.size()); 33 | eof_ = true; 34 | return true; 35 | } 36 | 37 | void Reset() override { 38 | eof_ = false; 39 | } 40 | 41 | [[nodiscard]] std::string GetCopyOfData() const override { return body_; } 42 | 43 | private: 44 | string body_; 45 | bool eof_ = false; 46 | }; 47 | 48 | 49 | } // impl 50 | 51 | std::unique_ptr RequestBody::CreateStringBody( 52 | std::string body) { 53 | 54 | return make_unique(std::move(body)); 55 | } 56 | 57 | } // restc_cpp 58 | 59 | -------------------------------------------------------------------------------- /include/restc-cpp/test_helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define RESTC_CPP_TEST_HELPER_H_ 4 | 5 | #include 6 | #include 7 | 8 | namespace restc_cpp { 9 | 10 | # define EXPECT_CLOSE(expect, value, slack) EXPECT_GE(value, (expect - slack)); EXPECT_LE(value, (expect + slack)); 11 | # define EXPECT_HTTP_OK(res) EXPECT_GE(res, 200); EXPECT_LE(res, 201) 12 | # define EXPECT_EQ_ENUM(a, b) EXPECT_EQ(static_cast(a), static_cast(b)) 13 | 14 | // Substitute localhost with whatever is in the environment-variable 15 | // RESTC_CPP_TEST_DOCKER_ADDRESS 16 | inline std::string GetDockerUrl(std::string url) { 17 | #ifdef _WIN32 18 | // On Windows, use _dupenv_s to safely retrieve the environment variable 19 | size_t len = 0; 20 | char* docker_addr = nullptr; 21 | errno_t err = _dupenv_s(&docker_addr, &len, "RESTC_CPP_TEST_DOCKER_ADDRESS"); 22 | if (err != 0 || docker_addr == nullptr) { 23 | docker_addr = nullptr; // Ensure docker_addr is nullptr if the variable isn't set 24 | } 25 | #else 26 | // On Linux/macOS, use std::getenv 27 | auto docker_addr = std::getenv("RESTC_CPP_TEST_DOCKER_ADDRESS"); 28 | #endif 29 | 30 | if (docker_addr) { 31 | boost::replace_all(url, "localhost", docker_addr); 32 | #ifdef _WIN32 33 | // Free the allocated memory on Windows 34 | free(docker_addr); 35 | #endif 36 | } 37 | 38 | return url; 39 | } 40 | 41 | } // namespace 42 | 43 | -------------------------------------------------------------------------------- /src/PlainReaderImpl.cpp: -------------------------------------------------------------------------------- 1 | #include "restc-cpp/restc-cpp.h" 2 | #include "restc-cpp/Connection.h" 3 | #include "restc-cpp/Socket.h" 4 | #include "restc-cpp/DataReader.h" 5 | #include "restc-cpp/error.h" 6 | 7 | using namespace std; 8 | 9 | namespace restc_cpp { 10 | 11 | class PlainReaderImpl : public DataReader { 12 | public: 13 | 14 | PlainReaderImpl(size_t contentLength, ptr_t&& source) 15 | : remaining_{contentLength}, 16 | source_{std::move(source)} {} 17 | 18 | [[nodiscard]] bool IsEof() const override { return remaining_ == 0; } 19 | 20 | void Finish() override { 21 | if (source_) { 22 | source_->Finish(); 23 | } 24 | } 25 | 26 | ::restc_cpp::boost_const_buffer ReadSome() override { 27 | 28 | if (IsEof()) { 29 | return {nullptr, 0}; 30 | } 31 | 32 | auto buffer = source_->ReadSome(); 33 | const auto bytes = boost::asio::buffer_size(buffer); 34 | 35 | if ((static_cast(remaining_) 36 | - static_cast(bytes)) < 0) { 37 | throw ProtocolException("Body-size exceeds content-size"); 38 | } 39 | remaining_ -= bytes; 40 | return buffer; 41 | } 42 | 43 | private: 44 | size_t remaining_; 45 | ptr_t source_; 46 | }; 47 | 48 | DataReader::ptr_t 49 | DataReader::CreatePlainReader(size_t contentLength, ptr_t&& source) { 50 | return make_unique(contentLength, std::move(source)); 51 | } 52 | 53 | 54 | } // namespace 55 | -------------------------------------------------------------------------------- /src/PlainWriterImpl.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "restc-cpp/restc-cpp.h" 3 | #include "restc-cpp/Connection.h" 4 | #include "restc-cpp/Socket.h" 5 | #include "restc-cpp/DataWriter.h" 6 | #include "restc-cpp/logging.h" 7 | 8 | using namespace std; 9 | 10 | namespace restc_cpp { 11 | 12 | 13 | class PlainWriterImpl : public DataWriter { 14 | public: 15 | PlainWriterImpl(size_t contentLength, ptr_t&& source) 16 | : next_{std::move(source)}, content_length_{contentLength} 17 | { 18 | } 19 | 20 | void WriteDirect(::restc_cpp::boost_const_buffer buffers) override { 21 | next_->WriteDirect(buffers); 22 | } 23 | 24 | void Write(::restc_cpp::boost_const_buffer buffers) override { 25 | next_->Write(buffers); 26 | } 27 | 28 | void Write(const write_buffers_t& buffers) override { 29 | next_->Write(buffers); 30 | } 31 | 32 | void Finish() override { 33 | next_->Finish(); 34 | } 35 | 36 | void SetHeaders(Request::headers_t& headers) override { 37 | static const string content_length{"Content-Length"}; 38 | headers[content_length] = to_string(content_length_); 39 | next_->SetHeaders(headers); 40 | } 41 | 42 | private: 43 | unique_ptr next_; 44 | const size_t content_length_; 45 | }; 46 | 47 | 48 | DataWriter::ptr_t 49 | DataWriter::CreatePlainWriter(size_t contentLength, ptr_t&& source) { 50 | return make_unique(contentLength, std::move(source)); 51 | } 52 | 53 | } // namespace 54 | 55 | 56 | -------------------------------------------------------------------------------- /tests/functional/UploadTests.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | // Include before boost::log headers 5 | #include "restc-cpp/logging.h" 6 | #include 7 | 8 | #include "restc-cpp/restc-cpp.h" 9 | #include "restc-cpp/error.h" 10 | #include "restc-cpp/RequestBuilder.h" 11 | 12 | #include "gtest/gtest.h" 13 | #include "restc-cpp/test_helper.h" 14 | 15 | using namespace std; 16 | using namespace restc_cpp; 17 | 18 | boost::filesystem::path temp_path; 19 | 20 | // The content is send un-encoded in the body 21 | TEST(Upload, Raw) 22 | { 23 | auto rest_client = RestClient::Create(); 24 | rest_client->ProcessWithPromise([&](Context& ctx) { 25 | 26 | auto reply = RequestBuilder(ctx) 27 | .Post(GetDockerUrl("http://localhost:3001/upload_raw/")) 28 | .Header("Content-Type", "application/octet-stream") 29 | .File(temp_path) 30 | .Execute(); 31 | 32 | EXPECT_EQ(200, reply->GetResponseCode()); 33 | 34 | }).get(); 35 | } 36 | 37 | 38 | int main( int argc, char * argv[] ) 39 | { 40 | temp_path = boost::filesystem::unique_path(); 41 | { 42 | ofstream file(temp_path.string()); 43 | for(int i = 0; i < 1000; i++) { 44 | file << "This is line #" << i << '\n'; 45 | } 46 | } 47 | 48 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 49 | ::testing::InitGoogleTest(&argc, argv); 50 | auto rval = RUN_ALL_TESTS(); 51 | 52 | boost::filesystem::remove(temp_path); 53 | 54 | return rval; 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/url_encode.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #include "restc-cpp/restc-cpp.h" 5 | #include "restc-cpp/url_encode.h" 6 | 7 | using namespace std; 8 | 9 | namespace restc_cpp { 10 | 11 | namespace { 12 | 13 | constexpr size_t bitset_size = 255; 14 | using allchars_t = std::bitset; 15 | 16 | allchars_t get_normal_ch() { 17 | allchars_t bits; 18 | for(size_t chv = 0; chv < bitset_size; ++chv) { 19 | const auto ch = static_cast(chv); 20 | if ((ch >= '0' && ch <= '9') 21 | || (ch >= 'a' && ch <= 'z') 22 | || (ch >= 'A' && ch <= 'Z') 23 | || ch == '-' || ch == '_' || ch == '.' 24 | || ch == '!' || ch == '~' || ch == '*' 25 | || ch == '\'' || ch == '(' || ch == ')' 26 | || ch == '/') 27 | 28 | { 29 | bits[ch] = true; 30 | } 31 | } 32 | 33 | return bits; 34 | } 35 | 36 | } // anonymous namespace 37 | 38 | std::string url_encode(const boost::string_ref& src) { 39 | constexpr auto magic_4 = 4; 40 | constexpr auto magic_0x0f = 0x0f; 41 | 42 | static const string hex{"0123456789ABCDEF"}; 43 | static auto normal_ch = get_normal_ch(); 44 | std::string rval; 45 | rval.reserve(src.size() * 2); 46 | 47 | for(auto ch : src) { 48 | if (normal_ch[static_cast(ch)]) { 49 | rval += ch; 50 | } else { 51 | rval += '%'; 52 | rval += hex[(ch >> magic_4) & magic_0x0f]; 53 | rval += hex[ch & magic_0x0f]; 54 | } 55 | } 56 | return rval; 57 | } 58 | 59 | } // namespace 60 | -------------------------------------------------------------------------------- /examples/cmake_normal/readme.md: -------------------------------------------------------------------------------- 1 | # example with cmake and find_package(restc-cpp) 2 | 3 | This example shows how to compile a program using restc-cpp from cmake. 4 | We assume that the library has been built and installed with "make install". It depends on the cmake configuration files from the restc-cpp library being available for cmake to find. 5 | 6 | Currently this example is only tested under Linux. 7 | 8 | 9 | ``` 10 | jgaa@vendetta:~/src/restc-cpp/examples/cmake_normal$ rm -rf build/ 11 | jgaa@vendetta:~/src/restc-cpp/examples/cmake_normal$ mkdir build 12 | jgaa@vendetta:~/src/restc-cpp/examples/cmake_normal$ cd build/ 13 | jgaa@vendetta:~/src/restc-cpp/examples/cmake_normal/build$ cmake .. 14 | ... 15 | -- restc-cpp found. 16 | -- Found ZLIB: /usr/lib/x86_64-linux-gnu/libz.so (found version "1.2.8") 17 | -- Found OpenSSL: /usr/lib/x86_64-linux-gnu/libssl.so;/usr/lib/x86_64-linux-gnu/libcrypto.so (found version "1.1.0f") 18 | -- Found Threads: TRUE 19 | -- Boost version: 1.62.0 20 | -- Found the following Boost libraries: 21 | -- system 22 | -- program_options 23 | -- filesystem 24 | -- date_time 25 | -- context 26 | -- coroutine 27 | -- chrono 28 | -- log 29 | -- thread 30 | -- log_setup 31 | -- regex 32 | -- atomic 33 | -- Configuring done 34 | -- Generating done 35 | -- Build files have been written to: /home/jgaa/src/restc-cpp/examples/cmake_normal/build 36 | jgaa@vendetta:~/src/restc-cpp/examples/cmake_normal/build$ make 37 | Scanning dependencies of target example 38 | [ 50%] Building CXX object CMakeFiles/example.dir/main.cpp.o 39 | [100%] Linking CXX executable example 40 | [100%] Built target example 41 | ``` 42 | -------------------------------------------------------------------------------- /doc/Logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | Logging is often a pain in a library like restc_cpp. 4 | 5 | restc_cpp use a C++ streams operator logging scheme internally. It is based on a 6 | few macros, and can be mapped to [logfault](https://github.com/jgaa/logfault), *boost.Log*, 7 | *std::clog* or an internal handler that allow you to register a callback to deal with log events. 8 | 9 | One of the following CMAKE macros can be enabled at compile time: 10 | 11 | - `RESTC_CPP_LOG_WITH_INTERNAL_LOG`: Use internal log handler and register a callback to handle log events 12 | - `RESTC_CPP_LOG_WITH_LOGFAULT`: Use logfault for logging 13 | - `RESTC_CPP_LOG_WITH_BOOST_LOG`: Use boost::log for logging 14 | - `RESTC_CPP_LOG_WITH_CLOG`: Just send log-events to std::clog 15 | 16 | The default (as of version 0.92 of restc_cpp) is `RESTC_CPP_LOG_WITH_INTERNAL_LOG`. 17 | 18 | ## Example 19 | 20 | This example assumes that the CMAKE variable `RESTC_CPP_LOG_WITH_INTERNAL_LOG` is `ON`. 21 | 22 | ```c++ 23 | 24 | restc_cpp::Logger::Instance().SetLogLevel(restc_cpp::LogLevel::DEBUG); 25 | 26 | restc_cpp::Logger::Instance().SetHandler([](restc_cpp::LogLevel level, 27 | const std::string& msg) { 28 | static const std::array levels = {"NONE", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"}; 29 | 30 | const auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); 31 | 32 | std::clog << std::put_time(std::localtime(&now), "%c") << ' ' 33 | << levels.at(static_cast(level)) 34 | << ' ' << std::this_thread::get_id() << ' ' 35 | << msg << std::endl; 36 | }); 37 | 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /tests/mock-nginx/proxy.conf.bin: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | gzip on; 5 | gzip_proxied any; 6 | 7 | 8 | location ~ ^/cookies(/?)(.*)$ { 9 | add_header Content-Type "application/json; charset=utf-8"; 10 | add_header Set-Cookie test1=yes; 11 | add_header Set-Cookie test2=maybe; 12 | add_header Set-Cookie test3=no; 13 | return 200 '{}'; 14 | } 15 | 16 | location ~ ^/normal(/?)(.*)$ { 17 | proxy_pass http://api/$2$is_args$args; 18 | } 19 | 20 | location ~ ^/close(/?)(.*)$ { 21 | proxy_pass http://api/$2$is_args$args; 22 | keepalive_timeout 0; 23 | } 24 | 25 | # Force nginx to resolve 'api' 26 | location /dns_workaround { 27 | proxy_pass http://api; 28 | } 29 | 30 | location ~ ^/loop(/?)(.*)$ { 31 | return 301 $scheme://$host:3001/loop/$2$is_args$args; 32 | } 33 | 34 | location ~ ^/redirect(/?)(.*)$ { 35 | return 301 $scheme://$host:3001/normal/$2$is_args$args; 36 | } 37 | 38 | location ~ ^/reredirect(/?)(.*)$ { 39 | return 301 $scheme://$host:3001/redirect/$2$is_args$args; 40 | } 41 | 42 | location ~ ^/restricted(/?)(.*)$ { 43 | auth_basic "Restricted Area"; 44 | auth_basic_user_file /etc/nginx/htpasswd; 45 | proxy_pass http://api/$2$is_args$args; 46 | } 47 | 48 | location ~ ^/upload_raw(/?)(.*)$ { 49 | limit_except POST { deny all; } 50 | return 200 "OK"; 51 | } 52 | 53 | 54 | error_page 500 502 503 504 /50x.html; 55 | location = /50x.html { 56 | root /usr/share/nginx/html; 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /ci/mock-backends/nginx/proxy.conf.bin: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | gzip on; 5 | gzip_proxied any; 6 | 7 | 8 | location ~ ^/cookies(/?)(.*)$ { 9 | add_header Content-Type "application/json; charset=utf-8"; 10 | add_header Set-Cookie test1=yes; 11 | add_header Set-Cookie test2=maybe; 12 | add_header Set-Cookie test3=no; 13 | return 200 '{}'; 14 | } 15 | 16 | location ~ ^/normal(/?)(.*)$ { 17 | proxy_pass http://json/$2$is_args$args; 18 | } 19 | 20 | location ~ ^/close(/?)(.*)$ { 21 | proxy_pass http://json/$2$is_args$args; 22 | keepalive_timeout 0; 23 | } 24 | 25 | # Force nginx to resolve 'json' 26 | location /dns_workaround { 27 | proxy_pass http://json; 28 | } 29 | 30 | location ~ ^/loop(/?)(.*)$ { 31 | return 301 $scheme://$host:3001/loop/$2$is_args$args; 32 | } 33 | 34 | location ~ ^/redirect(/?)(.*)$ { 35 | return 301 $scheme://$host:3001/normal/$2$is_args$args; 36 | } 37 | 38 | location ~ ^/reredirect(/?)(.*)$ { 39 | return 301 $scheme://$host:3001/redirect/$2$is_args$args; 40 | } 41 | 42 | location ~ ^/restricted(/?)(.*)$ { 43 | auth_basic "Restricted Area"; 44 | auth_basic_user_file /etc/nginx/htpasswd; 45 | proxy_pass http://json/$2$is_args$args; 46 | } 47 | 48 | location ~ ^/upload_raw(/?)(.*)$ { 49 | limit_except POST { deny all; } 50 | return 200 "OK"; 51 | } 52 | 53 | 54 | error_page 500 502 503 504 /50x.html; 55 | location = /50x.html { 56 | root /usr/share/nginx/html; 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /tests/unit/AsyncSleepTests.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | // Include before boost::log headers 5 | #include "restc-cpp/logging.h" 6 | #include 7 | #include 8 | 9 | #include "restc-cpp/restc-cpp.h" 10 | #include "restc-cpp/error.h" 11 | #include "restc-cpp/RequestBuilder.h" 12 | 13 | #include "gtest/gtest.h" 14 | #include "restc-cpp/test_helper.h" 15 | 16 | using namespace std; 17 | using namespace restc_cpp; 18 | 19 | using namespace std::literals::chrono_literals; 20 | 21 | TEST(AsyncSleep, SleepMilliseconds) 22 | { 23 | auto rest_client = RestClient::Create(); 24 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 25 | 26 | auto start = std::chrono::steady_clock::now(); 27 | ctx.Sleep(200ms); 28 | auto duration = std::chrono::duration_cast 29 | (std::chrono::steady_clock::now() - start).count(); 30 | EXPECT_CLOSE(200, duration, 50); 31 | 32 | }); 33 | 34 | EXPECT_NO_THROW(f.get()); 35 | } 36 | 37 | TEST(AsyncSleep, TestSleepSeconds) 38 | { 39 | auto rest_client = RestClient::Create(); 40 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 41 | 42 | auto start = std::chrono::steady_clock::now(); 43 | ctx.Sleep(1s); 44 | auto duration = std::chrono::duration_cast 45 | (std::chrono::steady_clock::now() - start).count(); 46 | EXPECT_CLOSE(1000, duration, 50); 47 | 48 | }); 49 | 50 | EXPECT_NO_THROW(f.get()); 51 | } 52 | 53 | 54 | int main( int argc, char * argv[] ) 55 | { 56 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 57 | ::testing::InitGoogleTest(&argc, argv); 58 | return RUN_ALL_TESTS();; 59 | } 60 | -------------------------------------------------------------------------------- /tests/functional/ConnectionPoolInstancesTest.cpp: -------------------------------------------------------------------------------- 1 | #include "restc-cpp/restc-cpp.h" 2 | #include "restc-cpp/logging.h" 3 | #include "restc-cpp/ConnectionPool.h" 4 | 5 | #include "../src/ReplyImpl.h" 6 | 7 | #include "gtest/gtest.h" 8 | #include "restc-cpp/test_helper.h" 9 | 10 | /* These url's points to a local Docker container with nginx, linked to 11 | * a jsonserver docker container with mock data. 12 | * The scripts to build and run these containers are in the ./tests directory. 13 | */ 14 | const string http_url = "http://localhost:3001/normal/posts"; 15 | const string http_url_many = "http://localhost:3001/normal/manyposts"; 16 | const string http_connection_close_url = "http://localhost:3001/close/posts"; 17 | 18 | using namespace std; 19 | using namespace restc_cpp; 20 | 21 | 22 | TEST(ConnectionPoolInstances, UseAfterDelete) { 23 | 24 | for(auto i = 0; i < 500; ++i) { 25 | 26 | RestClient::Create()->ProcessWithPromiseT([&](Context& ctx) { 27 | auto repl = ctx.Get(GetDockerUrl(http_url)); 28 | EXPECT_HTTP_OK(repl->GetHttpResponse().status_code); 29 | EXPECT_NO_THROW(repl->GetBodyAsString()); 30 | return 0; 31 | }).get(); 32 | 33 | RestClient::Create()->ProcessWithPromiseT([&](Context& ctx) { 34 | auto repl = ctx.Get(GetDockerUrl(http_url)); 35 | EXPECT_HTTP_OK(repl->GetHttpResponse().status_code); 36 | EXPECT_NO_THROW(repl->GetBodyAsString()); 37 | return 0; 38 | }).get(); 39 | 40 | if ((i % 100) == 0) { 41 | clog << '#' << (i + 1) << '\n'; 42 | } 43 | } 44 | } 45 | 46 | int main( int argc, char * argv[] ) 47 | { 48 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 49 | ::testing::InitGoogleTest(&argc, argv); 50 | return RUN_ALL_TESTS();; 51 | } 52 | -------------------------------------------------------------------------------- /ci/jenkins/Dockerfile.centos7: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | RUN echo "root:password" | chpasswd 6 | 7 | # We need to create a valid user on the same uid that jenkins use on the host machine. 8 | # Else, we run into failing commands because the current user cannot be resolved. 9 | RUN useradd -u 106 -m -d /home/jenkins -s /bin/bash jenkins 10 | RUN echo "jenkins:jenkins" | chpasswd 11 | 12 | RUN yum -y update &&\ 13 | yum -y install centos-release-scl &&\ 14 | yum -y install git jre-openjdk zlib-devel openssl-devel openssh-server bzip2 which make &&\ 15 | yum -y update &&\ 16 | yum -y install devtoolset-7-gcc* 17 | 18 | # Install cmake > verssion 3.0 19 | RUN mkdir -p /opt/cmake &&\ 20 | curl -L -o cmake.sh https://github.com/Kitware/CMake/releases/download/v3.12.4/cmake-3.12.4-Linux-x86_64.sh &&\ 21 | chmod +x cmake.sh &&\ 22 | ./cmake.sh --skip-license --prefix=/opt/cmake --exclude-subdir 23 | 24 | ENV PATH="/opt/cmake/bin:${PATH}" 25 | 26 | # Build boost and install to /opt/boost 27 | # Note: I skip some large boost libraries that is not used in the target-project 28 | RUN mkdir boost &&\ 29 | cd boost &&\ 30 | curl -o boost.tar.bz2 -L https://dl.bintray.com/boostorg/release/1.69.0/source/boost_1_69_0.tar.bz2 &&\ 31 | tar -xjf boost.tar.bz2 &&\ 32 | source scl_source enable devtoolset-7 &&\ 33 | cd boost_1_* &&\ 34 | ./bootstrap.sh --prefix=/opt/boost &&\ 35 | ./b2 install --prefix=/opt/boost --without-python --without-wave --without-graph --without-mpi --without-test -j8 &&\ 36 | cd && rm -rf boost 37 | 38 | # expose the ssh port 39 | EXPOSE 22 40 | 41 | # entrypoint by starting sshd 42 | CMD ["/usr/sbin/sshd", "-D"] 43 | 44 | # We need "scl enable devtoolset-7 bash" or "source scl_source enable devtoolset-7" in the build-script 45 | -------------------------------------------------------------------------------- /doc/RunningTheTests.md: -------------------------------------------------------------------------------- 1 | # Running the tests 2 | 3 | In order to run the test, you must have Docker installed. 4 | The docker containers will use localhost ports 3000, 3001, 3002, 3003. 5 | 6 | Create and start the Docker containers. 7 | 8 | This will create three containers, one with a generic JSON api server, 9 | one with nginx, configured to test authentication and redirects, and 10 | one with squid to test proxy traversal. 11 | 12 | Note that some of the tests use the containers, others use real 13 | API mock services on the Internet. 14 | 15 | ## Under Linux 16 | 17 | ```sh 18 | ./create-and-run-containers.sh 19 | ``` 20 | 21 | Run the tests. 22 | 23 | ```sh 24 | ./tests/run-tests.sh 25 | ``` 26 | 27 | ## Under Windows 28 | The scripts that create and use the docker containers are modified 29 | to work 'out of the box' under Windows. I use the cygwin shell that 30 | comes with the windows version of git to run these scripts. 31 | 32 | ```sh 33 | ./create-and-run-containers.sh 34 | ``` 35 | 36 | Run the tests. 37 | 38 | ```sh 39 | ./tests/run-tests.sh 40 | ``` 41 | 42 | ## Under MacOS 43 | 44 | MacOS will make it’s best effort to hinder you from doing what you want. 45 | 46 | After Docker is installed and running, I have found the commands below to work. 47 | 48 | ```sh 49 | ./create-and-run-containers.sh 50 | ``` 51 | 52 | Run the tests. 53 | Note that you must point DYLD_LIBRARY_PATH to wherever you have the boost dynamic libraries installed. The example show where they are on my machine. 54 | 55 | Also note that you must “source” the command (prefix with .) in order to keep the value of DYLD_LIBRARY_PATH (what part of “export” is it that macOS does not understand?). 56 | 57 | ```sh 58 | export DYLD_LIBRARY_PATH=/Users/jgaa/src/boost_1_60_0/stage/lib 59 | . ./tests/run-tests.sh 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /include/restc-cpp/RequestBody.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef RESTC_CPP_BODY_H_ 4 | #define RESTC_CPP_BODY_H_ 5 | 6 | #include "restc-cpp/error.h" 7 | #include "restc-cpp/DataWriter.h" 8 | 9 | namespace restc_cpp { 10 | 11 | /*! The body of the request. */ 12 | class RequestBody { 13 | public: 14 | enum class Type { 15 | /// Typically a string or a file 16 | FIXED_SIZE, 17 | 18 | /// Use GetData() to pull for data 19 | CHUNKED_LAZY_PULL, 20 | 21 | /// User will push directly to the data writer 22 | CHUNKED_LAZY_PUSH, 23 | }; 24 | 25 | virtual ~RequestBody() = default; 26 | 27 | virtual Type GetType() const noexcept = 0; 28 | 29 | /*! Typically the value of the content-length header */ 30 | virtual std::uint64_t GetFixedSize() const { 31 | throw NotImplementedException("GetFixedSize()"); 32 | } 33 | 34 | /*! Returns true if we added data */ 35 | virtual bool GetData(write_buffers_t& buffers) { 36 | throw NotImplementedException("GetFixedSize()"); 37 | } 38 | 39 | virtual void PushData(DataWriter& writer) { 40 | throw NotImplementedException("GetFixedSize()"); 41 | } 42 | 43 | // For unit testing 44 | virtual std::string GetCopyOfData() const { 45 | return {}; 46 | } 47 | 48 | /*! Set the body up for a new run. 49 | * 50 | * This is typically done if request fails and the client wants 51 | * to re-try. 52 | */ 53 | virtual void Reset() { 54 | ; 55 | } 56 | 57 | /*! Create a body with a string in it */ 58 | static std::unique_ptr CreateStringBody(std::string body); 59 | 60 | /*! Create a body from a file 61 | * 62 | * This will effectively upload the file. 63 | */ 64 | static std::unique_ptr CreateFileBody( 65 | boost::filesystem::path path); 66 | }; 67 | 68 | } // restc_cpp 69 | 70 | #endif // RESTC_CPP_BODY_H_ 71 | -------------------------------------------------------------------------------- /include/restc-cpp/DataReader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef RESTC_CPP_DATA_READER_H_ 4 | #define RESTC_CPP_DATA_READER_H_ 5 | 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "restc-cpp.h" 13 | #include "error.h" 14 | 15 | namespace restc_cpp { 16 | 17 | class Connection; 18 | class Context; 19 | class DataReaderStream; 20 | 21 | /*! Generic IO interface to read data from the server. 22 | * 23 | * This interface allows us to add functionality, such as 24 | * chunked response handling and decompression simply by 25 | * inserting readers into the chain. 26 | * 27 | * Each reader will perform some specialized actions, 28 | * and fetch data from the next one when needed. The last 29 | * reader in the chain will be the one that reads data 30 | * from the network. 31 | */ 32 | class DataReader { 33 | public: 34 | 35 | struct ReadConfig { 36 | int msReadTimeout = 0; 37 | }; 38 | 39 | 40 | using ptr_t = std::unique_ptr; 41 | using add_header_fn_t = std::function; 42 | 43 | DataReader() = default; 44 | virtual ~DataReader() = default; 45 | 46 | virtual bool IsEof() const = 0; 47 | virtual boost_const_buffer ReadSome() = 0; 48 | virtual void Finish() = 0; // Make sure there are no pending data for the current request 49 | 50 | static ptr_t CreateIoReader(const Connection::ptr_t& conn, 51 | Context& ctx, const ReadConfig& cfg); 52 | static ptr_t CreateGzipReader(std::unique_ptr&& source); 53 | static ptr_t CreateZipReader(std::unique_ptr&& source); 54 | static ptr_t CreatePlainReader(size_t contentLength, ptr_t&& source); 55 | static ptr_t CreateChunkedReader(add_header_fn_t, std::unique_ptr&& source); 56 | static ptr_t CreateNoBodyReader(); 57 | }; 58 | 59 | } // namespace 60 | 61 | #endif // RESTC_CPP_DATA_READER_H_ 62 | -------------------------------------------------------------------------------- /conan/Readme.md: -------------------------------------------------------------------------------- 1 | # Experimental support for the conan package manager 2 | 3 | Please see the [Conan home page](https://conan.io/) for information regarding conan. 4 | 5 | ## Status 6 | Currently, I am only experimenting with Conan under Windows 10 with Visual Studio 2017 Community edition 7 | 8 | Conan is not as mature as I hoped (or at least not the packages). At the moment, 9 | I am unable to build the dependencies (openssl, zlib and boost). 10 | 11 | I'll see if I can get it to work reliable. If not, I'll remove the conan support for now. 12 | 13 | ## How to build restc-cpp with conan 14 | 15 | Requirements 16 | - Visual Studio 2015, updated to the latest version 17 | - Git (with bash shell) 18 | - cmake (intalled with cmake in the Path) 19 | 20 | 21 | Requirements (for conan) 22 | - Python 2.7 (I used native python 2.7.13, 64 bit). The OpenSSL conan builder does not work with Python 3. 23 | - Perl (I use ActivePerl 5.24, installed with perl inthe path) 24 | 25 | To install conan 26 | ```sh 27 | pip install conan 28 | ``` 29 | 30 | Clone restc-cpp from github. 31 | 32 | Build dependencies and restc-cpp 33 | 34 | From the git shell 35 | ```sh 36 | git submodule init 37 | git submodule update 38 | ``` 39 | 40 | From the Visual Studio command prompt 41 | ```sh 42 | build_externals_vc2015.bat 43 | .\conan\build_with_conan_vc2015.bat 44 | ``` 45 | 46 | ## Testing with docker installed on your Windows machine 47 | 48 | From the git bash shell, in the projects root directory 49 | ```sh 50 | create-and-run-containers.sh 51 | ./tests/run-tests.sh build/bin 52 | ``` 53 | 54 | ## Testing with docker somewhere else 55 | 56 | Start the containers on your Docker host. 57 | For example, if you run Windows in a Virtual Box under Linux, and you have 58 | Docker installed on the host machine, then you can run the containers there and 59 | then issue the following commands in the git bash shell in Windows: 60 | 61 | ```sh 62 | export RESTC_CPP_TEST_DOCKER_ADDRESS=10.0.2.2 63 | ./tests/run-tests.sh build/bin 64 | 65 | ``` 66 | 67 | The RESTC_CPP_TEST_DOCKER_ADDRESS environment variable specifies the IP address or 68 | hostname to your Docker host, where the containers are available 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/IoWriterImpl.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "restc-cpp/restc-cpp.h" 3 | #include "restc-cpp/Connection.h" 4 | #include "restc-cpp/Socket.h" 5 | #include "restc-cpp/DataWriter.h" 6 | #include "restc-cpp/logging.h" 7 | #include "restc-cpp/IoTimer.h" 8 | 9 | using namespace std; 10 | 11 | namespace restc_cpp { 12 | 13 | 14 | class IoWriterImpl : public DataWriter { 15 | public: 16 | IoWriterImpl(const Connection::ptr_t& conn, Context& ctx, 17 | const WriteConfig& cfg) 18 | : ctx_{ctx}, cfg_{cfg}, connection_{conn} 19 | { 20 | } 21 | 22 | void WriteDirect(::restc_cpp::boost_const_buffer buffers) override { 23 | Write(buffers); 24 | } 25 | 26 | void Write(::restc_cpp::boost_const_buffer buffers) override { 27 | 28 | { 29 | auto timer = IoTimer::Create("IoWriterImpl", 30 | cfg_.msWriteTimeout, 31 | connection_); 32 | 33 | connection_->GetSocket().AsyncWrite(buffers, ctx_.GetYield()); 34 | } 35 | 36 | const auto bytes = boost::asio::buffer_size(buffers); 37 | 38 | RESTC_CPP_LOG_TRACE_("Wrote #" << bytes 39 | << " bytes to " << connection_); 40 | } 41 | 42 | void Write(const write_buffers_t& buffers) override { 43 | 44 | { 45 | auto timer = IoTimer::Create("IoWriterImpl", 46 | cfg_.msWriteTimeout, 47 | connection_); 48 | 49 | connection_->GetSocket().AsyncWrite(buffers, ctx_.GetYield()); 50 | } 51 | 52 | const auto bytes = boost::asio::buffer_size(buffers); 53 | 54 | RESTC_CPP_LOG_TRACE_("Wrote #" << bytes 55 | << " bytes to " << connection_); 56 | } 57 | 58 | void Finish() override { 59 | ; 60 | } 61 | 62 | void SetHeaders(Request::headers_t & /*headers*/) override { ; } 63 | 64 | private: 65 | Context& ctx_; 66 | WriteConfig cfg_; 67 | const Connection::ptr_t& connection_; 68 | }; 69 | 70 | 71 | 72 | DataWriter::ptr_t 73 | DataWriter::CreateIoWriter(const Connection::ptr_t& conn, Context& ctx, 74 | const WriteConfig& cfg) { 75 | return make_unique(conn, ctx, cfg); 76 | } 77 | 78 | } // namespace 79 | 80 | 81 | -------------------------------------------------------------------------------- /include/restc-cpp/helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef RESTC_CPP_HELPER_H_ 4 | #define RESTC_CPP_HELPER_H_ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #if defined(_WIN32) || defined(_WIN64) 14 | #define strcasecmp _stricmp 15 | #define strncasecmp _strnicmp 16 | #endif 17 | 18 | namespace restc_cpp { 19 | 20 | // recommended in Meyers, Effective STL when internationalization and embedded 21 | // NULLs aren't an issue. Much faster than the STL or Boost lex versions. 22 | // Source: http://stackoverflow.com/questions/1801892/making-mapfind-operation-case-insensitive 23 | struct ciLessLibC { 24 | bool operator()(const std::string &lhs, const std::string &rhs) const { 25 | return strcasecmp(lhs.c_str(), rhs.c_str()) < 0 ; 26 | } 27 | }; 28 | 29 | struct ciEqLibC { 30 | bool operator()(const std::string &lhs, const std::string &rhs) const { 31 | return strcasecmp(lhs.c_str(), rhs.c_str()) == 0 ; 32 | } 33 | }; 34 | 35 | /*! Merge map dst into map src if dst != nullptr */ 36 | template 37 | void merge_map(const boost::optional src, T& dst) { 38 | if (src) { 39 | for(const auto& it : *src) { 40 | dst.erase(it.first); 41 | dst.insert(it); 42 | } 43 | } 44 | } 45 | 46 | 47 | // TODO: See if I can get the buffers from the stream object and 48 | // map it directly to asio buffers, single or multiple fragments. 49 | class ToBuffer 50 | { 51 | public: 52 | ToBuffer(const std::ostringstream& stream) 53 | : buf_{stream.str()} 54 | { 55 | } 56 | 57 | ToBuffer(std::string&& str) 58 | : buf_{std::move(str)} 59 | { 60 | } 61 | 62 | #if BOOST_VERSION >= 107000 63 | // For Boost 1.70 and newer 64 | operator boost::asio::const_buffer() const { 65 | return {buf_.data(), buf_.size()}; 66 | } 67 | #else 68 | // For Boost versions older than 1.70 69 | operator boost::asio::const_buffers_1() const { 70 | return {buf_.c_str(), buf_.size()}; 71 | } 72 | #endif 73 | 74 | operator const boost::string_ref() const { 75 | return buf_; 76 | } 77 | 78 | private: 79 | const std::string buf_; 80 | }; 81 | 82 | 83 | 84 | } // restc_cpp 85 | 86 | #endif // RESTC_CPP_HELPER_H_ 87 | -------------------------------------------------------------------------------- /include/restc-cpp/DataReaderStream.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef RESTC_CPP_DATA_READER_STREAM_H_ 4 | #define RESTC_CPP_DATA_READER_STREAM_H_ 5 | 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "restc-cpp.h" 13 | #include "DataReader.h" 14 | 15 | namespace restc_cpp { 16 | 17 | /*! Helper class for reading HTTP headers and chunk headers 18 | * 19 | * This class allows us to treat the data-source as both a stream 20 | * and buffer for maximum flexibility and performance. 21 | */ 22 | class DataReaderStream : public DataReader { 23 | public: 24 | 25 | DataReaderStream(std::unique_ptr&& source); 26 | 27 | bool IsEof() const override { 28 | return eof_; 29 | } 30 | 31 | void Finish() override { 32 | if (source_) 33 | source_->Finish(); 34 | } 35 | 36 | /*! Read whatever we have buffered or can get downstream */ 37 | boost_const_buffer ReadSome() override; 38 | 39 | /*! Read up to maxBytes from whatever we have buffered or can get downstream.*/ 40 | boost_const_buffer GetData(size_t maxBytes); 41 | 42 | /*! Get one char 43 | * 44 | * \exception ParseException on end of stream 45 | */ 46 | char Getc() { 47 | if (eof_) { 48 | throw ParseException("Getc(): EOF"); 49 | } 50 | 51 | Fetch(); 52 | 53 | ++getc_bytes_; 54 | return *curr_; 55 | } 56 | 57 | void Ungetc() { 58 | assert(curr_ != nullptr); 59 | --curr_; 60 | --getc_bytes_; 61 | } 62 | 63 | void SetEof(); 64 | 65 | char GetCurrentCh() const { 66 | assert(curr_ != nullptr); 67 | assert(curr_ <= end_); 68 | return *curr_; 69 | } 70 | 71 | /*! Read standard HTTP headers as a stream until we get an empty line 72 | * 73 | * The next buffer-position will be at the start of the body 74 | */ 75 | void ReadServerResponse(Reply::HttpResponse& response); 76 | void ReadHeaderLines(const add_header_fn_t& addHeader); 77 | 78 | private: 79 | void Fetch(); 80 | std::string GetHeaderValue(); 81 | 82 | bool eof_ = false; 83 | const char *curr_ = nullptr; 84 | const char *end_ = nullptr; 85 | size_t getc_bytes_ = 0; 86 | DataReader::ptr_t source_; 87 | size_t num_headers_ = 0; 88 | }; 89 | 90 | } // namespace 91 | 92 | #endif // RESTC_CPP_DATA_READER_STREAM_H_ 93 | 94 | -------------------------------------------------------------------------------- /include/restc-cpp/DataWriter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef RESTC_CPP_DATA_WRITER_H_ 4 | #define RESTC_CPP_DATA_WRITER_H_ 5 | 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "restc-cpp.h" 13 | #include "error.h" 14 | 15 | namespace restc_cpp { 16 | 17 | class Connection; 18 | class Context; 19 | 20 | 21 | /*! Generic IO interface to write data from the server. 22 | * 23 | * This interface allows us to add functionality, such as 24 | * chunked response handling and decompression simply by 25 | * inserting writers into the chain. 26 | * 27 | * Each writer will perform some specialized actions, 28 | * and write data to the next one when required. The last 29 | * writer in the chain will be the one that writes data 30 | * to the network. 31 | */ 32 | class DataWriter { 33 | public: 34 | using ptr_t = std::unique_ptr; 35 | 36 | struct WriteConfig { 37 | int msWriteTimeout = 0; 38 | }; 39 | 40 | /*! Allows the user to set the headers in the chunked trailer if required */ 41 | using add_header_fn_t = std::function; 42 | 43 | DataWriter() = default; 44 | virtual ~DataWriter() = default; 45 | 46 | /*! Write some data */ 47 | virtual void Write(boost_const_buffer buffers) = 0; 48 | 49 | /*! Write without altering the data (headers) */ 50 | virtual void WriteDirect(boost_const_buffer buffers) = 0; 51 | 52 | /*! Write some data */ 53 | virtual void Write(const write_buffers_t& buffers) = 0; 54 | 55 | /*! Called when all data is written to flush the buffers */ 56 | virtual void Finish() = 0; 57 | 58 | /*! Set the headers required by the writer. 59 | * 60 | * For example, the plain data writer will set the content-length, 61 | * while the chunked writer will set the header for chunked 62 | * data. 63 | */ 64 | virtual void SetHeaders(Request::headers_t& headers) = 0; 65 | 66 | static ptr_t CreateIoWriter(const Connection::ptr_t& conn, Context& ctx, 67 | const WriteConfig& cfg); 68 | static ptr_t CreateGzipWriter(std::unique_ptr&& source); 69 | static ptr_t CreateZipWriter(std::unique_ptr&& source); 70 | static ptr_t CreatePlainWriter(size_t contentLength, ptr_t&& source); 71 | static ptr_t CreateChunkedWriter(add_header_fn_t, ptr_t&& source); 72 | static ptr_t CreateNoBodyWriter(); 73 | }; 74 | 75 | } // namespace 76 | 77 | #endif // RESTC_CPP_DATA_WRITER_H_ 78 | -------------------------------------------------------------------------------- /cmake_scripts/install.cmake: -------------------------------------------------------------------------------- 1 | include(GNUInstallDirs) 2 | install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Targets 3 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 4 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 5 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 6 | INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} 7 | PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} 8 | ) 9 | install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 10 | 11 | include(GenerateExportHeader) 12 | generate_export_header(${PROJECT_NAME} 13 | EXPORT_MACRO_NAME EXPORT 14 | NO_EXPORT_MACRO_NAME NO_EXPORT 15 | PREFIX_NAME RESTC_CPP_ 16 | EXPORT_FILE_NAME ${CMAKE_BINARY_DIR}/include-exports/${PROJECT_NAME}/export.h) 17 | target_include_directories(${PROJECT_NAME} 18 | PUBLIC $ $ 19 | ) 20 | install(DIRECTORY ${CMAKE_BINARY_DIR}/include-exports/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 21 | 22 | include(CMakePackageConfigHelpers) 23 | set_property(TARGET ${PROJECT_NAME} PROPERTY VERSION ${PROJECT_VERSION}) 24 | set_property(TARGET ${PROJECT_NAME} PROPERTY SOVERSION ${PROJECT_VERSION_MAJOR}) 25 | set_property(TARGET ${PROJECT_NAME} PROPERTY INTERFACE_${PROJECT_NAME}_MAJOR_VERSION ${PROJECT_VERSION_MAJOR}) 26 | set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY COMPATIBLE_INTERFACE_STRING ${PROJECT_VERSION_MAJOR}) 27 | write_basic_package_version_file( 28 | "${CMAKE_BINARY_DIR}/CMakePackage/${PROJECT_NAME}ConfigVersion.cmake" 29 | VERSION ${PROJECT_VERSION} 30 | COMPATIBILITY SameMajorVersion 31 | ) 32 | export(EXPORT ${PROJECT_NAME}Targets 33 | FILE "${CMAKE_BINARY_DIR}/CMakePackage/${PROJECT_NAME}.cmake" 34 | ) 35 | SET(CONFIG_SOURCE_DIR ${CMAKE_SOURCE_DIR}) 36 | SET(CONFIG_DIR ${CMAKE_BINARY_DIR}) 37 | SET(${PROJECT_NAME}_INCLUDE_DIR "\${${PROJECT_NAME}_SOURCE_DIR}/include") 38 | configure_package_config_file(${CMAKE_SOURCE_DIR}/cmake_scripts/${PROJECT_NAME}Config.cmake.in 39 | "${CMAKE_BINARY_DIR}/CMakePackage/${PROJECT_NAME}Config.cmake" 40 | INSTALL_DESTINATION lib/cmake/${PROJECT_NAME} 41 | PATH_VARS ${PROJECT_NAME}_INCLUDE_DIR) 42 | install(EXPORT ${PROJECT_NAME}Targets 43 | FILE ${PROJECT_NAME}.cmake 44 | DESTINATION lib/cmake/${PROJECT_NAME} 45 | ) 46 | install( 47 | FILES 48 | "${CMAKE_BINARY_DIR}/CMakePackage/${PROJECT_NAME}Config.cmake" 49 | "${CMAKE_BINARY_DIR}/CMakePackage/${PROJECT_NAME}ConfigVersion.cmake" 50 | DESTINATION lib/cmake/${PROJECT_NAME} 51 | COMPONENT Devel 52 | ) 53 | -------------------------------------------------------------------------------- /src/RequestBodyFileImpl.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include "restc-cpp/restc-cpp.h" 6 | #include "restc-cpp/logging.h" 7 | #include "restc-cpp/RequestBody.h" 8 | #include "restc-cpp/DataWriter.h" 9 | 10 | using namespace std; 11 | 12 | 13 | namespace restc_cpp { 14 | namespace impl { 15 | 16 | // TODO: Change to memory mapped IO 17 | 18 | class RequestBodyFileImpl : public RequestBody 19 | { 20 | public: 21 | RequestBodyFileImpl(boost::filesystem::path path) 22 | : path_{std::move(path)} 23 | , size_{boost::filesystem::file_size(path_)} 24 | { 25 | file_ = make_unique(path_.string(), ios::binary); 26 | } 27 | 28 | [[nodiscard]] Type GetType() const noexcept override { return Type::FIXED_SIZE; } 29 | 30 | [[nodiscard]] uint64_t GetFixedSize() const override { return size_; } 31 | 32 | bool GetData(write_buffers_t & buffers) override { 33 | const auto bytes_left = size_ - bytes_read_; 34 | if (bytes_left == 0) { 35 | eof_ = true; 36 | file_.reset(); 37 | RESTC_CPP_LOG_DEBUG_("Successfully uploaded file " 38 | << path_ 39 | << " of size " << size_ << " bytes."); 40 | return false; 41 | } 42 | 43 | auto want_bytes = min(buffer_.size(), static_cast(bytes_left)); 44 | file_->read(buffer_.data(), want_bytes); 45 | const auto read_this_time = static_cast(file_->gcount()); 46 | if (read_this_time == 0) { 47 | const auto err = errno; 48 | throw IoException(string{"file read failed: "} 49 | + to_string(err) + " " + std::system_category().message(err)); 50 | } 51 | 52 | bytes_read_ += read_this_time; 53 | buffers.emplace_back(buffer_.data(), read_this_time); 54 | return true; 55 | } 56 | 57 | void Reset() override { 58 | eof_ = false; 59 | bytes_read_ = 0; 60 | file_->clear(); 61 | file_->seekg(0); 62 | } 63 | 64 | private: 65 | bool eof_ = false; 66 | boost::filesystem::path path_; 67 | unique_ptr file_; 68 | uint64_t bytes_read_ = 0; 69 | const uint64_t size_; 70 | static constexpr size_t buffer_size_ = 1024 * 8; 71 | array buffer_ = {}; 72 | }; 73 | 74 | 75 | } // impl 76 | 77 | unique_ptr RequestBody::CreateFileBody( 78 | boost::filesystem::path path) { 79 | 80 | return make_unique(std::move(path)); 81 | } 82 | 83 | } // restc_cpp 84 | 85 | 86 | -------------------------------------------------------------------------------- /tests/functional/HttpsTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Include before boost::log headers 4 | #include "restc-cpp/logging.h" 5 | 6 | #include "restc-cpp/restc-cpp.h" 7 | #include "restc-cpp/RequestBuilder.h" 8 | 9 | #include "gtest/gtest.h" 10 | #include "restc-cpp/test_helper.h" 11 | 12 | using namespace std; 13 | using namespace restc_cpp; 14 | 15 | 16 | // C++ structure that match the Json entries received 17 | // from http://jsonplaceholder.typicode.com/posts/{id} 18 | struct Post { 19 | int userId = 0; 20 | int id = 0; 21 | string title; 22 | string body; 23 | }; 24 | 25 | // Add C++ reflection to the Post structure. 26 | // This allows our Json (de)serialization to do it's magic. 27 | BOOST_FUSION_ADAPT_STRUCT( 28 | Post, 29 | (int, userId) 30 | (int, id) 31 | (string, title) 32 | (string, body) 33 | ) 34 | 35 | 36 | // https://jsonplaceholder.typicode.com does not work with boost::asio 37 | // with tls on Fedora 25. It seems to be a problem with incompatible 38 | // cipher suites. I have to try with different versions of openssl/libressl 39 | // to see if it is a Fedora thing or a more general problem. 40 | // Anyway, I plan to refactor the tls code as a general DataReader 41 | // to get better control and more flexibility about what crypto 42 | // library to use (boost::asio only support openssl and compatible 43 | // libraries out of the box). 44 | 45 | //string https_url = "https://jsonplaceholder.typicode.com/posts/1"; 46 | 47 | string https_url = "https://lastviking.eu/files/api"; 48 | 49 | TEST(Https, GetJson) { 50 | shared_ptr const tls_ctx = make_shared( 51 | boost::asio::ssl::context{boost::asio::ssl::context::tlsv12_client}); 52 | 53 | EXPECT_NO_THROW(tls_ctx->set_options(boost::asio::ssl::context::default_workarounds 54 | | boost::asio::ssl::context::no_sslv2 55 | | boost::asio::ssl::context::no_sslv3 56 | // | boost::asio::ssl::context::no_tlsv1_1 57 | | boost::asio::ssl::context::single_dh_use); 58 | ); 59 | 60 | auto client = RestClient::Create(tls_ctx); 61 | auto f = client->ProcessWithPromise([&](Context& ctx) { 62 | Post post; 63 | auto reply = ctx.Get(https_url); 64 | EXPECT_HTTP_OK(reply->GetResponseCode()); 65 | SerializeFromJson(post, *reply); 66 | EXPECT_EQ(1, post.id); 67 | }); 68 | 69 | EXPECT_NO_THROW(f.get()); 70 | } 71 | 72 | int main(int argc, char * argv[]) 73 | { 74 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 75 | ::testing::InitGoogleTest(&argc, argv); 76 | return RUN_ALL_TESTS();; 77 | } 78 | -------------------------------------------------------------------------------- /tests/functional/RedirectTests.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Include before boost::log headers 4 | #include "restc-cpp/restc-cpp.h" 5 | #include "restc-cpp/logging.h" 6 | #include "restc-cpp/ConnectionPool.h" 7 | 8 | #include "../src/ReplyImpl.h" 9 | 10 | #include "gtest/gtest.h" 11 | #include "restc-cpp/test_helper.h" 12 | 13 | /* These url's points to a local Docker container with nginx, linked to 14 | * a jsonserver docker container with mock data. 15 | * The scripts to build and run these containers are in the ./tests directory. 16 | */ 17 | const string http_redirect_url = "http://localhost:3001/redirect/posts"; 18 | const string http_reredirect_url = "http://localhost:3001/reredirect/posts"; 19 | const string http_redirect_loop_url = "http://localhost:3001/loop/posts"; 20 | 21 | using namespace std; 22 | using namespace restc_cpp; 23 | 24 | TEST(Redirect, NoRedirects) 25 | { 26 | Request::Properties properties; 27 | properties.maxRedirects = 0; 28 | 29 | auto rest_client = RestClient::Create(properties); 30 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 31 | 32 | EXPECT_THROW( 33 | ctx.Get(GetDockerUrl(http_redirect_url)), ConstraintException); 34 | 35 | }); 36 | 37 | EXPECT_NO_THROW(f.get()); 38 | } 39 | 40 | TEST(Redirect, SingleRedirect) 41 | { 42 | auto rest_client = RestClient::Create(); 43 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 44 | 45 | auto repl = ctx.Get(GetDockerUrl(http_redirect_url)); 46 | EXPECT_EQ(200, repl->GetResponseCode()); 47 | // Discard all data 48 | while(repl->MoreDataToRead()) { 49 | repl->GetSomeData(); 50 | } 51 | 52 | }); 53 | 54 | EXPECT_NO_THROW(f.get()); 55 | } 56 | 57 | TEST(Redirect, DoubleRedirect) 58 | { 59 | auto rest_client = RestClient::Create(); 60 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 61 | 62 | auto repl = ctx.Get(GetDockerUrl(http_reredirect_url)); 63 | EXPECT_EQ(200, repl->GetResponseCode()); 64 | // Discard all data 65 | while(repl->MoreDataToRead()) { 66 | repl->GetSomeData(); 67 | } 68 | 69 | }); 70 | 71 | EXPECT_NO_THROW(f.get()); 72 | } 73 | 74 | TEST(Redirect, RedirectLoop) 75 | { 76 | auto rest_client = RestClient::Create(); 77 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 78 | 79 | EXPECT_THROW( 80 | ctx.Get(GetDockerUrl(http_redirect_loop_url)), ConstraintException); 81 | 82 | }); 83 | 84 | EXPECT_NO_THROW(f.get()); 85 | } 86 | 87 | int main( int argc, char * argv[] ) 88 | { 89 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 90 | ::testing::InitGoogleTest(&argc, argv); 91 | return RUN_ALL_TESTS();; 92 | } 93 | 94 | -------------------------------------------------------------------------------- /tests/unit/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(unittests) 2 | 3 | macro (ADD_AND_RUN_UNITTEST testname targetname) 4 | SET_CPP_STANDARD(${targetname}) 5 | add_test(NAME ${testname} COMMAND ${targetname}) 6 | if (RESTC_CPP_AUTORUN_UNIT_TESTS) 7 | add_custom_command( 8 | TARGET ${targetname} 9 | POST_BUILD 10 | COMMENT "running ${testname}" 11 | WORKING_DIRECTORY ${CMAKE_BINARY_DIR} 12 | COMMAND ${CMAKE_CTEST_COMMAND} -C $ -R "${testname}" --output-on-failure 13 | ) 14 | endif() 15 | endmacro() 16 | 17 | 18 | add_executable(url_tests UrlTests.cpp) 19 | target_link_libraries(url_tests 20 | ${GTEST_LIBRARIES} 21 | restc-cpp 22 | ${DEFAULT_LIBRARIES} 23 | ) 24 | add_dependencies(url_tests restc-cpp ${DEPENDS_GTEST}) 25 | ADD_AND_RUN_UNITTEST(URL_UNITTESTS url_tests) 26 | 27 | 28 | # ====================================== 29 | 30 | add_executable(json_serialize_tests JsonSerializeTests.cpp) 31 | target_link_libraries(json_serialize_tests 32 | ${GTEST_LIBRARIES} 33 | restc-cpp 34 | ${DEFAULT_LIBRARIES} 35 | ) 36 | 37 | add_dependencies(json_serialize_tests restc-cpp ${DEPENDS_GTEST}) 38 | 39 | ADD_AND_RUN_UNITTEST(JSON_UNITTESTS json_serialize_tests) 40 | 41 | 42 | # ====================================== 43 | 44 | add_executable(json_iostream_tests Iostream2JsonTests.cpp) 45 | target_link_libraries(json_iostream_tests 46 | ${GTEST_LIBRARIES} 47 | restc-cpp 48 | ${DEFAULT_LIBRARIES} 49 | ) 50 | 51 | add_dependencies(json_iostream_tests restc-cpp ${DEPENDS_GTEST}) 52 | 53 | ADD_AND_RUN_UNITTEST(JSON_IOSTREAM_UNITTESTS json_iostream_tests) 54 | 55 | 56 | 57 | # ====================================== 58 | 59 | add_executable(http_reply_tests HttpReplyTests.cpp) 60 | target_link_libraries(http_reply_tests 61 | ${GTEST_LIBRARIES} 62 | restc-cpp 63 | ${DEFAULT_LIBRARIES} 64 | ) 65 | add_dependencies(http_reply_tests restc-cpp ${DEPENDS_GTEST}) 66 | ADD_AND_RUN_UNITTEST(HTTP_REPLY_UNITTESTS http_reply_tests) 67 | 68 | 69 | # ====================================== 70 | 71 | add_executable(async_sleep_tests AsyncSleepTests.cpp) 72 | target_link_libraries(async_sleep_tests 73 | ${GTEST_LIBRARIES} 74 | restc-cpp 75 | ${DEFAULT_LIBRARIES} 76 | ) 77 | add_dependencies(async_sleep_tests restc-cpp ${DEPENDS_GTEST}) 78 | ADD_AND_RUN_UNITTEST(ASYNC_SLEEP_TESTS async_sleep_tests) 79 | 80 | 81 | # ====================================== 82 | 83 | add_executable(request_builder_tests RequestBuilder.cpp) 84 | target_link_libraries(request_builder_tests 85 | ${GTEST_LIBRARIES} 86 | restc-cpp 87 | ${DEFAULT_LIBRARIES} 88 | ) 89 | add_dependencies(request_builder_tests restc-cpp ${DEPENDS_GTEST}) 90 | ADD_AND_RUN_UNITTEST(REQUEST_BUILDER_TESTS request_builder_tests) 91 | 92 | -------------------------------------------------------------------------------- /tests/functional/ProxyTests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Include before boost::log headers 4 | #include "restc-cpp/logging.h" 5 | 6 | #include "restc-cpp/restc-cpp.h" 7 | #include "restc-cpp/RequestBuilder.h" 8 | 9 | #include "gtest/gtest.h" 10 | #include "restc-cpp/test_helper.h" 11 | 12 | using namespace std; 13 | using namespace restc_cpp; 14 | 15 | 16 | static const string defunct_proxy_address = GetDockerUrl("http://localhost:0"); 17 | static const string http_proxy_address = GetDockerUrl("http://localhost:3003"); 18 | static const string socks5_proxy_address = GetDockerUrl("localhost:3004"); 19 | 20 | TEST(Proxy, FailToConnect) 21 | { 22 | Request::Properties properties; 23 | properties.proxy.type = Request::Proxy::Type::HTTP; 24 | properties.proxy.address = defunct_proxy_address; 25 | 26 | // Create the client with our configuration 27 | auto rest_client = RestClient::Create(properties); 28 | 29 | 30 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 31 | auto reply = RequestBuilder(ctx) 32 | .Get("http://api.example.com/normal/posts/1") 33 | .Execute(); 34 | }); 35 | 36 | EXPECT_ANY_THROW(f.get()); 37 | } 38 | 39 | TEST(Proxy, WithHttpProxy) 40 | { 41 | Request::Properties properties; 42 | properties.proxy.type = Request::Proxy::Type::HTTP; 43 | properties.proxy.address = http_proxy_address; 44 | 45 | // Create the client with our configuration 46 | auto rest_client = RestClient::Create(properties); 47 | 48 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 49 | auto reply = RequestBuilder(ctx) 50 | .Get("http://api.example.com/normal/posts/1") 51 | .Execute(); 52 | 53 | EXPECT_HTTP_OK(reply->GetResponseCode()); 54 | cout << "Got: " << reply->GetBodyAsString() << '\n'; 55 | }); 56 | 57 | EXPECT_NO_THROW(f.get()); 58 | } 59 | 60 | TEST(Proxy, WithSocks5Proxy) 61 | { 62 | Request::Properties properties; 63 | properties.proxy.type = Request::Proxy::Type::SOCKS5; 64 | properties.proxy.address = socks5_proxy_address; 65 | 66 | // Create the client with our configuration 67 | auto rest_client = RestClient::Create(properties); 68 | 69 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 70 | auto reply = RequestBuilder(ctx) 71 | .Get("http://api.example.com/normal/posts/1") 72 | .Execute(); 73 | 74 | EXPECT_HTTP_OK(reply->GetResponseCode()); 75 | cout << "Got: " << reply->GetBodyAsString() << '\n'; 76 | }); 77 | 78 | EXPECT_NO_THROW(f.get()); 79 | } 80 | 81 | 82 | int main( int argc, char * argv[] ) 83 | { 84 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 85 | ::testing::InitGoogleTest(&argc, argv); 86 | return RUN_ALL_TESTS();; 87 | } 88 | -------------------------------------------------------------------------------- /examples/cmdline/main.cpp: -------------------------------------------------------------------------------- 1 | // We use the example from the main readme file 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #define BOOST_LOG_DYN_LINK 1 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | using namespace std; 16 | using namespace restc_cpp; 17 | namespace logging = boost::log; 18 | 19 | // C++ structure that match the JSON entries received 20 | // from http://jsonplaceholder.typicode.com/posts/{id} 21 | struct Post { 22 | int userId = 0; 23 | int id = 0; 24 | string title; 25 | string body; 26 | }; 27 | 28 | // Since C++ does not (yet) offer reflection, we need to tell the library how 29 | // to map json members to a type. We are doing this by declaring the 30 | // structs/classes with BOOST_FUSION_ADAPT_STRUCT from the boost libraries. 31 | // This allows us to convert the C++ classes to and from JSON. 32 | 33 | BOOST_FUSION_ADAPT_STRUCT( 34 | Post, 35 | (int, userId) 36 | (int, id) 37 | (string, title) 38 | (string, body) 39 | ) 40 | 41 | // The C++ main function - the place where any adventure starts 42 | int main() { 43 | // Set the log-level to a reasonable value 44 | logging::core::get()->set_filter 45 | ( 46 | logging::trivial::severity >= logging::trivial::info 47 | ); 48 | 49 | // Create an instance of the rest client 50 | auto rest_client = RestClient::Create(); 51 | 52 | // Create and instantiate a Post from data received from the server. 53 | Post my_post = rest_client->ProcessWithPromiseT([&](Context& ctx) { 54 | // This is a co-routine, running in a worker-thread 55 | 56 | // Instantiate a Post structure. 57 | Post post; 58 | 59 | // Serialize it asynchronously. The asynchronously part does not really matter 60 | // here, but it may if you receive huge data structures. 61 | SerializeFromJson(post, 62 | 63 | // Construct a request to the server 64 | RequestBuilder(ctx) 65 | .Get("http://jsonplaceholder.typicode.com/posts/1") 66 | 67 | // Add some headers for good taste 68 | .Header("X-Client", "RESTC_CPP") 69 | .Header("X-Client-Purpose", "Testing") 70 | 71 | // Send the request 72 | .Execute()); 73 | 74 | // Return the post instance trough a C++ future<> 75 | return post; 76 | }) 77 | 78 | // Get the Post instance from the future<>, or any C++ exception thrown 79 | // within the lambda. 80 | .get(); 81 | 82 | // Print the result for everyone to see. 83 | cout << "Received post# " << my_post.id << ", title: " << my_post.title 84 | << endl; 85 | } 86 | -------------------------------------------------------------------------------- /examples/cmake_normal/main.cpp: -------------------------------------------------------------------------------- 1 | // We use the example from the main readme file 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #define BOOST_LOG_DYN_LINK 1 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | using namespace std; 16 | using namespace restc_cpp; 17 | namespace logging = boost::log; 18 | 19 | // C++ structure that match the JSON entries received 20 | // from http://jsonplaceholder.typicode.com/posts/{id} 21 | struct Post { 22 | int userId = 0; 23 | int id = 0; 24 | string title; 25 | string body; 26 | }; 27 | 28 | // Since C++ does not (yet) offer reflection, we need to tell the library how 29 | // to map json members to a type. We are doing this by declaring the 30 | // structs/classes with BOOST_FUSION_ADAPT_STRUCT from the boost libraries. 31 | // This allows us to convert the C++ classes to and from JSON. 32 | 33 | BOOST_FUSION_ADAPT_STRUCT( 34 | Post, 35 | (int, userId) 36 | (int, id) 37 | (string, title) 38 | (string, body) 39 | ) 40 | 41 | // The C++ main function - the place where any adventure starts 42 | int main() { 43 | // Set the log-level to a reasonable value 44 | logging::core::get()->set_filter 45 | ( 46 | logging::trivial::severity >= logging::trivial::info 47 | ); 48 | 49 | // Create an instance of the rest client 50 | auto rest_client = RestClient::Create(); 51 | 52 | // Create and instantiate a Post from data received from the server. 53 | Post my_post = rest_client->ProcessWithPromiseT([&](Context& ctx) { 54 | // This is a co-routine, running in a worker-thread 55 | 56 | // Instantiate a Post structure. 57 | Post post; 58 | 59 | // Serialize it asynchronously. The asynchronously part does not really matter 60 | // here, but it may if you receive huge data structures. 61 | SerializeFromJson(post, 62 | 63 | // Construct a request to the server 64 | RequestBuilder(ctx) 65 | .Get("http://jsonplaceholder.typicode.com/posts/1") 66 | 67 | // Add some headers for good taste 68 | .Header("X-Client", "RESTC_CPP") 69 | .Header("X-Client-Purpose", "Testing") 70 | 71 | // Send the request 72 | .Execute()); 73 | 74 | // Return the post instance trough a C++ future<> 75 | return post; 76 | }) 77 | 78 | // Get the Post instance from the future<>, or any C++ exception thrown 79 | // within the lambda. 80 | .get(); 81 | 82 | // Print the result for everyone to see. 83 | cout << "Received post# " << my_post.id << ", title: " << my_post.title 84 | << endl; 85 | } 86 | -------------------------------------------------------------------------------- /examples/cmake_external_project/main.cpp: -------------------------------------------------------------------------------- 1 | // We use the example from the main readme file 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #define BOOST_LOG_DYN_LINK 1 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | using namespace std; 16 | using namespace restc_cpp; 17 | namespace logging = boost::log; 18 | 19 | // C++ structure that match the JSON entries received 20 | // from http://jsonplaceholder.typicode.com/posts/{id} 21 | struct Post { 22 | int userId = 0; 23 | int id = 0; 24 | string title; 25 | string body; 26 | }; 27 | 28 | // Since C++ does not (yet) offer reflection, we need to tell the library how 29 | // to map json members to a type. We are doing this by declaring the 30 | // structs/classes with BOOST_FUSION_ADAPT_STRUCT from the boost libraries. 31 | // This allows us to convert the C++ classes to and from JSON. 32 | 33 | BOOST_FUSION_ADAPT_STRUCT( 34 | Post, 35 | (int, userId) 36 | (int, id) 37 | (string, title) 38 | (string, body) 39 | ) 40 | 41 | // The C++ main function - the place where any adventure starts 42 | int main() { 43 | // Set the log-level to a reasonable value 44 | logging::core::get()->set_filter 45 | ( 46 | logging::trivial::severity >= logging::trivial::info 47 | ); 48 | 49 | // Create an instance of the rest client 50 | auto rest_client = RestClient::Create(); 51 | 52 | // Create and instantiate a Post from data received from the server. 53 | Post my_post = rest_client->ProcessWithPromiseT([&](Context& ctx) { 54 | // This is a co-routine, running in a worker-thread 55 | 56 | // Instantiate a Post structure. 57 | Post post; 58 | 59 | // Serialize it asynchronously. The asynchronously part does not really matter 60 | // here, but it may if you receive huge data structures. 61 | SerializeFromJson(post, 62 | 63 | // Construct a request to the server 64 | RequestBuilder(ctx) 65 | .Get("http://jsonplaceholder.typicode.com/posts/1") 66 | 67 | // Add some headers for good taste 68 | .Header("X-Client", "RESTC_CPP") 69 | .Header("X-Client-Purpose", "Testing") 70 | 71 | // Send the request 72 | .Execute()); 73 | 74 | // Return the post instance trough a C++ future<> 75 | return post; 76 | }) 77 | 78 | // Get the Post instance from the future<>, or any C++ exception thrown 79 | // within the lambda. 80 | .get(); 81 | 82 | // Print the result for everyone to see. 83 | cout << "Received post# " << my_post.id << ", title: " << my_post.title 84 | << endl; 85 | } 86 | -------------------------------------------------------------------------------- /examples/logip/logip.cpp: -------------------------------------------------------------------------------- 1 | /* Program to log external IP-changes on NAT networks 2 | * 3 | * I have an ISP from Hell, and one problem I have noticed recently is that 4 | * my external IP address change a lot. To monitor this problem, I wrote this little 5 | * example program who checks the external IP every 5 minutes, and log changes. 6 | * 7 | * The program can be run in the background: 8 | * 9 | * logip >> /var/tmp/ip.log 2>&1 & 10 | */ 11 | 12 | 13 | #include 14 | #include 15 | #include "restc-cpp/logging.h" 16 | 17 | #ifdef RESTC_CPP_LOG_WITH_BOOST_LOG 18 | #include 19 | #include 20 | #include 21 | #endif 22 | 23 | #include 24 | #include 25 | 26 | #include "restc-cpp/restc-cpp.h" 27 | #include "restc-cpp/RequestBuilder.h" 28 | #include "restc-cpp/SerializeJson.h" 29 | 30 | using namespace restc_cpp; 31 | using namespace std; 32 | 33 | // Data structure returned from api.ipify.org 34 | struct Data { 35 | string ip; 36 | }; 37 | 38 | BOOST_FUSION_ADAPT_STRUCT( 39 | Data, 40 | (string, ip) 41 | ) 42 | 43 | string now() { 44 | char date[32] = {}; 45 | auto now = time(nullptr); 46 | strftime(date, sizeof(date), "%Y-%m-%d %H:%M", localtime(&now)); 47 | return date; 48 | } 49 | 50 | int main(int /*argc*/, char * /*argv*/[]) 51 | { 52 | #ifdef RESTC_CPP_LOG_WITH_BOOST_LOG 53 | namespace logging = boost::log; 54 | logging::core::get()->set_filter 55 | ( 56 | logging::trivial::severity >= logging::trivial::info 57 | ); 58 | #endif 59 | 60 | const string url = "https://api.ipify.org"; 61 | 62 | auto client = RestClient::Create(); 63 | client->Process([&](Context& ctx) { 64 | 65 | string current_ip; 66 | Data data; 67 | 68 | while(true) { 69 | bool valid = false; 70 | try { 71 | SerializeFromJson(data, RequestBuilder(ctx) 72 | .Get(url) 73 | .Argument("format", "json") 74 | .Header("X-Client", "RESTC_CPP") 75 | .Execute()); 76 | valid = true; 77 | } catch (const boost::exception& ex) { 78 | clog << now() << "Caught boost exception: " << boost::diagnostic_information(ex) 79 | << '\n'; 80 | } catch (const exception& ex) { 81 | clog << now() << "Caught exception: " << ex.what() << '\n'; 82 | } 83 | 84 | if (valid && (current_ip != data.ip)) { 85 | clog << now() << ' ' << data.ip << '\n'; 86 | current_ip = data.ip; 87 | } 88 | 89 | std::this_thread::sleep_for(std::chrono::minutes(5)); 90 | } 91 | }); 92 | 93 | 94 | return 0; 95 | } 96 | -------------------------------------------------------------------------------- /include/restc-cpp/RapidJsonReader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef RESTC_CPP_RAPID_JSON_READER_H_ 4 | #define RESTC_CPP_RAPID_JSON_READER_H_ 5 | 6 | 7 | #include 8 | 9 | #include "restc-cpp/boost_compatibility.h" 10 | #include "rapidjson/reader.h" 11 | #include "restc-cpp/restc-cpp.h" 12 | 13 | namespace restc_cpp { 14 | 15 | 16 | /*! Rapidjson input stream implementation 17 | * 18 | * Ref: https://github.com/miloyip/rapidjson/blob/master/doc/stream.md 19 | */ 20 | class RapidJsonReader { 21 | public: 22 | 23 | RapidJsonReader(Reply& reply) 24 | : reply_{reply} 25 | { 26 | Read(); 27 | } 28 | 29 | using Ch = char; 30 | 31 | //! Read the current character from stream without moving the read cursor. 32 | Ch Peek() const noexcept { 33 | if (IsEof()) { 34 | return 0; // EOF 35 | } 36 | 37 | assert(ch_ && (ch_ != end_)); 38 | return *ch_; 39 | } 40 | 41 | //! Read the current character from stream and moving the read cursor to next character. 42 | Ch Take() { 43 | if (IsEof()) { 44 | return 0; // EOF 45 | } 46 | 47 | assert(ch_ && (ch_ != end_)); 48 | const auto ch = *ch_; 49 | ++pos_; 50 | 51 | if (++ch_ == end_) { 52 | Read(); 53 | } 54 | 55 | return ch; 56 | } 57 | 58 | //! Get the current read cursor. 59 | //! \return Number of characters read from start. 60 | size_t Tell() noexcept { 61 | return pos_; 62 | } 63 | 64 | bool IsEof() const noexcept { 65 | return ch_ == nullptr; 66 | } 67 | 68 | //! Begin writing operation at the current read pointer. 69 | //! \return The begin writer pointer. 70 | Ch* PutBegin() { 71 | assert(false); 72 | return nullptr; 73 | } 74 | 75 | //! Write a character. 76 | void Put(Ch c) { 77 | assert(false); 78 | } 79 | 80 | //! Flush the buffer. 81 | void Flush() { 82 | assert (false); 83 | } 84 | 85 | //! End the writing operation. 86 | //! \param begin The begin write pointer returned by PutBegin(). 87 | //! \return Number of characters written. 88 | size_t PutEnd(Ch* begin) { 89 | assert(false); 90 | return 0; 91 | } 92 | 93 | private: 94 | void Read() { 95 | const auto buffer = reply_.GetSomeData(); 96 | 97 | const auto len = boost::asio::buffer_size(buffer); 98 | 99 | if (len) { 100 | ch_ = boost_buffer_cast(buffer); 101 | end_ = ch_ + len; 102 | } else { 103 | ch_ = end_ = nullptr; 104 | } 105 | } 106 | 107 | const char *ch_ = nullptr; 108 | const char *end_ = nullptr; 109 | size_t pos_ = 0; 110 | Reply& reply_; 111 | }; 112 | 113 | 114 | 115 | } // restc_cpp 116 | 117 | #endif //RESTC_CPP_RAPID_JSON_READER_H_ 118 | 119 | -------------------------------------------------------------------------------- /include/restc-cpp/internals/for_each_member.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * \brief Allows iteration on member name and values of a Fusion adapted struct. 5 | * 6 | * 7 | * BOOST_FUSION_ADAPT_STRUCT(ns::point, 8 | * (int, x) 9 | * (int, y) 10 | * (int, z)); 11 | * 12 | * template 13 | * print_name_and_value(const char* name, T& value) const { 14 | * std::cout << name << "=" << value << std::endl; 15 | * } 16 | * 17 | * 18 | * int main(void) { 19 | * 20 | * ns::point mypoint; 21 | * 22 | * 23 | * boost::boost::fusion::for_each_member(mypoint, &print_name_and_value); 24 | * 25 | * 26 | * } 27 | * 28 | * ******** 29 | * Credits to Damien Buhl 30 | * https://gist.github.com/daminetreg/6b539973817ed8f4f87d 31 | */ 32 | #ifndef BOOST_FUSION_FOR_EACH_MEMBER_HPP 33 | #define BOOST_FUSION_FOR_EACH_MEMBER_HPP 34 | 35 | #include 36 | 37 | #include 38 | 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | 49 | namespace restc_cpp { 50 | namespace detail { 51 | 52 | template 53 | inline void 54 | for_each_member_linear(First const& first, 55 | Last const& last, 56 | F const& f, 57 | boost::mpl::true_) 58 | { 59 | } 60 | 61 | template 62 | inline void 63 | for_each_member_linear(First const& first, 64 | Last const& last, 65 | F const& f, 66 | boost::mpl::false_) { 67 | 68 | using type_t = typename std::remove_const::type>::type; 69 | 70 | auto name = boost::fusion::extension::struct_member_name::call(); 71 | 72 | f(name, *first); 73 | 74 | for_each_member_linear( 75 | next(first), 76 | last, 77 | f, 78 | boost::fusion::result_of::equal_to< typename boost::fusion::result_of::next::type, Last>() 79 | ); 80 | } 81 | 82 | template 83 | inline void 84 | for_each_member(Sequence& seq, F const& f) { 85 | 86 | detail::for_each_member_linear( 87 | boost::fusion::begin(seq), 88 | boost::fusion::end(seq), 89 | f, 90 | boost::fusion::result_of::equal_to< 91 | typename boost::fusion::result_of::begin::type, 92 | typename boost::fusion::result_of::end::type>() 93 | ); 94 | } 95 | 96 | } // detail 97 | 98 | template 99 | inline void 100 | for_each_member(Sequence& seq, F f) { 101 | detail::for_each_member(seq, f); 102 | } 103 | 104 | } // restc_cpp 105 | 106 | #endif 107 | -------------------------------------------------------------------------------- /src/ReplyImpl.h: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "restc-cpp/restc-cpp.h" 13 | #include "restc-cpp/Socket.h" 14 | #include "restc-cpp/IoTimer.h" 15 | #include "restc-cpp/DataReader.h" 16 | 17 | using namespace std; 18 | 19 | namespace restc_cpp { 20 | 21 | class ReplyImpl : public Reply { 22 | public: 23 | enum class ChunkedState 24 | { NOT_CHUNKED, GET_SIZE, IN_SEGMENT, IN_TRAILER, DONE }; 25 | 26 | ReplyImpl(Connection::ptr_t connection, Context& ctx, 27 | RestClient& owner, Request::Properties::ptr_t& properties, 28 | Request::Type type); 29 | 30 | ReplyImpl(Connection::ptr_t connection, Context& ctx, 31 | RestClient& owner, Request::Type type); 32 | 33 | ~ReplyImpl(); 34 | 35 | boost::optional GetHeader(const string& name) override; 36 | std::deque GetHeaders(const std::string& name) override; 37 | 38 | void StartReceiveFromServer(DataReader::ptr_t&& reader); 39 | 40 | int GetResponseCode() const override { 41 | return response_.status_code; 42 | } 43 | 44 | const HttpResponse& GetHttpResponse() const override { 45 | return response_; 46 | } 47 | 48 | boost_const_buffer GetSomeData() override; 49 | 50 | string GetBodyAsString(size_t maxSize 51 | = RESTC_CPP_SANE_DATA_LIMIT) override; 52 | 53 | void fetchAndIgnore() override; 54 | 55 | bool MoreDataToRead() override { 56 | return !IsEof(); 57 | } 58 | 59 | boost::uuids::uuid GetConnectionId() const override { 60 | return connection_id_; 61 | } 62 | 63 | static std::unique_ptr 64 | Create(Connection::ptr_t connection, 65 | Context& ctx, 66 | RestClient& owner, 67 | Request::Properties::ptr_t& properties, 68 | Request::Type type); 69 | 70 | static boost::string_ref b2sr(boost_const_buffer buffer) { 71 | return { boost_buffer_cast(buffer), 72 | boost::asio::buffer_size(buffer)}; 73 | } 74 | 75 | bool IsEof() const { 76 | return !reader_ || reader_->IsEof(); 77 | } 78 | 79 | 80 | protected: 81 | void CheckIfWeAreDone(); 82 | void ReleaseConnection(); 83 | void HandleDecompression(); 84 | void HandleContentType(std::unique_ptr&& stream); 85 | void HandleConnectionLifetime(); 86 | 87 | Connection::ptr_t connection_; 88 | Context& ctx_; 89 | Request::Properties::ptr_t properties_; 90 | RestClient& owner_; 91 | Reply::HttpResponse response_; 92 | headers_t headers_; 93 | bool do_close_connection_ = false; 94 | boost::optional content_length_; 95 | const boost::uuids::uuid connection_id_; 96 | std::unique_ptr reader_; 97 | const Request::Type request_type_; 98 | }; 99 | 100 | 101 | } // restc_cpp 102 | 103 | -------------------------------------------------------------------------------- /src/ChunkedWriterImpl.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include "restc-cpp/restc-cpp.h" 6 | #include "restc-cpp/Connection.h" 7 | #include "restc-cpp/Socket.h" 8 | #include "restc-cpp/DataWriter.h" 9 | #include "restc-cpp/logging.h" 10 | 11 | using namespace std; 12 | 13 | namespace restc_cpp { 14 | 15 | 16 | class ChunkedWriterImpl : public DataWriter { 17 | public: 18 | ChunkedWriterImpl(add_header_fn_t fn, ptr_t&& source) 19 | : next_{std::move(source)}, add_header_fn_{std::move(fn)} 20 | { 21 | } 22 | 23 | void WriteDirect(::restc_cpp::boost_const_buffer buffers) override { 24 | next_->WriteDirect(buffers); 25 | } 26 | 27 | void Write(::restc_cpp::boost_const_buffer buffers) override { 28 | const auto len = boost::asio::buffer_size(buffers); 29 | buffers_.resize(2); 30 | buffers_[1] = buffers; 31 | DoWrite(len); 32 | } 33 | 34 | void Write(const write_buffers_t& buffers) override { 35 | const auto len = boost::asio::buffer_size(buffers); 36 | buffers_.resize(1); 37 | std::copy(buffers.begin(), buffers.end(), std::back_inserter(buffers_)); 38 | DoWrite(len); 39 | } 40 | 41 | void Finish() override { 42 | static const std::string crlfx2{"\r\n\r\n"}; 43 | std::string finito = {"\r\n0"}; 44 | 45 | if (add_header_fn_) { 46 | finito += add_header_fn_(); 47 | } 48 | 49 | finito += crlfx2; 50 | 51 | next_->Write({finito.c_str(), finito.size()}); 52 | 53 | next_->Finish(); 54 | } 55 | 56 | void SetHeaders(Request::headers_t& headers) override { 57 | static const string transfer_encoding{"Transfer-Encoding"}; 58 | static const string chunked{"chunked"}; 59 | 60 | headers[transfer_encoding] = chunked; 61 | 62 | next_->SetHeaders(headers); 63 | } 64 | 65 | private: 66 | // Set the chunk header and send the data 67 | void DoWrite(const size_t len) { 68 | if (len == 0) { 69 | return; 70 | } 71 | 72 | if (len > RESTC_CPP_MAX_INPUT_BUFFER_LENGTH) { 73 | throw ConstraintException("Input buffer is too large"); 74 | } 75 | 76 | // The data part of buffers_ must be properly initialized 77 | assert(buffers_.size() > 1); 78 | std::ostringstream hdr; 79 | 80 | if (first_) { 81 | first_ = false; 82 | } else { 83 | hdr << '\r' << '\n'; 84 | } 85 | 86 | hdr << hex << len << '\r' << '\n'; 87 | const auto header = hdr.str(); 88 | 89 | buffers_[0] = {header.c_str(), header.size()}; 90 | next_->Write(buffers_); 91 | } 92 | 93 | bool first_ = true; 94 | unique_ptr next_; 95 | write_buffers_t buffers_; 96 | add_header_fn_t add_header_fn_; 97 | }; 98 | 99 | 100 | DataWriter::ptr_t 101 | DataWriter::CreateChunkedWriter(add_header_fn_t fn, ptr_t&& source) { 102 | return make_unique(std::move(fn), std::move(source)); 103 | } 104 | 105 | } // namespace 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /cmake_scripts/external-projects.cmake: -------------------------------------------------------------------------------- 1 | # Here are registered all external projects 2 | # 3 | # Usage: 4 | # add_dependencies(TARGET externalProjectName) 5 | # target_link_libraries(TARGET PRIVATE ExternalLibraryName) 6 | 7 | include(GNUInstallDirs) 8 | include(ExternalProject) 9 | 10 | set(EXTERNAL_PROJECTS_PREFIX ${CMAKE_BINARY_DIR}/external-projects) 11 | set(EXTERNAL_PROJECTS_INSTALL_PREFIX ${EXTERNAL_PROJECTS_PREFIX}/installed) 12 | set(RESTC_EXTERNAL_INSTALLED_LIB_DIR ${EXTERNAL_PROJECTS_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}) 13 | link_directories(${RESTC_EXTERNAL_INSTALLED_LIB_DIR}) 14 | 15 | if (restc_cpp_add_rapidjson) 16 | ExternalProject_Add( 17 | externalRapidJson 18 | PREFIX "${EXTERNAL_PROJECTS_PREFIX}" 19 | GIT_REPOSITORY "https://github.com/Tencent/rapidjson.git" 20 | GIT_TAG "master" 21 | CONFIGURE_COMMAND "" 22 | BUILD_COMMAND "" 23 | INSTALL_COMMAND "" 24 | LOG_DOWNLOAD ON 25 | LOG_INSTALL ON 26 | ) 27 | 28 | set(EXTERNAL_RAPIDJSON_INCLUDE_DIR ${EXTERNAL_PROJECTS_PREFIX}/src/externalRapidJson/include/rapidjson) 29 | 30 | message(STATUS "EXTERNAL_RAPIDJSON_INCLUDE_DIR: ${EXTERNAL_RAPIDJSON_INCLUDE_DIR}") 31 | 32 | include_directories(${EXTERNAL_PROJECTS_PREFIX}/src/externalRapidJson/include) 33 | 34 | if (INSTALL_RAPIDJSON_HEADERS ) 35 | install(DIRECTORY ${EXTERNAL_RAPIDJSON_INCLUDE_DIR} DESTINATION include) 36 | endif() 37 | endif() 38 | 39 | if (restc_cpp_add_logfault) 40 | ExternalProject_Add(externalLogfault 41 | PREFIX "${EXTERNAL_PROJECTS_PREFIX}" 42 | GIT_REPOSITORY "https://github.com/jgaa/logfault.git" 43 | GIT_TAG "${LOGFAULT_TAG}" 44 | CMAKE_ARGS 45 | -DCMAKE_INSTALL_PREFIX=${EXTERNAL_PROJECTS_INSTALL_PREFIX} 46 | -DCMAKE_GENERATOR=${CMAKE_GENERATOR} 47 | ) 48 | endif() 49 | 50 | include_directories(${EXTERNAL_PROJECTS_PREFIX}/installed/include) 51 | 52 | # If we compile the tests; download and install gtest if it's not found on the target 53 | # On ubuntu and debian, you can install `libgtest-dev` to avoid this step. 54 | if (RESTC_CPP_WITH_UNIT_TESTS OR RESTC_CPP_WITH_FUNCTIONALT_TESTS) 55 | find_package(GTest) 56 | if (GTest_FOUND) 57 | message("Using installed googletest") 58 | else() 59 | message("Will download and install googletest as a cmake included project") 60 | set(DEPENDS_GTEST googletest) 61 | set(GTEST_LIBRARIES gtest) 62 | message("GTEST_TAG: ${GTEST_TAG}") 63 | 64 | if (WIN32) 65 | set(GTEST_EXTRA_ARGS "-Dgtest_force_shared_crt=TRUE") 66 | endif() 67 | 68 | ExternalProject_Add(googletest 69 | GIT_TAG "${GTEST_TAG}" 70 | PREFIX "${EXTERNAL_PROJECTS_PREFIX}" 71 | GIT_REPOSITORY https://github.com/google/googletest.git 72 | CMAKE_ARGS 73 | -DCMAKE_INSTALL_PREFIX=${EXTERNAL_PROJECTS_INSTALL_PREFIX} 74 | -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} 75 | -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} 76 | -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} 77 | ${GTEST_EXTRA_ARGS} 78 | ) 79 | set(GTEST_LIB_DIR ${RESTC_EXTERNAL_INSTALLED_LIB_DIR}) 80 | endif() 81 | endif() 82 | -------------------------------------------------------------------------------- /src/Url.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include "restc-cpp/restc-cpp.h" 6 | #include "restc-cpp/Url.h" 7 | #include "restc-cpp/error.h" 8 | 9 | 10 | using namespace std; 11 | 12 | std::ostream& operator <<(std::ostream& out, 13 | const restc_cpp::Url::Protocol& protocol) { 14 | static const array names = {{"UNKNOWN", "HTTP", "HTTPS"}}; 15 | 16 | return out << names.at(static_cast(protocol)); 17 | } 18 | 19 | namespace restc_cpp { 20 | 21 | Url::Url(const char *url) 22 | { 23 | operator = (url); 24 | } 25 | 26 | Url& Url::operator = (const char *url) { 27 | constexpr auto magic_8 = 8; 28 | constexpr auto magic_7 = 7; 29 | 30 | assert(url != nullptr && "A valid URL is required"); 31 | protocol_name_ = boost::string_ref(url); 32 | if (protocol_name_.find("https://") == 0) { 33 | protocol_name_ = boost::string_ref(url, magic_8); 34 | protocol_ = Protocol::HTTPS; 35 | } else if (protocol_name_.find("http://") == 0) { 36 | protocol_name_ = boost::string_ref(url, magic_7); 37 | protocol_ = Protocol::HTTP; 38 | } else { 39 | throw ParseException("Invalid protocol in url. Must be 'http[s]://'"); 40 | } 41 | 42 | auto remains = boost::string_ref(protocol_name_.end()); 43 | const auto args_start = remains.find('?'); 44 | if (args_start != string::npos) { 45 | args_ = {remains.begin() + args_start + 1, 46 | remains.size() - (args_start + 1)}; 47 | remains = {remains.begin(), args_start}; 48 | } 49 | auto path_start = remains.find('/'); 50 | const auto port_start = remains.find(':'); 51 | if (port_start != string::npos && 52 | ( path_start == string::npos || 53 | port_start < path_start ) 54 | ) { 55 | if (remains.length() <= static_cast(port_start + 2)) { 56 | throw ParseException("Invalid host (no port after column)"); 57 | } 58 | //port_ = boost::string_ref(&remains[port_start+1]); 59 | //host_ = boost::string_ref(host_.data(), port_start); 60 | host_ = {remains.begin(), port_start}; 61 | remains = {remains.begin() + port_start + 1, remains.size() - (port_start + 1)}; 62 | 63 | if (path_start != string::npos) { 64 | //path_start = remains.find('/'); 65 | path_start -= port_start + 1; 66 | path_ = {remains.begin() + path_start, remains.size() - path_start};// &port_[path_start]; 67 | port_ = {remains.begin(), path_start}; 68 | remains = {}; 69 | } else { 70 | port_ = remains; 71 | } 72 | } else { 73 | if (path_start != string::npos) { 74 | //path_ = &host_[path_start]; 75 | //host_ = boost::string_ref(host_.data(), path_start); 76 | 77 | host_ = {remains.begin(), path_start}; 78 | path_ = {remains.begin() + host_.size(), remains.size() - host_.size()}; 79 | remains = {}; 80 | } else { 81 | host_ = remains; 82 | } 83 | } 84 | 85 | if (port_.empty()) { 86 | if (protocol_ == Protocol::HTTPS) { 87 | port_ = {"443"}; 88 | } else { 89 | port_ = {"80"}; 90 | } 91 | } 92 | 93 | return *this; 94 | } 95 | 96 | } // restc_cpp 97 | 98 | -------------------------------------------------------------------------------- /include/restc-cpp/Socket.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef RESTC_CPP_SOCKET_H_ 3 | #define RESTC_CPP_SOCKET_H_ 4 | 5 | #ifndef RESTC_CPP_H_ 6 | # error "Include restc-cpp.h first" 7 | #endif 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include "restc-cpp/boost_compatibility.h" 15 | #include "restc-cpp/typename.h" 16 | #include "restc-cpp/logging.h" 17 | 18 | #include "error.h" 19 | 20 | namespace restc_cpp { 21 | 22 | class Socket 23 | { 24 | public: 25 | enum class Reason { 26 | DONE, 27 | TIME_OUT 28 | }; 29 | 30 | using after_connect_cb_t = std::function; 31 | 32 | virtual ~Socket() = default; 33 | 34 | virtual boost::asio::ip::tcp::socket& GetSocket() = 0; 35 | 36 | virtual const boost::asio::ip::tcp::socket& GetSocket() const = 0; 37 | 38 | virtual std::size_t AsyncReadSome(boost_mutable_buffer buffers, 39 | boost::asio::yield_context& yield) = 0; 40 | 41 | virtual std::size_t AsyncRead(boost_mutable_buffer buffers, 42 | boost::asio::yield_context& yield) = 0; 43 | 44 | virtual void AsyncWrite(const boost_const_buffer& buffers, 45 | boost::asio::yield_context& yield) = 0; 46 | 47 | virtual void AsyncWrite(const write_buffers_t& buffers, 48 | boost::asio::yield_context& yield) = 0; 49 | 50 | template 51 | void AsyncWriteT(const T& buffer, boost::asio::yield_context& yield) { 52 | boost_const_buffer b{buffer.data(), buffer.size()}; 53 | AsyncWrite(b, yield); 54 | } 55 | 56 | virtual void AsyncConnect(const boost::asio::ip::tcp::endpoint& ep, 57 | const std::string &host, 58 | bool tcpNodelay, 59 | boost::asio::yield_context& yield) = 0; 60 | 61 | virtual void AsyncShutdown(boost::asio::yield_context& yield) = 0; 62 | 63 | virtual void Close(Reason reoson = Reason::DONE) = 0; 64 | 65 | virtual bool IsOpen() const noexcept = 0; 66 | 67 | friend std::ostream& operator << (std::ostream& o, const Socket& v) { 68 | return v.Print(o); 69 | } 70 | 71 | void SetAfterConnectCallback(after_connect_cb_t cb) { 72 | after_connect_cb_ = std::move(cb); 73 | } 74 | 75 | protected: 76 | virtual std::ostream& Print(std::ostream& o) const = 0; 77 | 78 | void OnAfterConnect() { 79 | if (after_connect_cb_) { 80 | after_connect_cb_(); 81 | } 82 | } 83 | 84 | after_connect_cb_t after_connect_cb_; 85 | }; 86 | 87 | 88 | class ExceptionWrapper { 89 | protected: 90 | template 91 | Tret WrapException(Tfn fn) { 92 | try { 93 | return fn(); 94 | } catch (const boost::system::system_error& ex) { 95 | 96 | RESTC_CPP_LOG_TRACE_("ExceptionWrapper: " << ex.what() 97 | << ", value=" << ex.code()); 98 | 99 | if (ex.code().value() == boost::system::errc::operation_canceled) { 100 | if (reason_ == Socket::Reason::TIME_OUT) { 101 | throw RequestTimeOutException(); 102 | } 103 | } 104 | throw; 105 | } 106 | } 107 | 108 | boost::optional reason_; 109 | }; 110 | 111 | } // restc_cpp 112 | 113 | 114 | #endif // RESTC_CPP_SOCKET_H_ 115 | 116 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 1 * *' # This line schedules the workflow to run at 00:00 on the first day of every month 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | build: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | include: 19 | - os: ubuntu-latest 20 | compiler: gcc 21 | - os: ubuntu-latest 22 | compiler: clang 23 | - os: windows-latest 24 | compiler: msvc 25 | - os: macos-latest 26 | compiler: 27 | 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | with: 32 | submodules: true 33 | 34 | - name: Cache 35 | uses: actions/cache@v4 36 | with: 37 | path: | 38 | ~/vcpkg 39 | ~/vcpkg_installed 40 | ${{ env.HOME }}/.cache/vcpkg/archives 41 | ${{ env.XDG_CACHE_HOME }}/vcpkg/archives 42 | ${{ env.LOCALAPPDATA }}\vcpkg\archives 43 | ${{ env.APPDATA }}\vcpkg\archives 44 | key: ${{ runner.os }}-${{ matrix.compiler }}-${{ env.BUILD_TYPE }}-${{ hashFiles('**/CMakeLists.txt') }}-${{ hashFiles('./vcpkg.json')}} 45 | restore-keys: | 46 | ${{ runner.os }}-${{ env.BUILD_TYPE }}- 47 | 48 | - name: Setup Cpp 49 | uses: aminya/setup-cpp@v1 50 | with: 51 | compiler: ${{ matrix.compiler }} 52 | vcvarsall: ${{ contains(matrix.os, 'windows') }} 53 | cmake: true 54 | ninja: true 55 | vcpkg: true 56 | cppcheck: false 57 | 58 | - name: Install compiler for Macos 59 | if: startsWith(matrix.os, 'macos') 60 | run: | 61 | brew install llvm 62 | 63 | - name: Prepare the PATH 64 | run: | 65 | if [[ "${{ runner.os }}" == "Windows" ]]; then 66 | echo "$env:USERPROFILE\vcpkg" >> $GITHUB_PATH 67 | echo "$env:USERPROFILE\ninja" >> $GITHUB_PATH 68 | else 69 | echo "$HOME/vcpkg" >> $GITHUB_PATH 70 | echo "$HOME/ninja" >> $GITHUB_PATH 71 | fi 72 | 73 | - name: Install dependencies 74 | run: | 75 | cp -v ci/vcpkg/vcpkg.json . 76 | vcpkg install 77 | 78 | - name: Build project 79 | run: | 80 | pushd ~ 81 | if [ -d build ]; then 82 | echo "Build dir exists" 83 | ls -la build 84 | else 85 | mkdir -v build 86 | fi 87 | cd build 88 | pwd 89 | set -x 90 | cmake -DVCPKG_INSTALLED_DIR=~/vcpkg_installed -DVCPKG_VERBOSE=ON -DRESTC_CPP_THREADED_CTX=ON -DCMAKE_BUILD_TYPE=Release -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=~/vcpkg/scripts/buildsystems/vcpkg.cmake ${GITHUB_WORKSPACE} 91 | cmake --build . 92 | popd 93 | continue-on-error: true 94 | 95 | - name: Dump diagnostics 96 | if: failure() 97 | run: | 98 | cd ~/build 99 | echo "---------------------------------" 100 | cat build.ninja 101 | echo "---------------------------------" 102 | 103 | - name: Run Unit Tests 104 | run: | 105 | pushd ~/build 106 | ctest -R UNITTESTS . -C Release 107 | popd 108 | -------------------------------------------------------------------------------- /src/IoReaderImpl.cpp: -------------------------------------------------------------------------------- 1 | #include "restc-cpp/restc-cpp.h" 2 | #include "restc-cpp/Connection.h" 3 | #include "restc-cpp/Socket.h" 4 | #include "restc-cpp/DataReader.h" 5 | #include "restc-cpp/logging.h" 6 | #include "restc-cpp/IoTimer.h" 7 | 8 | using namespace std; 9 | 10 | namespace restc_cpp { 11 | 12 | 13 | class IoReaderImpl : public DataReader { 14 | public: 15 | using buffer_t = std::array; 16 | 17 | IoReaderImpl(const Connection::ptr_t& conn, Context& ctx, 18 | const ReadConfig& cfg) 19 | : ctx_{ctx}, connection_{conn}, cfg_{cfg} 20 | { 21 | } 22 | 23 | void Finish() override { 24 | } 25 | 26 | 27 | ::restc_cpp::boost_const_buffer ReadSome() override { 28 | if (auto conn = connection_.lock()) { 29 | auto timer = IoTimer::Create("IoReaderImpl", 30 | cfg_.msReadTimeout, 31 | conn); 32 | 33 | for(size_t retries = 0;; ++retries) { 34 | size_t bytes = 0; 35 | try { 36 | if (retries != 0u) { 37 | RESTC_CPP_LOG_DEBUG_("IoReaderImpl::ReadSome: taking a nap"); 38 | ctx_.Sleep(retries * 20ms); 39 | RESTC_CPP_LOG_DEBUG_("IoReaderImpl::ReadSome: Waking up. Will try to read from the socket now."); 40 | } 41 | 42 | bytes = conn->GetSocket().AsyncReadSome( 43 | {buffer_.data(), buffer_.size()}, ctx_.GetYield()); 44 | } catch (const boost::system::system_error& ex) { 45 | if (ex.code() == boost::system::errc::resource_unavailable_try_again) { 46 | if ( retries < 32) { 47 | RESTC_CPP_LOG_DEBUG_("IoReaderImpl::ReadSome: Caught boost::system::system_error exception: " << ex.what() 48 | << ". I will continue the retry loop."); 49 | continue; 50 | } 51 | } 52 | RESTC_CPP_LOG_DEBUG_("IoReaderImpl::ReadSome: Caught boost::system::system_error exception: " << ex.what()); 53 | throw; 54 | } catch (const exception& ex) { 55 | RESTC_CPP_LOG_DEBUG_("IoReaderImpl::ReadSome: Caught exception: " << ex.what()); 56 | throw; 57 | } 58 | 59 | RESTC_CPP_LOG_TRACE_("Read #" << bytes 60 | << " bytes from " << conn); 61 | 62 | timer->Cancel(); 63 | return {buffer_.data(), bytes}; 64 | } 65 | } 66 | 67 | RESTC_CPP_LOG_DEBUG_("IoReaderImpl::ReadSome: Reached outer scope. Timed out?"); 68 | throw ObjectExpiredException("Connection expired"); 69 | } 70 | 71 | [[nodiscard]] bool IsEof() const override 72 | { 73 | if (auto conn = connection_.lock()) { 74 | return !conn->GetSocket().IsOpen(); 75 | } 76 | return true; 77 | } 78 | 79 | private: 80 | Context& ctx_; 81 | const std::weak_ptr connection_; 82 | const ReadConfig cfg_; 83 | buffer_t buffer_ = {}; 84 | }; 85 | 86 | 87 | 88 | DataReader::ptr_t 89 | DataReader::CreateIoReader(const Connection::ptr_t& conn, Context& ctx, 90 | const DataReader::ReadConfig& cfg) { 91 | return make_unique(conn, ctx, cfg); 92 | } 93 | 94 | } // namespace 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/SocketImpl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "restc-cpp/boost_compatibility.h" 10 | #include "restc-cpp/Socket.h" 11 | #include "restc-cpp/logging.h" 12 | 13 | namespace restc_cpp { 14 | 15 | class SocketImpl : public Socket, protected ExceptionWrapper { 16 | public: 17 | 18 | SocketImpl(boost_io_service& io_service) 19 | : socket_{io_service} 20 | { 21 | } 22 | 23 | boost::asio::ip::tcp::socket& GetSocket() override { 24 | return socket_; 25 | } 26 | 27 | const boost::asio::ip::tcp::socket& GetSocket() const override { 28 | return socket_; 29 | } 30 | 31 | std::size_t AsyncReadSome(boost_mutable_buffer buffers, 32 | boost::asio::yield_context& yield) override { 33 | return WrapException([&] { 34 | return socket_.async_read_some(buffers, yield); 35 | }); 36 | } 37 | 38 | std::size_t AsyncRead(boost_mutable_buffer buffers, 39 | boost::asio::yield_context& yield) override { 40 | return WrapException([&] { 41 | return boost::asio::async_read(socket_, buffers, yield); 42 | }); 43 | } 44 | 45 | void AsyncWrite(const boost_const_buffer& buffers, 46 | boost::asio::yield_context& yield) override { 47 | boost::asio::async_write(socket_, buffers, yield); 48 | } 49 | 50 | void AsyncWrite(const write_buffers_t& buffers, 51 | boost::asio::yield_context& yield)override { 52 | 53 | return WrapException([&] { 54 | boost::asio::async_write(socket_, buffers, yield); 55 | }); 56 | } 57 | 58 | void AsyncConnect(const boost::asio::ip::tcp::endpoint& ep, 59 | const std::string &host, 60 | bool tcpNodelay, 61 | boost::asio::yield_context& yield) override { 62 | return WrapException([&] { 63 | socket_.async_connect(ep, yield); 64 | socket_.lowest_layer().set_option(boost::asio::ip::tcp::no_delay(tcpNodelay)); 65 | OnAfterConnect(); 66 | }); 67 | } 68 | 69 | void AsyncShutdown(boost::asio::yield_context& yield) override { 70 | // Do nothing. 71 | } 72 | 73 | void Close(Reason reason) override { 74 | if (socket_.is_open()) { 75 | RESTC_CPP_LOG_TRACE_("Closing " << *this); 76 | socket_.close(); 77 | } 78 | reason_ = reason; 79 | } 80 | 81 | bool IsOpen() const noexcept override { 82 | return socket_.is_open(); 83 | } 84 | 85 | protected: 86 | std::ostream& Print(std::ostream& o) const override { 87 | if (IsOpen()) { 88 | const auto& socket = GetSocket(); 89 | o << "{Socket " 90 | << "socket# " 91 | << static_cast( 92 | const_cast(socket).native_handle()); 93 | try { 94 | return o << " " << socket.local_endpoint() 95 | << " <--> " << socket.remote_endpoint() << '}'; 96 | } catch (const std::exception& ex) { 97 | o << " {std exception: " << ex.what() << "}}"; 98 | } 99 | } 100 | 101 | return o << "{Socket (unused/closed)}"; 102 | } 103 | 104 | private: 105 | boost::asio::ip::tcp::socket socket_; 106 | }; 107 | 108 | 109 | } // restc_cpp 110 | 111 | -------------------------------------------------------------------------------- /tests/functional/InsertSerializerTest.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Include before boost::log headers 3 | #include 4 | 5 | #include "restc-cpp/restc-cpp.h" 6 | #include "restc-cpp/logging.h" 7 | #include "restc-cpp/RequestBuilder.h" 8 | #include "restc-cpp/SerializeJson.h" 9 | 10 | #include "gtest/gtest.h" 11 | #include "restc-cpp/test_helper.h" 12 | 13 | using namespace std; 14 | using namespace restc_cpp; 15 | 16 | struct Post { 17 | Post() = default; 18 | Post(string u, string m) 19 | : username{std::move(u)} 20 | , motto{std::move(m)} 21 | {} 22 | 23 | int id = 0; 24 | string username; 25 | string motto; 26 | }; 27 | 28 | BOOST_FUSION_ADAPT_STRUCT( 29 | Post, 30 | (int, id) 31 | (string, username) 32 | (string, motto) 33 | ) 34 | 35 | TEST(InsertSerializer, Inserter) 36 | { 37 | Post post{"catch22", "Carpe Diem!"}; 38 | EXPECT_EQ(0, post.id); 39 | 40 | 41 | auto rest_client = RestClient::Create(); 42 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 43 | 44 | auto reply = RequestBuilder(ctx) 45 | .Post(GetDockerUrl("http://localhost:3000/posts")) // URL 46 | .Data(post) // Data object to send 47 | .Execute(); // Do it! 48 | 49 | EXPECT_HTTP_OK(reply->GetResponseCode()); 50 | 51 | }); 52 | 53 | EXPECT_NO_THROW(f.get()); 54 | } 55 | 56 | TEST(InsertSerializer, FunctorWriter) 57 | { 58 | auto rest_client = RestClient::Create(); 59 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 60 | 61 | std::vector posts; 62 | posts.emplace_back("catch22", "Carpe Diem!"); 63 | posts.emplace_back("catch23", "Carpe Diem! again!"); 64 | posts.emplace_back("The answer is 42", "Really?"); 65 | 66 | auto reply = RequestBuilder(ctx) 67 | .Post(GetDockerUrl("http://localhost:3001/upload_raw/")) // URL 68 | .DataProvider([&](DataWriter& writer) { 69 | 70 | RapidJsonInserter inserter(writer, true); 71 | 72 | for(const auto& post : posts) { 73 | inserter.Add(post); 74 | } 75 | 76 | inserter.Done(); 77 | 78 | }) 79 | .Execute(); 80 | 81 | EXPECT_EQ(200, reply->GetResponseCode()); 82 | 83 | }); 84 | 85 | EXPECT_NO_THROW(f.get()); 86 | } 87 | 88 | TEST(InsertSerializer, ManualWriter) 89 | { 90 | auto rest_client = RestClient::Create(); 91 | auto f = rest_client->ProcessWithPromise([&](Context& ctx) { 92 | 93 | std::vector posts; 94 | posts.emplace_back("catch22", "Carpe Diem!"); 95 | posts.emplace_back("catch23", "Carpe Diem! again!"); 96 | posts.emplace_back("The answer is 42", "Really?"); 97 | 98 | auto request = RequestBuilder(ctx) 99 | .Post(GetDockerUrl("http://localhost:3001/upload_raw/")) // URL 100 | .Chunked() 101 | .Build(); 102 | 103 | 104 | { 105 | auto& writer = request->SendRequest(ctx); 106 | RapidJsonInserter inserter(writer, true); 107 | for(const auto& post : posts) { 108 | inserter.Add(post); 109 | } 110 | } 111 | 112 | auto reply = request->GetReply(ctx); 113 | EXPECT_HTTP_OK(reply->GetResponseCode()); 114 | 115 | }); 116 | 117 | EXPECT_NO_THROW(f.get()); 118 | } 119 | 120 | 121 | int main( int argc, char * argv[] ) 122 | { 123 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 124 | ::testing::InitGoogleTest(&argc, argv); 125 | return RUN_ALL_TESTS();; 126 | } 127 | -------------------------------------------------------------------------------- /tests/functional/CRUD_test.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Include before boost::log headers 4 | #include "restc-cpp/restc-cpp.h" 5 | #include "restc-cpp/logging.h" 6 | #include "restc-cpp/RequestBuilder.h" 7 | 8 | #include "gtest/gtest.h" 9 | #include "restc-cpp/test_helper.h" 10 | 11 | using namespace std; 12 | using namespace restc_cpp; 13 | 14 | 15 | /* These url's points to a local Docker container with nginx, linked to 16 | * a jsonserver docker container with mock data. 17 | * You can run /.create-and-run-containers.sh to start them. 18 | */ 19 | const string http_url = "http://localhost:3000/posts"; 20 | 21 | struct Post { 22 | string id; 23 | string username; 24 | string motto; 25 | }; 26 | 27 | BOOST_FUSION_ADAPT_STRUCT( 28 | Post, 29 | (string, id) 30 | (string, username) 31 | (string, motto) 32 | ) 33 | 34 | 35 | TEST(CRUD, Crud) { 36 | auto rest_client = RestClient::Create(); 37 | rest_client->ProcessWithPromise([&](Context& ctx) { 38 | 39 | Post post; 40 | post.username = "catch22"; 41 | post.motto = "Carpe Diem!"; 42 | 43 | EXPECT_EQ("", post.id); 44 | 45 | auto reply = RequestBuilder(ctx) 46 | .Post(GetDockerUrl(http_url)) // URL 47 | .Data(post) // Data object to send 48 | .Execute(); // Do it! 49 | 50 | 51 | // The mock server returns the new record. 52 | Post svr_post; 53 | SerializeFromJson(svr_post, *reply); 54 | 55 | EXPECT_EQ(post.username, svr_post.username); 56 | EXPECT_EQ(post.motto, svr_post.motto); 57 | EXPECT_FALSE(svr_post.id.empty()); 58 | 59 | // Change the data 60 | post = svr_post; 61 | post.motto = "Change!"; 62 | reply = RequestBuilder(ctx) 63 | .Put(GetDockerUrl(http_url) + "/" + post.id) // URL 64 | .Data(post) // Data object to update 65 | .Execute(); 66 | 67 | // Get the reply before we validate the operation. Else it may run in parallel 68 | reply->fetchAndIgnore(); 69 | 70 | // This test fails randomly on fast machines. 71 | // I believe the problem is a race condition in the test container. 72 | // Adding sleeps is generally the worst possible way to "fix" such errors, but it may work in this case. 73 | std::this_thread::sleep_for(1s); 74 | 75 | // Fetch again 76 | reply = RequestBuilder(ctx) 77 | .Get(GetDockerUrl(http_url) + "/" + post.id) // URL 78 | .Execute(); 79 | SerializeFromJson(svr_post, *reply); 80 | EXPECT_EQ(post.motto, svr_post.motto); 81 | 82 | // Delete 83 | RequestBuilder(ctx) 84 | .Delete(GetDockerUrl(http_url) + "/" + post.id) // URL 85 | .Execute()->fetchAndIgnore(); 86 | 87 | // Verify that it's gone 88 | EXPECT_THROW( 89 | RequestBuilder(ctx) 90 | .Get(GetDockerUrl(http_url) + "/" + post.id) // URL 91 | .Execute(), 92 | HttpNotFoundException 93 | ); 94 | 95 | }).get(); 96 | } 97 | 98 | TEST(CRUD, Options) { 99 | 100 | auto rest_client = RestClient::Create(); 101 | rest_client->ProcessWithPromise([&](Context& ctx) { 102 | 103 | auto reply = RequestBuilder(ctx) 104 | .Options(GetDockerUrl(http_url)) // URL 105 | .Execute(); // Do it! 106 | 107 | EXPECT_EQ(204, reply->GetResponseCode()); 108 | 109 | }).get(); 110 | 111 | } 112 | 113 | TEST(CRUD, HEAD) { 114 | 115 | auto rest_client = RestClient::Create(); 116 | rest_client->ProcessWithPromise([&](Context& ctx) { 117 | 118 | auto reply = RequestBuilder(ctx) 119 | .Head(GetDockerUrl(http_url)) // URL 120 | .Execute(); // Do it! 121 | 122 | EXPECT_EQ(200, reply->GetResponseCode()); 123 | 124 | }).get(); 125 | 126 | } 127 | 128 | int main( int argc, char * argv[] ) 129 | { 130 | RESTC_CPP_TEST_LOGGING_SETUP("info"); 131 | ::testing::InitGoogleTest(&argc, argv); 132 | return RUN_ALL_TESTS();; 133 | } 134 | -------------------------------------------------------------------------------- /tests/functional/OwnIoserviceTests.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | // Include before boost::log headers 5 | #include "restc-cpp/restc-cpp.h" 6 | 7 | #include "restc-cpp/logging.h" 8 | #include "restc-cpp/RequestBuilder.h" 9 | #include "restc-cpp/SerializeJson.h" 10 | #include "restc-cpp/IteratorFromJsonSerializer.h" 11 | 12 | #include "gtest/gtest.h" 13 | #include "restc-cpp/test_helper.h" 14 | 15 | using namespace std; 16 | using namespace restc_cpp; 17 | 18 | enum { CONNECTIONS = 20 }; 19 | //#define CONNECTIONS 1 20 | 21 | struct Post { 22 | string id; 23 | string username; 24 | string motto; 25 | }; 26 | 27 | BOOST_FUSION_ADAPT_STRUCT( 28 | Post, 29 | (string, id) 30 | (string, username) 31 | (string, motto) 32 | ) 33 | 34 | const string http_url = "http://localhost:3000/manyposts"; 35 | 36 | 37 | TEST(OwnIoservice, All) 38 | { 39 | boost_io_service ioservice; 40 | 41 | mutex mutex; 42 | mutex.lock(); 43 | 44 | std::vector> futures; 45 | std::vector> promises; 46 | 47 | futures.reserve(CONNECTIONS); 48 | promises.reserve(CONNECTIONS); 49 | 50 | Request::Properties properties; 51 | properties.cacheMaxConnections = CONNECTIONS; 52 | properties.cacheMaxConnectionsPerEndpoint = CONNECTIONS; 53 | 54 | auto rest_client = RestClient::Create(properties, ioservice); 55 | 56 | for(int i = 0; i < CONNECTIONS; ++i) { 57 | 58 | promises.emplace_back(); 59 | futures.push_back(promises.back().get_future()); 60 | 61 | rest_client->Process([i, &promises, &rest_client, &mutex](Context& ctx) { 62 | try { 63 | auto reply = RequestBuilder(ctx) 64 | .Get(GetDockerUrl(http_url)) 65 | .Execute(); 66 | 67 | // Use an iterator to make it simple to fetch some data and 68 | // then wait on the mutex before we finish. 69 | IteratorFromJsonSerializer results(*reply); 70 | 71 | auto it = results.begin(); 72 | RESTC_CPP_LOG_DEBUG_("Iteration #" << i 73 | << " Read item # " << it->id); 74 | 75 | // We can't just wait on the lock since we are in a co-routine. 76 | // So we use the async_wait() to poll in stead. 77 | while(!mutex.try_lock()) { 78 | boost::asio::steady_timer timer(rest_client->GetIoService(), 1ms); 79 | timer.async_wait(ctx.GetYield()); 80 | } 81 | mutex.unlock(); 82 | 83 | // Fetch the rest 84 | for (; it != results.end(); ++it) { 85 | ; 86 | } 87 | 88 | promises[i].set_value(i); 89 | } RESTC_CPP_IN_COROUTINE_CATCH_ALL { 90 | // If we got a "real" exception during the processing above 91 | EXPECT_TRUE(false); 92 | promises[i].set_exception(current_exception()); 93 | } 94 | }); 95 | } 96 | 97 | thread worker([&ioservice]() { 98 | cout << "ioservice is running" << '\n'; 99 | ioservice.run(); 100 | cout << "ioservice is done" << '\n'; 101 | }); 102 | 103 | mutex.unlock(); 104 | 105 | int successful_connections = 0; 106 | for(auto& future : futures) { 107 | try { 108 | auto i = future.get(); 109 | RESTC_CPP_LOG_DEBUG_("Iteration #" << i << " is done"); 110 | ++successful_connections; 111 | } catch (const std::exception& ex) { 112 | std::clog << "Future threw up: " << ex.what(); 113 | } 114 | } 115 | 116 | std::clog << "We had " << successful_connections 117 | << " successful connections."; 118 | 119 | EXPECT_EQ(CONNECTIONS, successful_connections); 120 | 121 | rest_client->CloseWhenReady(); 122 | ioservice.stop(); 123 | worker.join(); 124 | } 125 | 126 | int main( int argc, char * argv[] ) 127 | { 128 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 129 | ::testing::InitGoogleTest(&argc, argv); 130 | return RUN_ALL_TESTS();; 131 | } 132 | -------------------------------------------------------------------------------- /tests/unit/Iostream2JsonTests.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include "restc-cpp/logging.h" 6 | #include 7 | #include 8 | #include 9 | 10 | #include "restc-cpp/restc-cpp.h" 11 | #include "restc-cpp/SerializeJson.h" 12 | #include "rapidjson/writer.h" 13 | #include "rapidjson/stringbuffer.h" 14 | 15 | #include "gtest/gtest.h" 16 | #include "restc-cpp/test_helper.h" 17 | 18 | using namespace std; 19 | using namespace restc_cpp; 20 | using namespace rapidjson; 21 | 22 | struct Config { 23 | int max_something = {}; 24 | string name; 25 | string url; 26 | }; 27 | 28 | BOOST_FUSION_ADAPT_STRUCT( 29 | Config, 30 | (int, max_something) 31 | (string, name) 32 | (string, url) 33 | ) 34 | 35 | 36 | 37 | // ---------------------- # 58 38 | 39 | using LOCAL = vector; 40 | using GLOBAL = vector; 41 | using ADDRESS = vector; 42 | struct MAC 43 | { 44 | ADDRESS address; 45 | }; 46 | BOOST_FUSION_ADAPT_STRUCT( 47 | MAC, 48 | (ADDRESS, address) 49 | ) 50 | using MACLIST = vector; 51 | 52 | struct DeviceList{ 53 | LOCAL local; 54 | GLOBAL global; 55 | MACLIST maclst; 56 | }; 57 | BOOST_FUSION_ADAPT_STRUCT( 58 | DeviceList, 59 | (LOCAL, local) 60 | (GLOBAL, global) 61 | (MACLIST, maclst) 62 | ) 63 | using DeviceLst = vector; 64 | struct Config2 { 65 | int nIdSchedule = {}; 66 | int nDCUNo{}; 67 | DeviceLst lst; 68 | }; 69 | BOOST_FUSION_ADAPT_STRUCT( 70 | Config2, 71 | (int, nIdSchedule) 72 | (int, nDCUNo) 73 | (DeviceLst, lst) 74 | ) 75 | ///////////////////////////////// 76 | 77 | 78 | TEST(IOstream2Json, ReadConfigurationFromFile) { 79 | auto tmpname = boost::filesystem::unique_path(); 80 | BOOST_SCOPE_EXIT(&tmpname) { 81 | boost::filesystem::remove(tmpname); 82 | } BOOST_SCOPE_EXIT_END 83 | 84 | { 85 | ofstream json_out(tmpname.native()); 86 | json_out << '{' << '\n' 87 | << R"("max_something":100,)" << '\n' 88 | << R"("name":"Test Data",)" << '\n' 89 | << R"("url":"https://www.example.com")" << '\n' 90 | << '}'; 91 | } 92 | 93 | ifstream ifs(tmpname.native()); 94 | Config config; 95 | SerializeFromJson(config, ifs); 96 | 97 | EXPECT_TRUE(config.max_something == 100); 98 | EXPECT_TRUE(config.name == "Test Data"); 99 | EXPECT_TRUE(config.url == "https://www.example.com"); 100 | } 101 | 102 | TEST(IOstream2Json, WriteJsonToStream) { 103 | 104 | stringstream out; 105 | Config config; 106 | config.max_something = 100; 107 | config.name = "John"; 108 | config.url = "https://www.example.com"; 109 | 110 | SerializeToJson(config, out); 111 | 112 | EXPECT_EQ(out.str(), R"({"max_something":100,"name":"John","url":"https://www.example.com"})"); 113 | } 114 | 115 | TEST(IOstream2Json, issue58) { 116 | 117 | auto tmpname = boost::filesystem::unique_path(); 118 | BOOST_SCOPE_EXIT(&tmpname) { 119 | boost::filesystem::remove(tmpname); 120 | } BOOST_SCOPE_EXIT_END 121 | 122 | { 123 | ofstream json_out(tmpname.native()); 124 | json_out << R"({"nIdSchedule":5,"nDCUNo":104400,"lst":[{"local":[65,66,67,68,69,69,70,80],"global":[71,72,73,74,75,76,77,78],"maclst":[{"address":[48,49,65,73,74,75,76,78]}]}]})"; 125 | } 126 | 127 | Config2 config; 128 | ifstream ifs(tmpname.c_str()); 129 | if (ifs.is_open()) 130 | { 131 | // Read the ;config file into the config object. 132 | SerializeFromJson(config, ifs); 133 | cout << "done" << '\n'; 134 | } 135 | ofstream ofs(tmpname.c_str()); 136 | config.lst[0].maclst[0].address[2] = 11; 137 | config.lst[0].maclst[0].address[3] = 11; 138 | config.lst[0].maclst[0].address[4] = 11; 139 | SerializeToJson(config, ofs); 140 | cout << "done" << '\n'; 141 | } 142 | 143 | int main( int argc, char * argv[] ) 144 | { 145 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 146 | ::testing::InitGoogleTest(&argc, argv); 147 | return RUN_ALL_TESTS();; 148 | } 149 | -------------------------------------------------------------------------------- /include/restc-cpp/error.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef RESTC_CPP_ERROR_H_ 3 | #define RESTC_CPP_ERROR_H_ 4 | 5 | #include "restc-cpp.h" 6 | 7 | namespace restc_cpp { 8 | 9 | struct RestcCppException : public std::runtime_error { 10 | 11 | RestcCppException(const std::string& cause) 12 | : runtime_error(cause) {} 13 | }; 14 | 15 | struct RequestFailedWithErrorException : public RestcCppException { 16 | 17 | RequestFailedWithErrorException(const Reply::HttpResponse& response) 18 | : RestcCppException(std::string("Request failed with HTTP error: ") 19 | + std::to_string(response.status_code) 20 | + " " + response.reason_phrase) 21 | , http_response{response} 22 | { 23 | } 24 | 25 | const Reply::HttpResponse http_response; 26 | }; 27 | 28 | struct HttpAuthenticationException: public RequestFailedWithErrorException { 29 | 30 | HttpAuthenticationException(const Reply::HttpResponse& response) 31 | : RequestFailedWithErrorException(response) { } 32 | }; 33 | 34 | struct HttpForbiddenException: public RequestFailedWithErrorException { 35 | 36 | HttpForbiddenException(const Reply::HttpResponse& response) 37 | : RequestFailedWithErrorException(response) { } 38 | }; 39 | 40 | struct HttpNotFoundException: public RequestFailedWithErrorException { 41 | 42 | HttpNotFoundException(const Reply::HttpResponse& response) 43 | : RequestFailedWithErrorException(response) { } 44 | }; 45 | 46 | struct HttpMethodNotAllowedException: public RequestFailedWithErrorException { 47 | 48 | HttpMethodNotAllowedException(const Reply::HttpResponse& response) 49 | : RequestFailedWithErrorException(response) { } 50 | }; 51 | 52 | struct HttpNotAcceptableException: public RequestFailedWithErrorException { 53 | 54 | HttpNotAcceptableException(const Reply::HttpResponse& response) 55 | : RequestFailedWithErrorException(response) { } 56 | }; 57 | 58 | struct HttpProxyAuthenticationRequiredException 59 | : public RequestFailedWithErrorException { 60 | 61 | HttpProxyAuthenticationRequiredException( 62 | const Reply::HttpResponse& response) 63 | : RequestFailedWithErrorException(response) { } 64 | }; 65 | 66 | struct HttpRequestTimeOutException: public RequestFailedWithErrorException { 67 | 68 | HttpRequestTimeOutException(const Reply::HttpResponse& response) 69 | : RequestFailedWithErrorException(response) { } 70 | }; 71 | 72 | 73 | struct ParseException : public RestcCppException 74 | { 75 | ParseException(const std::string& cause) 76 | : RestcCppException(cause) {} 77 | }; 78 | 79 | struct UnknownPropertyException : public RestcCppException 80 | { 81 | UnknownPropertyException(const std::string& cause) 82 | : RestcCppException(cause) {} 83 | }; 84 | 85 | 86 | struct ProtocolException : public RestcCppException 87 | { 88 | ProtocolException(const std::string& cause) 89 | : RestcCppException(cause) {} 90 | }; 91 | 92 | struct AccessDeniedException : public RestcCppException 93 | { 94 | AccessDeniedException(const std::string& cause) 95 | : RestcCppException(cause) {} 96 | }; 97 | 98 | struct ConstraintException : public RestcCppException 99 | { 100 | ConstraintException(const std::string& cause) 101 | : RestcCppException(cause) {} 102 | }; 103 | 104 | struct NotSupportedException : public RestcCppException 105 | { 106 | NotSupportedException(const std::string& cause) 107 | : RestcCppException(cause) {} 108 | }; 109 | 110 | struct CommunicationException : public RestcCppException 111 | { 112 | CommunicationException(const std::string& cause) 113 | : RestcCppException(cause) {} 114 | }; 115 | 116 | struct FailedToConnectException : public CommunicationException 117 | { 118 | FailedToConnectException(const std::string& cause) 119 | : CommunicationException(cause) {} 120 | }; 121 | 122 | struct DecompressException : public RestcCppException 123 | { 124 | DecompressException(const std::string& cause) 125 | : RestcCppException(cause) {} 126 | }; 127 | 128 | struct NoDataException : public RestcCppException 129 | { 130 | NoDataException(const std::string& cause) 131 | : RestcCppException(cause) {} 132 | }; 133 | 134 | struct CannotIncrementEndException : public RestcCppException 135 | { 136 | CannotIncrementEndException(const std::string& cause) 137 | : RestcCppException(cause) {} 138 | }; 139 | 140 | struct NotImplementedException : public RestcCppException 141 | { 142 | NotImplementedException(const std::string& cause) 143 | : RestcCppException(cause) {} 144 | }; 145 | 146 | struct IoException : public RestcCppException 147 | { 148 | IoException(const std::string& cause) 149 | : RestcCppException(cause) {} 150 | }; 151 | 152 | struct ObjectExpiredException : public RestcCppException 153 | { 154 | ObjectExpiredException(const std::string& cause) 155 | : RestcCppException(cause) {} 156 | }; 157 | 158 | struct RequestTimeOutException : public RestcCppException 159 | { 160 | RequestTimeOutException() 161 | : RestcCppException("Request Timed Out") {} 162 | }; 163 | 164 | struct FailedToResolveEndpointException : public RestcCppException 165 | { 166 | FailedToResolveEndpointException(const std::string& what) 167 | : RestcCppException(what) {} 168 | }; 169 | 170 | } // namespace 171 | #endif // RESTC_CPP_ERROR_H_ 172 | 173 | -------------------------------------------------------------------------------- /src/TlsSocketImpl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "restc-cpp/restc-cpp.h" 8 | #include "restc-cpp/logging.h" 9 | 10 | #include 11 | #include 12 | 13 | #include "restc-cpp/Socket.h" 14 | #include "restc-cpp/config.h" 15 | 16 | #if !defined(RESTC_CPP_WITH_TLS) 17 | # error "Do not include when compiling without TLS" 18 | #endif 19 | 20 | namespace restc_cpp { 21 | 22 | class TlsSocketImpl : public Socket, protected ExceptionWrapper { 23 | public: 24 | 25 | using ssl_socket_t = boost::asio::ssl::stream; 26 | 27 | TlsSocketImpl(boost_io_service& io_service, shared_ptr ctx) 28 | { 29 | ssl_socket_ = std::make_unique(io_service, *ctx); 30 | } 31 | 32 | boost::asio::ip::tcp::socket& GetSocket() override { 33 | return static_cast( 34 | ssl_socket_->lowest_layer()); 35 | } 36 | 37 | const boost::asio::ip::tcp::socket& GetSocket() const override { 38 | return static_cast( 39 | ssl_socket_->lowest_layer()); 40 | } 41 | 42 | std::size_t AsyncReadSome(boost_mutable_buffer buffers, 43 | boost::asio::yield_context& yield) override { 44 | return WrapException([&] { 45 | return ssl_socket_->async_read_some(buffers, yield); 46 | }); 47 | } 48 | 49 | std::size_t AsyncRead(boost_mutable_buffer buffers, 50 | boost::asio::yield_context& yield) override { 51 | return WrapException([&] { 52 | return boost::asio::async_read(*ssl_socket_, buffers, yield); 53 | }); 54 | } 55 | 56 | void AsyncWrite(const boost_const_buffer& buffers, 57 | boost::asio::yield_context& yield) override { 58 | boost::asio::async_write(*ssl_socket_, buffers, yield); 59 | } 60 | 61 | void AsyncWrite(const write_buffers_t& buffers, 62 | boost::asio::yield_context& yield) override { 63 | return WrapException([&] { 64 | boost::asio::async_write(*ssl_socket_, buffers, yield); 65 | }); 66 | } 67 | 68 | void AsyncConnect(const boost::asio::ip::tcp::endpoint& ep, 69 | const std::string &host, 70 | bool tcpNodelay, 71 | boost::asio::yield_context& yield) override { 72 | return WrapException([&] { 73 | //TLS-SNI (without this option, handshakes attempts with hosts behind CDNs will fail, 74 | //due to the fact that the CDN does not have enough information at the TLS layer 75 | //to decide where to forward the handshake attempt). 76 | 77 | RESTC_CPP_LOG_TRACE_("AsyncConnect - Calling SSL_set_tlsext_host_name --> " << host); 78 | SSL_set_tlsext_host_name(ssl_socket_->native_handle(), host.c_str()); 79 | 80 | RESTC_CPP_LOG_TRACE_("AsyncConnect - Calling async_connect"); 81 | GetSocket().async_connect(ep, yield); 82 | 83 | RESTC_CPP_LOG_TRACE_("AsyncConnect - Calling lowest_layer().set_option"); 84 | ssl_socket_->lowest_layer().set_option( 85 | boost::asio::ip::tcp::no_delay(tcpNodelay)); 86 | 87 | RESTC_CPP_LOG_TRACE_("AsyncConnect - Calling OnAfterConnect()"); 88 | OnAfterConnect(); 89 | 90 | RESTC_CPP_LOG_TRACE_("AsyncConnect - Calling async_handshake"); 91 | ssl_socket_->async_handshake(boost::asio::ssl::stream_base::client, 92 | yield); 93 | 94 | RESTC_CPP_LOG_TRACE_("AsyncConnect - Done"); 95 | }); 96 | } 97 | 98 | void AsyncShutdown(boost::asio::yield_context& yield) override { 99 | return WrapException([&] { 100 | ssl_socket_->async_shutdown(yield); 101 | }); 102 | } 103 | 104 | void Close(Reason reason) override { 105 | if (ssl_socket_->lowest_layer().is_open()) { 106 | RESTC_CPP_LOG_TRACE_("Closing " << *this); 107 | ssl_socket_->lowest_layer().close(); 108 | } 109 | reason_ = reason; 110 | } 111 | 112 | bool IsOpen() const noexcept override { 113 | return ssl_socket_->lowest_layer().is_open(); 114 | } 115 | 116 | protected: 117 | std::ostream& Print(std::ostream& o) const override { 118 | if (IsOpen()) { 119 | const auto& socket = GetSocket(); 120 | o << "{TlsSocket " 121 | << "socket# " 122 | << static_cast( 123 | const_cast(socket).native_handle()); 124 | try { 125 | return o << " " << socket.local_endpoint() 126 | << " <--> " << socket.remote_endpoint() << '}'; 127 | } catch (const std::exception& ex) { 128 | o << " {std exception: " << ex.what() << "}}"; 129 | } 130 | } 131 | 132 | return o << "{TlsSocket (unused/closed)}"; 133 | } 134 | 135 | 136 | private: 137 | std::unique_ptr ssl_socket_; 138 | }; 139 | 140 | } // restc_cpp 141 | 142 | 143 | -------------------------------------------------------------------------------- /tests/functional/ManyConnectionsTest.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | // Include before boost::log headers 5 | #include "restc-cpp/restc-cpp.h" 6 | #include "restc-cpp/logging.h" 7 | #include "restc-cpp/RequestBuilder.h" 8 | #include "restc-cpp/IteratorFromJsonSerializer.h" 9 | 10 | #include "gtest/gtest.h" 11 | #include "restc-cpp/test_helper.h" 12 | 13 | using namespace std; 14 | using namespace restc_cpp; 15 | 16 | 17 | const string http_url = "http://localhost:3000/manyposts"; 18 | 19 | /* The goal is to test with 1000 connections. 20 | * However, I am unable to get more than 500 working reliable (with 100 21 | * connections increment) before I see connection errors. On OS X, 22 | * I was unable to get more than 100 connections working reliable. 23 | * (I have yet to figure out if the limitation is in the library 24 | * or in the test setup / Docker container). 25 | * 26 | * I don't know at this time if it is caused by OS limits in the test 27 | * application, Docker, the container with the mock server or the Linux 28 | * machine itself. 29 | * 30 | * May be I have to write a simple HTTP Mock sever in C++ or use 31 | * nginx-lua with some tweaking / load-balancing to get this test 32 | * to work with the 1000 connection goal. 33 | * 34 | * There is also a problem where several tests hit's the test containers 35 | * from Jenkins. So for now 100 connections must suffice. 36 | * 37 | * 100 connections is sufficient to prove that the client 38 | * works as expected with many co-routines in parallel. 39 | */ 40 | 41 | enum { CONNECTIONS = 100 }; 42 | 43 | struct Post { 44 | string id; 45 | string username; 46 | string motto; 47 | }; 48 | 49 | BOOST_FUSION_ADAPT_STRUCT( 50 | Post, 51 | (string, id) 52 | (string, username) 53 | (string, motto) 54 | ) 55 | 56 | struct Locker { 57 | Locker(mutex& m) : m_{m} {} 58 | ~Locker() { 59 | if (locked_) { 60 | m_.unlock(); 61 | } 62 | } 63 | 64 | bool try_lock() { 65 | assert(!locked_); 66 | locked_ = m_.try_lock(); 67 | return locked_; 68 | } 69 | 70 | void unlock() { 71 | assert(locked_); 72 | m_.unlock(); 73 | locked_ = false; 74 | } 75 | 76 | mutex& m_; 77 | bool locked_{}; 78 | }; 79 | 80 | TEST(ManyConnections, CRUD) { 81 | mutex mutex; 82 | mutex.lock(); 83 | 84 | std::vector> futures; 85 | std::vector> promises; 86 | 87 | futures.reserve(CONNECTIONS); 88 | promises.reserve(CONNECTIONS); 89 | 90 | Request::Properties properties; 91 | properties.cacheMaxConnections = CONNECTIONS; 92 | properties.cacheMaxConnectionsPerEndpoint = CONNECTIONS; 93 | auto rest_client = RestClient::Create(properties); 94 | 95 | for(int i = 0; i < CONNECTIONS; ++i) { 96 | 97 | promises.emplace_back(); 98 | futures.push_back(promises.back().get_future()); 99 | 100 | rest_client->Process([i, &promises, &rest_client, &mutex](Context& ctx) { 101 | Locker locker(mutex); 102 | try { 103 | auto reply = RequestBuilder(ctx) 104 | .Get(GetDockerUrl(http_url)) 105 | .Execute(); 106 | 107 | // Use an iterator to make it simple to fetch some data and 108 | // then wait on the mutex before we finish. 109 | IteratorFromJsonSerializer results(*reply); 110 | 111 | auto it = results.begin(); 112 | RESTC_CPP_LOG_DEBUG_("Iteration #" << i 113 | << " Read item # " << it->id); 114 | 115 | promises[i].set_value(i); 116 | // Wait for all connections to be ready 117 | 118 | // We can't just wait on the lock since we are in a co-routine. 119 | // So we use the async_wait() to poll in stead. 120 | while(!locker.try_lock()) { 121 | boost::asio::steady_timer timer(rest_client->GetIoService(), 1ms); 122 | timer.async_wait(ctx.GetYield()); 123 | } 124 | locker.unlock(); 125 | 126 | // Fetch the rest 127 | for (; it != results.end(); ++it) { 128 | ; 129 | } 130 | 131 | } catch (const std::exception& ex) { 132 | RESTC_CPP_LOG_ERROR_("Failed to fetch data: " << ex.what()); 133 | promises[i].set_exception(std::current_exception()); 134 | } 135 | }); 136 | } 137 | 138 | int successful_connections = 0; 139 | for(auto& future : futures) { 140 | try { 141 | auto i = future.get(); 142 | RESTC_CPP_LOG_DEBUG_("Iteration #" << i << " is done"); 143 | ++successful_connections; 144 | } catch (const std::exception& ex) { 145 | RESTC_CPP_LOG_ERROR_("Future threw up: " << ex.what()); 146 | } 147 | } 148 | 149 | RESTC_CPP_LOG_INFO_("We had " << successful_connections 150 | << " successful connections."); 151 | 152 | EXPECT_EQ(CONNECTIONS, successful_connections); 153 | 154 | mutex.unlock(); 155 | 156 | rest_client->CloseWhenReady(); 157 | } 158 | 159 | 160 | int main( int argc, char * argv[] ) 161 | { 162 | RESTC_CPP_TEST_LOGGING_SETUP("info"); 163 | ::testing::InitGoogleTest(&argc, argv); 164 | return RUN_ALL_TESTS();; 165 | } 166 | -------------------------------------------------------------------------------- /src/ZipReaderImpl.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "restc-cpp/restc-cpp.h" 4 | #include "restc-cpp/DataReader.h" 5 | #include "restc-cpp/logging.h" 6 | 7 | using namespace std; 8 | 9 | namespace restc_cpp { 10 | 11 | class ZipReaderImpl : public DataReader { 12 | public: 13 | enum class Format { DEFLATE, GZIP }; 14 | 15 | ZipReaderImpl(std::unique_ptr&& source, 16 | const Format format) 17 | : source_{std::move(source)} 18 | { 19 | const auto wsize = (format == Format::GZIP) ? (MAX_WBITS | 16) : MAX_WBITS; 20 | 21 | if (inflateInit2(&strm_, wsize) != Z_OK) { 22 | throw DecompressException("Failed to initialize decompression"); 23 | } 24 | } 25 | 26 | ZipReaderImpl(const ZipReaderImpl&) = delete; 27 | ZipReaderImpl(ZipReaderImpl&&) = delete; 28 | 29 | ZipReaderImpl& operator = (const ZipReaderImpl&) = delete; 30 | ZipReaderImpl& operator = (ZipReaderImpl&&) = delete; 31 | 32 | 33 | ~ZipReaderImpl() override { 34 | inflateEnd(&strm_); 35 | } 36 | 37 | [[nodiscard]] bool IsEof() const override { return done_; } 38 | 39 | void Finish() override { 40 | if (source_) { 41 | source_->Finish(); 42 | } 43 | } 44 | 45 | [[nodiscard]] bool HaveMoreBufferedInput() const noexcept { return strm_.avail_in > 0; } 46 | 47 | ::restc_cpp::boost_const_buffer ReadSome() override { 48 | 49 | size_t data_len = 0; 50 | 51 | while(!done_) { 52 | boost::string_ref src; 53 | if (HaveMoreBufferedInput()) { 54 | src = {}; 55 | } else { 56 | const auto buffers = source_->ReadSome(); 57 | src = { 58 | boost_buffer_cast(buffers), 59 | boost::asio::buffer_size(buffers)}; 60 | 61 | if (src.empty()) { 62 | throw DecompressException("Decompression failed - premature end of stream."); 63 | } 64 | } 65 | 66 | boost::string_ref out = {out_buffer_.data() + data_len, 67 | out_buffer_.size() - data_len}; 68 | 69 | // Decompress sets leftover to cover unread input data 70 | Decompress(src, out); 71 | data_len += out.size(); 72 | 73 | if ((out_buffer_.size() - data_len) == 0) { 74 | break; 75 | } 76 | } 77 | 78 | return {out_buffer_.data(), data_len}; 79 | } 80 | 81 | private: 82 | void Decompress(boost::string_ref& src, 83 | boost::string_ref& dst) { 84 | 85 | RESTC_CPP_LOG_TRACE_("ZipReaderImpl::Decompress: " << src.size() << " bytes"); 86 | 87 | if (!HaveMoreBufferedInput()) { 88 | strm_.next_in = const_cast( 89 | reinterpret_cast(src.data())); 90 | strm_.avail_in 91 | = static_cast(src.size()); 92 | } 93 | 94 | assert(strm_.avail_in > 0); 95 | 96 | strm_.avail_out 97 | = static_cast(dst.size()); 98 | strm_.next_out = const_cast( 99 | reinterpret_cast(dst.data())); 100 | 101 | assert(strm_.avail_out > 0); 102 | 103 | const auto result = inflate(&strm_, Z_SYNC_FLUSH); 104 | switch (result) { 105 | case Z_OK: 106 | break; 107 | case Z_NEED_DICT: 108 | case Z_DATA_ERROR: 109 | case Z_MEM_ERROR: 110 | case Z_STREAM_ERROR: { 111 | std::string errmsg = "Decompression failed"; 112 | if (strm_.msg != nullptr) { 113 | errmsg += ": "; 114 | errmsg += strm_.msg; 115 | } 116 | throw DecompressException(errmsg); 117 | } 118 | case Z_STREAM_END: 119 | RESTC_CPP_LOG_TRACE_("ZipReaderImpl::Decompress(): End Zstream. Done."); 120 | done_ = true; 121 | break; 122 | default: { 123 | std::string errmsg = 124 | string("Decompression failed with unexpected value ") 125 | + to_string(result); 126 | if (strm_.msg != nullptr) { 127 | errmsg += ": "; 128 | errmsg += strm_.msg; 129 | } 130 | throw DecompressException(errmsg); 131 | } 132 | } 133 | 134 | dst = {dst.data(), dst.size() - strm_.avail_out}; 135 | RESTC_CPP_LOG_TRACE_("ZipReaderImpl::Decompress: src=" << dec << src.size() << " bytes, dst=" << dst.size() << " bytes"); 136 | } 137 | 138 | unique_ptr source_; 139 | static constexpr size_t out_buffer_len_ = 1024*8; 140 | array out_buffer_ = {}; 141 | z_stream strm_ = {}; 142 | bool done_ = false; 143 | }; 144 | 145 | 146 | std::unique_ptr 147 | DataReader::CreateZipReader(std::unique_ptr&& source) { 148 | return make_unique(std::move(source), 149 | ZipReaderImpl::Format::DEFLATE); 150 | } 151 | 152 | std::unique_ptr 153 | DataReader::CreateGzipReader(std::unique_ptr&& source) { 154 | return make_unique(std::move(source), 155 | ZipReaderImpl::Format::GZIP); 156 | } 157 | 158 | } // namepsace 159 | 160 | 161 | -------------------------------------------------------------------------------- /include/restc-cpp/IoTimer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef RESTC_CPP_IO_TIMER_H_ 4 | #define RESTC_CPP_IO_TIMER_H_ 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include "restc-cpp/restc-cpp.h" 11 | #include "restc-cpp/logging.h" 12 | #include "restc-cpp/Socket.h" 13 | #include "restc-cpp/Connection.h" 14 | #include "restc-cpp/boost_compatibility.h" 15 | 16 | namespace restc_cpp { 17 | 18 | class IoTimer : public std::enable_shared_from_this 19 | { 20 | public: 21 | using ptr_t = std::shared_ptr; 22 | using close_t = std::function; 23 | 24 | #if (BOOST_VERSION >= 106900) 25 | using asio_premature_deprecation_workaround_t = boost::asio::ip::tcp::socket::executor_type; 26 | #else 27 | using asio_premature_deprecation_workaround_t = boost::asio::io_service; 28 | #endif 29 | 30 | class Wrapper 31 | { 32 | public: 33 | Wrapper(ptr_t&& timer) 34 | : timer_{std::move(timer)} {} 35 | 36 | Wrapper() {} 37 | 38 | ~Wrapper() { 39 | if (timer_) { 40 | timer_->Cancel(); 41 | } 42 | } 43 | 44 | void Cancel() { 45 | if (timer_) { 46 | timer_->Cancel(); 47 | } 48 | } 49 | 50 | private: 51 | ptr_t timer_ = nullptr; 52 | }; 53 | 54 | using wrapper_t = std::unique_ptr; 55 | 56 | ~IoTimer() { 57 | } 58 | 59 | void Handler(const boost::system::error_code& error) { 60 | 61 | if (error) { 62 | return; 63 | } 64 | 65 | if (is_active_) { 66 | is_active_ = false; 67 | is_expiered_ = true; 68 | close_(); 69 | } 70 | } 71 | 72 | void Cancel() { 73 | if (is_active_) { 74 | is_active_ = false; 75 | RESTC_CPP_LOG_TRACE_("Canceled timer " << timer_name_); 76 | } 77 | } 78 | 79 | bool IsExpiered() const noexcept { return is_expiered_; } 80 | 81 | static ptr_t Create( 82 | const std::string& timerName, 83 | int milliseconds_timeout, 84 | const asio_premature_deprecation_workaround_t& io_service, 85 | close_t close) { 86 | 87 | ptr_t timer; 88 | // Private constructor, we cannot use std::make_shared() 89 | timer.reset(new IoTimer(timerName, const_cast(io_service), close)); 90 | timer->Start(milliseconds_timeout); 91 | return timer; 92 | } 93 | 94 | /*! Convenience factory. 95 | * 96 | * Creates an instance of a timer, wrapped in unique_ptr 97 | * that will cancel the timer when the wrapper goes 98 | * out of scope. 99 | * 100 | * \param connection Connection to watch. If the timer expires 101 | * before cancel is called, the connection is closed. 102 | */ 103 | static wrapper_t Create(const std::string& timerName, 104 | int milliseconds_timeout, 105 | const Connection::ptr_t& connection) { 106 | 107 | if (!connection || (milliseconds_timeout <= 0)) { 108 | return std::make_unique(); 109 | } 110 | 111 | std::weak_ptr weak_connection = connection; 112 | 113 | RESTC_CPP_LOG_TRACE_("Created timer " << timerName 114 | << " for " << *connection); 115 | 116 | return std::make_unique(Create( 117 | timerName, 118 | milliseconds_timeout, 119 | #if (BOOST_VERSION >= 106900) 120 | connection->GetSocket().GetSocket().get_executor(), 121 | #else 122 | connection->GetSocket().GetSocket().get_io_service(), 123 | #endif 124 | [weak_connection, timerName]() { 125 | if (auto connection = weak_connection.lock()) { 126 | if (connection->GetSocket().GetSocket().is_open()) { 127 | RESTC_CPP_LOG_TRACE_("Timer " << timerName << ": " 128 | << *connection 129 | << " timed out."); 130 | try { 131 | connection->GetSocket().Close(Socket::Reason::TIME_OUT); 132 | } catch(std::exception& ex) { 133 | RESTC_CPP_LOG_WARN_("Caught exception while closing socket: " << ex.what()); 134 | } 135 | } 136 | } 137 | })); 138 | } 139 | 140 | 141 | private: 142 | IoTimer(const std::string& timerName, asio_premature_deprecation_workaround_t& io_service, 143 | close_t close) 144 | : close_{close}, timer_{io_service}, timer_name_{timerName} 145 | {} 146 | 147 | void Start(int millisecondsTimeOut) 148 | { 149 | timer_.RESTC_CPP_STEADY_TIMER_EXPIRES_AFTER(std::chrono::milliseconds{millisecondsTimeOut}); 150 | is_active_ = true; 151 | try { 152 | timer_.async_wait(std::bind( 153 | &IoTimer::Handler, 154 | shared_from_this(), 155 | std::placeholders::_1)); 156 | } catch (const std::exception&) { 157 | is_active_ = false; 158 | } 159 | } 160 | 161 | private: 162 | bool is_active_ = false; 163 | bool is_expiered_ = false; 164 | close_t close_; 165 | boost::asio::steady_timer timer_; 166 | const std::string timer_name_; 167 | }; 168 | 169 | } // restc_cpp 170 | 171 | #endif // RESTC_CPP_IO_TIMER_H_ 172 | 173 | -------------------------------------------------------------------------------- /src/ChunkedReaderImpl.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "restc-cpp/restc-cpp.h" 6 | #include "restc-cpp/DataReader.h" 7 | #include "restc-cpp/DataReaderStream.h" 8 | #include "restc-cpp/error.h" 9 | #include "restc-cpp/logging.h" 10 | 11 | using namespace std; 12 | 13 | namespace restc_cpp { 14 | 15 | 16 | class ChunkedReaderImpl : public DataReader { 17 | public: 18 | 19 | ChunkedReaderImpl(add_header_fn_t&& fn, unique_ptr&& source) 20 | : stream_{std::move(source)}, add_header_(std::move(fn)) 21 | { 22 | } 23 | 24 | [[nodiscard]] bool IsEof() const override { return stream_->IsEof(); } 25 | 26 | void Finish() override { 27 | ReadSome(); 28 | if (!IsEof()) { 29 | throw ProtocolException("Failed to finish chunked payload"); 30 | } 31 | 32 | if (stream_) { 33 | stream_->Finish(); 34 | } 35 | } 36 | 37 | [[nodiscard]] static string ToPrintable(boost::string_ref buf) 38 | { 39 | ostringstream out; 40 | locale const loc; 41 | auto pos = 0; 42 | out << '\n'; 43 | 44 | for(const auto ch : buf) { 45 | if ((++pos % line_length) == 0u) { 46 | out << '\n'; 47 | } 48 | if (std::isprint(ch, loc)) { 49 | out << ch; 50 | } else { 51 | out << '.'; 52 | } 53 | } 54 | 55 | return out.str(); 56 | } 57 | 58 | static void Log(const ::restc_cpp::boost_const_buffer buffers, const char * tag) 59 | { 60 | const auto buf_len = boost_buffer_size(buffers); 61 | 62 | // At the time of the implementation, there are never multiple buffers. 63 | RESTC_CPP_LOG_TRACE_(tag << ' ' << "# " << buf_len 64 | << " bytes: " 65 | << ToPrintable({ 66 | boost_buffer_cast(buffers), buf_len})); 67 | } 68 | 69 | ::restc_cpp::boost_const_buffer ReadSome() override { 70 | 71 | EatPadding(); 72 | 73 | if (stream_->IsEof()) { 74 | return {nullptr, 0}; 75 | } 76 | 77 | if (chunk_len_ == 0) { 78 | RESTC_CPP_LOG_TRACE_("ChunkedReaderImpl::ReadSome(): Need new chunk."); 79 | chunk_len_ = GetNextChunkLen(); 80 | RESTC_CPP_LOG_TRACE_("ChunkedReaderImpl::ReadSome(): " 81 | << "Next chunk is " << chunk_len_ << " bytes (" 82 | << hex << chunk_len_ << " hex)"); 83 | if (chunk_len_ == 0) { 84 | // Read the trailer 85 | RESTC_CPP_LOG_TRACE_("ChunkedReaderImpl::ReadSome(): End of chunked stream - reading headers"); 86 | stream_->ReadHeaderLines(add_header_); 87 | stream_->SetEof(); 88 | RESTC_CPP_LOG_TRACE_("ChunkedReaderImpl::ReadSome(): End of chunked stream. Done."); 89 | return {nullptr, 0}; 90 | } 91 | } 92 | 93 | auto data = GetData(); 94 | 95 | Log(data, "ChunkedReaderImpl::ReadSome()"); 96 | 97 | return data; 98 | } 99 | 100 | private: 101 | void EatPadding() { 102 | if (eat_chunk_padding_) { 103 | eat_chunk_padding_ = false; 104 | 105 | if (stream_->Getc() != '\r') { 106 | throw ParseException("Chunk: Missing padding CR!"); 107 | } 108 | 109 | if (stream_->Getc() != '\n') { 110 | throw ParseException("Chunk: Missing padding LF!"); 111 | } 112 | } 113 | } 114 | 115 | ::restc_cpp::boost_const_buffer GetData() { 116 | 117 | auto rval = stream_->GetData(chunk_len_); 118 | const auto seg_len = boost::asio::buffer_size(rval); 119 | chunk_len_ -= seg_len; 120 | 121 | if (chunk_len_ == 0) { 122 | eat_chunk_padding_ = true; 123 | } 124 | 125 | return rval; 126 | } 127 | 128 | size_t GetNextChunkLen() { 129 | static constexpr size_t magic_16 = 16; 130 | static constexpr size_t magic_10 = 10; 131 | size_t chunk_len = 0; 132 | char ch = stream_->Getc(); 133 | 134 | if (isxdigit(ch) == 0) { 135 | throw ParseException("Missing chunk-length in new chunk."); 136 | } 137 | 138 | for (; isxdigit(ch) != 0; ch = stream_->Getc()) { 139 | chunk_len *= magic_16; 140 | if (ch >= 'a') { 141 | chunk_len += magic_10 + (ch - 'a'); 142 | } else if (ch >= 'A') { 143 | chunk_len += magic_10 + (ch - 'A'); 144 | } else { 145 | chunk_len += ch - '0'; 146 | } 147 | } 148 | 149 | for (; ch != '\r'; ch = stream_->Getc()) { 150 | ; 151 | } 152 | 153 | if (ch != '\r') { 154 | throw ParseException("Missing CR in first chunk line"); 155 | } 156 | 157 | if ((ch = stream_->Getc()) != '\n') { 158 | throw ParseException("Missing LF in first chunk line"); 159 | } 160 | 161 | return chunk_len; 162 | } 163 | 164 | size_t chunk_len_ = 0; 165 | bool eat_chunk_padding_ = false; 166 | std::unique_ptr stream_; 167 | //boost::string_ref buffer; 168 | add_header_fn_t add_header_; 169 | }; 170 | 171 | DataReader::ptr_t 172 | DataReader::CreateChunkedReader(add_header_fn_t fn, unique_ptr&& source) { 173 | return make_unique(std::move(fn), std::move(source)); 174 | } 175 | 176 | 177 | } // namespace 178 | 179 | -------------------------------------------------------------------------------- /tests/functional/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(functionaltests) 2 | 3 | 4 | # ====================================== 5 | message("DEFAULT_LIBRARIES: ${DEFAULT_LIBRARIES}") 6 | 7 | add_executable(basic_tests BasicTests.cpp) 8 | SET_CPP_STANDARD(basic_tests) 9 | add_dependencies(basic_tests restc-cpp ${DEPENDS_GTEST}) 10 | target_link_libraries(basic_tests PRIVATE ${GTEST_LIBRARIES} restc-cpp ${DEFAULT_LIBRARIES}) 11 | add_test(BASIC_TESTS basic_tests) 12 | 13 | 14 | # ====================================== 15 | 16 | add_executable(readme_tests ReadmeTests.cpp) 17 | target_link_libraries(readme_tests 18 | ${GTEST_LIBRARIES} 19 | restc-cpp 20 | ${DEFAULT_LIBRARIES} 21 | ) 22 | SET_CPP_STANDARD(readme_tests) 23 | add_dependencies(readme_tests restc-cpp ${DEPENDS_GTEST}) 24 | add_test(README_TESTS readme_tests) 25 | 26 | # ====================================== 27 | 28 | add_executable(auth_tests AuthTest.cpp) 29 | target_link_libraries(auth_tests 30 | ${GTEST_LIBRARIES} 31 | restc-cpp 32 | ${DEFAULT_LIBRARIES} 33 | ) 34 | add_dependencies(auth_tests restc-cpp ${DEPENDS_GTEST}) 35 | SET_CPP_STANDARD(auth_tests) 36 | add_test(AUTH_TESTS auth_tests) 37 | 38 | 39 | # ====================================== 40 | 41 | add_executable(upload_tests UploadTests.cpp) 42 | target_link_libraries(upload_tests 43 | ${GTEST_LIBRARIES} 44 | restc-cpp 45 | ${DEFAULT_LIBRARIES} 46 | ) 47 | add_dependencies(upload_tests restc-cpp ${DEPENDS_GTEST}) 48 | SET_CPP_STANDARD(upload_tests) 49 | add_test(UPLOAD_TESTS upload_tests) 50 | 51 | 52 | # # ====================================== 53 | 54 | add_executable(inserter_serializer_tests InsertSerializerTest.cpp) 55 | target_link_libraries(inserter_serializer_tests 56 | ${GTEST_LIBRARIES} 57 | restc-cpp 58 | ${DEFAULT_LIBRARIES} 59 | ) 60 | add_dependencies(inserter_serializer_tests restc-cpp ${DEPENDS_GTEST}) 61 | SET_CPP_STANDARD(inserter_serializer_tests) 62 | add_test(INSERTER_SERIALIZER_TESTS inserter_serializer_tests) 63 | 64 | 65 | # # ====================================== 66 | 67 | add_executable(ccache_tests ConnectionCacheTests.cpp) 68 | target_link_libraries(ccache_tests 69 | ${GTEST_LIBRARIES} 70 | restc-cpp 71 | ${DEFAULT_LIBRARIES} 72 | ) 73 | add_dependencies(ccache_tests restc-cpp ${DEPENDS_GTEST}) 74 | SET_CPP_STANDARD(ccache_tests) 75 | add_test(CCACHE_FUNCTIONAL_TESTS ccache_tests) 76 | 77 | # # ====================================== 78 | 79 | add_executable(redirect_tests RedirectTests.cpp) 80 | target_link_libraries(redirect_tests 81 | ${GTEST_LIBRARIES} 82 | restc-cpp 83 | ${DEFAULT_LIBRARIES} 84 | ) 85 | add_dependencies(redirect_tests restc-cpp ${DEPENDS_GTEST}) 86 | SET_CPP_STANDARD(redirect_tests) 87 | add_test(REDIRECT_FUNCTIONAL_TESTS redirect_tests) 88 | 89 | 90 | # # ====================================== 91 | 92 | add_executable(crud_tests CRUD_test.cpp) 93 | target_link_libraries(crud_tests 94 | ${GTEST_LIBRARIES} 95 | restc-cpp 96 | ${DEFAULT_LIBRARIES} 97 | ) 98 | add_dependencies(crud_tests restc-cpp ${DEPENDS_GTEST}) 99 | SET_CPP_STANDARD(crud_tests) 100 | add_test(CRUD_FUNCTIONAL_TESTS crud_tests) 101 | 102 | 103 | # ====================================== 104 | 105 | add_executable(many_connections_tests ManyConnectionsTest.cpp) 106 | target_link_libraries(many_connections_tests 107 | ${GTEST_LIBRARIES} 108 | restc-cpp 109 | ${DEFAULT_LIBRARIES} 110 | ) 111 | add_dependencies(many_connections_tests restc-cpp ${DEPENDS_GTEST}) 112 | SET_CPP_STANDARD(many_connections_tests) 113 | add_test(MANY_CONNECTIONS_FUNCTIONAL_TESTS many_connections_tests) 114 | 115 | 116 | # ====================================== 117 | 118 | if (RESTC_CPP_WITH_TLS) 119 | add_executable(https_tests HttpsTest.cpp) 120 | target_link_libraries(https_tests 121 | ${GTEST_LIBRARIES} 122 | restc-cpp 123 | ${DEFAULT_LIBRARIES} 124 | ) 125 | add_dependencies(https_tests restc-cpp ${DEPENDS_GTEST}) 126 | SET_CPP_STANDARD(https_tests) 127 | add_test(HTTPS_FUNCTIONAL_TESTS https_tests) 128 | endif() 129 | 130 | 131 | # ====================================== 132 | 133 | add_executable(own_ioservice_tests OwnIoserviceTests.cpp) 134 | target_link_libraries(own_ioservice_tests 135 | ${GTEST_LIBRARIES} 136 | restc-cpp 137 | ${DEFAULT_LIBRARIES} 138 | ) 139 | add_dependencies(own_ioservice_tests restc-cpp ${DEPENDS_GTEST}) 140 | SET_CPP_STANDARD(own_ioservice_tests) 141 | add_test(OWN_IOSERVICE_TESTS own_ioservice_tests) 142 | 143 | 144 | 145 | # ====================================== 146 | 147 | add_executable(connection_pool_instances_test ConnectionPoolInstancesTest.cpp) 148 | target_link_libraries(connection_pool_instances_test 149 | ${GTEST_LIBRARIES} 150 | restc-cpp 151 | ${DEFAULT_LIBRARIES} 152 | ) 153 | add_dependencies(connection_pool_instances_test restc-cpp ${DEPENDS_GTEST}) 154 | SET_CPP_STANDARD(connection_pool_instances_test) 155 | add_test(CONNECTION_POOL_INSTANCES_TEST connection_pool_instances_test) 156 | 157 | 158 | 159 | # ====================================== 160 | 161 | add_executable(proxy-tests ProxyTests.cpp) 162 | target_link_libraries(proxy-tests 163 | ${GTEST_LIBRARIES} 164 | restc-cpp 165 | ${DEFAULT_LIBRARIES} 166 | ) 167 | add_dependencies(proxy-tests restc-cpp ${DEPENDS_GTEST}) 168 | SET_CPP_STANDARD(proxy-tests) 169 | add_test(PROXY_TESTS proxy-tests) 170 | 171 | 172 | # ====================================== 173 | 174 | add_executable(cookie-tests CookieTests.cpp) 175 | target_link_libraries(cookie-tests 176 | ${GTEST_LIBRARIES} 177 | restc-cpp 178 | ${DEFAULT_LIBRARIES} 179 | ) 180 | add_dependencies(cookie-tests restc-cpp ${DEPENDS_GTEST}) 181 | SET_CPP_STANDARD(cookie-tests) 182 | add_test(COOKIE_TESTS cookie-tests) 183 | 184 | # ====================================== 185 | 186 | add_executable(properties-tests PropertiesTests.cpp) 187 | target_link_libraries(properties-tests 188 | ${GTEST_LIBRARIES} 189 | restc-cpp 190 | ${DEFAULT_LIBRARIES} 191 | ) 192 | add_dependencies(properties-tests restc-cpp ${DEPENDS_GTEST}) 193 | SET_CPP_STANDARD(properties-tests) 194 | add_test(PROPERTIES_TESTS properties-tests) 195 | 196 | -------------------------------------------------------------------------------- /tests/functional/ConnectionCacheTests.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Include before boost::log headers 3 | #include "restc-cpp/restc-cpp.h" 4 | #include "restc-cpp/logging.h" 5 | #include "restc-cpp/ConnectionPool.h" 6 | #include "restc-cpp/boost_compatibility.h" 7 | 8 | #include "../src/ReplyImpl.h" 9 | 10 | #include "gtest/gtest.h" 11 | #include "restc-cpp/test_helper.h" 12 | 13 | /* These url's points to a local Docker container with nginx, linked to 14 | * a jsonserver docker container with mock data. 15 | * The scripts to build and run these containers are in the ./tests directory. 16 | */ 17 | const string http_url = "http://localhost:3001/normal/posts"; 18 | const string http_url_many = "http://localhost:3001/normal/manyposts"; 19 | const string http_connection_close_url = "http://localhost:3001/close/posts"; 20 | 21 | using namespace std; 22 | using namespace restc_cpp; 23 | 24 | 25 | TEST(ConnectionCache, ConnectionRecycling) { 26 | 27 | auto rest_client = RestClient::Create(); 28 | rest_client->ProcessWithPromise([&](Context& ctx) { 29 | 30 | auto repl_one = ctx.Get(GetDockerUrl(http_url)); 31 | auto first_conn_id = repl_one->GetConnectionId(); 32 | EXPECT_EQ(200, repl_one->GetResponseCode()); 33 | // Discard all data 34 | while(repl_one->MoreDataToRead()) { 35 | repl_one->GetSomeData(); 36 | } 37 | 38 | auto repl_two = ctx.Get(GetDockerUrl(http_url)); 39 | auto second_conn_id = repl_two->GetConnectionId(); 40 | EXPECT_EQ(200, repl_two->GetResponseCode()); 41 | // Discard all data 42 | while(repl_two->MoreDataToRead()) { 43 | repl_two->GetSomeData(); 44 | } 45 | 46 | EXPECT_EQ(first_conn_id, second_conn_id); 47 | 48 | }).get(); 49 | } 50 | 51 | // Test that we honor 'Connection: close' server header 52 | TEST(ConnectionCache, ConnectionClose) { 53 | auto rest_client = RestClient::Create(); 54 | rest_client->ProcessWithPromise([&](Context& ctx) { 55 | 56 | auto repl_one = ctx.Get(GetDockerUrl(http_connection_close_url)); 57 | 58 | EXPECT_EQ(200, repl_one->GetResponseCode()); 59 | 60 | // Discard all data 61 | while(repl_one->MoreDataToRead()) { 62 | repl_one->GetSomeData(); 63 | } 64 | 65 | EXPECT_EQ(0, static_cast( 66 | rest_client->GetConnectionPool()->GetIdleConnections())); 67 | 68 | }).get(); 69 | } 70 | 71 | TEST(ConnectionCache, MaxConnectionsToEndpoint) { 72 | auto rest_client = RestClient::Create(); 73 | auto pool = rest_client->GetConnectionPool(); 74 | auto config = rest_client->GetConnectionProperties(); 75 | 76 | std::vector connections; 77 | const auto ep = boost_create_endpoint("127.0.0.1", 80); 78 | for(size_t i = 0; i < config->cacheMaxConnectionsPerEndpoint; ++i) { 79 | connections.push_back(pool->GetConnection(ep, restc_cpp::Connection::Type::HTTP)); 80 | } 81 | 82 | EXPECT_THROW(pool->GetConnection(ep, 83 | restc_cpp::Connection::Type::HTTP), std::runtime_error); 84 | } 85 | 86 | TEST(ConnectionCache, MaxConnections) { 87 | auto rest_client = RestClient::Create(); 88 | auto pool = rest_client->GetConnectionPool(); 89 | auto config = rest_client->GetConnectionProperties(); 90 | 91 | auto addr = boost_convert_ipv4_to_uint("127.0.0.1"); 92 | 93 | std::vector connections; 94 | decltype(addr) i = 0; 95 | for(; i < config->cacheMaxConnections; ++i) { 96 | connections.push_back(pool->GetConnection( 97 | boost::asio::ip::tcp::endpoint{ 98 | boost::asio::ip::address_v4{static_cast(addr + i)}, 80}, 99 | restc_cpp::Connection::Type::HTTP)); 100 | } 101 | 102 | EXPECT_THROW(pool->GetConnection( 103 | boost::asio::ip::tcp::endpoint{ 104 | boost::asio::ip::address_v4{static_cast(addr + i)}, 80}, 105 | restc_cpp::Connection::Type::HTTP), std::runtime_error); 106 | } 107 | 108 | TEST(ConnectionCache, CleanupTimer) { 109 | auto rest_client = RestClient::Create(); 110 | auto pool = rest_client->GetConnectionPool(); 111 | auto config = rest_client->GetConnectionProperties(); 112 | 113 | config->cacheTtlSeconds = 2; 114 | config->cacheCleanupIntervalSeconds = 1; 115 | 116 | rest_client->ProcessWithPromise([&](Context& ctx) { 117 | auto repl = ctx.Get(GetDockerUrl(http_url)); 118 | EXPECT_EQ(200, repl->GetResponseCode()); 119 | 120 | // Discard all data 121 | while(repl->MoreDataToRead()) { 122 | repl->GetSomeData(); 123 | } 124 | 125 | }).get(); 126 | 127 | EXPECT_EQ(1, static_cast(pool->GetIdleConnections())); 128 | 129 | std::this_thread::sleep_for(std::chrono::seconds(4)); 130 | 131 | EXPECT_EQ(0, static_cast(pool->GetIdleConnections())); 132 | } 133 | 134 | TEST(ConnectionCache, PrematureCloseNotRecycled) { 135 | auto rest_client = RestClient::Create(); 136 | rest_client->ProcessWithPromise([&](Context& ctx) { 137 | 138 | auto repl_one = ctx.Get(GetDockerUrl(http_url_many)); 139 | 140 | EXPECT_EQ(200, repl_one->GetResponseCode()); 141 | 142 | repl_one.reset(); 143 | 144 | EXPECT_EQ(0, static_cast( 145 | rest_client->GetConnectionPool()->GetIdleConnections())); 146 | 147 | }).get(); 148 | } 149 | 150 | TEST(ConnectionCache, OverrideMaxConnectionsToEndpoint) { 151 | auto rest_client = RestClient::Create(); 152 | auto pool = rest_client->GetConnectionPool(); 153 | auto config = rest_client->GetConnectionProperties(); 154 | 155 | std::vector connections; 156 | auto const ep = boost_create_endpoint("127.0.0.1", 80); 157 | for(size_t i = 0; i < config->cacheMaxConnectionsPerEndpoint; ++i) { 158 | connections.push_back(pool->GetConnection(ep, restc_cpp::Connection::Type::HTTP)); 159 | } 160 | 161 | connections.push_back(pool->GetConnection(ep, restc_cpp::Connection::Type::HTTP, true)); 162 | } 163 | 164 | int main( int argc, char * argv[] ) 165 | { 166 | RESTC_CPP_TEST_LOGGING_SETUP("debug"); 167 | ::testing::InitGoogleTest(&argc, argv); 168 | return RUN_ALL_TESTS();; 169 | } 170 | -------------------------------------------------------------------------------- /include/restc-cpp/IteratorFromJsonSerializer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "restc-cpp/restc-cpp.h" 4 | #include "restc-cpp/SerializeJson.h" 5 | 6 | namespace restc_cpp { 7 | 8 | 9 | /*! Serialize as a container 10 | * 11 | * Serialize only one list-object at a time. Follow 12 | * the InputIterator contract. 13 | * ref: http://en.cppreference.com/w/cpp/concept/InputIterator 14 | */ 15 | template 16 | class IteratorFromJsonSerializer 17 | { 18 | public: 19 | using data_t = typename std::remove_const::type>::type; 20 | 21 | class Iterator { 22 | public: 23 | using iterator_category = std::input_iterator_tag; 24 | using value_type = data_t; 25 | using difference_type = std::ptrdiff_t; 26 | using pointer = value_type*; 27 | using reference = value_type&; 28 | 29 | public: 30 | Iterator() {} 31 | 32 | /*! Copy constructor 33 | * \note This constructor mailnly is supplied to make it++ work. 34 | * It is not optimized for performance. 35 | * As always, prefer ++it. 36 | */ 37 | Iterator(const Iterator& it) 38 | : owner_{it.owner_} 39 | { 40 | if (it.data_) { 41 | data_ = std::make_unique(*it.data_); 42 | } 43 | } 44 | 45 | Iterator(Iterator&& it) 46 | : owner_{it.owner_}, data_{std::move(it.data_)} {} 47 | 48 | Iterator(IteratorFromJsonSerializer *owner) 49 | : owner_{owner} {} 50 | 51 | Iterator& operator++() { 52 | prepare(); 53 | fetch(); 54 | return *this; 55 | } 56 | 57 | Iterator operator++(int) { 58 | prepare(); 59 | Iterator retval = *this; 60 | fetch(); 61 | return retval; 62 | } 63 | 64 | Iterator& operator = (const Iterator& it) { 65 | owner_ = it.owner_; 66 | if (it.data_) { 67 | data_ = std::make_unique(*it.data_); 68 | } 69 | return *this; 70 | } 71 | 72 | Iterator& operator = (Iterator&& it) { 73 | owner_ = it.owner_; 74 | it.data_ = std::move(it.data_); 75 | } 76 | 77 | bool operator == (const Iterator& other) const { 78 | prepare(); 79 | return data_.get() == other.data_.get(); 80 | } 81 | 82 | bool operator != (const Iterator& other) const { 83 | return ! operator == (other); 84 | } 85 | 86 | data_t& operator*() const { 87 | prepare(); 88 | return get(); 89 | } 90 | 91 | data_t *operator -> () const { 92 | prepare(); 93 | return data_.get(); 94 | } 95 | 96 | private: 97 | void prepare() const { 98 | if (virgin_) { 99 | virgin_ = false; 100 | 101 | const_cast(this)->fetch(); 102 | } 103 | } 104 | 105 | void fetch() { 106 | if (owner_) { 107 | data_ = owner_->fetch(); 108 | } else { 109 | throw CannotIncrementEndException( 110 | "this is an end() iterator."); 111 | } 112 | } 113 | 114 | data_t& get() const { 115 | if (!data_) { 116 | throw NoDataException("data_ is empty"); 117 | } 118 | return *data_; 119 | } 120 | 121 | IteratorFromJsonSerializer *owner_ = nullptr; 122 | std::unique_ptr data_; 123 | mutable bool virgin_ = true; 124 | }; 125 | 126 | 127 | using iterator_t = Iterator; 128 | 129 | IteratorFromJsonSerializer( 130 | Reply& reply, 131 | const serialize_properties_t *properties = nullptr, 132 | bool withoutLeadInSquereBrancket = false) 133 | : reply_stream_{reply}, properties_{properties} 134 | { 135 | if (!properties_) { 136 | pbuf = std::make_unique(); 137 | properties_ = pbuf.get(); 138 | } 139 | if (withoutLeadInSquereBrancket) { 140 | state_ = State::ITERATING; 141 | } 142 | } 143 | 144 | iterator_t begin() { 145 | return Iterator{this}; 146 | } 147 | 148 | iterator_t end() { 149 | return Iterator{}; 150 | } 151 | 152 | private: 153 | std::unique_ptr fetch() { 154 | 155 | if(state_ == State::PRE) { 156 | const auto ch = reply_stream_.Take(); 157 | if (ch != '[') { 158 | throw ParseException("Expected leading json '['"); 159 | } 160 | state_ = State::ITERATING; 161 | } 162 | 163 | if (state_ == State::ITERATING) { 164 | while(true) { 165 | const auto ch = reply_stream_.Peek(); 166 | if ((ch == ' ') || (ch == '\t') || (ch == ',') || (ch == '\n') || (ch == '\r')) { 167 | reply_stream_.Take(); 168 | continue; 169 | } else if (ch == '{') { 170 | auto data = std::make_unique(); 171 | RapidJsonDeserializer handler( 172 | *data, *properties_); 173 | json_reader_.Parse(reply_stream_, handler); 174 | return std::move(data); 175 | } else if (ch == ']') { 176 | reply_stream_.Take(); 177 | state_ = State::DONE; 178 | break; 179 | } else { 180 | static const std::string err_msg{"IteratorFromJsonSerializer: Unexpected character in input stream: "}; 181 | throw ParseException(err_msg + std::string(1, ch)); 182 | } 183 | } 184 | } 185 | 186 | assert(state_ == State::DONE); 187 | return {}; 188 | } 189 | 190 | enum class State { PRE, ITERATING, DONE }; 191 | State state_ = State::PRE; 192 | RapidJsonReader reply_stream_; 193 | rapidjson::Reader json_reader_; 194 | std::unique_ptr pbuf; 195 | const serialize_properties_t *properties_ = nullptr; 196 | }; 197 | 198 | } // namespace 199 | 200 | -------------------------------------------------------------------------------- /src/DataReaderStream.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "restc-cpp/DataReaderStream.h" 3 | #include "restc-cpp/error.h" 4 | #include "restc-cpp/url_encode.h" 5 | #include "restc-cpp/logging.h" 6 | #include 7 | 8 | using namespace std; 9 | 10 | namespace restc_cpp { 11 | 12 | DataReaderStream::DataReaderStream(std::unique_ptr&& source) 13 | : source_{std::move(source)} { 14 | RESTC_CPP_LOG_TRACE_("DataReaderStream: Chained to " 15 | << RESTC_CPP_TYPENAME(decltype(*source_))); 16 | } 17 | 18 | 19 | void DataReaderStream::Fetch() { 20 | if (++curr_ >= end_) { 21 | auto buf = source_->ReadSome(); 22 | 23 | RESTC_CPP_LOG_TRACE_("DataReaderStream::Fetch: Fetched buffer with " 24 | << boost::asio::buffer_size(buf) << " bytes."); 25 | 26 | const auto bytes = boost::asio::buffer_size(buf); 27 | if (bytes == 0) { 28 | RESTC_CPP_LOG_TRACE_("DataReaderStream::Fetch: EOF"); 29 | throw ProtocolException("Fetch(): EOF"); 30 | } 31 | curr_ = boost_buffer_cast(buf); 32 | end_ = curr_ + boost::asio::buffer_size(buf); 33 | } 34 | } 35 | 36 | ::restc_cpp::boost_const_buffer 37 | DataReaderStream::ReadSome() { 38 | Fetch(); 39 | 40 | ::restc_cpp::boost_const_buffer rval = {curr_, 41 | static_cast(end_ - curr_)}; 42 | curr_ = end_; 43 | RESTC_CPP_LOG_TRACE_("DataReaderStream::ReadSome: Returning buffer with " 44 | << boost::asio::buffer_size(rval) << " bytes."); 45 | 46 | if (source_->IsEof()) { 47 | SetEof(); 48 | } 49 | 50 | return rval; 51 | } 52 | 53 | ::restc_cpp::boost_const_buffer 54 | DataReaderStream::GetData(size_t maxBytes) { 55 | Fetch(); 56 | 57 | const auto diff = end_ - curr_; 58 | assert(diff >= 0); 59 | const auto seg_len = std::min(maxBytes, diff); 60 | ::restc_cpp::boost_const_buffer rval = {curr_, seg_len}; 61 | if (seg_len > 0) { 62 | curr_ += seg_len - 1; 63 | } 64 | 65 | RESTC_CPP_LOG_TRACE_("DataReaderStream::GetData(" << maxBytes << "): Returning buffer with " 66 | << boost::asio::buffer_size(rval) << " bytes."); 67 | 68 | return rval; 69 | } 70 | 71 | 72 | void DataReaderStream::ReadServerResponse(Reply::HttpResponse& response) 73 | { 74 | static const string http_1_1{"HTTP/1.1"}; 75 | constexpr size_t max_version_len = 16; 76 | constexpr size_t max_phrase_len = 256; 77 | char ch = {}; 78 | getc_bytes_ = 0; 79 | 80 | // Get HTTP version 81 | std::string value; 82 | for(ch = Getc(); ch != ' '; ch = Getc()) { 83 | value += ch; 84 | if (value.size() > max_version_len) { 85 | throw ProtocolException("ReadHeaders(): Too much HTTP version!"); 86 | } 87 | } 88 | if (ch != ' ') { 89 | throw ProtocolException("ReadHeaders(): No space after HTTP version"); 90 | } 91 | if (value.empty()) { 92 | throw ProtocolException("ReadHeaders(): No HTTP version"); 93 | } 94 | if (ciEqLibC()(value, http_1_1)) { 95 | ; // Do nothing HTTP 1.1 is the default value 96 | } else { 97 | throw ProtocolException( 98 | string("ReadHeaders(): unsupported HTTP version: ") 99 | + url_encode(value)); 100 | } 101 | 102 | // Get response code 103 | value.clear(); 104 | for(ch = Getc(); ch != ' '; ch = Getc()) { 105 | value += ch; 106 | if (value.size() > 3) { 107 | throw ProtocolException("ReadHeaders(): Too much HTTP response code!"); 108 | } 109 | } 110 | if (value.size() != 3) { 111 | throw ProtocolException( 112 | string("ReadHeaders(): Incorrect length of HTTP response code!: ") 113 | + value); 114 | } 115 | 116 | response.status_code = stoi(value); 117 | 118 | if (ch != ' ') { 119 | throw ProtocolException("ReadHeaders(): No space after HTTP response code"); 120 | } 121 | 122 | // Get response text 123 | value.clear(); 124 | for(ch = Getc(); ch != '\r'; ch = Getc()) { 125 | value += ch; 126 | if (value.size() > max_phrase_len) { 127 | throw ConstraintException("ReadHeaders(): Too long HTTP response phrase!"); 128 | } 129 | } 130 | 131 | // Skip CRLF 132 | assert(ch == '\r'); 133 | ch = Getc(); 134 | if (ch != '\n') { 135 | throw ProtocolException("ReadHeaders(): No CR/LF after HTTP response phrase!"); 136 | } 137 | 138 | response.reason_phrase = std::move(value); 139 | RESTC_CPP_LOG_TRACE_("ReadServerResponse: getc_bytes is " << getc_bytes_); 140 | 141 | RESTC_CPP_LOG_TRACE_("HTTP Response: " 142 | << (response.http_version == Reply::HttpResponse::HttpVersion::HTTP_1_1 143 | ? "HTTP/1.1" : "???") 144 | << ' ' << response.status_code 145 | << ' ' << response.reason_phrase); 146 | } 147 | 148 | void DataReaderStream::ReadHeaderLines(const add_header_fn_t& addHeader) { 149 | constexpr size_t max_name_len = 256; 150 | constexpr size_t max_headers = 256; 151 | 152 | while(true) { 153 | char ch = 0; 154 | string name; 155 | string value; 156 | for(ch = Getc(); ch != '\r'; ch = Getc()) { 157 | if (ch == ' ' || ch == '\t') { 158 | continue; 159 | } 160 | if (ch == ':') { 161 | value = GetHeaderValue(); 162 | ch = '\n'; 163 | break; 164 | } 165 | name += ch; 166 | if (name.size() > max_name_len) { 167 | throw ConstraintException("Chunk Trailer: Header name too long!"); 168 | } 169 | } 170 | 171 | if (ch == '\r') { 172 | ch = Getc(); 173 | } 174 | 175 | if (ch != '\n') { 176 | throw ProtocolException("Chunk Trailer: Missing LF after parse!"); 177 | } 178 | 179 | if (name.empty()) { 180 | if (!value.empty()) { 181 | throw ProtocolException("Chunk Trailer: Header value without name!"); 182 | } 183 | RESTC_CPP_LOG_TRACE_("ReadHeaderLines: getc_bytes is " << getc_bytes_); 184 | getc_bytes_ = 0; 185 | return; // An empty line marks the end of the trailer 186 | } 187 | 188 | if (++num_headers_ > max_headers) { 189 | throw ConstraintException("Chunk Trailer: Too many lines in header!"); 190 | } 191 | 192 | RESTC_CPP_LOG_TRACE_(name << ": " << value); 193 | addHeader(std::move(name), std::move(value)); 194 | name.clear(); 195 | value.clear(); 196 | } 197 | } 198 | 199 | std::string DataReaderStream::GetHeaderValue() { 200 | constexpr size_t max_header_value_len = 1024 * 4; 201 | std::string value; 202 | char ch = 0; 203 | 204 | while(true) { 205 | for (ch = Getc(); ch == ' ' || ch == '\t'; ch = Getc()) { 206 | ; // skip space 207 | } 208 | 209 | for (; ch != '\r'; ch = Getc()) { 210 | value += ch; 211 | if (value.size() > max_header_value_len) { 212 | throw ConstraintException("Chunk Trailer: Header value too long!"); 213 | } 214 | } 215 | 216 | if (ch != '\r') { 217 | throw ProtocolException("Chunk Trailer: Missing CR!"); 218 | } 219 | 220 | if ((ch = Getc()) != '\n') { 221 | throw ProtocolException("Chunk Trailer: Missing LF!"); 222 | } 223 | 224 | // Peek 225 | ch = Getc(); 226 | if ((ch != ' ') && (ch != '\t')) { 227 | Ungetc(); 228 | return value; 229 | } 230 | 231 | value += ' '; 232 | } 233 | } 234 | 235 | 236 | void DataReaderStream::SetEof() { 237 | RESTC_CPP_LOG_TRACE_("Reached EOF"); 238 | eof_ = true; 239 | } 240 | 241 | } // namespace 242 | --------------------------------------------------------------------------------