├── tests ├── CMakeLists.txt ├── external │ └── googletest ├── test_query.cpp ├── test_cookie.cpp ├── test_header.cpp ├── test_form.cpp ├── test_request.cpp └── test_response.cpp ├── external └── seal_lake ├── include └── hot_teacup │ ├── query_view.h │ ├── query.h │ ├── response_view.h │ ├── form_view.h │ ├── cookie_view.h │ ├── header_view.h │ ├── form.h │ ├── cookie.h │ ├── header.h │ ├── request_view.h │ ├── request.h │ ├── response.h │ └── types.h ├── src ├── query_view.cpp ├── query.cpp ├── cookie_view.cpp ├── response_view.cpp ├── header_view.cpp ├── cookie.cpp ├── form.cpp ├── header.cpp ├── response.cpp ├── request_view.cpp ├── form_view.cpp └── request.cpp ├── CMakeLists.txt ├── .github └── workflows │ └── build_and_test.yml ├── .clang-format ├── README.md ├── LICENSE.md └── CMakePresets.json /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(test_hot_teacup) 3 | 4 | SealLake_GoogleTest( 5 | SOURCES 6 | test_request.cpp 7 | test_response.cpp 8 | test_cookie.cpp 9 | test_header.cpp 10 | test_query.cpp 11 | test_form.cpp 12 | LIBRARIES 13 | hot_teacup::hot_teacup 14 | ) 15 | -------------------------------------------------------------------------------- /external/seal_lake: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | set(SEAL_LAKE_VERSION v0.2.0) 3 | set(FETCHCONTENT_QUIET FALSE) 4 | FetchContent_Declare(seal_lake_${SEAL_LAKE_VERSION} 5 | SOURCE_DIR seal_lake_${SEAL_LAKE_VERSION} 6 | GIT_REPOSITORY "https://github.com/kamchatka-volcano/seal_lake.git" 7 | GIT_TAG ${SEAL_LAKE_VERSION} 8 | ) 9 | FetchContent_MakeAvailable(seal_lake_${SEAL_LAKE_VERSION}) 10 | include(${seal_lake_${SEAL_LAKE_VERSION}_SOURCE_DIR}/seal_lake.cmake) -------------------------------------------------------------------------------- /tests/external/googletest: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | Set(FETCHCONTENT_QUIET FALSE) 3 | if (NOT google_FOUND) 4 | FetchContent_Declare( 5 | googletest 6 | GIT_REPOSITORY https://github.com/google/googletest.git 7 | GIT_TAG release-1.12.1 8 | GIT_SHALLOW ON 9 | GIT_PROGRESS TRUE 10 | ) 11 | # For Windows: Prevent overriding the parent project's compiler/linker settings 12 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 13 | FetchContent_MakeAvailable(googletest) 14 | endif() -------------------------------------------------------------------------------- /include/hot_teacup/query_view.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_QUERY_VIEW_H 2 | #define HOT_TEACUP_QUERY_VIEW_H 3 | 4 | #include 5 | #include 6 | 7 | namespace http { 8 | 9 | class QueryView { 10 | public: 11 | QueryView(std::string_view name, std::string_view value); 12 | std::string_view name() const; 13 | std::string_view value() const; 14 | friend bool operator==(const QueryView& lhs, const QueryView& rhs); 15 | 16 | private: 17 | std::string_view name_; 18 | std::string_view value_; 19 | }; 20 | 21 | std::vector queriesFromString(std::string_view input); 22 | } //namespace http 23 | 24 | #endif //HOT_TEACUP_QUERY_VIEW_H 25 | -------------------------------------------------------------------------------- /include/hot_teacup/query.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_QUERY_H 2 | #define HOT_TEACUP_QUERY_H 3 | 4 | #include 5 | #include 6 | 7 | namespace http { 8 | class QueryView; 9 | 10 | class Query { 11 | public: 12 | explicit Query(const QueryView&); 13 | Query(std::string name, std::string value); 14 | const std::string& name() const; 15 | const std::string& value() const; 16 | std::string toString() const; 17 | friend bool operator==(const Query& lhs, const Query& rhs); 18 | 19 | private: 20 | std::string name_; 21 | std::string value_; 22 | }; 23 | 24 | std::string pathWithQueries(const std::string& path, const std::vector& queries); 25 | std::string queriesToString(const std::vector& queries); 26 | std::vector makeQueries(const std::vector& queryViewList); 27 | } //namespace http 28 | 29 | #endif //HOT_TEACUP_QUERY_H 30 | -------------------------------------------------------------------------------- /include/hot_teacup/response_view.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_RESPONSE_VIEW_H 2 | #define HOT_TEACUP_RESPONSE_VIEW_H 3 | 4 | #include "cookie_view.h" 5 | #include "header_view.h" 6 | #include "types.h" 7 | #include 8 | 9 | namespace http { 10 | 11 | class ResponseView { 12 | public: 13 | ResponseView( 14 | ResponseStatus status, 15 | std::string_view body = {}, 16 | std::vector cookies = {}, 17 | std::vector headers = {}); 18 | 19 | ResponseStatus status() const; 20 | std::string_view body() const; 21 | const std::vector& cookies() const; 22 | const std::vector& headers() const; 23 | 24 | private: 25 | ResponseStatus status_ = ResponseStatus::_404_Not_Found; 26 | std::string body_; 27 | std::vector cookies_; 28 | std::vector headers_; 29 | }; 30 | 31 | std::optional responseFromString(std::string_view, ResponseMode mode = ResponseMode::Http); 32 | 33 | } //namespace http 34 | 35 | #endif //HOT_TEACUP_RESPONSE_VIEW_H 36 | -------------------------------------------------------------------------------- /include/hot_teacup/form_view.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_FORM_VIEW_H 2 | #define HOT_TEACUP_FORM_VIEW_H 3 | 4 | #include "types.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace http { 13 | 14 | class FormFieldView { 15 | struct FormFile { 16 | std::string_view fileData; 17 | std::string_view fileName; 18 | std::optional mimeType; 19 | }; 20 | 21 | public: 22 | explicit FormFieldView(std::string_view value = {}); 23 | FormFieldView(std::string_view fileData, std::string_view fileName, std::optional fileType = {}); 24 | 25 | FormFieldType type() const; 26 | bool hasFile() const; 27 | 28 | std::string_view fileName() const; 29 | std::string_view fileType() const; 30 | std::string_view value() const; 31 | 32 | private: 33 | std::variant value_; 34 | }; 35 | 36 | using FormView = std::map; 37 | 38 | FormView formFromString(std::string_view contentTypeHeader, std::string_view contentFields); 39 | 40 | } //namespace http 41 | 42 | #endif //HOT_TEACUP_FORM_VIEW_H 43 | -------------------------------------------------------------------------------- /include/hot_teacup/cookie_view.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_COOKIE_VIEW_H 2 | #define HOT_TEACUP_COOKIE_VIEW_H 3 | 4 | #include "header_view.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace http { 12 | 13 | class CookieView { 14 | 15 | public: 16 | CookieView(std::string_view name, std::string_view value); 17 | 18 | std::string_view name() const; 19 | std::string_view value() const; 20 | std::optional domain() const; 21 | std::optional path() const; 22 | std::optional maxAge() const; 23 | bool isSecure() const; 24 | bool isRemoved() const; 25 | const HeaderView& asHeader() const; 26 | 27 | friend bool operator==(const CookieView& lhs, const CookieView& rhs); 28 | friend std::optional cookieFromHeader(const HeaderView& header); 29 | 30 | private: 31 | explicit CookieView(HeaderView header); 32 | 33 | private: 34 | HeaderView header_; 35 | }; 36 | 37 | std::vector cookiesFromString(std::string_view input); 38 | std::optional cookieFromHeader(const HeaderView& header); 39 | 40 | } //namespace http 41 | 42 | #endif //HOT_TEACUP_COOKIE_VIEW_H 43 | -------------------------------------------------------------------------------- /include/hot_teacup/header_view.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_HEADER_VIEW_H 2 | #define HOT_TEACUP_HEADER_VIEW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace http { 11 | 12 | class HeaderParamView { 13 | public: 14 | explicit HeaderParamView(std::string_view name); 15 | HeaderParamView(std::string_view name, std::string_view value); 16 | std::string_view name() const; 17 | std::string_view value() const; 18 | bool hasValue() const; 19 | 20 | private: 21 | std::string_view name_; 22 | std::optional value_; 23 | }; 24 | 25 | class HeaderView { 26 | public: 27 | HeaderView(std::string_view name, std::string_view value, std::vector params); 28 | std::string_view name() const; 29 | std::string_view value() const; 30 | std::string_view param(std::string_view name) const; 31 | const std::vector& params() const; 32 | bool hasParam(std::string_view name) const; 33 | 34 | private: 35 | std::string_view name_; 36 | std::string_view value_; 37 | std::vector params_; 38 | }; 39 | 40 | std::optional headerFromString(std::string_view); 41 | 42 | } //namespace http 43 | 44 | #endif //HOT_TEACUP_HEADER_VIEW_H -------------------------------------------------------------------------------- /include/hot_teacup/form.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_FORM_H 2 | #define HOT_TEACUP_FORM_H 3 | 4 | #include "types.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace http { 13 | class FormFieldView; 14 | 15 | class FormField { 16 | struct FormFile { 17 | std::string fileData; 18 | std::string fileName; 19 | std::optional mimeType; 20 | }; 21 | 22 | public: 23 | explicit FormField(const FormFieldView&); 24 | explicit FormField(std::string value = {}); 25 | FormField(std::string fileData, std::string fileName, std::optional fileType = {}); 26 | 27 | FormFieldType type() const; 28 | bool hasFile() const; 29 | 30 | const std::string& fileName() const; 31 | const std::string& fileType() const; 32 | const std::string& value() const; 33 | 34 | private: 35 | std::variant value_; 36 | static inline const std::string valueNotFound = {}; 37 | }; 38 | 39 | using Form = std::map; 40 | std::string multipartFormToString(const Form& form, const std::string& formBoundary); 41 | std::string urlEncodedFormToString(const Form& form); 42 | 43 | Form makeForm(const std::map& formView); 44 | 45 | } //namespace http 46 | 47 | #endif //HOT_TEACUP_FORM_H 48 | -------------------------------------------------------------------------------- /src/query_view.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace http { 6 | 7 | QueryView::QueryView(std::string_view name, std::string_view value) 8 | : name_{name} 9 | , value_{value} 10 | { 11 | } 12 | 13 | std::string_view QueryView::name() const 14 | { 15 | return name_; 16 | } 17 | 18 | std::string_view QueryView::value() const 19 | { 20 | return value_; 21 | } 22 | 23 | bool operator==(const QueryView& lhs, const QueryView& rhs) 24 | { 25 | return lhs.name_ == rhs.name_ && lhs.value_ == rhs.value_; 26 | } 27 | 28 | std::vector queriesFromString(std::string_view input) 29 | { 30 | auto result = std::vector{}; 31 | const auto queries = sfun::split(input, "&"); 32 | for (const auto& query : queries) { 33 | const auto namePart = sfun::before(query, "="); 34 | const auto valuePart = sfun::after(query, "="); 35 | if (!namePart.has_value()) { 36 | const auto name = sfun::trim(query); 37 | if (!name.empty()) 38 | result.emplace_back(name, ""); 39 | } 40 | else { 41 | const auto name = sfun::trim(namePart.value()); 42 | if (!name.empty()) 43 | result.emplace_back(name, sfun::trim(valuePart.value_or(""))); 44 | } 45 | } 46 | return result; 47 | } 48 | 49 | } //namespace http 50 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(hot_teacup 3 | VERSION 3.3.0 4 | DESCRIPTION "C++17 library for parsing HTTP requests data received over the FastCGI connection and forming HTTP responses." 5 | ) 6 | include(external/seal_lake) 7 | 8 | SealLake_Bundle( 9 | NAME hot_teacup_sfun 10 | GIT_REPOSITORY https://github.com/kamchatka-volcano/sfun.git 11 | GIT_TAG v5.1.0 12 | TEXT_REPLACEMENTS 13 | "namespace sfun" "namespace http::sfun" 14 | ) 15 | 16 | set(SRC 17 | src/cookie.cpp 18 | src/cookie_view.cpp 19 | src/form.cpp 20 | src/form_view.cpp 21 | src/header.cpp 22 | src/header_view.cpp 23 | src/query.cpp 24 | src/query_view.cpp 25 | src/request.cpp 26 | src/request_view.cpp 27 | src/response.cpp 28 | src/response_view.cpp 29 | ) 30 | 31 | set(PUBLIC_HEADERS 32 | include/hot_teacup/cookie.h 33 | include/hot_teacup/form.h 34 | include/hot_teacup/header.h 35 | include/hot_teacup/query.h 36 | include/hot_teacup/request.h 37 | include/hot_teacup/response.h 38 | include/hot_teacup/types.h 39 | ) 40 | 41 | SealLake_StaticLibrary( 42 | SOURCES ${SRC} 43 | PUBLIC_HEADERS ${PUBLIC_HEADERS} 44 | COMPILE_FEATURES cxx_std_17 45 | PROPERTIES 46 | CXX_EXTENSIONS OFF 47 | VERSION ${PROJECT_VERSION} 48 | POSITION_INDEPENDENT_CODE ON 49 | LIBRARIES hot_teacup_sfun::hot_teacup_sfun 50 | ) 51 | 52 | SealLake_OptionalSubProjects(tests) -------------------------------------------------------------------------------- /include/hot_teacup/cookie.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_COOKIE_H 2 | #define HOT_TEACUP_COOKIE_H 3 | 4 | #include "header.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace http { 12 | class CookieView; 13 | 14 | class Cookie { 15 | 16 | public: 17 | explicit Cookie(const CookieView& cookieView); 18 | Cookie(std::string name, 19 | std::string value, 20 | std::optional domain = {}, 21 | std::optional path = {}, 22 | std::optional maxAge = {}, 23 | bool secure = false, 24 | bool removed = false); 25 | 26 | const std::string& name() const; 27 | const std::string& value() const; 28 | std::optional domain() const; 29 | std::optional path() const; 30 | std::optional maxAge() const; 31 | bool isSecure() const; 32 | bool isRemoved() const; 33 | 34 | void setDomain(std::string domain); 35 | void setPath(std::string path); 36 | void setMaxAge(const std::chrono::seconds& maxAge); 37 | void setSecure(); 38 | void setRemoved(); 39 | 40 | std::string toString() const; 41 | friend bool operator==(const Cookie& lhs, const Cookie& rhs); 42 | 43 | private: 44 | explicit Cookie(Header header); 45 | 46 | private: 47 | Header header_; 48 | }; 49 | 50 | std::string cookiesToString(const std::vector& cookies); 51 | 52 | std::vector makeCookies(const std::vector& cookieViewList); 53 | 54 | } //namespace http 55 | 56 | #endif //HOT_TEACUP_COOKIE_H 57 | -------------------------------------------------------------------------------- /include/hot_teacup/header.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_HEADER_H 2 | #define HOT_TEACUP_HEADER_H 3 | 4 | #include "types.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace http { 12 | class HeaderParamView; 13 | class HeaderView; 14 | 15 | class HeaderParam { 16 | public: 17 | explicit HeaderParam(const HeaderParamView&); 18 | explicit HeaderParam(std::string name); 19 | HeaderParam(std::string name, std::string value); 20 | const std::string& name() const; 21 | const std::string& value() const; 22 | std::string toString(HeaderQuotingMode quotingMode) const; 23 | 24 | private: 25 | std::string name_; 26 | std::optional value_; 27 | static inline const std::string valueNotFound; 28 | }; 29 | 30 | class Header { 31 | public: 32 | explicit Header(const HeaderView&); 33 | Header(std::string name, std::string value); 34 | void setParam(std::string name); 35 | void setParam(std::string name, std::string value); 36 | void setQuotingMode(HeaderQuotingMode mode); 37 | std::string toString() const; 38 | 39 | const std::string& name() const; 40 | const std::string& value() const; 41 | const std::string& param(std::string_view name) const; 42 | const std::vector& params() const; 43 | bool hasParam(std::string_view name) const; 44 | 45 | private: 46 | std::string name_; 47 | std::string value_; 48 | std::vector params_; 49 | HeaderQuotingMode quotingMode_ = HeaderQuotingMode::None; 50 | }; 51 | 52 | std::vector makeHeaderParams(const std::vector& headerParamViewList); 53 | std::vector
makeHeaders(const std::vector& headerViewList); 54 | 55 | } //namespace http 56 | 57 | #endif //HOT_TEACUP_HEADER_H -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: build & test (clang, gcc, MSVC) 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "master", "dev" ] 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: [ "master", "dev" ] 11 | paths-ignore: 12 | - '**.md' 13 | 14 | jobs: 15 | build: 16 | name: ${{ matrix.config.name }} 17 | runs-on: ${{ matrix.config.os }} 18 | env: 19 | CC: ${{ matrix.config.cc }} 20 | CXX: ${{ matrix.config.cxx }} 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | config: 26 | - { 27 | name: "Ubuntu Latest gcc", 28 | os: ubuntu-latest, 29 | cmake-preset: gcc-release 30 | } 31 | - { 32 | name: "Ubuntu Latest clang", 33 | os: ubuntu-latest, 34 | cmake-preset: clang-release 35 | } 36 | - { 37 | name: "Windows Latest MSVC", 38 | os: windows-latest, 39 | cmake-preset: msvc-release 40 | } 41 | 42 | steps: 43 | - name: Install ninja (Windows) 44 | if: matrix.config.os == 'windows-latest' 45 | run: choco install ninja 46 | - name: Install ninja (Linux) 47 | if: matrix.config.os == 'ubuntu-latest' 48 | run: sudo apt install ninja-build 49 | - uses: actions/checkout@v4 50 | 51 | - uses: rui314/setup-mold@v1 52 | - uses: hendrikmuhs/ccache-action@v1.2 53 | - uses: ilammy/msvc-dev-cmd@v1 54 | 55 | - name: Configure CMake 56 | run: cmake -B ${{github.workspace}}/build -DENABLE_TESTS=ON --preset="${{ matrix.config.cmake-preset }}" 57 | 58 | - name: Build 59 | run: cmake --build ${{github.workspace}}/build 60 | 61 | - name: Test 62 | working-directory: ${{github.workspace}}/build 63 | run: ctest 64 | 65 | -------------------------------------------------------------------------------- /src/query.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace http { 8 | 9 | Query::Query(const QueryView& queryView) 10 | : name_{queryView.name()} 11 | , value_{queryView.value()} 12 | { 13 | } 14 | 15 | Query::Query(std::string name, std::string value) 16 | : name_{std::move(name)} 17 | , value_{std::move(value)} 18 | { 19 | } 20 | 21 | const std::string& Query::name() const 22 | { 23 | return name_; 24 | } 25 | 26 | const std::string& Query::value() const 27 | { 28 | return value_; 29 | } 30 | 31 | std::string Query::toString() const 32 | { 33 | return name_ + "=" + value_; 34 | } 35 | 36 | bool operator==(const Query& lhs, const Query& rhs) 37 | { 38 | return lhs.name_ == rhs.name_ && lhs.value_ == rhs.value_; 39 | } 40 | 41 | std::string queriesToString(const std::vector& queries) 42 | { 43 | auto result = std::string{}; 44 | for (const auto& query : queries) { 45 | result += query.toString() + "&"; 46 | } 47 | if (!result.empty()) 48 | result.resize(result.size() - 1); //remove last '&' 49 | return result; 50 | } 51 | 52 | std::string pathWithQueries(const std::string& path, const std::vector& queries) 53 | { 54 | if (queries.empty()) 55 | return path; 56 | return path + "?" + queriesToString(queries); 57 | } 58 | 59 | std::vector makeQueries(const std::vector& queryViewList) 60 | { 61 | auto result = std::vector{}; 62 | std::transform( 63 | queryViewList.begin(), 64 | queryViewList.end(), 65 | std::back_inserter(result), 66 | [](const auto& queryView) 67 | { 68 | return Query{queryView}; 69 | }); 70 | return result; 71 | } 72 | 73 | } //namespace http 74 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: WebKit 3 | AlignAfterOpenBracket: AlwaysBreak 4 | AllowAllArgumentsOnNextLine: false 5 | AllowAllConstructorInitializersOnNextLine: false 6 | AllowAllParametersOfDeclarationOnNextLine: false 7 | AllowShortFunctionsOnASingleLine: Empty 8 | AllowShortLambdasOnASingleLine: Empty 9 | AllowShortEnumsOnASingleLine: false 10 | AllowShortIfStatementsOnASingleLine: Never 11 | AllowShortLoopsOnASingleLine: false 12 | AllowShortBlocksOnASingleLine: Empty 13 | AlwaysBreakTemplateDeclarations: Yes 14 | BinPackArguments: false 15 | BinPackParameters: false 16 | BraceWrapping: 17 | AfterFunction: true 18 | BeforeElse: true 19 | BeforeLambdaBody: true 20 | BeforeWhile: true 21 | BeforeCatch: true 22 | BreakBeforeBraces: Custom 23 | BreakBeforeBinaryOperators: None 24 | BreakInheritanceList: AfterComma 25 | ColumnLimit: 120 26 | ContinuationIndentWidth: 8 27 | Cpp11BracedListStyle: true 28 | NamespaceIndentation: None 29 | PenaltyBreakBeforeFirstCallParameter: 0 30 | PenaltyReturnTypeOnItsOwnLine: 1000 31 | PenaltyBreakAssignment: 10 32 | SpaceBeforeCpp11BracedList: false 33 | SpaceInEmptyBlock: false 34 | SpaceInEmptyParentheses: false 35 | SpaceAfterTemplateKeyword: false 36 | SpacesInLineCommentPrefix: 37 | Minimum: 0 38 | Maximum: -1 39 | FixNamespaceComments: true 40 | UseCRLF: false 41 | IncludeCategories: 42 | # Headers in <> without extension. 43 | - Regex: '<[[:alnum:]\-_]+>' 44 | Priority: 6 45 | # Headers in <> from specific external libraries. 46 | - Regex: '<(gtest|gmock|boost|gsl)\/' 47 | Priority: 5 48 | # Headers in <> with subdirectory. 49 | - Regex: '<[[:alnum:]\-_]+\/' 50 | Priority: 4 51 | # Headers in <> with extension. 52 | - Regex: '<[[:alnum:].\-_]+>' 53 | Priority: 3 54 | # Headers in "" with subdirectory. 55 | - Regex: '"[[:alnum:]\-_]+\/' 56 | Priority: 2 57 | # Headers in "" with extension. 58 | - Regex: '"[[:alnum:].\-_]+"' 59 | Priority: 1 60 | ... 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ☕hot_teacup 2 | [![build & test (clang, gcc, MSVC)](https://github.com/kamchatka-volcano/hot_teacup/actions/workflows/build_and_test.yml/badge.svg?branch=master)](https://github.com/kamchatka-volcano/hot_teacup/actions/workflows/build_and_test.yml) 3 | 4 | **hot_teacup** is a C++17 library for parsing HTTP request data received over a FastCGI connection and forming HTTP responses. It supports reading HTTP headers, cookies, query strings, URL encoded forms, and multipart forms. The library is designed for use with the FastCGI protocol, so input data is expected to be percent-decoded by the web server. The headers are self-explanatory, so there is no documentation. If you need examples of usage, you can also check the unit tests. 5 | 6 | ### Installation 7 | Download and link the library from your project's CMakeLists.txt: 8 | ``` 9 | cmake_minimum_required(VERSION 3.14) 10 | 11 | include(FetchContent) 12 | 13 | FetchContent_Declare(hot_teacup 14 | GIT_REPOSITORY "https://github.com/kamchatka-volcano/hot_teacup.git" 15 | GIT_TAG "origin/master" 16 | ) 17 | 18 | #uncomment if you need to install hot_teacup with your target 19 | #set(INSTALL_HOT_TEACUP ON) 20 | FetchContent_MakeAvailable(hot_teacup) 21 | 22 | add_executable(${PROJECT_NAME}) 23 | target_link_libraries(${PROJECT_NAME} PRIVATE hot_teacup::hot_teacup) 24 | ``` 25 | 26 | To install the library system-wide, use the following commands: 27 | ``` 28 | git clone https://github.com/kamchatka-volcano/hot_teacup.git 29 | cd hot_teacup 30 | cmake -S . -B build 31 | cmake --build build 32 | cmake --install build 33 | ``` 34 | 35 | After installation, you can use the `find_package()` command to make the installed library available inside your project: 36 | ``` 37 | find_package(hot_teacup 1.0.0 REQUIRED) 38 | target_link_libraries(${PROJECT_NAME} PRIVATE hot_teacup::hot_teacup) 39 | ``` 40 | 41 | ### Running tests 42 | ``` 43 | cd hot_teacup 44 | cmake -S . -B build -DENABLE_TESTS=ON 45 | cmake --build build 46 | cd build/tests && ctest 47 | ``` 48 | 49 | ### License 50 | **hot_teacup** is licensed under the [MS-PL license](/LICENSE.md) 51 | -------------------------------------------------------------------------------- /include/hot_teacup/request_view.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_REQUEST_VIEW_H 2 | #define HOT_TEACUP_REQUEST_VIEW_H 3 | 4 | #include "cookie_view.h" 5 | #include "form_view.h" 6 | #include "query_view.h" 7 | #include "types.h" 8 | #include 9 | #include 10 | 11 | namespace http { 12 | 13 | class RequestView { 14 | public: 15 | RequestView( 16 | std::string_view fcgiParamRequestMethod, 17 | std::string_view fcgiParamRemoteAddr, 18 | std::string_view fcgiParamHttpHost, 19 | std::string_view fcgiParamRequestUri, 20 | std::string_view fcgiParamQueryString, 21 | std::string_view fcgiParamHttpCookie, 22 | std::string_view fcgiParamContentType, 23 | std::string_view fcgiStdIn); 24 | 25 | RequestMethod method() const; 26 | std::string_view ipAddress() const; 27 | std::string_view domainName() const; 28 | std::string_view path() const; 29 | 30 | const std::vector& queries() const; 31 | std::string_view query(std::string_view name) const; 32 | bool hasQuery(std::string_view name) const; 33 | 34 | const std::vector& cookies() const; 35 | std::string_view cookie(std::string_view name) const; 36 | bool hasCookie(std::string_view name) const; 37 | 38 | const FormView& form() const; 39 | std::string_view formField(std::string_view name, int index = 0) const; 40 | std::vector formFieldList() const; 41 | std::vector fileList() const; 42 | int formFieldCount(std::string_view name) const; 43 | bool hasFormField(std::string_view name) const; 44 | 45 | std::string_view fileData(std::string_view name, int index = 0) const; 46 | int fileCount(std::string_view name) const; 47 | bool hasFile(std::string_view name) const; 48 | std::string_view fileName(std::string_view name, int index = 0) const; 49 | std::string_view fileType(std::string_view name, int index = 0) const; 50 | bool hasFiles() const; 51 | 52 | private: 53 | RequestMethod method_; 54 | std::string_view ipAddress_; 55 | std::string_view domainName_; 56 | std::string_view path_; 57 | std::vector queries_; 58 | std::vector cookies_; 59 | FormView form_; 60 | }; 61 | 62 | } //namespace http 63 | 64 | #endif //HOT_TEACUP_REQUEST_VIEW_H 65 | -------------------------------------------------------------------------------- /include/hot_teacup/request.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_REQUEST_H 2 | #define HOT_TEACUP_REQUEST_H 3 | 4 | #include "cookie.h" 5 | #include "form.h" 6 | #include "query.h" 7 | #include "types.h" 8 | #include 9 | #include 10 | 11 | namespace http { 12 | class RequestView; 13 | 14 | struct RequestFcgiData { 15 | std::map params; 16 | std::string stdIn; 17 | }; 18 | 19 | class Request { 20 | public: 21 | explicit Request(const RequestView&); 22 | Request(RequestMethod, std::string path, std::vector = {}, std::vector = {}, Form = {}); 23 | 24 | RequestMethod method() const; 25 | const std::string& path() const; 26 | 27 | const std::vector& queries() const; 28 | const std::string& query(std::string_view name) const; 29 | bool hasQuery(std::string_view name) const; 30 | 31 | const std::vector& cookies() const; 32 | const std::string& cookie(std::string_view name) const; 33 | bool hasCookie(std::string_view name) const; 34 | 35 | const Form& form() const; 36 | const std::string& formField(std::string_view name, int index = 0) const; 37 | std::vector formFieldList() const; 38 | std::vector fileList() const; 39 | int formFieldCount(std::string_view name) const; 40 | bool hasFormField(std::string_view name) const; 41 | 42 | const std::string& fileData(std::string_view name, int index = 0) const; 43 | int fileCount(std::string_view name) const; 44 | bool hasFile(std::string_view name) const; 45 | const std::string& fileName(std::string_view name, int index = 0) const; 46 | const std::string& fileType(std::string_view name, int index = 0) const; 47 | bool hasFiles() const; 48 | 49 | RequestFcgiData toFcgiData(FormType) const; 50 | 51 | void setQueries(const std::vector&); 52 | void setCookies(const std::vector&); 53 | void setForm(const Form&); 54 | void setFcgiParams(const std::map& params); 55 | 56 | private: 57 | RequestMethod method_; 58 | std::string path_; 59 | std::vector queries_; 60 | std::vector cookies_; 61 | Form form_; 62 | std::map fcgiParams_; 63 | 64 | private: 65 | static inline const std::string valueNotFound = {}; 66 | }; 67 | 68 | } //namespace http 69 | 70 | #endif //HOT_TEACUP_REQUEST_H 71 | -------------------------------------------------------------------------------- /tests/test_query.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | TEST(Query, ToStringSingle) 6 | { 7 | auto query = http::Query{"foo", "bar"}; 8 | EXPECT_EQ(query.toString(), "foo=bar"); 9 | } 10 | 11 | TEST(Query, ToString) 12 | { 13 | { 14 | auto queries = std::vector{{"name", "test"}, {"foo", "bar"}}; 15 | EXPECT_EQ(http::queriesToString(queries), "name=test&foo=bar"); 16 | } 17 | } 18 | 19 | TEST(Query, PathWithQueries) 20 | { 21 | { 22 | auto query = http::Query{"name", "test"}; 23 | auto path = "/test/"; 24 | EXPECT_EQ(http::pathWithQueries(path, {query}), "/test/?name=test"); 25 | } 26 | { 27 | auto queries = std::vector{{"name", "test"}, {"foo", "bar"}}; 28 | auto path = "/test/"; 29 | EXPECT_EQ(http::pathWithQueries(path, queries), "/test/?name=test&foo=bar"); 30 | } 31 | } 32 | 33 | TEST(QueryView, FromString) 34 | { 35 | { 36 | auto queries = http::queriesFromString("name=test&foo=bar"); 37 | auto expectedQueries = std::vector{{"name", "test"}, {"foo", "bar"}}; 38 | EXPECT_EQ(queries, expectedQueries); 39 | } 40 | 41 | { 42 | auto queries = http::queriesFromString(""); 43 | auto expectedQueries = std::vector{}; 44 | EXPECT_EQ(queries, expectedQueries); 45 | } 46 | 47 | { 48 | auto queries = http::queriesFromString("="); 49 | auto expectedQueries = std::vector{}; 50 | EXPECT_EQ(queries, expectedQueries); 51 | } 52 | { 53 | auto queries = http::queriesFromString("&"); 54 | auto expectedQueries = std::vector{}; 55 | EXPECT_EQ(queries, expectedQueries); 56 | } 57 | { 58 | auto queries = http::queriesFromString("&&"); 59 | auto expectedQueries = std::vector{}; 60 | EXPECT_EQ(queries, expectedQueries); 61 | } 62 | { 63 | auto queries = http::queriesFromString("=&=&="); 64 | auto expectedQueries = std::vector{}; 65 | EXPECT_EQ(queries, expectedQueries); 66 | } 67 | } 68 | 69 | TEST(QueryView, QueryFromQueryView) 70 | { 71 | auto queries = http::queriesFromString("name=test&foo=bar"); 72 | auto expectedQueries = std::vector{{"name", "test"}, {"foo", "bar"}}; 73 | EXPECT_EQ(http::makeQueries(queries), expectedQueries); 74 | } 75 | -------------------------------------------------------------------------------- /include/hot_teacup/response.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_RESPONSE_H 2 | #define HOT_TEACUP_RESPONSE_H 3 | 4 | #include "cookie.h" 5 | #include "header.h" 6 | #include "query.h" 7 | #include "types.h" 8 | #include 9 | 10 | namespace http { 11 | class ResponseView; 12 | 13 | struct Redirect { 14 | std::string path; 15 | RedirectType type = RedirectType::Found; 16 | }; 17 | 18 | class Response { 19 | public: 20 | explicit Response(const ResponseView&); 21 | Response( 22 | ResponseStatus status, 23 | std::string body = {}, 24 | std::vector cookies = {}, 25 | std::vector
headers = {}); 26 | Response( 27 | ResponseStatus status, 28 | std::string body, 29 | ContentType contentType, 30 | std::vector cookies = {}, 31 | std::vector
headers = {}); 32 | Response( 33 | ResponseStatus status, 34 | std::string body, 35 | std::string contentType, 36 | std::vector cookies = {}, 37 | std::vector
headers = {}); 38 | Response( 39 | std::string body, 40 | ContentType contentType, 41 | std::vector cookies = {}, 42 | std::vector
headers = {}); 43 | Response( 44 | std::string body, 45 | std::string contentType, 46 | std::vector cookies = {}, 47 | std::vector
headers = {}); 48 | Response(std::string path, RedirectType type, std::vector cookies = {}, std::vector
headers = {}); 49 | Response(Redirect redirect, std::vector cookies = {}, std::vector
headers = {}); 50 | Response(std::string body, std::vector cookies = {}, std::vector
headers = {}); 51 | 52 | ResponseStatus status() const; 53 | const std::string& body() const; 54 | const std::vector& cookies() const; 55 | const std::vector
& headers() const; 56 | std::string data(ResponseMode mode = ResponseMode::Http) const; 57 | 58 | void setBody(const std::string& body); 59 | void addCookie(Cookie cookie); 60 | void addHeader(Header header); 61 | void addCookies(const std::vector& cookies); 62 | void addHeaders(const std::vector
& headers); 63 | 64 | private: 65 | std::string statusData(ResponseMode mode) const; 66 | std::string cookiesData() const; 67 | std::string headersData() const; 68 | 69 | private: 70 | ResponseStatus status_ = ResponseStatus::_404_Not_Found; 71 | std::string body_; 72 | std::vector cookies_; 73 | std::vector
headers_; 74 | }; 75 | 76 | } //namespace http 77 | 78 | #endif //HOT_TEACUP_RESPONSE_H -------------------------------------------------------------------------------- /src/cookie_view.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace http { 5 | 6 | CookieView::CookieView(std::string_view name, std::string_view value) 7 | : header_{"Set-Cookie", "", {HeaderParamView{name, value}}} 8 | { 9 | } 10 | 11 | CookieView::CookieView(HeaderView header) 12 | : header_{std::move(header)} 13 | { 14 | } 15 | 16 | std::string_view CookieView::name() const 17 | { 18 | return header_.params().at(0).name(); 19 | } 20 | 21 | std::string_view CookieView::value() const 22 | { 23 | return header_.params().at(0).value(); 24 | } 25 | 26 | std::optional CookieView::domain() const 27 | { 28 | if (header_.hasParam("Domain")) 29 | return header_.param("Domain"); 30 | else 31 | return {}; 32 | } 33 | 34 | std::optional CookieView::path() const 35 | { 36 | if (header_.hasParam("Path")) 37 | return header_.param("Path"); 38 | else 39 | return {}; 40 | } 41 | 42 | std::optional CookieView::maxAge() const 43 | { 44 | if (header_.hasParam("Max-Age")) { 45 | try { 46 | return std::chrono::seconds{std::stoi(std::string{header_.param("Max-Age")})}; 47 | } 48 | catch (...) { 49 | return {}; 50 | } 51 | } 52 | else 53 | return {}; 54 | } 55 | 56 | bool CookieView::isSecure() const 57 | { 58 | return header_.hasParam("Secure"); 59 | } 60 | 61 | bool CookieView::isRemoved() const 62 | { 63 | return header_.hasParam("Max-Age") && header_.param("Max-Age") == "0"; 64 | } 65 | 66 | const HeaderView& CookieView::asHeader() const 67 | { 68 | return header_; 69 | } 70 | 71 | bool operator==(const CookieView& lhs, const CookieView& rhs) 72 | { 73 | return lhs.name() == rhs.name() && lhs.value() == rhs.value(); 74 | } 75 | 76 | std::vector cookiesFromString(std::string_view input) 77 | { 78 | auto result = std::vector{}; 79 | auto cookies = sfun::split(input, ";"); 80 | for (const auto& cookie : cookies) { 81 | const auto namePart = sfun::before(cookie, "="); 82 | const auto valuePart = sfun::after(cookie, "="); 83 | if (!namePart.has_value()) 84 | continue; 85 | const auto name = sfun::trim(namePart.value()); 86 | if (!name.empty()) 87 | result.emplace_back(name, valuePart.value_or("")); 88 | } 89 | return result; 90 | } 91 | 92 | std::optional cookieFromHeader(const HeaderView& header) 93 | { 94 | if (header.params().empty()) 95 | return std::nullopt; 96 | 97 | return CookieView{header}; 98 | } 99 | 100 | } //namespace http 101 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Microsoft Public License (Ms-PL) 2 | Copyright 2021 Gorelyy PA 3 | 4 | This license governs use of the accompanying software. If you use the software, 5 | you accept this license. If you do not accept the license, do not use the 6 | software. 7 | 8 | 1. Definitions 9 | 10 | The terms "reproduce," "reproduction," "derivative works," and "distribution" 11 | have the same meaning here as under U.S. copyright law. 12 | 13 | A "contribution" is the original software, or any additions or changes to the 14 | software. 15 | 16 | A "contributor" is any person that distributes its contribution under this 17 | license. 18 | 19 | "Licensed patents" are a contributor's patent claims that read directly on its 20 | contribution. 21 | 22 | 2. Grant of Rights 23 | 24 | (A) Copyright Grant- Subject to the terms of this license, including the 25 | license conditions and limitations in section 3, each contributor grants you a 26 | non-exclusive, worldwide, royalty-free copyright license to reproduce its 27 | contribution, prepare derivative works of its contribution, and distribute its 28 | contribution or any derivative works that you create. 29 | 30 | (B) Patent Grant- Subject to the terms of this license, including the license 31 | conditions and limitations in section 3, each contributor grants you a 32 | non-exclusive, worldwide, royalty-free license under its licensed patents to 33 | make, have made, use, sell, offer for sale, import, and/or otherwise dispose of 34 | its contribution in the software or derivative works of the contribution in the 35 | software. 36 | 37 | 3. Conditions and Limitations 38 | 39 | (A) No Trademark License- This license does not grant you rights to use any 40 | contributors' name, logo, or trademarks. 41 | 42 | (B) If you bring a patent claim against any contributor over patents that you 43 | claim are infringed by the software, your patent license from such contributor 44 | to the software ends automatically. 45 | 46 | (C) If you distribute any portion of the software, you must retain all 47 | copyright, patent, trademark, and attribution notices that are present in the 48 | software. 49 | 50 | (D) If you distribute any portion of the software in source code form, you may 51 | do so only under this license by including a complete copy of this license with 52 | your distribution. If you distribute any portion of the software in compiled or 53 | object code form, you may only do so under a license that complies with this 54 | license. 55 | 56 | (E) The software is licensed "as-is." You bear the risk of using it. The 57 | contributors give no express warranties, guarantees or conditions. You may have 58 | additional consumer rights under your local laws which this license cannot 59 | change. To the extent permitted under your local laws, the contributors exclude 60 | the implied warranties of merchantability, fitness for a particular purpose and 61 | non-infringement. 62 | -------------------------------------------------------------------------------- /src/response_view.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace http { 8 | 9 | ResponseView::ResponseView( 10 | ResponseStatus status, 11 | std::string_view body, 12 | std::vector cookies, 13 | std::vector headers) 14 | : status_(status) 15 | , body_(body) 16 | , cookies_(std::move(cookies)) 17 | , headers_(std::move(headers)) 18 | { 19 | } 20 | 21 | ResponseStatus ResponseView::status() const 22 | { 23 | return status_; 24 | } 25 | 26 | std::string_view ResponseView::body() const 27 | { 28 | return body_; 29 | } 30 | 31 | const std::vector& ResponseView::cookies() const 32 | { 33 | return cookies_; 34 | } 35 | 36 | const std::vector& ResponseView::headers() const 37 | { 38 | return headers_; 39 | } 40 | 41 | namespace { 42 | template 43 | auto makeStatusRegex() 44 | { 45 | if constexpr (mode == ResponseMode::Http) 46 | return std::regex{"HTTP/1.1 (\\d+) ?(.*)"}; 47 | else 48 | return std::regex{"Status: (\\d+) ?(.*)"}; 49 | } 50 | 51 | template 52 | std::optional statusCodeFromString(const std::string& statusStr) 53 | { 54 | static const auto statusRegex = makeStatusRegex(); 55 | auto statusMatch = std::smatch{}; 56 | if (!std::regex_match(statusStr, statusMatch, statusRegex)) 57 | return std::nullopt; 58 | auto statusCode = std::stoi(statusMatch[1]); 59 | return detail::statusFromCode(statusCode); 60 | } 61 | 62 | std::string_view getStringLine(std::string_view input, std::size_t& pos, std::string_view lineSeparator = "\r\n") 63 | { 64 | auto lastPos = input.find(lineSeparator, pos); 65 | auto separatorSize = lineSeparator.size(); 66 | if (lastPos == std::string::npos) { 67 | lastPos = input.size(); 68 | separatorSize = 0; 69 | } 70 | auto lineSize = lastPos - pos; 71 | auto linePos = pos; 72 | pos += lineSize + separatorSize; 73 | return input.substr(linePos, lineSize); 74 | } 75 | 76 | } //namespace 77 | 78 | std::optional responseFromString(std::string_view data, ResponseMode mode) 79 | { 80 | auto pos = std::size_t{}; 81 | auto statusLine = getStringLine(data, pos); 82 | auto status = (mode == ResponseMode::Http) ? statusCodeFromString(std::string{statusLine}) 83 | : statusCodeFromString(std::string{statusLine}); 84 | if (status == std::nullopt) 85 | return std::nullopt; 86 | 87 | auto cookies = std::vector{}; 88 | auto headers = std::vector{}; 89 | while (true) { 90 | auto headerLine = getStringLine(data, pos); 91 | if (headerLine.empty()) 92 | break; 93 | 94 | auto header = headerFromString(headerLine); 95 | if (header == std::nullopt) 96 | return std::nullopt; 97 | if (header->name() == "Set-Cookie") { 98 | auto cookie = cookieFromHeader(*header); 99 | if (cookie) 100 | cookies.emplace_back(std::move(*cookie)); 101 | } 102 | else 103 | headers.emplace_back(*header); 104 | } 105 | auto body = data.substr(pos, data.size() - pos); 106 | return ResponseView{*status, body, std::move(cookies), std::move(headers)}; 107 | } 108 | 109 | } //namespace http 110 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 6, 3 | "configurePresets": [ 4 | { 5 | "name": "base-linux", 6 | "hidden": true, 7 | "displayName": "linux base preset", 8 | "generator": "Ninja", 9 | "binaryDir": "build-${presetName}", 10 | "cacheVariables": { 11 | "CMAKE_EXE_LINKER_FLAGS": "-fuse-ld=mold", 12 | "CMAKE_CXX_COMPILER_LAUNCHER": "ccache", 13 | "CPM_SOURCE_CACHE": "cpm_cache" 14 | } 15 | }, 16 | { 17 | "name": "clang-base", 18 | "hidden": true, 19 | "displayName": "clang base preset", 20 | "inherits": "base-linux", 21 | "cacheVariables": { 22 | "CMAKE_CXX_COMPILER": "clang++", 23 | "CMAKE_C_COMPILER": "clang", 24 | "CMAKE_CXX_FLAGS": "-Wall -Wextra -Wpedantic -Werror -Wcast-align -Wnon-virtual-dtor -Woverloaded-virtual -Wunused" 25 | } 26 | }, 27 | { 28 | "name": "clang-debug", 29 | "displayName": "clang (Debug)", 30 | "inherits": "clang-base", 31 | "cacheVariables": { 32 | "CMAKE_BUILD_TYPE": "Debug" 33 | } 34 | }, 35 | { 36 | "name": "clang-release", 37 | "displayName": "clang (Release)", 38 | "inherits": "clang-base", 39 | "cacheVariables": { 40 | "CMAKE_BUILD_TYPE": "Release" 41 | } 42 | }, 43 | { 44 | "name": "gcc-base", 45 | "hidden": true, 46 | "displayName": "gcc base preset", 47 | "inherits": "base-linux", 48 | "cacheVariables": { 49 | "CMAKE_CXX_COMPILER": "g++", 50 | "CMAKE_C_COMPILER": "gcc", 51 | "CMAKE_CXX_FLAGS": "-Wall -Wextra -Wpedantic -Werror -Wcast-align -Wnon-virtual-dtor -Woverloaded-virtual -Wunused" 52 | } 53 | }, 54 | { 55 | "name": "gcc-debug", 56 | "displayName": "gcc (Debug)", 57 | "inherits": "gcc-base", 58 | "cacheVariables": { 59 | "CMAKE_BUILD_TYPE": "Debug" 60 | } 61 | }, 62 | { 63 | "name": "gcc-release", 64 | "displayName": "gcc (Release)", 65 | "inherits": "gcc-base", 66 | "cacheVariables": { 67 | "CMAKE_BUILD_TYPE": "Release" 68 | } 69 | }, 70 | { 71 | "name": "base-windows", 72 | "displayName": "windows base preset", 73 | "hidden": true, 74 | "generator": "Ninja", 75 | "binaryDir": "build-${presetName}", 76 | "architecture": { 77 | "value": "x64", 78 | "strategy": "external" 79 | }, 80 | "cacheVariables": { 81 | "CPM_SOURCE_CACHE": "cpm_cache", 82 | "CMAKE_CXX_COMPILER_LAUNCHER": "ccache" 83 | }, 84 | "vendor": { 85 | "microsoft.com/VisualStudioSettings/CMake/1.0": { 86 | "hostOS": [ 87 | "Windows" 88 | ] 89 | }, 90 | "jetbrains.com/clion": { 91 | "toolchain": "Visual Studio" 92 | } 93 | } 94 | }, 95 | { 96 | "name": "msvc-base", 97 | "hidden": true, 98 | "displayName": "msvc base preset", 99 | "inherits": "base-windows", 100 | "cacheVariables": { 101 | "CMAKE_CXX_COMPILER": "cl.exe", 102 | "CMAKE_C_COMPILER": "cl.exe", 103 | "CMAKE_CXX_FLAGS": "/EHsc /W4 /WX /wd4267" 104 | } 105 | }, 106 | { 107 | "name": "msvc-debug", 108 | "displayName": "msvc (Debug)", 109 | "inherits": "msvc-base", 110 | "cacheVariables": { 111 | "CMAKE_BUILD_TYPE": "Debug" 112 | } 113 | }, 114 | { 115 | "name": "msvc-release", 116 | "displayName": "msvc (Release)", 117 | "inherits": "msvc-base", 118 | "cacheVariables": { 119 | "CMAKE_BUILD_TYPE": "Release" 120 | } 121 | } 122 | ] 123 | } -------------------------------------------------------------------------------- /src/header_view.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace http { 7 | 8 | HeaderParamView::HeaderParamView(std::string_view name) 9 | : name_{name} 10 | { 11 | } 12 | 13 | HeaderParamView::HeaderParamView(std::string_view name, std::string_view value) 14 | : name_{name} 15 | , value_{value} 16 | { 17 | } 18 | 19 | std::string_view HeaderParamView::name() const 20 | { 21 | return name_; 22 | } 23 | 24 | std::string_view HeaderParamView::value() const 25 | { 26 | return value_.value_or(std::string_view{}); 27 | } 28 | 29 | bool HeaderParamView::hasValue() const 30 | { 31 | return value_.has_value(); 32 | } 33 | 34 | HeaderView::HeaderView(std::string_view name, std::string_view value, std::vector params) 35 | : name_{name} 36 | , value_{value} 37 | , params_{std::move(params)} 38 | { 39 | } 40 | 41 | const std::vector& HeaderView::params() const 42 | { 43 | return params_; 44 | } 45 | 46 | std::string_view HeaderView::param(std::string_view name) const 47 | { 48 | for (const auto& param : params_) 49 | if (param.name() == name) 50 | return param.value(); 51 | throw std::out_of_range{"Header doesn't contain param '" + std::string{name} + "'"}; 52 | } 53 | 54 | bool HeaderView::hasParam(std::string_view name) const 55 | { 56 | for (const auto& param : params_) 57 | if (param.name() == name) 58 | return true; 59 | return false; 60 | } 61 | 62 | namespace { 63 | std::string_view unquoted(std::string_view str) 64 | { 65 | if (sfun::starts_with(str, "\"")) 66 | str.remove_prefix(1); 67 | if (sfun::ends_with(str, "\"")) 68 | str.remove_suffix(1); 69 | return str; 70 | } 71 | 72 | std::optional makeParam(std::string_view paramPart) 73 | { 74 | if (paramPart.find('=') == std::string::npos) 75 | return {}; 76 | const auto name = sfun::trim_front(sfun::before(paramPart, "=").value()); 77 | const auto value = unquoted(sfun::after(paramPart, "=").value()); 78 | return HeaderParamView{name, value}; 79 | } 80 | 81 | } //namespace 82 | 83 | std::optional headerFromString(std::string_view input) 84 | { 85 | const auto parts = sfun::split(input, ";", false); 86 | if (parts.empty()) 87 | return {}; 88 | 89 | if (parts[0].find(':') == std::string::npos) 90 | return {}; 91 | 92 | const auto name = sfun::trim(sfun::before(parts[0], ":").value()); 93 | const auto value = unquoted(sfun::trim_front(sfun::after(parts[0], ":").value())); 94 | if (name.empty()) 95 | return {}; 96 | 97 | const auto valueIsParam = (value.find('=') != std::string::npos); 98 | if (valueIsParam) { 99 | auto params = std::vector{}; 100 | auto param = makeParam(value); 101 | if (param) 102 | params.emplace_back(*param); 103 | for (auto i = 1u; i < parts.size(); ++i) { 104 | param = makeParam(parts[i]); 105 | if (param) 106 | params.emplace_back(*param); 107 | } 108 | return HeaderView{name, {}, std::move(params)}; 109 | } 110 | 111 | auto params = std::vector{}; 112 | for (auto i = 1u; i < parts.size(); ++i) 113 | if (const auto param = makeParam(parts[i])) 114 | params.emplace_back(*param); 115 | return HeaderView{name, value, std::move(params)}; 116 | 117 | } 118 | 119 | std::string_view HeaderView::name() const 120 | { 121 | return name_; 122 | } 123 | 124 | std::string_view HeaderView::value() const 125 | { 126 | return value_; 127 | } 128 | 129 | } //namespace http 130 | -------------------------------------------------------------------------------- /src/cookie.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace http { 8 | 9 | Cookie::Cookie(const CookieView& cookieView) 10 | : Cookie{Header{cookieView.asHeader()}} 11 | { 12 | } 13 | 14 | Cookie::Cookie( 15 | std::string name, 16 | std::string value, 17 | std::optional domain, 18 | std::optional path, 19 | std::optional maxAge, 20 | bool secure, 21 | bool removed) 22 | : header_{"Set-Cookie", ""} 23 | { 24 | header_.setParam(std::move(name), std::move(value)); 25 | if (domain) 26 | setDomain(*domain); 27 | if (path) 28 | setPath(*path); 29 | if (maxAge) 30 | setMaxAge(*maxAge); 31 | if (secure) 32 | setSecure(); 33 | if (removed) 34 | setRemoved(); 35 | } 36 | 37 | Cookie::Cookie(Header header) 38 | : header_(std::move(header)) 39 | { 40 | } 41 | 42 | const std::string& Cookie::name() const 43 | { 44 | return header_.params().at(0).name(); 45 | } 46 | 47 | const std::string& Cookie::value() const 48 | { 49 | return header_.params().at(0).value(); 50 | } 51 | 52 | std::optional Cookie::domain() const 53 | { 54 | if (header_.hasParam("Domain")) 55 | return header_.param("Domain"); 56 | else 57 | return {}; 58 | } 59 | 60 | std::optional Cookie::path() const 61 | { 62 | if (header_.hasParam("Path")) 63 | return header_.param("Path"); 64 | else 65 | return {}; 66 | } 67 | 68 | std::optional Cookie::maxAge() const 69 | { 70 | if (header_.hasParam("Max-Age")) { 71 | try { 72 | return std::chrono::seconds{std::stoi(header_.param("Max-Age"))}; 73 | } 74 | catch (...) { 75 | return {}; 76 | } 77 | } 78 | else 79 | return {}; 80 | } 81 | 82 | bool Cookie::isSecure() const 83 | { 84 | return header_.hasParam("Secure"); 85 | } 86 | 87 | bool Cookie::isRemoved() const 88 | { 89 | return header_.hasParam("Max-Age") && header_.param("Max-Age") == "0"; 90 | } 91 | 92 | void Cookie::setDomain(std::string domain) 93 | { 94 | header_.setParam("Domain", std::move(domain)); 95 | } 96 | 97 | void Cookie::setPath(std::string path) 98 | { 99 | header_.setParam("Path", std::move(path)); 100 | } 101 | 102 | void Cookie::setMaxAge(const std::chrono::seconds& maxAge) 103 | { 104 | header_.setParam("Max-Age", std::to_string(maxAge.count())); 105 | } 106 | 107 | void Cookie::setRemoved() 108 | { 109 | header_.setParam("Max-Age", "0"); 110 | } 111 | 112 | void Cookie::setSecure() 113 | { 114 | header_.setParam("Secure"); 115 | } 116 | 117 | std::string Cookie::toString() const 118 | { 119 | return header_.toString(); 120 | } 121 | 122 | bool operator==(const Cookie& lhs, const Cookie& rhs) 123 | { 124 | return lhs.name() == rhs.name() && lhs.value() == rhs.value(); 125 | } 126 | 127 | std::string cookiesToString(const std::vector& cookies) 128 | { 129 | auto result = std::string{}; 130 | for (const auto& cookie : cookies) 131 | result += cookie.name() + "=" + cookie.value() + "; "; 132 | if (!result.empty()) 133 | result.resize(result.size() - 2); //remove last '; ' 134 | return result; 135 | } 136 | 137 | std::vector makeCookies(const std::vector& cookieViewList) 138 | { 139 | std::vector result; 140 | std::transform( 141 | cookieViewList.begin(), 142 | cookieViewList.end(), 143 | std::back_inserter(result), 144 | [](const auto& cookieView) 145 | { 146 | return Cookie{cookieView}; 147 | }); 148 | return result; 149 | } 150 | 151 | } //namespace http 152 | -------------------------------------------------------------------------------- /src/form.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace http { 9 | 10 | FormField::FormField(const FormFieldView& fieldView) 11 | { 12 | if (fieldView.hasFile()) 13 | value_ = FormFile{ 14 | std::string{fieldView.value()}, 15 | std::string{fieldView.fileName()}, 16 | std::string{fieldView.fileType()}}; 17 | else 18 | value_ = std::string{fieldView.value()}; 19 | } 20 | 21 | FormField::FormField(std::string value) 22 | : value_{std::move(value)} 23 | { 24 | } 25 | 26 | FormField::FormField(std::string fileData, std::string fileName, std::optional fileType) 27 | : value_{FormFile{std::move(fileData), std::move(fileName), std::move(fileType)}} 28 | { 29 | } 30 | 31 | FormFieldType FormField::type() const 32 | { 33 | return std::holds_alternative(value_) ? FormFieldType::Param : FormFieldType::File; 34 | } 35 | 36 | bool FormField::hasFile() const 37 | { 38 | if (type() == FormFieldType::File && !std::get(value_).fileName.empty()) 39 | return true; 40 | else 41 | return false; 42 | } 43 | 44 | const std::string& FormField::fileName() const 45 | { 46 | if (type() == FormFieldType::File) 47 | return std::get(value_).fileName; 48 | else 49 | return valueNotFound; 50 | } 51 | 52 | const std::string& FormField::fileType() const 53 | { 54 | if (type() == FormFieldType::File) { 55 | const auto& res = std::get(value_).mimeType; 56 | return res ? *res : valueNotFound; 57 | } 58 | else 59 | return valueNotFound; 60 | } 61 | 62 | const std::string& FormField::value() const 63 | { 64 | if (type() == FormFieldType::Param) 65 | return std::get(value_); 66 | else 67 | return std::get(value_).fileData; 68 | } 69 | 70 | std::string urlEncodedFormToString(const Form& form) 71 | { 72 | auto result = std::string{}; 73 | for (const auto& [name, field] : form) { 74 | if (!result.empty()) 75 | result += "&"; 76 | result += name + "=" + (field.type() == http::FormFieldType::Param ? field.value() : field.fileName()); 77 | } 78 | return result; 79 | } 80 | 81 | std::string multipartFormToString(const Form& form, const std::string& formBoundary) 82 | { 83 | auto result = std::string{}; 84 | for (const auto& [name, field] : form) { 85 | result += "--" + formBoundary + "\r\n"; 86 | auto header = http::Header{"Content-Disposition", "form-data"}; 87 | header.setQuotingMode(http::HeaderQuotingMode::ParamValue); 88 | header.setParam("name", name); 89 | if (field.type() == FormFieldType::Param) 90 | result += header.toString() + "\r\n\r\n" + field.value() + "\r\n"; 91 | else { 92 | header.setParam("filename", field.fileName()); 93 | auto fileHeader = std::optional{}; 94 | if (!field.fileType().empty()) 95 | fileHeader = http::Header{"Content-Type", field.fileType()}; 96 | 97 | result += header.toString() + "\r\n"; 98 | if (fileHeader) 99 | result += fileHeader->toString() + "\r\n"; 100 | result += "\r\n"; 101 | result += field.value() + "\r\n"; 102 | } 103 | } 104 | if (!form.empty()) 105 | result += "--" + formBoundary + "--\r\n"; 106 | return result; 107 | } 108 | 109 | Form makeForm(const std::map& formView) 110 | { 111 | auto result = Form{}; 112 | std::transform( 113 | formView.begin(), 114 | formView.end(), 115 | std::inserter(result, result.end()), 116 | [](const auto& fieldViewPair) 117 | { 118 | return std::pair{fieldViewPair.first, FormField{fieldViewPair.second}}; 119 | }); 120 | return result; 121 | } 122 | 123 | } //namespace http -------------------------------------------------------------------------------- /src/header.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace http { 9 | 10 | HeaderParam::HeaderParam(const HeaderParamView& paramView) 11 | : name_{paramView.name()} 12 | { 13 | if (paramView.hasValue()) 14 | value_ = std::string{paramView.value()}; 15 | } 16 | 17 | HeaderParam::HeaderParam(std::string name) 18 | : name_{std::move(name)} 19 | { 20 | } 21 | 22 | HeaderParam::HeaderParam(std::string name, std::string value) 23 | : name_{std::move(name)} 24 | , value_{std::move(value)} 25 | { 26 | } 27 | 28 | const std::string& HeaderParam::name() const 29 | { 30 | return name_; 31 | } 32 | 33 | const std::string& HeaderParam::value() const 34 | { 35 | if (value_) 36 | return *value_; 37 | return valueNotFound; 38 | } 39 | 40 | std::string HeaderParam::toString(HeaderQuotingMode quotingMode) const 41 | { 42 | auto res = name_; 43 | if (!value_) 44 | return res; 45 | res += "="; 46 | switch (quotingMode) { 47 | case HeaderQuotingMode::ParamValue: 48 | case HeaderQuotingMode::AllValues: 49 | res += "\"" + *value_ + "\""; 50 | break; 51 | default: 52 | res += *value_; 53 | } 54 | return res; 55 | } 56 | 57 | Header::Header(const HeaderView& headerView) 58 | : name_{headerView.name()} 59 | , value_{headerView.value()} 60 | , params_{makeHeaderParams(headerView.params())} 61 | { 62 | } 63 | 64 | Header::Header(std::string name, std::string value) 65 | : name_{std::move(name)} 66 | , value_{std::move(value)} 67 | { 68 | } 69 | 70 | void Header::setParam(std::string name) 71 | { 72 | if (name.empty()) 73 | return; 74 | 75 | for (auto& param : params_) 76 | if (param.name() == name) { 77 | param = HeaderParam{std::move(name)}; 78 | return; 79 | } 80 | params_.emplace_back(std::move(name)); 81 | } 82 | 83 | void Header::setParam(std::string name, std::string value) 84 | { 85 | if (name.empty()) 86 | return; 87 | 88 | for (auto& param : params_) 89 | if (param.name() == name) { 90 | param = HeaderParam{std::move(name), std::move(value)}; 91 | return; 92 | } 93 | params_.emplace_back(std::move(name), std::move(value)); 94 | } 95 | 96 | void Header::setQuotingMode(HeaderQuotingMode mode) 97 | { 98 | quotingMode_ = mode; 99 | } 100 | 101 | namespace { 102 | std::string valueStr(const std::string& value, bool hasParams, HeaderQuotingMode quotingMode) 103 | { 104 | if (value.empty()) { 105 | if (!hasParams) 106 | return "\"\""; 107 | else 108 | return {}; 109 | } 110 | 111 | switch (quotingMode) { 112 | case HeaderQuotingMode::HeaderValue: 113 | case HeaderQuotingMode::AllValues: 114 | return "\"" + value + "\""; 115 | default: 116 | return value; 117 | } 118 | } 119 | 120 | } //namespace 121 | 122 | const std::vector& Header::params() const 123 | { 124 | return params_; 125 | } 126 | 127 | const std::string& Header::param(std::string_view name) const 128 | { 129 | for (const auto& param : params_) 130 | if (param.name() == name) 131 | return param.value(); 132 | throw std::out_of_range{"Header doesn't contain param '" + std::string{name} + "'"}; 133 | } 134 | 135 | bool Header::hasParam(std::string_view name) const 136 | { 137 | for (const auto& param : params_) 138 | if (param.name() == name) 139 | return true; 140 | return false; 141 | } 142 | 143 | std::string Header::toString() const 144 | { 145 | auto result = name_ + ": " + valueStr(value_, !params_.empty(), quotingMode_); 146 | if (!value_.empty() && !params_.empty()) 147 | result += "; "; 148 | for (const auto& param : params_) { 149 | result += param.toString(quotingMode_); 150 | result += +"; "; 151 | } 152 | if (!params_.empty()) 153 | result.resize(result.size() - 2); // remove last ; 154 | return result; 155 | } 156 | 157 | const std::string& Header::name() const 158 | { 159 | return name_; 160 | } 161 | 162 | const std::string& Header::value() const 163 | { 164 | return value_; 165 | } 166 | 167 | std::vector makeHeaderParams(const std::vector& headerParamViewList) 168 | { 169 | auto result = std::vector{}; 170 | std::transform( 171 | headerParamViewList.begin(), 172 | headerParamViewList.end(), 173 | std::back_inserter(result), 174 | [](const auto& headerParamView) 175 | { 176 | return HeaderParam{headerParamView}; 177 | }); 178 | return result; 179 | } 180 | 181 | std::vector
makeHeaders(const std::vector& headerViewList) 182 | { 183 | auto result = std::vector
{}; 184 | std::transform( 185 | headerViewList.begin(), 186 | headerViewList.end(), 187 | std::back_inserter(result), 188 | [](const auto& headerView) 189 | { 190 | return Header{headerView}; 191 | }); 192 | return result; 193 | } 194 | 195 | } //namespace http 196 | -------------------------------------------------------------------------------- /src/response.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace http { 8 | 9 | Response::Response(const ResponseView& responseView) 10 | : Response{ 11 | responseView.status(), 12 | std::string{responseView.body()}, 13 | makeCookies(responseView.cookies()), 14 | makeHeaders(responseView.headers())} 15 | { 16 | } 17 | 18 | Response::Response(ResponseStatus status, std::string body, std::vector cookies, std::vector
headers) 19 | : status_{status} 20 | , body_{std::move(body)} 21 | , cookies_{std::move(cookies)} 22 | , headers_{std::move(headers)} 23 | { 24 | if (!body_.empty()) { 25 | if (std::find_if( 26 | headers_.begin(), 27 | headers_.end(), 28 | [](const Header& header) 29 | { 30 | return header.name() == "Content-Type"; 31 | }) == headers_.end()) 32 | headers_.emplace_back("Content-Type", detail::contentTypeToString(ContentType::Html)); 33 | } 34 | } 35 | 36 | namespace { 37 | std::vector
concatHeaders(std::vector
lhs, const Header& rhs) 38 | { 39 | lhs.push_back(rhs); 40 | return lhs; 41 | } 42 | } //namespace 43 | 44 | Response::Response( 45 | ResponseStatus status, 46 | std::string body, 47 | std::string contentType, 48 | std::vector cookies, 49 | std::vector
headers) 50 | : Response{ 51 | status, 52 | std::move(body), 53 | std::move(cookies), 54 | concatHeaders(std::move(headers), {"Content-Type", std::move(contentType)})} 55 | { 56 | } 57 | 58 | Response::Response( 59 | ResponseStatus status, 60 | std::string body, 61 | ContentType contentType, 62 | std::vector cookies, 63 | std::vector
headers) 64 | : Response{ 65 | status, 66 | std::move(body), 67 | detail::contentTypeToString(contentType), 68 | std::move(cookies), 69 | std::move(headers)} 70 | { 71 | } 72 | 73 | Response::Response(std::string body, std::string contentType, std::vector cookies, std::vector
headers) 74 | : Response{ResponseStatus::_200_Ok, std::move(body), std::move(contentType), std::move(cookies), std::move(headers)} 75 | { 76 | } 77 | 78 | Response::Response(std::string body, ContentType contentType, std::vector cookies, std::vector
headers) 79 | : Response{ResponseStatus::_200_Ok, std::move(body), contentType, std::move(cookies), std::move(headers)} 80 | { 81 | } 82 | 83 | Response::Response(std::string body, std::vector cookies, std::vector
headers) 84 | : Response{ResponseStatus::_200_Ok, std::move(body), std::move(cookies), std::move(headers)} 85 | { 86 | } 87 | 88 | Response::Response(std::string path, RedirectType type, std::vector cookies, std::vector
headers) 89 | : Response{detail::redirectTypeStatus(type), {}, std::move(cookies), std::move(headers)} 90 | { 91 | addHeader({"Location", std::move(path)}); 92 | } 93 | 94 | Response::Response(Redirect redirect, std::vector cookies, std::vector
headers) 95 | : Response{std::move(redirect.path), redirect.type, std::move(cookies), std::move(headers)} 96 | { 97 | } 98 | 99 | ResponseStatus Response::status() const 100 | { 101 | return status_; 102 | } 103 | 104 | const std::string& Response::body() const 105 | { 106 | return body_; 107 | } 108 | 109 | const std::vector& Response::cookies() const 110 | { 111 | return cookies_; 112 | } 113 | 114 | const std::vector
& Response::headers() const 115 | { 116 | return headers_; 117 | } 118 | 119 | void Response::setBody(const std::string& body) 120 | { 121 | body_ = body; 122 | } 123 | 124 | void Response::addCookie(Cookie cookie) 125 | { 126 | cookies_.emplace_back(std::move(cookie)); 127 | } 128 | 129 | void Response::addHeader(Header header) 130 | { 131 | headers_.emplace_back(std::move(header)); 132 | } 133 | 134 | void Response::addCookies(const std::vector& cookies) 135 | { 136 | std::copy(cookies.begin(), cookies.end(), std::back_inserter(cookies_)); 137 | } 138 | 139 | void Response::addHeaders(const std::vector
& headers) 140 | { 141 | std::copy(headers.begin(), headers.end(), std::back_inserter(headers_)); 142 | } 143 | 144 | std::string Response::statusData(ResponseMode mode) const 145 | { 146 | return (mode == ResponseMode::Cgi ? "Status: " : "HTTP/1.1 ") + std::string{detail::statusToString(status_)} + 147 | "\r\n"; 148 | } 149 | 150 | std::string Response::cookiesData() const 151 | { 152 | auto result = std::string{}; 153 | for (const auto& cookie : cookies_) 154 | result += cookie.toString() + "\r\n"; 155 | return result; 156 | } 157 | 158 | std::string Response::headersData() const 159 | { 160 | auto result = std::string{}; 161 | for (auto& header : headers_) 162 | result += header.toString() + "\r\n"; 163 | return result; 164 | } 165 | 166 | std::string Response::data(ResponseMode mode) const 167 | { 168 | return statusData(mode) + headersData() + cookiesData() + "\r\n" + body_; 169 | } 170 | 171 | } //namespace http 172 | -------------------------------------------------------------------------------- /tests/test_cookie.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | TEST(Cookie, ToString) 8 | { 9 | { 10 | auto cookie = http::Cookie{"foo", "bar"}; 11 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar"); 12 | } 13 | 14 | { 15 | auto cookie = http::Cookie{"foo", "bar"}; 16 | cookie.setMaxAge(std::chrono::seconds{10}); 17 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Max-Age=10"); 18 | } 19 | 20 | { 21 | auto cookie = http::Cookie{"foo", "bar"}; 22 | cookie.setMaxAge(std::chrono::minutes{1}); 23 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Max-Age=60"); 24 | } 25 | 26 | { 27 | auto cookie = http::Cookie{"foo", "bar"}; 28 | cookie.setDomain("localhost"); 29 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Domain=localhost"); 30 | } 31 | 32 | { 33 | auto cookie = http::Cookie{"foo", "bar"}; 34 | cookie.setMaxAge(std::chrono::seconds{10}); 35 | cookie.setDomain("localhost"); 36 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Max-Age=10; Domain=localhost"); 37 | } 38 | 39 | { 40 | auto cookie = http::Cookie{"foo", "bar"}; 41 | cookie.setPath("/test"); 42 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Path=/test"); 43 | } 44 | 45 | { 46 | auto cookie = http::Cookie{"foo", "bar"}; 47 | cookie.setMaxAge(std::chrono::seconds{10}); 48 | cookie.setPath("/test"); 49 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Max-Age=10; Path=/test"); 50 | } 51 | 52 | { 53 | auto cookie = http::Cookie{"foo", "bar"}; 54 | cookie.setDomain("localhost"); 55 | cookie.setPath("/test"); 56 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Domain=localhost; Path=/test"); 57 | } 58 | 59 | { 60 | auto cookie = http::Cookie{"foo", "bar"}; 61 | cookie.setMaxAge(std::chrono::seconds{10}); 62 | cookie.setDomain("localhost"); 63 | cookie.setPath("/test"); 64 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Max-Age=10; Domain=localhost; Path=/test"); 65 | } 66 | 67 | { 68 | auto cookie = http::Cookie{"foo", "bar"}; 69 | cookie.setDomain("localhost"); 70 | cookie.setPath("/test"); 71 | cookie.setRemoved(); 72 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Domain=localhost; Path=/test; Max-Age=0"); 73 | } 74 | 75 | { 76 | auto cookie = http::Cookie{"foo", "bar"}; 77 | cookie.setDomain("localhost"); 78 | cookie.setPath("/test"); 79 | cookie.setSecure(); 80 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Domain=localhost; Path=/test; Secure"); 81 | } 82 | 83 | { 84 | auto cookie = http::Cookie{"foo", "bar"}; 85 | cookie.setDomain("localhost"); 86 | cookie.setPath("/test"); 87 | cookie.setRemoved(); 88 | cookie.setSecure(); 89 | EXPECT_EQ(cookie.toString(), "Set-Cookie: foo=bar; Domain=localhost; Path=/test; Max-Age=0; Secure"); 90 | } 91 | 92 | { 93 | auto cookies = std::vector{}; 94 | cookies.emplace_back(http::Cookie{"foo", "bar"}); 95 | cookies.emplace_back(http::Cookie{"Hello", "world"}); 96 | EXPECT_EQ(cookiesToString(cookies), "foo=bar; Hello=world"); 97 | } 98 | } 99 | 100 | TEST(CookieView, FromString) 101 | { 102 | { 103 | auto cookies = http::cookiesFromString("name=foo"); 104 | auto expectedCookies = std::vector{{"name", "foo"}}; 105 | EXPECT_EQ(cookies, expectedCookies); 106 | } 107 | { 108 | auto cookies = http::cookiesFromString("name=foo;test=bar"); 109 | auto expectedCookies = std::vector{{"name", "foo"}, {"test", "bar"}}; 110 | EXPECT_EQ(cookies, expectedCookies); 111 | } 112 | { 113 | auto cookies = http::cookiesFromString(""); 114 | auto expectedCookies = std::vector{}; 115 | EXPECT_EQ(cookies, expectedCookies); 116 | } 117 | { 118 | auto cookies = http::cookiesFromString("="); 119 | auto expectedCookies = std::vector{}; 120 | EXPECT_EQ(cookies, expectedCookies); 121 | } 122 | { 123 | auto cookies = http::cookiesFromString(";"); 124 | auto expectedCookies = std::vector{}; 125 | EXPECT_EQ(cookies, expectedCookies); 126 | } 127 | { 128 | auto cookies = http::cookiesFromString(";;"); 129 | auto expectedCookies = std::vector{}; 130 | EXPECT_EQ(cookies, expectedCookies); 131 | } 132 | { 133 | auto cookies = http::cookiesFromString("=;=;="); 134 | auto expectedCookies = std::vector{}; 135 | EXPECT_EQ(cookies, expectedCookies); 136 | } 137 | } 138 | 139 | TEST(CookieView, FromHeader) 140 | { 141 | { 142 | auto header = http::HeaderView{ 143 | "Set-Cookie", 144 | "", 145 | {{"foo", "bar"}, {"Max-Age", "10"}, {"Domain", "localhost"}, {"Path", "/test"}, {"Secure", ""}}}; 146 | auto cookie = http::cookieFromHeader(header); 147 | ASSERT_TRUE(cookie); 148 | EXPECT_EQ(cookie->name(), "foo"); 149 | EXPECT_EQ(cookie->value(), "bar"); 150 | EXPECT_EQ(cookie->maxAge(), std::chrono::seconds{10}); 151 | EXPECT_EQ(cookie->domain(), "localhost"); 152 | EXPECT_EQ(cookie->isSecure(), true); 153 | } 154 | { 155 | auto header = http::HeaderView{"Set-Cookie", "", {{"foo", "bar"}, {"Max-Age", "0"}}}; 156 | auto cookie = http::cookieFromHeader(header); 157 | ASSERT_TRUE(cookie); 158 | EXPECT_EQ(cookie->name(), "foo"); 159 | EXPECT_EQ(cookie->value(), "bar"); 160 | EXPECT_EQ(cookie->isRemoved(), true); 161 | } 162 | } 163 | 164 | TEST(CookieView, CookieFormCookieView) 165 | { 166 | auto header = http::HeaderView{ 167 | "Set-Cookie", 168 | "", 169 | {{"foo", "bar"}, {"Max-Age", "10"}, {"Domain", "localhost"}, {"Path", "/test"}, {"Secure", ""}}}; 170 | auto cookieView = http::cookieFromHeader(header); 171 | ASSERT_TRUE(cookieView); 172 | auto cookie = http::Cookie{*cookieView}; 173 | EXPECT_EQ(cookie.name(), "foo"); 174 | EXPECT_EQ(cookie.value(), "bar"); 175 | EXPECT_EQ(cookie.maxAge(), std::chrono::seconds{10}); 176 | EXPECT_EQ(cookie.domain(), "localhost"); 177 | EXPECT_EQ(cookie.isSecure(), true); 178 | } 179 | -------------------------------------------------------------------------------- /src/request_view.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | using namespace std::string_literals; 6 | 7 | namespace http { 8 | 9 | RequestView::RequestView( 10 | std::string_view fcgiParamRequestMethod, 11 | std::string_view fcgiParamRemoteAddr, 12 | std::string_view fcgiParamHttpHost, 13 | std::string_view fcgiParamRequestUri, 14 | std::string_view fcgiParamQueryString, 15 | std::string_view fcgiParamHttpCookie, 16 | std::string_view fcgiParamContentType, 17 | std::string_view fcgiStdIn) 18 | : method_{methodFromString(fcgiParamRequestMethod)} 19 | , ipAddress_{fcgiParamRemoteAddr} 20 | , domainName_{sfun::before(fcgiParamHttpHost, ":").value_or(fcgiParamHttpHost)} 21 | , path_{sfun::before(fcgiParamRequestUri, "?").value_or(fcgiParamRequestUri)} 22 | , queries_{queriesFromString(fcgiParamQueryString)} 23 | , cookies_{cookiesFromString(fcgiParamHttpCookie)} 24 | , form_{formFromString(fcgiParamContentType, fcgiStdIn)} 25 | { 26 | } 27 | 28 | RequestMethod RequestView::method() const 29 | { 30 | return method_; 31 | } 32 | 33 | std::string_view RequestView::ipAddress() const 34 | { 35 | return ipAddress_; 36 | } 37 | 38 | std::string_view RequestView::domainName() const 39 | { 40 | return domainName_; 41 | } 42 | 43 | std::string_view RequestView::path() const 44 | { 45 | return path_; 46 | } 47 | 48 | std::string_view RequestView::query(std::string_view name) const 49 | { 50 | auto it = std::find_if( 51 | queries_.begin(), 52 | queries_.end(), 53 | [&name](const auto& query) 54 | { 55 | return query.name() == name; 56 | }); 57 | if (it != queries_.end()) 58 | return it->value(); 59 | 60 | return {}; 61 | } 62 | 63 | bool RequestView::hasQuery(std::string_view name) const 64 | { 65 | auto it = std::find_if( 66 | queries_.begin(), 67 | queries_.end(), 68 | [&name](const auto& query) 69 | { 70 | return query.name() == name; 71 | }); 72 | return (it != queries_.end()); 73 | } 74 | 75 | std::string_view RequestView::cookie(std::string_view name) const 76 | { 77 | auto it = std::find_if( 78 | cookies_.begin(), 79 | cookies_.end(), 80 | [&name](const auto& cookie) 81 | { 82 | return cookie.name() == name; 83 | }); 84 | if (it != cookies_.end()) 85 | return it->value(); 86 | 87 | return {}; 88 | } 89 | 90 | bool RequestView::hasCookie(std::string_view name) const 91 | { 92 | auto it = std::find_if( 93 | cookies_.begin(), 94 | cookies_.end(), 95 | [&name](const auto& cookie) 96 | { 97 | return cookie.name() == name; 98 | }); 99 | return (it != cookies_.end()); 100 | } 101 | 102 | const FormView& RequestView::form() const 103 | { 104 | return form_; 105 | } 106 | 107 | std::string_view RequestView::formField(std::string_view name, int index) const 108 | { 109 | auto i = 0; 110 | for (const auto& [formFieldName, formField] : form_) { 111 | if (formFieldName == name && formField.type() == FormFieldType::Param) { 112 | if (i++ == index) 113 | return formField.value(); 114 | } 115 | } 116 | return {}; 117 | } 118 | 119 | int RequestView::formFieldCount(std::string_view name) const 120 | { 121 | return static_cast(std::count_if( 122 | form_.begin(), 123 | form_.end(), 124 | [&name](const auto& namedFormField) 125 | { 126 | const auto& [formFieldName, formField] = namedFormField; 127 | return formFieldName == name && formField.type() == FormFieldType::Param; 128 | })); 129 | } 130 | 131 | bool RequestView::hasFormField(std::string_view name) const 132 | { 133 | return formFieldCount(name) != 0; 134 | } 135 | 136 | std::string_view RequestView::fileData(std::string_view name, int index) const 137 | { 138 | auto i = 0; 139 | for (const auto& [formFieldName, formField] : form_) 140 | if (formField.hasFile() && formFieldName == name) 141 | if (i++ == index) 142 | return formField.value(); 143 | 144 | return {}; 145 | } 146 | 147 | int RequestView::fileCount(std::string_view name) const 148 | { 149 | auto result = 0; 150 | for (const auto& [formFieldName, formField] : form_) 151 | if (formField.hasFile() && formFieldName == name) 152 | result++; 153 | return result; 154 | } 155 | 156 | bool RequestView::hasFile(std::string_view name) const 157 | { 158 | return fileCount(name) != 0; 159 | } 160 | 161 | std::string_view RequestView::fileName(std::string_view name, int index) const 162 | { 163 | auto i = 0; 164 | for (const auto& [formFieldName, formField] : form_) 165 | if (formField.hasFile() && formFieldName == name) 166 | if (i++ == index) 167 | return formField.fileName(); 168 | 169 | return {}; 170 | } 171 | 172 | std::string_view RequestView::fileType(std::string_view name, int index) const 173 | { 174 | auto i = 0; 175 | for (const auto& [formFieldName, formField] : form_) 176 | if (formField.hasFile() && formFieldName == name) 177 | if (i++ == index) 178 | return formField.fileType(); 179 | 180 | return {}; 181 | } 182 | 183 | const std::vector& RequestView::queries() const 184 | { 185 | return queries_; 186 | } 187 | 188 | const std::vector& RequestView::cookies() const 189 | { 190 | return cookies_; 191 | } 192 | 193 | std::vector RequestView::formFieldList() const 194 | { 195 | auto result = std::vector{}; 196 | for (const auto& [formFieldName, formField] : form_) 197 | if (formField.type() == FormFieldType::Param) 198 | result.push_back(formFieldName); 199 | return result; 200 | } 201 | 202 | std::vector RequestView::fileList() const 203 | { 204 | auto result = std::vector{}; 205 | for (const auto& [formFieldName, formField] : form_) 206 | if (formField.hasFile()) 207 | result.push_back(formFieldName); 208 | return result; 209 | } 210 | 211 | bool RequestView::hasFiles() const 212 | { 213 | return std::any_of( 214 | form_.begin(), 215 | form_.end(), 216 | [](const auto& formFieldPair) 217 | { 218 | return formFieldPair.second.hasFile(); 219 | }); 220 | } 221 | 222 | } //namespace http 223 | -------------------------------------------------------------------------------- /src/form_view.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace http { 7 | 8 | FormFieldView::FormFieldView(std::string_view value) 9 | : value_{value} 10 | { 11 | } 12 | 13 | FormFieldView::FormFieldView( 14 | std::string_view fileData, 15 | std::string_view fileName, 16 | std::optional fileType) 17 | : value_{FormFile{fileData, fileName, fileType}} 18 | { 19 | } 20 | 21 | FormFieldType FormFieldView::type() const 22 | { 23 | return std::holds_alternative(value_) ? FormFieldType::Param : FormFieldType::File; 24 | } 25 | 26 | bool FormFieldView::hasFile() const 27 | { 28 | if (type() == FormFieldType::File && !std::get(value_).fileName.empty()) 29 | return true; 30 | else 31 | return false; 32 | } 33 | 34 | std::string_view FormFieldView::fileName() const 35 | { 36 | if (type() == FormFieldType::File) 37 | return std::get(value_).fileName; 38 | else 39 | return {}; 40 | } 41 | 42 | std::string_view FormFieldView::fileType() const 43 | { 44 | if (type() == FormFieldType::File) { 45 | const auto& res = std::get(value_).mimeType; 46 | return res ? *res : std::string_view{}; 47 | } 48 | else 49 | return {}; 50 | } 51 | 52 | std::string_view FormFieldView::value() const 53 | { 54 | if (type() == FormFieldType::Param) 55 | return std::get(value_); 56 | else 57 | return std::get(value_).fileData; 58 | } 59 | 60 | namespace { 61 | std::string_view getStringLine(std::string_view input, std::size_t& pos, std::string_view lineSeparator = "\r\n") 62 | { 63 | auto lastPos = input.find(lineSeparator, pos); 64 | auto separatorSize = lineSeparator.size(); 65 | if (lastPos == std::string::npos) { 66 | lastPos = input.size(); 67 | separatorSize = 0; 68 | } 69 | auto lineSize = lastPos - pos; 70 | auto linePos = pos; 71 | pos += lineSize + separatorSize; 72 | return input.substr(linePos, lineSize); 73 | } 74 | 75 | /// Reads HTTP headers between two blank lines 76 | /// Returns a tuple { Content-Disposition header if found, 77 | /// Content-Type header if found 78 | /// } 79 | /// if input is not at the end and has a valid state 80 | /// 81 | std::optional, std::optional>> readContentHeaders( 82 | std::string_view input, 83 | std::size_t& pos) 84 | { 85 | auto headerLine = getStringLine(input, pos); 86 | if (!headerLine.empty() || pos == input.size()) 87 | return {}; 88 | else 89 | headerLine = getStringLine(input, pos); 90 | 91 | auto contentDisposition = std::optional{}; 92 | auto contentType = std::optional{}; 93 | while (!headerLine.empty()) { 94 | auto header = headerFromString(headerLine); 95 | if (header.has_value()) { 96 | if (header->name() == "Content-Disposition") 97 | contentDisposition = std::move(header); 98 | else if (header->name() == "Content-Type") 99 | contentType = std::move(header); 100 | } 101 | headerLine = getStringLine(input, pos); 102 | } 103 | return std::make_tuple(std::move(contentDisposition), std::move(contentType)); 104 | } 105 | 106 | FormView parseFormFieldViews(std::string_view input, std::string_view boundary) 107 | { 108 | const auto separator = "--" + std::string{boundary}; 109 | const auto endSeparator = "--" + std::string{boundary} + "--"; 110 | 111 | auto pos = std::size_t{}; 112 | auto firstSeparatorLine = getStringLine(input, pos, separator); 113 | if (!firstSeparatorLine.empty()) //a form must start with a "--" separator 114 | return {}; 115 | 116 | auto result = FormView{}; 117 | while (pos < input.size()) { 118 | auto contentHeaders = readContentHeaders(input, pos); 119 | if (!contentHeaders) 120 | return result; 121 | 122 | auto& [contentDisposition, contentType] = *contentHeaders; 123 | auto content = getStringLine(input, pos, separator); 124 | if (!contentDisposition.has_value() || !contentDisposition->hasParam("name") || 125 | contentDisposition->param("name").empty()) 126 | continue; 127 | 128 | if (content.size() >= 2) 129 | content.remove_suffix(2); //remove \r\n 130 | 131 | const auto& paramName = contentDisposition->param("name"); 132 | if (contentDisposition->hasParam("filename")) { 133 | const auto& fileName = contentDisposition->param("filename"); 134 | auto fileType = std::optional{}; 135 | if (contentType.has_value()) 136 | fileType = contentType->value(); 137 | result.emplace(std::string{paramName}, FormFieldView{content, fileName, fileType}); 138 | } 139 | else 140 | result.emplace(std::string{paramName}, FormFieldView{content}); 141 | } 142 | return result; 143 | } 144 | 145 | std::tuple parseUrlEncodedParamString(std::string_view paramStr) 146 | { 147 | const auto namePart = sfun::before(paramStr, "="); 148 | if (!namePart.has_value()) 149 | return {}; 150 | 151 | const auto name = sfun::trim(namePart.value()); 152 | if (name.empty()) 153 | return {}; 154 | 155 | const auto val = sfun::after(paramStr, "=").value(); 156 | return {name, val}; 157 | } 158 | 159 | FormView parseUrlEncodedFields(std::string_view input) 160 | { 161 | auto pos = std::size_t{0u}; 162 | auto result = FormView{}; 163 | do { 164 | auto param = getStringLine(input, pos, "&"); 165 | auto [paramName, paramValue] = parseUrlEncodedParamString(param); 166 | if (paramName.empty()) 167 | continue; 168 | result.emplace(paramName, FormFieldView{paramValue}); 169 | } 170 | while (pos < input.size()); 171 | return result; 172 | } 173 | } //namespace 174 | 175 | FormView formFromString(std::string_view contentParam, std::string_view contentFields) 176 | { 177 | auto contentTypeHeader = "Content-Type: " + std::string{contentParam}; 178 | auto contentType = headerFromString(contentTypeHeader); 179 | if (!contentType.has_value()) 180 | return {}; 181 | 182 | if (contentType->value() == "multipart/form-data" && contentType->hasParam("boundary")) 183 | return parseFormFieldViews(contentFields, contentType->param("boundary")); 184 | else if (contentType->value() == "application/x-www-form-urlencoded") 185 | return parseUrlEncodedFields(contentFields); 186 | 187 | return {}; 188 | } 189 | 190 | } //namespace http -------------------------------------------------------------------------------- /tests/test_header.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | TEST(Header, ToString) 6 | { 7 | { 8 | auto header = http::Header{"Test-Header", "test"}; 9 | EXPECT_EQ(header.toString(), "Test-Header: test"); 10 | } 11 | { 12 | auto header = http::Header{"Test-Header", "test"}; 13 | header.setParam("name", "foo"); 14 | header.setParam("name2", "bar"); 15 | EXPECT_EQ(header.toString(), "Test-Header: test; name=foo; name2=bar"); 16 | } 17 | } 18 | 19 | TEST(Header, ToStringWithoutHeaderValue) 20 | { 21 | { 22 | auto header = http::Header{"Test-Header", ""}; 23 | EXPECT_EQ(header.toString(), "Test-Header: \"\""); 24 | } 25 | { 26 | auto header = http::Header{"Test-Header", ""}; 27 | header.setParam("name", "foo"); 28 | header.setParam("name2", "bar"); 29 | EXPECT_EQ(header.toString(), "Test-Header: name=foo; name2=bar"); 30 | } 31 | } 32 | 33 | TEST(Header, ToStringQuotingMode) 34 | { 35 | { 36 | auto header = http::Header{"Test-Header", "test"}; 37 | header.setParam("name", "foo"); 38 | header.setParam("name2", "bar"); 39 | header.setQuotingMode(http::HeaderQuotingMode::ParamValue); 40 | EXPECT_EQ(header.toString(), "Test-Header: test; name=\"foo\"; name2=\"bar\""); 41 | } 42 | { 43 | auto header = http::Header{"Test-Header", "test"}; 44 | header.setParam("name", "foo"); 45 | header.setParam("name2", "bar"); 46 | header.setQuotingMode(http::HeaderQuotingMode::HeaderValue); 47 | EXPECT_EQ(header.toString(), "Test-Header: \"test\"; name=foo; name2=bar"); 48 | } 49 | { 50 | auto header = http::Header{"Test-Header", "test"}; 51 | header.setParam("name", "foo"); 52 | header.setParam("name2", "bar"); 53 | header.setQuotingMode(http::HeaderQuotingMode::AllValues); 54 | EXPECT_EQ(header.toString(), "Test-Header: \"test\"; name=\"foo\"; name2=\"bar\""); 55 | } 56 | } 57 | TEST(Header, ToStringWithoutHeaderValueQuotingMode) 58 | { 59 | { 60 | auto header = http::Header{"Test-Header", ""}; 61 | header.setParam("name", "foo"); 62 | header.setParam("name2", "bar"); 63 | header.setQuotingMode(http::HeaderQuotingMode::ParamValue); 64 | EXPECT_EQ(header.toString(), "Test-Header: name=\"foo\"; name2=\"bar\""); 65 | } 66 | { 67 | auto header = http::Header{"Test-Header", ""}; 68 | header.setParam("name", "foo"); 69 | header.setParam("name2", "bar"); 70 | header.setQuotingMode(http::HeaderQuotingMode::HeaderValue); 71 | EXPECT_EQ(header.toString(), "Test-Header: name=foo; name2=bar"); 72 | } 73 | { 74 | auto header = http::Header{"Test-Header", "test"}; 75 | header.setParam("name", "foo"); 76 | header.setParam("name2", "bar"); 77 | header.setQuotingMode(http::HeaderQuotingMode::AllValues); 78 | EXPECT_EQ(header.toString(), "Test-Header: \"test\"; name=\"foo\"; name2=\"bar\""); 79 | } 80 | } 81 | 82 | TEST(HeaderView, FromString) 83 | { 84 | { 85 | auto header = http::headerFromString("Test-Header: test"); 86 | ASSERT_TRUE(header.has_value()); 87 | EXPECT_EQ(header->name(), "Test-Header"); 88 | EXPECT_EQ(header->value(), "test"); 89 | EXPECT_TRUE(header->params().empty()); 90 | } 91 | { 92 | auto header = http::headerFromString("Test-Header: test; name=foo; name2=bar"); 93 | ASSERT_TRUE(header.has_value()); 94 | EXPECT_EQ(header->name(), "Test-Header"); 95 | EXPECT_EQ(header->value(), "test"); 96 | EXPECT_EQ(header->params().size(), 2); 97 | ASSERT_TRUE(header->hasParam("name")); 98 | EXPECT_EQ(header->param("name"), "foo"); 99 | ASSERT_TRUE(header->hasParam("name2")); 100 | EXPECT_EQ(header->param("name2"), "bar"); 101 | } 102 | } 103 | 104 | TEST(HeaderView, HeaderFromHeaderView) 105 | { 106 | auto headerView = http::headerFromString("Test-Header: test; name=foo; name2=bar"); 107 | ASSERT_TRUE(headerView.has_value()); 108 | auto header = http::Header{*headerView}; 109 | EXPECT_EQ(header.name(), "Test-Header"); 110 | EXPECT_EQ(header.value(), "test"); 111 | EXPECT_EQ(header.params().size(), 2); 112 | ASSERT_TRUE(header.hasParam("name")); 113 | EXPECT_EQ(header.param("name"), "foo"); 114 | ASSERT_TRUE(header.hasParam("name2")); 115 | EXPECT_EQ(header.param("name2"), "bar"); 116 | } 117 | 118 | TEST(HeaderView, FromStringWithoutHeaderValue) 119 | { 120 | { 121 | auto header = http::headerFromString("Test-Header: \"\""); 122 | ASSERT_TRUE(header.has_value()); 123 | EXPECT_EQ(header->name(), "Test-Header"); 124 | EXPECT_EQ(header->value(), ""); 125 | EXPECT_TRUE(header->params().empty()); 126 | } 127 | { 128 | auto header = http::headerFromString("Test-Header: name=foo; name2=bar"); 129 | ASSERT_TRUE(header.has_value()); 130 | EXPECT_EQ(header->name(), "Test-Header"); 131 | EXPECT_EQ(header->value(), ""); 132 | EXPECT_EQ(header->params().size(), 2); 133 | ASSERT_TRUE(header->hasParam("name")); 134 | EXPECT_EQ(header->param("name"), "foo"); 135 | ASSERT_TRUE(header->hasParam("name2")); 136 | EXPECT_EQ(header->param("name2"), "bar"); 137 | } 138 | } 139 | 140 | TEST(HeaderView, FromStringWithQuoting) 141 | { 142 | { 143 | auto header = http::headerFromString("Test-Header: \"test\""); 144 | ASSERT_TRUE(header.has_value()); 145 | EXPECT_EQ(header->name(), "Test-Header"); 146 | EXPECT_EQ(header->value(), "test"); 147 | EXPECT_TRUE(header->params().empty()); 148 | } 149 | { 150 | auto header = http::headerFromString("Test-Header: \"test\"; name=\"foo\"; name2=bar"); 151 | ASSERT_TRUE(header.has_value()); 152 | EXPECT_EQ(header->name(), "Test-Header"); 153 | EXPECT_EQ(header->value(), "test"); 154 | EXPECT_EQ(header->params().size(), 2); 155 | ASSERT_TRUE(header->hasParam("name")); 156 | EXPECT_EQ(header->param("name"), "foo"); 157 | ASSERT_TRUE(header->hasParam("name2")); 158 | EXPECT_EQ(header->param("name2"), "bar"); 159 | } 160 | } 161 | 162 | TEST(HeaderView, FromStringWithoutHeaderValueWithQuoting) 163 | { 164 | auto header = http::headerFromString("Test-Header: name=\"foo\"; name2=bar"); 165 | ASSERT_TRUE(header.has_value()); 166 | EXPECT_EQ(header->name(), "Test-Header"); 167 | EXPECT_EQ(header->value(), ""); 168 | EXPECT_EQ(header->params().size(), 2); 169 | ASSERT_TRUE(header->hasParam("name")); 170 | EXPECT_EQ(header->param("name"), "foo"); 171 | ASSERT_TRUE(header->hasParam("name2")); 172 | EXPECT_EQ(header->param("name2"), "bar"); 173 | } 174 | 175 | TEST(HeaderView, FromStringWithoutHeaderName) 176 | { 177 | { 178 | auto header = http::headerFromString(": name=\"foo\"; name2=bar"); 179 | ASSERT_FALSE(header.has_value()); 180 | } 181 | { 182 | auto header = http::headerFromString(":"); 183 | ASSERT_FALSE(header.has_value()); 184 | } 185 | { 186 | auto header = http::headerFromString(" \t : name=\"foo\"; name2=bar"); 187 | ASSERT_FALSE(header.has_value()); 188 | } 189 | { 190 | auto header = http::headerFromString(""); 191 | ASSERT_FALSE(header.has_value()); 192 | } 193 | { 194 | auto header = http::headerFromString("Hello world!"); 195 | ASSERT_FALSE(header.has_value()); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/request.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | using namespace std::string_literals; 6 | 7 | namespace http { 8 | 9 | Request::Request(const RequestView& requestView) 10 | : method_{requestView.method()} 11 | , path_{requestView.path()} 12 | , queries_{makeQueries(requestView.queries())} 13 | , cookies_{makeCookies(requestView.cookies())} 14 | , form_{makeForm(requestView.form())} 15 | { 16 | } 17 | 18 | Request::Request( 19 | RequestMethod method, 20 | std::string path, 21 | std::vector queries, 22 | std::vector cookies, 23 | Form form) 24 | : method_{method} 25 | , path_{std::move(path)} 26 | , queries_{std::move(queries)} 27 | , cookies_{std::move(cookies)} 28 | , form_{std::move(form)} 29 | { 30 | } 31 | 32 | void Request::setQueries(const std::vector& queries) 33 | { 34 | queries_ = queries; 35 | } 36 | 37 | void Request::setCookies(const std::vector& cookies) 38 | { 39 | cookies_ = cookies; 40 | } 41 | 42 | void Request::setForm(const Form& form) 43 | { 44 | form_ = form; 45 | } 46 | 47 | void Request::setFcgiParams(const std::map& params) 48 | { 49 | fcgiParams_ = params; 50 | } 51 | 52 | RequestMethod Request::method() const 53 | { 54 | return method_; 55 | } 56 | 57 | const std::string& Request::path() const 58 | { 59 | return path_; 60 | } 61 | 62 | const std::string& Request::query(std::string_view name) const 63 | { 64 | auto it = std::find_if( 65 | queries_.begin(), 66 | queries_.end(), 67 | [&name](const auto& query) 68 | { 69 | return query.name() == name; 70 | }); 71 | if (it != queries_.end()) 72 | return it->value(); 73 | 74 | return valueNotFound; 75 | } 76 | 77 | bool Request::hasQuery(std::string_view name) const 78 | { 79 | auto it = std::find_if( 80 | queries_.begin(), 81 | queries_.end(), 82 | [&name](const auto& query) 83 | { 84 | return query.name() == name; 85 | }); 86 | return (it != queries_.end()); 87 | } 88 | 89 | const std::string& Request::cookie(std::string_view name) const 90 | { 91 | auto it = std::find_if( 92 | cookies_.begin(), 93 | cookies_.end(), 94 | [&name](const auto& cookie) 95 | { 96 | return cookie.name() == name; 97 | }); 98 | if (it != cookies_.end()) 99 | return it->value(); 100 | 101 | return valueNotFound; 102 | } 103 | 104 | bool Request::hasCookie(std::string_view name) const 105 | { 106 | auto it = std::find_if( 107 | cookies_.begin(), 108 | cookies_.end(), 109 | [&name](const auto& cookie) 110 | { 111 | return cookie.name() == name; 112 | }); 113 | return (it != cookies_.end()); 114 | } 115 | 116 | const Form& Request::form() const 117 | { 118 | return form_; 119 | } 120 | 121 | const std::string& Request::formField(std::string_view name, int index) const 122 | { 123 | auto i = 0; 124 | for (const auto& [formFieldName, formField] : form_) { 125 | if (formFieldName == name && formField.type() == FormFieldType::Param) { 126 | if (i++ == index) 127 | return formField.value(); 128 | } 129 | } 130 | return valueNotFound; 131 | } 132 | 133 | int Request::formFieldCount(std::string_view name) const 134 | { 135 | return static_cast(std::count_if( 136 | form_.begin(), 137 | form_.end(), 138 | [&name](const auto& namedFormField) 139 | { 140 | const auto& [formFieldName, formField] = namedFormField; 141 | return formFieldName == name && formField.type() == FormFieldType::Param; 142 | })); 143 | } 144 | 145 | bool Request::hasFormField(std::string_view name) const 146 | { 147 | return formFieldCount(name) != 0; 148 | } 149 | 150 | const std::string& Request::fileData(std::string_view name, int index) const 151 | { 152 | auto i = 0; 153 | for (const auto& [formFieldName, formField] : form_) 154 | if (formField.hasFile() && formFieldName == name) 155 | if (i++ == index) 156 | return formField.value(); 157 | 158 | return valueNotFound; 159 | } 160 | 161 | int Request::fileCount(std::string_view name) const 162 | { 163 | auto result = 0; 164 | for (const auto& [formFieldName, formField] : form_) 165 | if (formField.hasFile() && formFieldName == name) 166 | result++; 167 | return result; 168 | } 169 | 170 | bool Request::hasFile(std::string_view name) const 171 | { 172 | return fileCount(name) != 0; 173 | } 174 | 175 | const std::string& Request::fileName(std::string_view name, int index) const 176 | { 177 | auto i = 0; 178 | for (const auto& [formFieldName, formField] : form_) 179 | if (formField.hasFile() && formFieldName == name) 180 | if (i++ == index) 181 | return formField.fileName(); 182 | 183 | return valueNotFound; 184 | } 185 | 186 | const std::string& Request::fileType(std::string_view name, int index) const 187 | { 188 | auto i = 0; 189 | for (const auto& [formFieldName, formField] : form_) 190 | if (formField.hasFile() && formFieldName == name) 191 | if (i++ == index) 192 | return formField.fileType(); 193 | 194 | return valueNotFound; 195 | } 196 | 197 | const std::vector& Request::queries() const 198 | { 199 | return queries_; 200 | } 201 | 202 | const std::vector& Request::cookies() const 203 | { 204 | return cookies_; 205 | } 206 | 207 | std::vector Request::formFieldList() const 208 | { 209 | auto result = std::vector{}; 210 | for (const auto& [formFieldName, formField] : form_) 211 | if (formField.type() == FormFieldType::Param) 212 | result.push_back(formFieldName); 213 | return result; 214 | } 215 | 216 | std::vector Request::fileList() const 217 | { 218 | auto result = std::vector{}; 219 | for (const auto& [formFieldName, formField] : form_) 220 | if (formField.hasFile()) 221 | result.push_back(formFieldName); 222 | return result; 223 | } 224 | 225 | bool Request::hasFiles() const 226 | { 227 | for (const auto& [formFieldName, formField] : form_) 228 | if (formField.hasFile()) 229 | return true; 230 | 231 | return false; 232 | } 233 | 234 | RequestFcgiData Request::toFcgiData(FormType formType) const 235 | { 236 | const auto formBoundary = "----asyncgiFormBoundary"s; 237 | 238 | auto makeFcgiParams = [&] 239 | { 240 | auto res = fcgiParams_; 241 | res["REQUEST_METHOD"] = methodToString(method_); 242 | if (!path_.empty()) 243 | res["REQUEST_URI"] = path_; 244 | if (!queries_.empty()) 245 | res["QUERY_STRING"] = queriesToString(queries_); 246 | if (!cookies_.empty()) 247 | res["HTTP_COOKIE"] = cookiesToString(cookies_); 248 | if (!form_.empty()) { 249 | if (formType == FormType::Multipart) 250 | res["CONTENT_TYPE"] = "multipart/form-data; boundary=" + formBoundary; 251 | else 252 | res["CONTENT_TYPE"] = "application/x-www-form-urlencoded"; 253 | } 254 | return res; 255 | }; 256 | auto makeFcgiStdIn = [&] 257 | { 258 | if (formType == FormType::Multipart) 259 | return multipartFormToString(form_, formBoundary); 260 | else 261 | return urlEncodedFormToString(form_); 262 | }; 263 | 264 | return {makeFcgiParams(), makeFcgiStdIn()}; 265 | } 266 | 267 | } //namespace http 268 | -------------------------------------------------------------------------------- /include/hot_teacup/types.h: -------------------------------------------------------------------------------- 1 | #ifndef HOT_TEACUP_TYPES_H 2 | #define HOT_TEACUP_TYPES_H 3 | 4 | #include 5 | #include 6 | 7 | namespace http { 8 | 9 | enum class ResponseMode { 10 | Http, 11 | Cgi 12 | }; 13 | 14 | enum class ResponseStatus { 15 | _100_Continue, 16 | _101_Switching_Protocol, 17 | _102_Processing, 18 | _103_Early_Hints, 19 | _200_Ok, 20 | _201_Created, 21 | _202_Accepted, 22 | _203_Non_Authoritative_Information, 23 | _204_No_Content, 24 | _205_Reset_Content, 25 | _206_Partial_Content, 26 | _207_Multi_Status, 27 | _208_Already_Reported, 28 | _226_IM_Used, 29 | _300_Multiple_Choice, 30 | _301_Moved_Permanently, 31 | _302_Found, 32 | _303_See_Other, 33 | _304_Not_Modified, 34 | _307_Temporary_Redirect, 35 | _308_Permanent_Redirect, 36 | _400_Bad_Request, 37 | _401_Unauthorized, 38 | _402_Payment_Required, 39 | _403_Forbidden, 40 | _404_Not_Found, 41 | _405_Method_Not_Allowed, 42 | _406_Not_Acceptable, 43 | _407_Proxy_Authentication_Required, 44 | _408_Request_Timeout, 45 | _409_Conflict, 46 | _410_Gone, 47 | _411_Length_Required, 48 | _412_Precondition_Failed, 49 | _413_Payload_Too_Large, 50 | _414_URI_Too_Long, 51 | _415_Unsupported_Media_Type, 52 | _416_Range_Not_Satisfiable, 53 | _417_Expectation_Failed, 54 | _418_Im_a_teapot, 55 | _421_Misdirected_Request, 56 | _422_Unprocessable_Entity, 57 | _423_Locked, 58 | _424_Failed_Dependency, 59 | _425_Too_Early, 60 | _426_Upgrade_Required, 61 | _428_Precondition_Required, 62 | _429_Too_Many_Requests, 63 | _431_Request_Header_Fields_Too_Large, 64 | _451_Unavailable_For_Legal_Reasons, 65 | _500_Internal_Server_Error, 66 | _501_Not_Implemented, 67 | _502_Bad_Gateway, 68 | _503_Service_Unavailable, 69 | _504_Gateway_Timeout, 70 | _505_HTTP_Version_Not_Supported, 71 | _506_Variant_Also_Negotiates, 72 | _507_Insufficient_Storage, 73 | _508_Loop_Detected, 74 | _510_Not_Extended, 75 | _511_Network_Authentication_Required 76 | }; 77 | 78 | enum class ContentType { 79 | Html, 80 | Xhtml, 81 | PlainText, 82 | Json 83 | }; 84 | 85 | enum class RedirectType { 86 | MovedPermanently, 87 | PermanentRedirect, 88 | Found, 89 | SeeOther, 90 | TemporaryRedirect, 91 | MultipleChoice, 92 | NotModified 93 | }; 94 | 95 | enum class RequestMethod { 96 | Get, 97 | Head, 98 | Post, 99 | Put, 100 | Delete, 101 | Connect, 102 | Options, 103 | Trace, 104 | Patch 105 | }; 106 | 107 | namespace detail { 108 | [[noreturn]] inline void ensureNotReachable() noexcept 109 | { 110 | std::terminate(); 111 | } 112 | } //namespace detail 113 | 114 | constexpr RequestMethod methodFromString(std::string_view typeStr) 115 | { 116 | if (typeStr == "GET") 117 | return RequestMethod::Get; 118 | else if (typeStr == "HEAD") 119 | return RequestMethod::Head; 120 | else if (typeStr == "POST") 121 | return RequestMethod::Post; 122 | else if (typeStr == "PUT") 123 | return RequestMethod::Put; 124 | else if (typeStr == "DELETE") 125 | return RequestMethod::Delete; 126 | else if (typeStr == "CONNECT") 127 | return RequestMethod::Connect; 128 | else if (typeStr == "OPTIONS") 129 | return RequestMethod::Options; 130 | else if (typeStr == "TRACE") 131 | return RequestMethod::Trace; 132 | else if (typeStr == "PATCH") 133 | return RequestMethod::Patch; 134 | else 135 | return {}; 136 | } 137 | 138 | constexpr const char* methodToString(RequestMethod method) 139 | { 140 | switch (method) { 141 | case RequestMethod::Get: 142 | return "GET"; 143 | case RequestMethod::Head: 144 | return "HEAD"; 145 | case RequestMethod::Post: 146 | return "POST"; 147 | case RequestMethod::Put: 148 | return "PUT"; 149 | case RequestMethod::Delete: 150 | return "DELETE"; 151 | case RequestMethod::Connect: 152 | return "CONNECT"; 153 | case RequestMethod::Options: 154 | return "OPTIONS"; 155 | case RequestMethod::Trace: 156 | return "TRACE"; 157 | case RequestMethod::Patch: 158 | return "PATCH"; 159 | } 160 | detail::ensureNotReachable(); 161 | } 162 | 163 | enum class FormFieldType { 164 | Param, 165 | File 166 | }; 167 | 168 | enum class FormType { 169 | UrlEncoded, 170 | Multipart 171 | }; 172 | 173 | enum class HeaderQuotingMode { 174 | None, 175 | HeaderValue, 176 | ParamValue, 177 | AllValues 178 | }; 179 | 180 | namespace detail { 181 | constexpr ResponseStatus redirectTypeStatus(RedirectType redirectType) 182 | { 183 | using Type = RedirectType; 184 | using Status = ResponseStatus; 185 | switch (redirectType) { 186 | case Type::MovedPermanently: 187 | return Status::_301_Moved_Permanently; 188 | case Type::PermanentRedirect: 189 | return Status::_308_Permanent_Redirect; 190 | case Type::Found: 191 | return Status::_302_Found; 192 | case Type::SeeOther: 193 | return Status::_303_See_Other; 194 | case Type::TemporaryRedirect: 195 | return Status::_307_Temporary_Redirect; 196 | case Type::MultipleChoice: 197 | return Status::_300_Multiple_Choice; 198 | case Type::NotModified: 199 | return Status::_304_Not_Modified; 200 | } 201 | detail::ensureNotReachable(); 202 | } 203 | 204 | constexpr const char* statusToString(ResponseStatus status) 205 | { 206 | using Status = ResponseStatus; 207 | switch (status) { 208 | case Status::_100_Continue: 209 | return "100 Continue"; 210 | case Status::_101_Switching_Protocol: 211 | return "101 Switching Protocol"; 212 | case Status::_102_Processing: 213 | return "102 Processing"; 214 | case Status::_103_Early_Hints: 215 | return "103 Early Hints"; 216 | case Status::_200_Ok: 217 | return "200 OK"; 218 | case Status::_201_Created: 219 | return "201 Created"; 220 | case Status::_202_Accepted: 221 | return "202 Accepted"; 222 | case Status::_203_Non_Authoritative_Information: 223 | return "203 Non-Authoritative Information"; 224 | case Status::_204_No_Content: 225 | return "204 No Content"; 226 | case Status::_205_Reset_Content: 227 | return "205 Reset Content"; 228 | case Status::_206_Partial_Content: 229 | return "206 Partial Content"; 230 | case Status::_207_Multi_Status: 231 | return "207 Multi-Status"; 232 | case Status::_208_Already_Reported: 233 | return "208 Already Reported"; 234 | case Status::_226_IM_Used: 235 | return "226 IM Used"; 236 | case Status::_300_Multiple_Choice: 237 | return "300 Multiple Choice"; 238 | case Status::_301_Moved_Permanently: 239 | return "301 Moved Permanently"; 240 | case Status::_302_Found: 241 | return "302 Found"; 242 | case Status::_303_See_Other: 243 | return "303 See Other"; 244 | case Status::_304_Not_Modified: 245 | return "304 Not Modified"; 246 | case Status::_307_Temporary_Redirect: 247 | return "307 Temporary Redirect"; 248 | case Status::_308_Permanent_Redirect: 249 | return "308 Permanent Redirect"; 250 | case Status::_400_Bad_Request: 251 | return "400 Bad Request"; 252 | case Status::_401_Unauthorized: 253 | return "401 Unauthorized"; 254 | case Status::_402_Payment_Required: 255 | return "402 Payment Required"; 256 | case Status::_403_Forbidden: 257 | return "403 Forbidden"; 258 | case Status::_404_Not_Found: 259 | return "404 Not Found"; 260 | case Status::_405_Method_Not_Allowed: 261 | return "405 Method Not Allowed"; 262 | case Status::_406_Not_Acceptable: 263 | return "406 Not Acceptable"; 264 | case Status::_407_Proxy_Authentication_Required: 265 | return "407 Proxy Authentication Required"; 266 | case Status::_408_Request_Timeout: 267 | return "408 Request Timeout"; 268 | case Status::_409_Conflict: 269 | return "409 Conflict"; 270 | case Status::_410_Gone: 271 | return "410 Gone"; 272 | case Status::_411_Length_Required: 273 | return "411 Length Required"; 274 | case Status::_412_Precondition_Failed: 275 | return "412 Precondition Failed"; 276 | case Status::_413_Payload_Too_Large: 277 | return "413 Payload Too Large"; 278 | case Status::_414_URI_Too_Long: 279 | return "414 URI Too Long"; 280 | case Status::_415_Unsupported_Media_Type: 281 | return "415 Unsupported Media Type"; 282 | case Status::_416_Range_Not_Satisfiable: 283 | return "416 Range Not Satisfiable"; 284 | case Status::_417_Expectation_Failed: 285 | return "417 Expectation Failed"; 286 | case Status::_418_Im_a_teapot: 287 | return "418 I'm a teapot"; 288 | case Status::_421_Misdirected_Request: 289 | return "421 Misdirected Request"; 290 | case Status::_422_Unprocessable_Entity: 291 | return "422 Unprocessable Entity"; 292 | case Status::_423_Locked: 293 | return "423 Locked"; 294 | case Status::_424_Failed_Dependency: 295 | return "424 Failed Dependency"; 296 | case Status::_425_Too_Early: 297 | return "425 Too Early"; 298 | case Status::_426_Upgrade_Required: 299 | return "426 Upgrade Required"; 300 | case Status::_428_Precondition_Required: 301 | return "428 Precondition Required"; 302 | case Status::_429_Too_Many_Requests: 303 | return "429 Too Many Requests"; 304 | case Status::_431_Request_Header_Fields_Too_Large: 305 | return "431 Request Header Fields Too Large"; 306 | case Status::_451_Unavailable_For_Legal_Reasons: 307 | return "451 Unavailable For Legal Reasons"; 308 | case Status::_500_Internal_Server_Error: 309 | return "500 Internal Server Error"; 310 | case Status::_501_Not_Implemented: 311 | return "501 Not Implemented"; 312 | case Status::_502_Bad_Gateway: 313 | return "502 Bad Gateway"; 314 | case Status::_503_Service_Unavailable: 315 | return "503 Service Unavailable"; 316 | case Status::_504_Gateway_Timeout: 317 | return "504 Gateway Timeout"; 318 | case Status::_505_HTTP_Version_Not_Supported: 319 | return "505 HTTP Version Not Supported"; 320 | case Status::_506_Variant_Also_Negotiates: 321 | return "506 Variant Also Negotiates"; 322 | case Status::_507_Insufficient_Storage: 323 | return "507 Insufficient Storage"; 324 | case Status::_508_Loop_Detected: 325 | return "508 Loop Detected"; 326 | case Status::_510_Not_Extended: 327 | return "510 Not Extended"; 328 | case Status::_511_Network_Authentication_Required: 329 | return "511 Network Authentication Required"; 330 | } 331 | detail::ensureNotReachable(); 332 | } 333 | 334 | constexpr ResponseStatus statusFromCode(int statusCode) 335 | { 336 | using Status = ResponseStatus; 337 | switch (statusCode) { 338 | case 100: 339 | return Status::_100_Continue; 340 | case 101: 341 | return Status::_101_Switching_Protocol; 342 | case 102: 343 | return Status::_102_Processing; 344 | case 103: 345 | return Status::_103_Early_Hints; 346 | case 200: 347 | return Status::_200_Ok; 348 | case 201: 349 | return Status::_201_Created; 350 | case 202: 351 | return Status::_202_Accepted; 352 | case 203: 353 | return Status::_203_Non_Authoritative_Information; 354 | case 204: 355 | return Status::_204_No_Content; 356 | case 205: 357 | return Status::_205_Reset_Content; 358 | case 206: 359 | return Status::_206_Partial_Content; 360 | case 207: 361 | return Status::_207_Multi_Status; 362 | case 208: 363 | return Status::_208_Already_Reported; 364 | case 226: 365 | return Status::_226_IM_Used; 366 | case 300: 367 | return Status::_300_Multiple_Choice; 368 | case 301: 369 | return Status::_301_Moved_Permanently; 370 | case 302: 371 | return Status::_302_Found; 372 | case 303: 373 | return Status::_303_See_Other; 374 | case 304: 375 | return Status::_304_Not_Modified; 376 | case 307: 377 | return Status::_307_Temporary_Redirect; 378 | case 308: 379 | return Status::_308_Permanent_Redirect; 380 | case 400: 381 | return Status::_400_Bad_Request; 382 | case 401: 383 | return Status::_401_Unauthorized; 384 | case 402: 385 | return Status::_402_Payment_Required; 386 | case 403: 387 | return Status::_403_Forbidden; 388 | case 404: 389 | return Status::_404_Not_Found; 390 | case 405: 391 | return Status::_405_Method_Not_Allowed; 392 | case 406: 393 | return Status::_406_Not_Acceptable; 394 | case 407: 395 | return Status::_407_Proxy_Authentication_Required; 396 | case 408: 397 | return Status::_408_Request_Timeout; 398 | case 409: 399 | return Status::_409_Conflict; 400 | case 410: 401 | return Status::_410_Gone; 402 | case 411: 403 | return Status::_411_Length_Required; 404 | case 412: 405 | return Status::_412_Precondition_Failed; 406 | case 413: 407 | return Status::_413_Payload_Too_Large; 408 | case 414: 409 | return Status::_414_URI_Too_Long; 410 | case 415: 411 | return Status::_415_Unsupported_Media_Type; 412 | case 416: 413 | return Status::_416_Range_Not_Satisfiable; 414 | case 417: 415 | return Status::_417_Expectation_Failed; 416 | case 418: 417 | return Status::_418_Im_a_teapot; 418 | case 421: 419 | return Status::_421_Misdirected_Request; 420 | case 422: 421 | return Status::_422_Unprocessable_Entity; 422 | case 423: 423 | return Status::_423_Locked; 424 | case 424: 425 | return Status::_424_Failed_Dependency; 426 | case 425: 427 | return Status::_425_Too_Early; 428 | case 426: 429 | return Status::_426_Upgrade_Required; 430 | case 428: 431 | return Status::_428_Precondition_Required; 432 | case 429: 433 | return Status::_429_Too_Many_Requests; 434 | case 431: 435 | return Status::_431_Request_Header_Fields_Too_Large; 436 | case 451: 437 | return Status::_451_Unavailable_For_Legal_Reasons; 438 | case 500: 439 | return Status::_500_Internal_Server_Error; 440 | case 501: 441 | return Status::_501_Not_Implemented; 442 | case 502: 443 | return Status::_502_Bad_Gateway; 444 | case 503: 445 | return Status::_503_Service_Unavailable; 446 | case 504: 447 | return Status::_504_Gateway_Timeout; 448 | case 505: 449 | return Status::_505_HTTP_Version_Not_Supported; 450 | case 506: 451 | return Status::_506_Variant_Also_Negotiates; 452 | case 507: 453 | return Status::_507_Insufficient_Storage; 454 | case 508: 455 | return Status::_508_Loop_Detected; 456 | case 510: 457 | return Status::_510_Not_Extended; 458 | case 511: 459 | return Status::_511_Network_Authentication_Required; 460 | default: 461 | return {}; 462 | } 463 | } 464 | 465 | constexpr const char* contentTypeToString(ContentType type) 466 | { 467 | switch (type) { 468 | case ContentType::Html: 469 | return "text/html"; 470 | case ContentType::Xhtml: 471 | return "application/xhtml+xml"; 472 | case ContentType::PlainText: 473 | return "text/plain"; 474 | case ContentType::Json: 475 | return "application/json"; 476 | } 477 | detail::ensureNotReachable(); 478 | } 479 | 480 | } //namespace detail 481 | 482 | } //namespace http 483 | 484 | #endif //HOT_TEACUP_TYPES_H 485 | -------------------------------------------------------------------------------- /tests/test_form.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | TEST(FormView, WithoutFileFromString) 6 | { 7 | const auto formContentType = "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx"; 8 | const auto formData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 9 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 10 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 11 | "Content-Disposition: form-data; name=\"param2\"\r\n\r\nbar \r\n" 12 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 13 | 14 | const auto form = http::formFromString(formContentType, formData); 15 | 16 | ASSERT_EQ(form.size(), 2); 17 | EXPECT_EQ(form.count("param1"), 1); 18 | EXPECT_EQ(form.at("param1").value(), "foo"); 19 | EXPECT_EQ(form.at("param1").type(), http::FormFieldType::Param); 20 | EXPECT_EQ(form.count("param2"), 1); 21 | EXPECT_EQ(form.at("param2").value(), "bar "); 22 | EXPECT_EQ(form.at("param2").type(), http::FormFieldType::Param); 23 | } 24 | 25 | TEST(Form, WithoutFileToString) 26 | { 27 | const auto expectedUrlEncodedFormData = "param1=foo¶m2=bar "; 28 | const auto expectedMultipartFormData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 29 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 30 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 31 | "Content-Disposition: form-data; name=\"param2\"\r\n\r\nbar \r\n" 32 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 33 | 34 | auto form = http::Form{}; 35 | form["param1"] = http::FormField{"foo"}; 36 | form["param2"] = http::FormField{"bar "}; 37 | 38 | EXPECT_EQ(http::urlEncodedFormToString(form), expectedUrlEncodedFormData); 39 | EXPECT_EQ(http::multipartFormToString(form, "----WebKitFormBoundaryHQl9TEASIs9QyFWx"), expectedMultipartFormData); 40 | } 41 | 42 | TEST(FormView, WithEmptyFileFromString) 43 | { 44 | const auto formContentType = "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx"; 45 | const auto formData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 46 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 47 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 48 | "Content-Disposition: form-data; name=\"param2\"\r\n\r\nbar \r\n" 49 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 50 | "Content-Disposition: form-data; name=\"param3\"; filename=\"\"\r\n" 51 | "Content-Type: application/octet-stream\r\n\r\n\r\n" 52 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 53 | 54 | const auto form = http::formFromString(formContentType, formData); 55 | 56 | ASSERT_EQ(form.size(), 3); 57 | EXPECT_EQ(form.count("param1"), 1); 58 | EXPECT_EQ(form.at("param1").value(), "foo"); 59 | EXPECT_EQ(form.at("param1").type(), http::FormFieldType::Param); 60 | EXPECT_EQ(form.count("param2"), 1); 61 | EXPECT_EQ(form.at("param2").value(), "bar "); 62 | EXPECT_EQ(form.at("param2").type(), http::FormFieldType::Param); 63 | EXPECT_EQ(form.count("param3"), 1); 64 | EXPECT_EQ(form.at("param3").value(), ""); 65 | EXPECT_EQ(form.at("param3").type(), http::FormFieldType::File); 66 | EXPECT_EQ(form.at("param3").hasFile(), false); 67 | } 68 | 69 | TEST(Form, WithEmptyFileToString) 70 | { 71 | const auto expectedUrlEncodedFormData = "param1=foo¶m2=bar ¶m3="; 72 | const auto expectedMultipartFormData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 73 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 74 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 75 | "Content-Disposition: form-data; name=\"param2\"\r\n\r\nbar \r\n" 76 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 77 | "Content-Disposition: form-data; name=\"param3\"; filename=\"\"\r\n" 78 | "Content-Type: application/octet-stream\r\n\r\n\r\n" 79 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 80 | 81 | auto form = http::Form{}; 82 | form["param1"] = http::FormField{"foo"}; 83 | form["param2"] = http::FormField{"bar "}; 84 | form["param3"] = http::FormField{"", "", "application/octet-stream"}; 85 | 86 | EXPECT_EQ(http::urlEncodedFormToString(form), expectedUrlEncodedFormData); 87 | EXPECT_EQ(http::multipartFormToString(form, "----WebKitFormBoundaryHQl9TEASIs9QyFWx"), expectedMultipartFormData); 88 | } 89 | 90 | TEST(FormView, WithFileFromString) 91 | { 92 | const auto formContentType = "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx"; 93 | const auto formData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 94 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 95 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 96 | "Content-Disposition: form-data; name=\"param2\"\r\n\r\nbar \r\n" 97 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 98 | "Content-Disposition: form-data; name=\"param3\"; filename=\"test.gif\"\r\n" 99 | "Content-Type: image/gif\r\n\r\ntest-gif-data\r\n" 100 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 101 | 102 | const auto form = http::formFromString(formContentType, formData); 103 | 104 | ASSERT_EQ(form.size(), 3); 105 | EXPECT_EQ(form.count("param1"), 1); 106 | EXPECT_EQ(form.at("param1").value(), "foo"); 107 | EXPECT_EQ(form.at("param1").type(), http::FormFieldType::Param); 108 | EXPECT_EQ(form.count("param2"), 1); 109 | EXPECT_EQ(form.at("param2").value(), "bar "); 110 | EXPECT_EQ(form.at("param2").type(), http::FormFieldType::Param); 111 | EXPECT_EQ(form.count("param3"), 1); 112 | EXPECT_EQ(form.at("param3").type(), http::FormFieldType::File); 113 | EXPECT_EQ(form.at("param3").hasFile(), true); 114 | EXPECT_EQ(form.at("param3").fileName(), "test.gif"); 115 | EXPECT_EQ(form.at("param3").fileType(), "image/gif"); 116 | EXPECT_EQ(form.at("param3").value(), "test-gif-data"); 117 | } 118 | 119 | TEST(FormView, FormFromFormView) 120 | { 121 | const auto formContentType = "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx"; 122 | const auto formData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 123 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 124 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 125 | "Content-Disposition: form-data; name=\"param2\"\r\n\r\nbar \r\n" 126 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 127 | "Content-Disposition: form-data; name=\"param3\"; filename=\"test.gif\"\r\n" 128 | "Content-Type: image/gif\r\n\r\ntest-gif-data\r\n" 129 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 130 | 131 | const auto formView = http::formFromString(formContentType, formData); 132 | const auto form = http::makeForm(formView); 133 | ASSERT_EQ(form.size(), 3); 134 | EXPECT_EQ(form.count("param1"), 1); 135 | EXPECT_EQ(form.at("param1").value(), "foo"); 136 | EXPECT_EQ(form.at("param1").type(), http::FormFieldType::Param); 137 | EXPECT_EQ(form.count("param2"), 1); 138 | EXPECT_EQ(form.at("param2").value(), "bar "); 139 | EXPECT_EQ(form.at("param2").type(), http::FormFieldType::Param); 140 | EXPECT_EQ(form.count("param3"), 1); 141 | EXPECT_EQ(form.at("param3").type(), http::FormFieldType::File); 142 | EXPECT_EQ(form.at("param3").hasFile(), true); 143 | EXPECT_EQ(form.at("param3").fileName(), "test.gif"); 144 | EXPECT_EQ(form.at("param3").fileType(), "image/gif"); 145 | EXPECT_EQ(form.at("param3").value(), "test-gif-data"); 146 | } 147 | 148 | TEST(Form, WithFileToString) 149 | { 150 | const auto expectedUrlEncodedFormData = "param1=foo¶m2=bar ¶m3=test.gif"; 151 | const auto expectedMultipartFormData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 152 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 153 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 154 | "Content-Disposition: form-data; name=\"param2\"\r\n\r\nbar \r\n" 155 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 156 | "Content-Disposition: form-data; name=\"param3\"; filename=\"test.gif\"\r\n" 157 | "Content-Type: image/gif\r\n\r\ntest-gif-data\r\n" 158 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 159 | 160 | auto form = http::Form{}; 161 | form["param1"] = http::FormField{"foo"}; 162 | form["param2"] = http::FormField{"bar "}; 163 | form["param3"] = http::FormField{"test-gif-data", "test.gif", "image/gif"}; 164 | 165 | EXPECT_EQ(http::urlEncodedFormToString(form), expectedUrlEncodedFormData); 166 | EXPECT_EQ(http::multipartFormToString(form, "----WebKitFormBoundaryHQl9TEASIs9QyFWx"), expectedMultipartFormData); 167 | } 168 | 169 | TEST(Form, WithFileWithoutFileTypeToString) 170 | { 171 | const auto expectedUrlEncodedFormData = "param1=foo¶m2=bar ¶m3=test.gif"; 172 | const auto expectedMultipartFormData = 173 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 174 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 175 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 176 | "Content-Disposition: form-data; name=\"param2\"\r\n\r\nbar \r\n" 177 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 178 | "Content-Disposition: form-data; name=\"param3\"; filename=\"test.gif\"\r\n\r\ntest-gif-data\r\n" 179 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 180 | 181 | auto form = http::Form{}; 182 | form["param1"] = http::FormField{"foo"}; 183 | form["param2"] = http::FormField{"bar "}; 184 | form["param3"] = http::FormField{"test-gif-data", "test.gif"}; 185 | 186 | EXPECT_EQ(http::urlEncodedFormToString(form), expectedUrlEncodedFormData); 187 | EXPECT_EQ(http::multipartFormToString(form, "----WebKitFormBoundaryHQl9TEASIs9QyFWx"), expectedMultipartFormData); 188 | } 189 | 190 | TEST(FormView, WithoutNameFromString) 191 | { 192 | { 193 | const auto formContentType = "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx"; 194 | const auto formData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 195 | "Content-Disposition: form-data;\r\n\r\nfoo\r\n" 196 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 197 | 198 | const auto form = http::formFromString(formContentType, formData); 199 | ASSERT_EQ(form.size(), 0); 200 | } 201 | { 202 | const auto formContentType = "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx"; 203 | const auto formData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 204 | "Content-Disposition: form-data; =\"param1\"\r\n\r\nfoo\r\n" 205 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 206 | 207 | const auto form = http::formFromString(formContentType, formData); 208 | ASSERT_EQ(form.size(), 0); 209 | } 210 | } 211 | 212 | TEST(FormView, WithoutBoundaryFromString) 213 | { 214 | { 215 | const auto formContentType = "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx"; 216 | const auto formData = "------WebKitFormBoundaryHello\r\n" 217 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 218 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n"; 219 | const auto form = http::formFromString(formContentType, formData); 220 | ASSERT_EQ(form.size(), 0); 221 | } 222 | { 223 | const auto formContentType = "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx"; 224 | const auto formData = "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 225 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 226 | const auto form = http::formFromString(formContentType, formData); 227 | ASSERT_EQ(form.size(), 0); 228 | } 229 | { 230 | const auto formContentType = "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx"; 231 | const auto formData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx" 232 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 233 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 234 | const auto form = http::formFromString(formContentType, formData); 235 | ASSERT_EQ(form.size(), 0); 236 | } 237 | { 238 | const auto formContentType = "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx"; 239 | const auto formData = ""; 240 | const auto form = http::formFromString(formContentType, formData); 241 | ASSERT_EQ(form.size(), 0); 242 | } 243 | } 244 | 245 | TEST(FormView, UrlEncodedFromString) 246 | { 247 | const auto formContentType = "application/x-www-form-urlencoded"; 248 | const auto formData = "param1=foo¶m2=bar&flag¶m4="; 249 | 250 | const auto form = http::formFromString(formContentType, formData); 251 | ASSERT_EQ(form.size(), 3); 252 | EXPECT_EQ(form.count("param1"), 1); 253 | EXPECT_EQ(form.at("param1").value(), "foo"); 254 | EXPECT_EQ(form.count("param2"), 1); 255 | EXPECT_EQ(form.at("param2").value(), "bar"); 256 | EXPECT_EQ(form.count("param4"), 1); 257 | EXPECT_EQ(form.at("param4").value(), ""); 258 | } 259 | 260 | TEST(FormView, FormFromUrlEncodedFormView) 261 | { 262 | const auto formContentType = "application/x-www-form-urlencoded"; 263 | const auto formData = "param1=foo¶m2=bar&flag¶m4="; 264 | 265 | const auto formView = http::formFromString(formContentType, formData); 266 | const auto form = http::makeForm(formView); 267 | ASSERT_EQ(form.size(), 3); 268 | EXPECT_EQ(form.count("param1"), 1); 269 | EXPECT_EQ(form.at("param1").value(), "foo"); 270 | EXPECT_EQ(form.count("param2"), 1); 271 | EXPECT_EQ(form.at("param2").value(), "bar"); 272 | EXPECT_EQ(form.count("param4"), 1); 273 | EXPECT_EQ(form.at("param4").value(), ""); 274 | } 275 | 276 | TEST(FormView, UrlEncodedWithoutNameFromString) 277 | { 278 | const auto formContentType = "application/x-www-form-urlencoded"; 279 | { 280 | const auto formData = "="; 281 | const auto form = http::formFromString(formContentType, formData); 282 | 283 | ASSERT_EQ(form.size(), 0); 284 | } 285 | { 286 | const auto formData = ""; 287 | const auto form = http::formFromString(formContentType, formData); 288 | 289 | ASSERT_EQ(form.size(), 0); 290 | } 291 | { 292 | const auto formData = " \t =foo"; 293 | const auto form = http::formFromString(formContentType, formData); 294 | 295 | ASSERT_EQ(form.size(), 0); 296 | } 297 | { 298 | const auto formData = " \t =foo¶m2=bar"; 299 | const auto form = http::formFromString(formContentType, formData); 300 | 301 | ASSERT_EQ(form.size(), 1); 302 | EXPECT_EQ(form.count("param2"), 1); 303 | EXPECT_EQ(form.at("param2").value(), "bar"); 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /tests/test_request.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | TEST(RequestView, RequestMethodParam) 8 | { 9 | auto testRequestType = [&](std::string_view typeStr, http::RequestMethod expectedMethod) 10 | { 11 | auto request = http::RequestView{typeStr, {}, {}, {}, {}, {}, {}, {}}; 12 | EXPECT_EQ(request.method(), expectedMethod); 13 | }; 14 | 15 | testRequestType("GET", http::RequestMethod::Get); 16 | testRequestType("POST", http::RequestMethod::Post); 17 | testRequestType("PUT", http::RequestMethod::Put); 18 | testRequestType("HEAD", http::RequestMethod::Head); 19 | testRequestType("PATCH", http::RequestMethod::Patch); 20 | testRequestType("TRACE", http::RequestMethod::Trace); 21 | testRequestType("DELETE", http::RequestMethod::Delete); 22 | testRequestType("CONNECT", http::RequestMethod::Connect); 23 | testRequestType("OPTIONS", http::RequestMethod::Options); 24 | } 25 | 26 | TEST(RequestView, RequestFromRequestViewWithMethodParam) 27 | { 28 | auto requestView = http::RequestView{"GET", {}, {}, {}, {}, {}, {}, {}}; 29 | auto request = http::Request{requestView}; 30 | EXPECT_EQ(request.method(), http::RequestMethod::Get); 31 | } 32 | 33 | TEST(RequestView, RequestIpAddress) 34 | { 35 | auto request = http::RequestView{{}, "127.0.0.1", {}, {}, {}, {}, {}, {}}; 36 | EXPECT_EQ(request.ipAddress(), "127.0.0.1"); 37 | } 38 | 39 | TEST(RequestView, RequestDomain) 40 | { 41 | auto request = http::RequestView{{}, {}, "localhost", {}, {}, {}, {}, {}}; 42 | EXPECT_EQ(request.domainName(), "localhost"); 43 | } 44 | 45 | TEST(RequestView, RequestPath) 46 | { 47 | auto request = http::RequestView{{}, {}, {}, "/test", {}, {}, {}, {}}; 48 | EXPECT_EQ(request.path(), "/test"); 49 | } 50 | 51 | TEST(RequestView, RequestFromRequestViewWithPath) 52 | { 53 | auto requestView = http::RequestView{{}, {}, {}, "/test", {}, {}, {}, {}}; 54 | auto request = http::Request{requestView}; 55 | EXPECT_EQ(request.path(), "/test"); 56 | } 57 | 58 | TEST(RequestView, Queries) 59 | { 60 | const auto request = http::RequestView{"GET", {}, {}, {}, "param1=foo¶m2=bar", {}, {}, {}}; 61 | const auto expectedQueries = std::vector{{"param1", "foo"}, {"param2", "bar"}}; 62 | EXPECT_EQ(request.queries(), expectedQueries); 63 | EXPECT_TRUE(request.hasQuery("param1")); 64 | EXPECT_EQ(request.query("param1"), "foo"); 65 | EXPECT_TRUE(request.hasQuery("param2")); 66 | EXPECT_EQ(request.query("param2"), "bar"); 67 | 68 | EXPECT_FALSE(request.hasQuery("param3")); 69 | EXPECT_EQ(request.query("param3"), ""); 70 | } 71 | 72 | TEST(RequestView, RequestFromRequestViewWithQueries) 73 | { 74 | const auto requestView = http::RequestView{"GET", {}, {}, {}, "param1=foo¶m2=bar", {}, {}, {}}; 75 | const auto request = http::Request{requestView}; 76 | const auto expectedQueries = std::vector{{"param1", "foo"}, {"param2", "bar"}}; 77 | EXPECT_EQ(request.queries(), expectedQueries); 78 | EXPECT_TRUE(request.hasQuery("param1")); 79 | EXPECT_EQ(request.query("param1"), "foo"); 80 | EXPECT_TRUE(request.hasQuery("param2")); 81 | EXPECT_EQ(request.query("param2"), "bar"); 82 | 83 | EXPECT_FALSE(request.hasQuery("param3")); 84 | EXPECT_EQ(request.query("param3"), ""); 85 | } 86 | 87 | TEST(Request, Queries) 88 | { 89 | const auto expectedQueries = std::vector{{"param1", "foo"}, {"param2", "bar"}}; 90 | const auto request = http::Request{http::RequestMethod::Get, "/", expectedQueries, {}, {}}; 91 | EXPECT_EQ(request.queries(), expectedQueries); 92 | EXPECT_TRUE(request.hasQuery("param1")); 93 | EXPECT_EQ(request.query("param1"), "foo"); 94 | EXPECT_TRUE(request.hasQuery("param2")); 95 | EXPECT_EQ(request.query("param2"), "bar"); 96 | 97 | EXPECT_FALSE(request.hasQuery("param3")); 98 | EXPECT_EQ(request.query("param3"), ""); 99 | } 100 | 101 | TEST(RequestView, Cookies) 102 | { 103 | const auto request = http::RequestView{"GET", {}, {}, {}, {}, "param1=foo;param2=bar", {}, {}}; 104 | const auto expectedCookieList = std::vector{"param1", "param2"}; 105 | EXPECT_TRUE(request.hasCookie("param1")); 106 | EXPECT_EQ(request.cookie("param1"), "foo"); 107 | EXPECT_TRUE(request.hasCookie("param2")); 108 | EXPECT_EQ(request.cookie("param2"), "bar"); 109 | 110 | EXPECT_FALSE(request.hasCookie("param3")); 111 | EXPECT_EQ(request.cookie("param3"), ""); 112 | } 113 | 114 | TEST(RequestView, RequestFromRequestViewWithCookies) 115 | { 116 | const auto requestView = http::RequestView{"GET", {}, {}, {}, {}, "param1=foo;param2=bar", {}, {}}; 117 | const auto request = http::Request{requestView}; 118 | const auto expectedCookieList = std::vector{"param1", "param2"}; 119 | EXPECT_TRUE(request.hasCookie("param1")); 120 | EXPECT_EQ(request.cookie("param1"), "foo"); 121 | EXPECT_TRUE(request.hasCookie("param2")); 122 | EXPECT_EQ(request.cookie("param2"), "bar"); 123 | 124 | EXPECT_FALSE(request.hasCookie("param3")); 125 | EXPECT_EQ(request.cookie("param3"), ""); 126 | } 127 | 128 | TEST(Request, Cookies) 129 | { 130 | const auto expectedCookies = std::vector{{"param1", "foo"}, {"param2", "bar"}}; 131 | const auto request = http::Request{http::RequestMethod::Get, "/", {}, expectedCookies}; 132 | EXPECT_TRUE(request.hasCookie("param1")); 133 | EXPECT_EQ(request.cookie("param1"), "foo"); 134 | EXPECT_TRUE(request.hasCookie("param2")); 135 | EXPECT_EQ(request.cookie("param2"), "bar"); 136 | 137 | EXPECT_FALSE(request.hasCookie("param3")); 138 | EXPECT_EQ(request.cookie("param3"), ""); 139 | } 140 | 141 | TEST(RequestView, MultipartFormWithFile) 142 | { 143 | const auto formData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 144 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 145 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 146 | "Content-Disposition: form-data; name=\"param2\"\r\n\r\nbar \r\n" 147 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 148 | "Content-Disposition: form-data; name=\"param3\"; filename=\"test.gif\"\r\n" 149 | "Content-Type: image/gif\r\n\r\ntest-gif-data\r\n" 150 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 151 | 152 | const auto request = http::RequestView{ 153 | "GET", 154 | {}, 155 | {}, 156 | {}, 157 | {}, 158 | {}, 159 | "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx", 160 | formData}; 161 | const auto expectedFormFieldList = std::vector{"param1", "param2"}; 162 | EXPECT_EQ(request.formFieldList(), expectedFormFieldList); 163 | EXPECT_TRUE(request.hasFormField("param1")); 164 | EXPECT_EQ(request.formField("param1"), "foo"); 165 | EXPECT_TRUE(request.hasFormField("param2")); 166 | EXPECT_EQ(request.formField("param2"), "bar "); 167 | 168 | EXPECT_TRUE(request.hasFiles()); 169 | EXPECT_FALSE(request.hasFormField("param3")); 170 | EXPECT_EQ(request.formField("param3"), ""); 171 | EXPECT_TRUE(request.hasFile("param3")); 172 | EXPECT_EQ(request.fileData("param3"), "test-gif-data"); 173 | EXPECT_EQ(request.fileName("param3"), "test.gif"); 174 | EXPECT_EQ(request.fileType("param3"), "image/gif"); 175 | } 176 | 177 | TEST(RequestView, FormFromMultipartFormViewWithFile) 178 | { 179 | const auto formData = "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 180 | "Content-Disposition: form-data; name=\"param1\"\r\n\r\nfoo\r\n" 181 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 182 | "Content-Disposition: form-data; name=\"param2\"\r\n\r\nbar \r\n" 183 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx\r\n" 184 | "Content-Disposition: form-data; name=\"param3\"; filename=\"test.gif\"\r\n" 185 | "Content-Type: image/gif\r\n\r\ntest-gif-data\r\n" 186 | "------WebKitFormBoundaryHQl9TEASIs9QyFWx--\r\n"; 187 | 188 | const auto requestView = http::RequestView{ 189 | "GET", 190 | {}, 191 | {}, 192 | {}, 193 | {}, 194 | {}, 195 | "multipart/form-data; boundary=----WebKitFormBoundaryHQl9TEASIs9QyFWx", 196 | formData}; 197 | const auto request = http::Request{requestView}; 198 | const auto expectedFormFieldList = std::vector{"param1", "param2"}; 199 | EXPECT_EQ(request.formFieldList(), expectedFormFieldList); 200 | EXPECT_TRUE(request.hasFormField("param1")); 201 | EXPECT_EQ(request.formField("param1"), "foo"); 202 | EXPECT_TRUE(request.hasFormField("param2")); 203 | EXPECT_EQ(request.formField("param2"), "bar "); 204 | 205 | EXPECT_TRUE(request.hasFiles()); 206 | EXPECT_FALSE(request.hasFormField("param3")); 207 | EXPECT_EQ(request.formField("param3"), ""); 208 | EXPECT_TRUE(request.hasFile("param3")); 209 | EXPECT_EQ(request.fileData("param3"), "test-gif-data"); 210 | EXPECT_EQ(request.fileName("param3"), "test.gif"); 211 | EXPECT_EQ(request.fileType("param3"), "image/gif"); 212 | } 213 | 214 | TEST(Request, MultipartFormWithFile) 215 | { 216 | auto form = http::Form{ 217 | {"param1", http::FormField{"foo"}}, 218 | {"param2", http::FormField{"bar "}}, 219 | {"param3", http::FormField{"test-gif-data", "test.gif", "image/gif"}}}; 220 | const auto request = http::Request{http::RequestMethod::Post, "/", {}, {}, form}; 221 | 222 | const auto expectedFormFieldList = std::vector{"param1", "param2"}; 223 | EXPECT_EQ(request.formFieldList(), expectedFormFieldList); 224 | EXPECT_TRUE(request.hasFormField("param1")); 225 | EXPECT_EQ(request.formField("param1"), "foo"); 226 | EXPECT_TRUE(request.hasFormField("param2")); 227 | EXPECT_EQ(request.formField("param2"), "bar "); 228 | 229 | EXPECT_TRUE(request.hasFiles()); 230 | EXPECT_FALSE(request.hasFormField("param3")); 231 | EXPECT_EQ(request.formField("param3"), ""); 232 | EXPECT_TRUE(request.hasFile("param3")); 233 | EXPECT_EQ(request.fileData("param3"), "test-gif-data"); 234 | EXPECT_EQ(request.fileName("param3"), "test.gif"); 235 | EXPECT_EQ(request.fileType("param3"), "image/gif"); 236 | } 237 | 238 | TEST(RequestView, UrlEncodedForm) 239 | { 240 | const auto formData = "param1=foo¶m2=bar&flag¶m4="; 241 | 242 | const auto request = http::RequestView{"GET", {}, {}, {}, {}, {}, "application/x-www-form-urlencoded", formData}; 243 | auto expectedFormFieldList = std::vector{"param1", "param2", "param4"}; 244 | EXPECT_EQ(request.formFieldList(), expectedFormFieldList); 245 | EXPECT_TRUE(request.hasFormField("param1")); 246 | EXPECT_EQ(request.formField("param1"), "foo"); 247 | EXPECT_TRUE(request.hasFormField("param2")); 248 | EXPECT_EQ(request.formField("param2"), "bar"); 249 | EXPECT_TRUE(request.hasFormField("param4")); 250 | EXPECT_EQ(request.formField("param4"), ""); 251 | 252 | EXPECT_FALSE(request.hasFiles()); 253 | EXPECT_FALSE(request.hasFormField("param3")); 254 | EXPECT_EQ(request.formField("param3"), ""); 255 | EXPECT_FALSE(request.hasFile("param3")); 256 | EXPECT_EQ(request.fileData("param3"), ""); 257 | EXPECT_EQ(request.fileName("param3"), ""); 258 | EXPECT_EQ(request.fileType("param3"), ""); 259 | } 260 | 261 | TEST(Request, UrlEncodedForm) 262 | { 263 | const auto form = http::Form{ 264 | {"param1", http::FormField{"foo"}}, 265 | {"param2", http::FormField{"bar"}}, 266 | {"param4", http::FormField{}}}; 267 | 268 | const auto request = http::Request{http::RequestMethod::Post, "/", {}, {}, form}; 269 | 270 | auto expectedFormFieldList = std::vector{"param1", "param2", "param4"}; 271 | EXPECT_EQ(request.formFieldList(), expectedFormFieldList); 272 | EXPECT_TRUE(request.hasFormField("param1")); 273 | EXPECT_EQ(request.formField("param1"), "foo"); 274 | EXPECT_TRUE(request.hasFormField("param2")); 275 | EXPECT_EQ(request.formField("param2"), "bar"); 276 | EXPECT_TRUE(request.hasFormField("param4")); 277 | EXPECT_EQ(request.formField("param4"), ""); 278 | 279 | EXPECT_FALSE(request.hasFiles()); 280 | EXPECT_FALSE(request.hasFormField("param3")); 281 | EXPECT_EQ(request.formField("param3"), ""); 282 | EXPECT_FALSE(request.hasFile("param3")); 283 | EXPECT_EQ(request.fileData("param3"), ""); 284 | EXPECT_EQ(request.fileName("param3"), ""); 285 | EXPECT_EQ(request.fileType("param3"), ""); 286 | } 287 | 288 | TEST(Request, ToFcgiDataWithPathOnly) 289 | { 290 | const auto request = http::Request{http::RequestMethod::Get, "/"}; 291 | const auto fcgiData = request.toFcgiData(http::FormType::UrlEncoded); 292 | 293 | EXPECT_EQ(fcgiData.params.size(), 2); 294 | EXPECT_TRUE(fcgiData.stdIn.empty()); 295 | ASSERT_TRUE(fcgiData.params.count("REQUEST_METHOD")); 296 | EXPECT_EQ(fcgiData.params.at("REQUEST_METHOD"), "GET"); 297 | ASSERT_TRUE(fcgiData.params.count("REQUEST_URI")); 298 | EXPECT_EQ(fcgiData.params.at("REQUEST_URI"), "/"); 299 | } 300 | 301 | TEST(Request, ToFcgiDataWithPathOnlyMultipart) 302 | { 303 | const auto request = http::Request{http::RequestMethod::Get, "/"}; 304 | const auto fcgiData = request.toFcgiData(http::FormType::Multipart); 305 | 306 | EXPECT_EQ(fcgiData.params.size(), 2); 307 | EXPECT_TRUE(fcgiData.stdIn.empty()); 308 | ASSERT_TRUE(fcgiData.params.count("REQUEST_METHOD")); 309 | EXPECT_EQ(fcgiData.params.at("REQUEST_METHOD"), "GET"); 310 | ASSERT_TRUE(fcgiData.params.count("REQUEST_URI")); 311 | EXPECT_EQ(fcgiData.params.at("REQUEST_URI"), "/"); 312 | } 313 | 314 | TEST(Request, ToFcgiDataWithQueries) 315 | { 316 | const auto request = http::Request{http::RequestMethod::Get, "/", {{"id", "100"}}}; 317 | const auto fcgiData = request.toFcgiData(http::FormType::UrlEncoded); 318 | 319 | EXPECT_EQ(fcgiData.params.size(), 3); 320 | EXPECT_TRUE(fcgiData.stdIn.empty()); 321 | ASSERT_TRUE(fcgiData.params.count("REQUEST_METHOD")); 322 | EXPECT_EQ(fcgiData.params.at("REQUEST_METHOD"), "GET"); 323 | ASSERT_TRUE(fcgiData.params.count("REQUEST_URI")); 324 | EXPECT_EQ(fcgiData.params.at("REQUEST_URI"), "/"); 325 | ASSERT_TRUE(fcgiData.params.count("QUERY_STRING")); 326 | EXPECT_EQ(fcgiData.params.at("QUERY_STRING"), "id=100"); 327 | } 328 | 329 | TEST(Request, ToFcgiDataWithCookies) 330 | { 331 | const auto request = http::Request{http::RequestMethod::Get, "/", {}, {{"id", "100"}}}; 332 | const auto fcgiData = request.toFcgiData(http::FormType::UrlEncoded); 333 | 334 | EXPECT_EQ(fcgiData.params.size(), 3); 335 | EXPECT_TRUE(fcgiData.stdIn.empty()); 336 | ASSERT_TRUE(fcgiData.params.count("REQUEST_METHOD")); 337 | EXPECT_EQ(fcgiData.params.at("REQUEST_METHOD"), "GET"); 338 | ASSERT_TRUE(fcgiData.params.count("REQUEST_URI")); 339 | EXPECT_EQ(fcgiData.params.at("REQUEST_URI"), "/"); 340 | ASSERT_TRUE(fcgiData.params.count("HTTP_COOKIE")); 341 | EXPECT_EQ(fcgiData.params.at("HTTP_COOKIE"), "id=100"); 342 | } 343 | 344 | TEST(Request, ToFcgiDataWithUrlEncodedForm) 345 | { 346 | const auto form = http::Form{{"id", http::FormField{"100"}}, {"name", http::FormField{"foo"}}}; 347 | const auto request = http::Request{http::RequestMethod::Get, "/", {}, {}, form}; 348 | const auto fcgiData = request.toFcgiData(http::FormType::UrlEncoded); 349 | 350 | EXPECT_FALSE(fcgiData.stdIn.empty()); 351 | EXPECT_EQ(fcgiData.stdIn, "id=100&name=foo"); 352 | EXPECT_EQ(fcgiData.params.size(), 3); 353 | ASSERT_TRUE(fcgiData.params.count("REQUEST_METHOD")); 354 | EXPECT_EQ(fcgiData.params.at("REQUEST_METHOD"), "GET"); 355 | ASSERT_TRUE(fcgiData.params.count("REQUEST_URI")); 356 | EXPECT_EQ(fcgiData.params.at("REQUEST_URI"), "/"); 357 | ASSERT_TRUE(fcgiData.params.count("CONTENT_TYPE")); 358 | EXPECT_EQ(fcgiData.params.at("CONTENT_TYPE"), "application/x-www-form-urlencoded"); 359 | } 360 | 361 | TEST(Request, ToFcgiDataWithMultipartForm) 362 | { 363 | const auto form = http::Form{{"id", http::FormField{"100"}}, {"name", http::FormField{"foo"}}}; 364 | const auto request = http::Request{http::RequestMethod::Get, "/", {}, {}, form}; 365 | const auto fcgiData = request.toFcgiData(http::FormType::Multipart); 366 | const auto expectedFormData = 367 | std::string{"------asyncgiFormBoundary\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\n100\r\n" 368 | "------asyncgiFormBoundary\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nfoo\r\n" 369 | "------asyncgiFormBoundary--\r\n"}; 370 | 371 | EXPECT_EQ(fcgiData.stdIn, expectedFormData); 372 | EXPECT_EQ(fcgiData.params.size(), 3); 373 | ASSERT_TRUE(fcgiData.params.count("REQUEST_METHOD")); 374 | EXPECT_EQ(fcgiData.params.at("REQUEST_METHOD"), "GET"); 375 | ASSERT_TRUE(fcgiData.params.count("REQUEST_URI")); 376 | EXPECT_EQ(fcgiData.params.at("REQUEST_URI"), "/"); 377 | ASSERT_TRUE(fcgiData.params.count("CONTENT_TYPE")); 378 | EXPECT_EQ(fcgiData.params.at("CONTENT_TYPE"), "multipart/form-data; boundary=----asyncgiFormBoundary"); 379 | } 380 | -------------------------------------------------------------------------------- /tests/test_response.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace { 7 | 8 | void testResponseWithCookies(const http::Response& testResponse, const std::string& expectedResponse) 9 | { 10 | { 11 | auto response = testResponse; 12 | response.addCookie(http::Cookie{"name", "foo"}); 13 | response.addCookie(http::Cookie{"age", "77"}); 14 | EXPECT_EQ(response.data(), expectedResponse); 15 | } 16 | { 17 | auto response = testResponse; 18 | response.addCookies({http::Cookie{"name", "foo"}, http::Cookie{"age", "77"}}); 19 | EXPECT_EQ(response.data(), expectedResponse); 20 | } 21 | } 22 | 23 | const auto cookiesResponsePart = std::string{"Set-Cookie: name=foo\r\n" 24 | "Set-Cookie: age=77\r\n"}; 25 | 26 | void testResponseWithHeaders(const http::Response& testResponse, const std::string& expectedResponse) 27 | { 28 | { 29 | auto response = testResponse; 30 | response.addHeader(http::Header{"Host", "HotTeacup"}); 31 | response.addHeader(http::Header{"User-Agent", "gtest"}); 32 | EXPECT_EQ(response.data(), expectedResponse); 33 | } 34 | { 35 | auto response = testResponse; 36 | response.addHeaders({http::Header{"Host", "HotTeacup"}, http::Header{"User-Agent", "gtest"}}); 37 | EXPECT_EQ(response.data(), expectedResponse); 38 | } 39 | } 40 | 41 | const auto headersResponsePart = std::string{"Host: HotTeacup\r\n" 42 | "User-Agent: gtest\r\n"}; 43 | 44 | void testResponseWithCookiesAndHeaders(const http::Response& testResponse, const std::string& expectedResponse) 45 | { 46 | auto response = testResponse; 47 | response.addCookie(http::Cookie{"name", "foo"}); 48 | response.addHeader(http::Header{"Host", "HotTeacup"}); 49 | response.addCookie(http::Cookie{"age", "77"}); 50 | EXPECT_EQ(response.data(), expectedResponse); 51 | } 52 | 53 | const auto cookiesAndHeadersResponsePart = std::string{"Host: HotTeacup\r\n" 54 | "Set-Cookie: name=foo\r\n" 55 | "Set-Cookie: age=77\r\n"}; 56 | 57 | } //namespace 58 | 59 | TEST(Response, Status) 60 | { 61 | using Status = http::ResponseStatus; 62 | auto response = http::Response{Status::_100_Continue}; 63 | EXPECT_EQ(response.data(), "HTTP/1.1 100 Continue\r\n\r\n"); 64 | response = http::Response{Status::_101_Switching_Protocol}; 65 | EXPECT_EQ(response.data(), "HTTP/1.1 101 Switching Protocol\r\n\r\n"); 66 | response = http::Response{Status::_102_Processing}; 67 | EXPECT_EQ(response.data(), "HTTP/1.1 102 Processing\r\n\r\n"); 68 | response = http::Response{Status::_103_Early_Hints}; 69 | EXPECT_EQ(response.data(), "HTTP/1.1 103 Early Hints\r\n\r\n"); 70 | response = http::Response{Status::_200_Ok}; 71 | EXPECT_EQ(response.data(), "HTTP/1.1 200 OK\r\n\r\n"); 72 | response = http::Response{Status::_201_Created}; 73 | EXPECT_EQ(response.data(), "HTTP/1.1 201 Created\r\n\r\n"); 74 | response = http::Response{Status::_202_Accepted}; 75 | EXPECT_EQ(response.data(), "HTTP/1.1 202 Accepted\r\n\r\n"); 76 | response = http::Response{Status::_203_Non_Authoritative_Information}; 77 | EXPECT_EQ(response.data(), "HTTP/1.1 203 Non-Authoritative Information\r\n\r\n"); 78 | response = http::Response{Status::_204_No_Content}; 79 | EXPECT_EQ(response.data(), "HTTP/1.1 204 No Content\r\n\r\n"); 80 | response = http::Response{Status::_205_Reset_Content}; 81 | EXPECT_EQ(response.data(), "HTTP/1.1 205 Reset Content\r\n\r\n"); 82 | response = http::Response{Status::_206_Partial_Content}; 83 | EXPECT_EQ(response.data(), "HTTP/1.1 206 Partial Content\r\n\r\n"); 84 | response = http::Response{Status::_207_Multi_Status}; 85 | EXPECT_EQ(response.data(), "HTTP/1.1 207 Multi-Status\r\n\r\n"); 86 | response = http::Response{Status::_208_Already_Reported}; 87 | EXPECT_EQ(response.data(), "HTTP/1.1 208 Already Reported\r\n\r\n"); 88 | response = http::Response{Status::_226_IM_Used}; 89 | EXPECT_EQ(response.data(), "HTTP/1.1 226 IM Used\r\n\r\n"); 90 | response = http::Response{Status::_300_Multiple_Choice}; 91 | EXPECT_EQ(response.data(), "HTTP/1.1 300 Multiple Choice\r\n\r\n"); 92 | response = http::Response{Status::_301_Moved_Permanently}; 93 | EXPECT_EQ(response.data(), "HTTP/1.1 301 Moved Permanently\r\n\r\n"); 94 | response = http::Response{Status::_302_Found}; 95 | EXPECT_EQ(response.data(), "HTTP/1.1 302 Found\r\n\r\n"); 96 | response = http::Response{Status::_303_See_Other}; 97 | EXPECT_EQ(response.data(), "HTTP/1.1 303 See Other\r\n\r\n"); 98 | response = http::Response{Status::_304_Not_Modified}; 99 | EXPECT_EQ(response.data(), "HTTP/1.1 304 Not Modified\r\n\r\n"); 100 | response = http::Response{Status::_307_Temporary_Redirect}; 101 | EXPECT_EQ(response.data(), "HTTP/1.1 307 Temporary Redirect\r\n\r\n"); 102 | response = http::Response{Status::_308_Permanent_Redirect}; 103 | EXPECT_EQ(response.data(), "HTTP/1.1 308 Permanent Redirect\r\n\r\n"); 104 | response = http::Response{Status::_400_Bad_Request}; 105 | EXPECT_EQ(response.data(), "HTTP/1.1 400 Bad Request\r\n\r\n"); 106 | response = http::Response{Status::_401_Unauthorized}; 107 | EXPECT_EQ(response.data(), "HTTP/1.1 401 Unauthorized\r\n\r\n"); 108 | response = http::Response{Status::_402_Payment_Required}; 109 | EXPECT_EQ(response.data(), "HTTP/1.1 402 Payment Required\r\n\r\n"); 110 | response = http::Response{Status::_403_Forbidden}; 111 | EXPECT_EQ(response.data(), "HTTP/1.1 403 Forbidden\r\n\r\n"); 112 | response = http::Response{Status::_404_Not_Found}; 113 | EXPECT_EQ(response.data(), "HTTP/1.1 404 Not Found\r\n\r\n"); 114 | response = http::Response{Status::_405_Method_Not_Allowed}; 115 | EXPECT_EQ(response.data(), "HTTP/1.1 405 Method Not Allowed\r\n\r\n"); 116 | response = http::Response{Status::_406_Not_Acceptable}; 117 | EXPECT_EQ(response.data(), "HTTP/1.1 406 Not Acceptable\r\n\r\n"); 118 | response = http::Response{Status::_407_Proxy_Authentication_Required}; 119 | EXPECT_EQ(response.data(), "HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"); 120 | response = http::Response{Status::_408_Request_Timeout}; 121 | EXPECT_EQ(response.data(), "HTTP/1.1 408 Request Timeout\r\n\r\n"); 122 | response = http::Response{Status::_409_Conflict}; 123 | EXPECT_EQ(response.data(), "HTTP/1.1 409 Conflict\r\n\r\n"); 124 | response = http::Response{Status::_410_Gone}; 125 | EXPECT_EQ(response.data(), "HTTP/1.1 410 Gone\r\n\r\n"); 126 | response = http::Response{Status::_411_Length_Required}; 127 | EXPECT_EQ(response.data(), "HTTP/1.1 411 Length Required\r\n\r\n"); 128 | response = http::Response{Status::_412_Precondition_Failed}; 129 | EXPECT_EQ(response.data(), "HTTP/1.1 412 Precondition Failed\r\n\r\n"); 130 | response = http::Response{Status::_413_Payload_Too_Large}; 131 | EXPECT_EQ(response.data(), "HTTP/1.1 413 Payload Too Large\r\n\r\n"); 132 | response = http::Response{Status::_414_URI_Too_Long}; 133 | EXPECT_EQ(response.data(), "HTTP/1.1 414 URI Too Long\r\n\r\n"); 134 | response = http::Response{Status::_415_Unsupported_Media_Type}; 135 | EXPECT_EQ(response.data(), "HTTP/1.1 415 Unsupported Media Type\r\n\r\n"); 136 | response = http::Response{Status::_416_Range_Not_Satisfiable}; 137 | EXPECT_EQ(response.data(), "HTTP/1.1 416 Range Not Satisfiable\r\n\r\n"); 138 | response = http::Response{Status::_417_Expectation_Failed}; 139 | EXPECT_EQ(response.data(), "HTTP/1.1 417 Expectation Failed\r\n\r\n"); 140 | response = http::Response{Status::_418_Im_a_teapot}; 141 | EXPECT_EQ(response.data(), "HTTP/1.1 418 I'm a teapot\r\n\r\n"); 142 | response = http::Response{Status::_421_Misdirected_Request}; 143 | EXPECT_EQ(response.data(), "HTTP/1.1 421 Misdirected Request\r\n\r\n"); 144 | response = http::Response{Status::_422_Unprocessable_Entity}; 145 | EXPECT_EQ(response.data(), "HTTP/1.1 422 Unprocessable Entity\r\n\r\n"); 146 | response = http::Response{Status::_423_Locked}; 147 | EXPECT_EQ(response.data(), "HTTP/1.1 423 Locked\r\n\r\n"); 148 | response = http::Response{Status::_424_Failed_Dependency}; 149 | EXPECT_EQ(response.data(), "HTTP/1.1 424 Failed Dependency\r\n\r\n"); 150 | response = http::Response{Status::_425_Too_Early}; 151 | EXPECT_EQ(response.data(), "HTTP/1.1 425 Too Early\r\n\r\n"); 152 | response = http::Response{Status::_426_Upgrade_Required}; 153 | EXPECT_EQ(response.data(), "HTTP/1.1 426 Upgrade Required\r\n\r\n"); 154 | response = http::Response{Status::_428_Precondition_Required}; 155 | EXPECT_EQ(response.data(), "HTTP/1.1 428 Precondition Required\r\n\r\n"); 156 | response = http::Response{Status::_429_Too_Many_Requests}; 157 | EXPECT_EQ(response.data(), "HTTP/1.1 429 Too Many Requests\r\n\r\n"); 158 | response = http::Response{Status::_431_Request_Header_Fields_Too_Large}; 159 | EXPECT_EQ(response.data(), "HTTP/1.1 431 Request Header Fields Too Large\r\n\r\n"); 160 | response = http::Response{Status::_451_Unavailable_For_Legal_Reasons}; 161 | EXPECT_EQ(response.data(), "HTTP/1.1 451 Unavailable For Legal Reasons\r\n\r\n"); 162 | response = http::Response{Status::_500_Internal_Server_Error}; 163 | EXPECT_EQ(response.data(), "HTTP/1.1 500 Internal Server Error\r\n\r\n"); 164 | response = http::Response{Status::_501_Not_Implemented}; 165 | EXPECT_EQ(response.data(), "HTTP/1.1 501 Not Implemented\r\n\r\n"); 166 | response = http::Response{Status::_502_Bad_Gateway}; 167 | EXPECT_EQ(response.data(), "HTTP/1.1 502 Bad Gateway\r\n\r\n"); 168 | response = http::Response{Status::_503_Service_Unavailable}; 169 | EXPECT_EQ(response.data(), "HTTP/1.1 503 Service Unavailable\r\n\r\n"); 170 | response = http::Response{Status::_504_Gateway_Timeout}; 171 | EXPECT_EQ(response.data(), "HTTP/1.1 504 Gateway Timeout\r\n\r\n"); 172 | response = http::Response{Status::_505_HTTP_Version_Not_Supported}; 173 | EXPECT_EQ(response.data(), "HTTP/1.1 505 HTTP Version Not Supported\r\n\r\n"); 174 | response = http::Response{Status::_506_Variant_Also_Negotiates}; 175 | EXPECT_EQ(response.data(), "HTTP/1.1 506 Variant Also Negotiates\r\n\r\n"); 176 | response = http::Response{Status::_507_Insufficient_Storage}; 177 | EXPECT_EQ(response.data(), "HTTP/1.1 507 Insufficient Storage\r\n\r\n"); 178 | response = http::Response{Status::_508_Loop_Detected}; 179 | EXPECT_EQ(response.data(), "HTTP/1.1 508 Loop Detected\r\n\r\n"); 180 | response = http::Response{Status::_510_Not_Extended}; 181 | EXPECT_EQ(response.data(), "HTTP/1.1 510 Not Extended\r\n\r\n"); 182 | response = http::Response{Status::_511_Network_Authentication_Required}; 183 | EXPECT_EQ(response.data(), "HTTP/1.1 511 Network Authentication Required\r\n\r\n"); 184 | 185 | response = http::Response{Status::_404_Not_Found, "Not Found"}; 186 | EXPECT_EQ(response.data(), "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\nNot Found"); 187 | response = http::Response{Status::_404_Not_Found, "Not Found", http::ContentType::PlainText}; 188 | EXPECT_EQ(response.data(), "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n\r\nNot Found"); 189 | response = http::Response{Status::_404_Not_Found, "Not Found", "text/csv"}; 190 | EXPECT_EQ(response.data(), "HTTP/1.1 404 Not Found\r\nContent-Type: text/csv\r\n\r\nNot Found"); 191 | } 192 | 193 | TEST(Response, StatusCGI) 194 | { 195 | using Status = http::ResponseStatus; 196 | auto response = http::Response{Status::_200_Ok}; 197 | EXPECT_EQ(response.data(http::ResponseMode::Cgi), "Status: 200 OK\r\n\r\n"); 198 | response = http::Response{Status::_404_Not_Found}; 199 | EXPECT_EQ(response.data(http::ResponseMode::Cgi), "Status: 404 Not Found\r\n\r\n"); 200 | response = http::Response{Status::_404_Not_Found, "Not Found"}; 201 | EXPECT_EQ( 202 | response.data(http::ResponseMode::Cgi), 203 | "Status: 404 Not Found\r\nContent-Type: text/html\r\n\r\nNot Found"); 204 | response = http::Response{Status::_404_Not_Found, "Not Found", http::ContentType::PlainText}; 205 | EXPECT_EQ( 206 | response.data(http::ResponseMode::Cgi), 207 | "Status: 404 Not Found\r\nContent-Type: text/plain\r\n\r\nNot Found"); 208 | response = http::Response{Status::_404_Not_Found, "Not Found", "text/csv"}; 209 | EXPECT_EQ( 210 | response.data(http::ResponseMode::Cgi), 211 | "Status: 404 Not Found\r\nContent-Type: text/csv\r\n\r\nNot Found"); 212 | } 213 | 214 | TEST(Response, StatusWithCookies) 215 | { 216 | auto expectedResponse = "HTTP/1.1 404 Not Found\r\n" + cookiesResponsePart + "\r\n"; 217 | auto response = http::Response{http::ResponseStatus::_404_Not_Found}; 218 | testResponseWithCookies(response, expectedResponse); 219 | } 220 | 221 | TEST(Response, StatusWithHeaders) 222 | { 223 | auto expectedResponse = "HTTP/1.1 404 Not Found\r\n" + headersResponsePart + "\r\n"; 224 | auto response = http::Response{http::ResponseStatus::_404_Not_Found}; 225 | testResponseWithHeaders(response, expectedResponse); 226 | } 227 | 228 | TEST(Response, StatusWithCookiesAndHeaders) 229 | { 230 | auto expectedResponse = "HTTP/1.1 404 Not Found\r\n" + cookiesAndHeadersResponsePart + "\r\n"; 231 | auto response = http::Response{http::ResponseStatus::_404_Not_Found}; 232 | testResponseWithCookiesAndHeaders(response, expectedResponse); 233 | } 234 | 235 | TEST(Response, Text) 236 | { 237 | auto response = http::Response{"Hello world"}; 238 | EXPECT_EQ(response.data(), "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nHello world"); 239 | response = http::Response{"Hello world", http::ContentType::Html}; 240 | EXPECT_EQ(response.data(), "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nHello world"); 241 | response = http::Response{"Hello world", http::ContentType::Xhtml}; 242 | EXPECT_EQ(response.data(), "HTTP/1.1 200 OK\r\nContent-Type: application/xhtml+xml\r\n\r\nHello world"); 243 | response = http::Response{"Hello world", http::ContentType::PlainText}; 244 | EXPECT_EQ(response.data(), "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello world"); 245 | } 246 | 247 | TEST(Response, TextWithCookies) 248 | { 249 | auto expectedResponse = "HTTP/1.1 200 OK\r\n" 250 | "Content-Type: text/html\r\n" + 251 | cookiesResponsePart + "\r\nHello world"; 252 | auto response = http::Response{"Hello world", http::ContentType::Html}; 253 | testResponseWithCookies(response, expectedResponse); 254 | } 255 | 256 | TEST(Response, TextWithHeaders) 257 | { 258 | auto expectedResponse = "HTTP/1.1 200 OK\r\n" 259 | "Content-Type: text/html\r\n" + 260 | headersResponsePart + "\r\nHello world"; 261 | auto response = http::Response{"Hello world", http::ContentType::Html}; 262 | testResponseWithHeaders(response, expectedResponse); 263 | } 264 | 265 | TEST(Response, TextWithCookiesAndHeaders) 266 | { 267 | auto expectedResponse = "HTTP/1.1 200 OK\r\n" 268 | "Content-Type: text/html\r\n" + 269 | cookiesAndHeadersResponsePart + "\r\nHello world"; 270 | auto response = http::Response{"Hello world", http::ContentType::Html}; 271 | testResponseWithCookiesAndHeaders(response, expectedResponse); 272 | } 273 | 274 | TEST(Response, Redirect) 275 | { 276 | auto response = http::Response{"/", http::RedirectType::Found}; 277 | EXPECT_EQ(response.data(), "HTTP/1.1 302 Found\r\nLocation: /\r\n\r\n"); 278 | response = http::Response{"/", http::RedirectType::SeeOther}; 279 | EXPECT_EQ(response.data(), "HTTP/1.1 303 See Other\r\nLocation: /\r\n\r\n"); 280 | response = http::Response{"/", http::RedirectType::NotModified}; 281 | EXPECT_EQ(response.data(), "HTTP/1.1 304 Not Modified\r\nLocation: /\r\n\r\n"); 282 | response = http::Response{"/", http::RedirectType::MultipleChoice}; 283 | EXPECT_EQ(response.data(), "HTTP/1.1 300 Multiple Choice\r\nLocation: /\r\n\r\n"); 284 | response = http::Response{"/", http::RedirectType::MovedPermanently}; 285 | EXPECT_EQ(response.data(), "HTTP/1.1 301 Moved Permanently\r\nLocation: /\r\n\r\n"); 286 | response = http::Response{"/", http::RedirectType::PermanentRedirect}; 287 | EXPECT_EQ(response.data(), "HTTP/1.1 308 Permanent Redirect\r\nLocation: /\r\n\r\n"); 288 | response = http::Response{"/", http::RedirectType::TemporaryRedirect}; 289 | EXPECT_EQ(response.data(), "HTTP/1.1 307 Temporary Redirect\r\nLocation: /\r\n\r\n"); 290 | 291 | auto response2 = http::Response{http::Redirect{"/"}}; 292 | EXPECT_EQ(response2.data(), "HTTP/1.1 302 Found\r\nLocation: /\r\n\r\n"); 293 | response2 = http::Redirect{"/", http::RedirectType::SeeOther}; 294 | EXPECT_EQ(response2.data(), "HTTP/1.1 303 See Other\r\nLocation: /\r\n\r\n"); 295 | } 296 | 297 | TEST(Response, RedirectWithCookies) 298 | { 299 | auto expectedResponse = "HTTP/1.1 302 Found\r\nLocation: /\r\n" + cookiesResponsePart + "\r\n"; 300 | auto response = http::Response{"/", http::RedirectType::Found}; 301 | testResponseWithCookies(response, expectedResponse); 302 | } 303 | 304 | TEST(Response, RedirectWithHeaders) 305 | { 306 | auto expectedResponse = "HTTP/1.1 302 Found\r\nLocation: /\r\n" + headersResponsePart + "\r\n"; 307 | auto response = http::Response{"/", http::RedirectType::Found}; 308 | testResponseWithHeaders(response, expectedResponse); 309 | } 310 | 311 | TEST(Response, RedirectWithCookiesAndHeaders) 312 | { 313 | auto expectedResponse = "HTTP/1.1 302 Found\r\nLocation: /\r\n" + cookiesAndHeadersResponsePart + "\r\n"; 314 | auto response = http::Response{"/", http::RedirectType::Found}; 315 | testResponseWithCookiesAndHeaders(response, expectedResponse); 316 | } 317 | 318 | TEST(ResponseView, HttpResponseFromString) 319 | { 320 | auto responseString = std::string{"HTTP/1.1 302 Found\r\nLocation: /\r\n" 321 | "\r\n"}; 322 | auto response = http::responseFromString(responseString); 323 | ASSERT_TRUE(response); 324 | EXPECT_EQ(response->status(), http::ResponseStatus::_302_Found); 325 | EXPECT_EQ(response->headers().at(0).name(), "Location"); 326 | EXPECT_EQ(response->headers().at(0).value(), "/"); 327 | EXPECT_EQ(response->body(), ""); 328 | } 329 | 330 | TEST(ResponseView, CgiResponseFromString) 331 | { 332 | auto responseString = std::string{"Status: 302 Found\r\nLocation: /\r\n" 333 | "\r\n"}; 334 | auto response = http::responseFromString(responseString, http::ResponseMode::Cgi); 335 | ASSERT_TRUE(response); 336 | EXPECT_EQ(response->status(), http::ResponseStatus::_302_Found); 337 | EXPECT_EQ(response->headers().at(0).name(), "Location"); 338 | EXPECT_EQ(response->headers().at(0).value(), "/"); 339 | EXPECT_EQ(response->body(), ""); 340 | } 341 | 342 | TEST(ResponseView, ResponseFromString2) 343 | { 344 | auto responseString = std::string{"HTTP/1.1 200 OK\r\nSet-Cookie: id=hello; Max-Age=60\r\nLocation: /\r\n" 345 | "\r\nHello world"}; 346 | auto response = http::responseFromString(responseString); 347 | ASSERT_TRUE(response); 348 | EXPECT_EQ(response->status(), http::ResponseStatus::_200_Ok); 349 | EXPECT_EQ(response->cookies().at(0).name(), "id"); 350 | EXPECT_EQ(response->cookies().at(0).value(), "hello"); 351 | EXPECT_EQ(response->cookies().at(0).maxAge(), std::chrono::minutes{1}); 352 | EXPECT_EQ(response->headers().at(0).name(), "Location"); 353 | EXPECT_EQ(response->headers().at(0).value(), "/"); 354 | EXPECT_EQ(response->body(), "Hello world"); 355 | } 356 | 357 | TEST(ResponseView, ResponseFromResponseView) 358 | { 359 | auto responseString = std::string{"HTTP/1.1 200 OK\r\nSet-Cookie: id=hello; Max-Age=60\r\nLocation: /\r\n" 360 | "\r\nHello world"}; 361 | auto responseView = http::responseFromString(responseString); 362 | ASSERT_TRUE(responseView); 363 | auto response = http::Response(*responseView); 364 | EXPECT_EQ(response.status(), http::ResponseStatus::_200_Ok); 365 | EXPECT_EQ(response.cookies().at(0).name(), "id"); 366 | EXPECT_EQ(response.cookies().at(0).value(), "hello"); 367 | EXPECT_EQ(response.cookies().at(0).maxAge(), std::chrono::minutes{1}); 368 | EXPECT_EQ(response.headers().at(0).name(), "Location"); 369 | EXPECT_EQ(response.headers().at(0).value(), "/"); 370 | EXPECT_EQ(response.body(), "Hello world"); 371 | } 372 | 373 | TEST(ResponseView, ResponseFromStringPartialStatus) 374 | { 375 | auto responseString = std::string{"HTTP/1.1 302 \r\nLocation: /\r\n" 376 | "\r\n"}; 377 | auto response = http::responseFromString(responseString); 378 | ASSERT_TRUE(response); 379 | EXPECT_EQ(response->status(), http::ResponseStatus::_302_Found); 380 | EXPECT_EQ(response->headers().at(0).name(), "Location"); 381 | EXPECT_EQ(response->headers().at(0).value(), "/"); 382 | EXPECT_EQ(response->body(), ""); 383 | } 384 | 385 | TEST(ResponseView, ResponseFromStringPartialStatus2) 386 | { 387 | auto responseString = std::string{"HTTP/1.1 302\r\nLocation: /\r\n" 388 | "\r\n"}; 389 | auto response = http::responseFromString(responseString); 390 | ASSERT_TRUE(response); 391 | EXPECT_EQ(response->status(), http::ResponseStatus::_302_Found); 392 | EXPECT_EQ(response->headers().at(0).name(), "Location"); 393 | EXPECT_EQ(response->headers().at(0).value(), "/"); 394 | EXPECT_EQ(response->body(), ""); 395 | } --------------------------------------------------------------------------------