├── .editorconfig ├── .github └── workflows │ ├── check_on_push.yaml │ ├── packaging.yml │ ├── publish.yaml │ └── test.yml ├── .gitignore ├── .luacheckrc ├── AUTHORS ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── FindLuaCheck.cmake ├── FindLuaCov.cmake ├── FindLuaCovCoveralls.cmake ├── FindLuaTest.cmake └── FindTarantool.cmake ├── debian ├── .gitignore ├── changelog ├── compat ├── control ├── copyright ├── docs ├── prebuild.sh ├── rules └── source │ └── format ├── deps.sh ├── http-scm-1.rockspec ├── http ├── CMakeLists.txt ├── codes.lua ├── httpfast.h ├── lib.c ├── mime_types.lua ├── server.lua ├── sslsocket.lua ├── tpleval.h └── version.lua ├── roles ├── CMakeLists.txt └── httpd.lua ├── rpm ├── prebuild.sh └── tarantool-http.spec └── test ├── controllers └── module │ └── controller.lua ├── helpers.lua ├── integration ├── http_log_requests_test.lua ├── http_server_delete_test.lua ├── http_server_options_test.lua ├── http_server_requests_test.lua ├── http_server_url_for_test.lua ├── http_server_url_match_test.lua ├── http_tls_enabled_test.lua ├── http_tls_enabled_validate_test.lua └── httpd_role_test.lua ├── mocks └── mock_role.lua ├── public ├── hello.html └── lorem.txt ├── ssl_data ├── ca.crt ├── ca.key ├── ca.srl ├── client.crt ├── client.enc.key ├── client.key ├── generate.sh ├── passwd ├── passwords ├── server.crt ├── server.enc.key └── server.key ├── templates ├── helper.html.el ├── module │ └── controller │ │ ├── action.html.el │ │ └── action.js.el └── test.html.el └── unit ├── http_custom_socket_test.lua ├── http_params_test.lua ├── http_parse_request_test.lua ├── http_setcookie_test.lua ├── http_split_uri_test.lua ├── http_template_test.lua └── httpd_role_test.lua /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [CMakeLists.txt] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.cmake] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.lua] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [*.{h,c,cc}] 22 | indent_style = tab 23 | tab_width = 8 24 | -------------------------------------------------------------------------------- /.github/workflows/check_on_push.yaml: -------------------------------------------------------------------------------- 1 | name: Static analysis 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | static-analysis: 9 | if: | 10 | github.event_name == 'push' || 11 | github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@master 15 | 16 | - name: Setup Tarantool 17 | uses: tarantool/setup-tarantool@v3 18 | with: 19 | tarantool-version: '2.11' 20 | 21 | - name: Setup luacheck 22 | run: tarantoolctl rocks install luacheck 0.25.0 23 | 24 | - run: cmake -S . -B build 25 | 26 | - name: Run luacheck 27 | run: make -C build luacheck 28 | -------------------------------------------------------------------------------- /.github/workflows/packaging.yml: -------------------------------------------------------------------------------- 1 | name: packaging 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - 'master' 9 | tags: 10 | - '*' 11 | 12 | jobs: 13 | # Run not only on tags, otherwise dependent job will skip. 14 | version-check: 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - name: Check module version 18 | # We need this step to run only on push with tag. 19 | if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }} 20 | uses: tarantool/actions/check-module-version@master 21 | with: 22 | module-name: 'http.server' 23 | 24 | package: 25 | runs-on: ubuntu-latest 26 | needs: version-check 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | platform: 32 | - { os: 'debian', dist: 'stretch' } 33 | - { os: 'debian', dist: 'buster' } 34 | - { os: 'debian', dist: 'bullseye' } 35 | - { os: 'el', dist: '7' } 36 | - { os: 'el', dist: '8' } 37 | - { os: 'fedora', dist: '30' } 38 | - { os: 'fedora', dist: '31' } 39 | - { os: 'fedora', dist: '32' } 40 | - { os: 'fedora', dist: '33' } 41 | - { os: 'fedora', dist: '34' } 42 | - { os: 'fedora', dist: '35' } 43 | - { os: 'fedora', dist: '36' } 44 | - { os: 'ubuntu', dist: 'xenial' } 45 | - { os: 'ubuntu', dist: 'bionic' } 46 | - { os: 'ubuntu', dist: 'focal' } 47 | - { os: 'ubuntu', dist: 'groovy' } 48 | - { os: 'ubuntu', dist: 'jammy' } 49 | 50 | env: 51 | OS: ${{ matrix.platform.os }} 52 | DIST: ${{ matrix.platform.dist }} 53 | 54 | steps: 55 | - name: Clone the module 56 | uses: actions/checkout@v3 57 | # `actions/checkout` performs shallow clone of repo. To provide 58 | # proper version of the package to `packpack` we need to have 59 | # complete repository, otherwise it will be `0.0.1`. 60 | with: 61 | fetch-depth: 0 62 | 63 | - name: Clone the packpack tool 64 | uses: actions/checkout@v3 65 | with: 66 | repository: packpack/packpack 67 | path: packpack 68 | 69 | - name: Fetch tags 70 | # Found that Github checkout Actions pulls all the tags, but 71 | # right it deannotates the testing tag, check: 72 | # https://github.com/actions/checkout/issues/290 73 | # But we use 'git describe ..' calls w/o '--tags' flag and it 74 | # prevents us from getting the needed tag for packages version 75 | # setup. To avoid of it, let's fetch it manually, to be sure 76 | # that all tags will exist always. 77 | run: git fetch --tags -f 78 | 79 | - name: Create packages 80 | run: ./packpack/packpack 81 | 82 | - name: Deploy packages 83 | # We need this step to run only on push with tag. 84 | if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }} 85 | env: 86 | RWS_URL_PART: https://rws.tarantool.org/tarantool-modules 87 | RWS_AUTH: ${{ secrets.RWS_AUTH }} 88 | PRODUCT_NAME: tarantool-http 89 | working-directory: build 90 | run: | 91 | CURL_CMD="curl -LfsS \ 92 | -X PUT ${RWS_URL_PART}/${OS}/${DIST} \ 93 | -u ${RWS_AUTH} \ 94 | -F product=${PRODUCT_NAME}" 95 | 96 | shopt -s nullglob 97 | for f in *.deb *.rpm *.dsc *.tar.xz *.tar.gz; do 98 | CURL_CMD+=" -F $(basename ${f})=@${f}" 99 | done 100 | 101 | echo ${CURL_CMD} 102 | 103 | ${CURL_CMD} 104 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | tags: ['*'] 7 | 8 | jobs: 9 | version-check: 10 | # We need this job to run only on push with tag. 11 | if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }} 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Check module version 15 | uses: tarantool/actions/check-module-version@master 16 | with: 17 | module-name: 'http.server' 18 | 19 | publish-scm-1: 20 | if: github.ref == 'refs/heads/master' 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: tarantool/rocks.tarantool.org/github-action@master 25 | with: 26 | auth: ${{ secrets.ROCKS_AUTH }} 27 | files: http-scm-1.rockspec 28 | 29 | publish-tag: 30 | if: startsWith(github.ref, 'refs/tags/') 31 | needs: version-check 32 | runs-on: ubuntu-latest 33 | steps: 34 | # Create a source tarball for the release (.src.rock). 35 | # 36 | # `tarantoolctl rocks pack ` creates a source 37 | # tarball. It speeds up 38 | # `tarantoolctl rocks install ` and 39 | # frees it from dependency on git. 40 | # 41 | # Important: Don't confuse this command with 42 | # `tarantoolctl rocks pack []`, which 43 | # creates a **binary** rock or .all.rock (see [1]). Don't 44 | # upload a binary rock of a Lua/C module to 45 | # rocks.tarantool.org. Lua/C modules are platform dependent. 46 | # 47 | # A 'pure Lua' module is packed into the .all.rock tarball. 48 | # Feel free to upload such rock to rocks.tarantool.org. 49 | # Don't be confused by the 'pure Lua' words: usage of 50 | # LuaJIT's FFI and tarantool specific features are okay. 51 | # 52 | # [1]: https://github.com/luarocks/luarocks/wiki/Types-of-rocks 53 | - uses: actions/checkout@v3 54 | - uses: tarantool/setup-tarantool@v3 55 | with: 56 | tarantool-version: '2.11' 57 | 58 | # Make a release 59 | - run: echo TAG=${GITHUB_REF##*/} >> $GITHUB_ENV 60 | - run: tarantoolctl rocks new_version --tag ${{ env.TAG }} 61 | - run: tarantoolctl rocks install http-${{ env.TAG }}-1.rockspec 62 | - run: tarantoolctl rocks pack http-${{ env.TAG }}-1.rockspec 63 | 64 | # Upload .rockspec and .src.rock. 65 | - uses: tarantool/rocks.tarantool.org/github-action@master 66 | with: 67 | auth: ${{ secrets.ROCKS_AUTH }} 68 | files: | 69 | http-${{ env.TAG }}-1.rockspec 70 | http-${{ env.TAG }}-1.src.rock 71 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | tarantool: ['1.10', '2.10', '2.11', '3.1', '3.2'] 14 | coveralls: [false] 15 | include: 16 | - tarantool: '2.11' 17 | coveralls: true 18 | runs-on: [ubuntu-22.04] 19 | steps: 20 | - uses: actions/checkout@master 21 | - uses: tarantool/setup-tarantool@v3 22 | with: 23 | tarantool-version: ${{ matrix.tarantool }} 24 | 25 | - name: Prepare the repo 26 | run: curl -L https://tarantool.io/release/2/installer.sh | bash 27 | env: 28 | DEBIAN_FRONTEND: noninteractive 29 | 30 | - name: Install tt cli 31 | run: sudo apt install -y tt=2.5.2 32 | env: 33 | DEBIAN_FRONTEND: noninteractive 34 | 35 | - name: Install Tarantool 36 | run: tt install tarantool ${{ matrix.tarantool }} --dynamic 37 | 38 | - name: Cache rocks 39 | uses: actions/cache@v3 40 | id: cache-rocks 41 | with: 42 | path: .rocks/ 43 | key: cache-rocks-${{ matrix.tarantool }}-05 44 | 45 | - run: echo $PWD/.rocks/bin >> $GITHUB_PATH 46 | 47 | - run: ./deps.sh 48 | 49 | - name: Build module 50 | run: | 51 | cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -S . -B build 52 | make -C build 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - name: Run tests without code coverage analysis 57 | run: make -C build luatest 58 | if: matrix.coveralls != true 59 | 60 | - name: Send code coverage to coveralls.io 61 | run: make -C build coveralls 62 | if: ${{ matrix.coveralls }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeFiles/ 2 | CMakeCache.txt 3 | Makefile 4 | cmake_*.cmake 5 | install_manifest.txt 6 | *.a 7 | *.cbp 8 | *.d 9 | *.dylib 10 | *.gcno 11 | *.gcda 12 | *.user 13 | *.o 14 | *.reject 15 | *.so 16 | *~ 17 | .gdb_history 18 | VERSION 19 | Testing 20 | CTestTestfile.cmake 21 | *.snap 22 | *.xlog 23 | .rocks 24 | .DS_Store 25 | .idea/ 26 | build/ 27 | packpack/ 28 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "luajit" 2 | globals = {"box", "_TARANTOOL", "tonumber64", "utf8"} 3 | ignore = { 4 | -- Accessing an undefined field of a global variable . 5 | "143/debug", 6 | -- Accessing an undefined field of a global variable . 7 | "143/os", 8 | -- Accessing an undefined field of a global variable . 9 | "143/string", 10 | -- Accessing an undefined field of a global variable . 11 | "143/table", 12 | -- Accessing an undefined field of a global variable . 13 | "143/package", 14 | -- Unused argument . 15 | "212/self", 16 | -- Redefining a local variable. 17 | "411", 18 | -- Redefining an argument. 19 | "412", 20 | -- Shadowing a local variable. 21 | "421", 22 | -- Shadowing an upvalue. 23 | "431", 24 | -- Shadowing an upvalue argument. 25 | "432", 26 | } 27 | 28 | include_files = { 29 | "**/*.lua", 30 | } 31 | 32 | exclude_files = { 33 | "build/**/*.lua", 34 | ".rocks/**/*.lua", 35 | ".git/**/*.lua", 36 | "http/mime_types.lua", 37 | } 38 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # 2 | # Below is complete list of people, who contributed their 3 | # code. 4 | # 5 | # NOTE: If you can commit a change this list, please do not hesitate 6 | # to add your name to it. 7 | # 8 | 9 | Dmitry E. Oboukhov 10 | Roman Tsisyk 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | 11 | ### Changed 12 | 13 | ### Fixed 14 | 15 | ## [1.7.0] - 2024-11-15 16 | 17 | The release introduces a TLS support. 18 | 19 | ### Added 20 | 21 | - SSL support (#35). 22 | - SSL support into roles (#199). 23 | 24 | ## [1.6.0] - 2024-09-13 25 | 26 | The release introduces a role for Tarantool 3. 27 | 28 | ### Fixed 29 | 30 | - Fixed request crash with empty body and unexpected header 31 | Content-Type (#189). 32 | 33 | ### Added 34 | 35 | - `roles.httpd` role to configure one or more HTTP servers (#196). 36 | - `httpd:delete(name)` method to delete named routes (#197). 37 | 38 | ## [1.5.0] - 2023-03-29 39 | 40 | ### Added 41 | 42 | - Add versioning support. 43 | 44 | ### Fixed 45 | 46 | - Allow dot in path segment. 47 | 48 | ## [1.4.0] - 2022-12-30 49 | 50 | ### Added 51 | 52 | - Add path_raw field. This field contains request path without encoding. 53 | 54 | ## [1.3.0] - 2022-07-27 55 | 56 | ### Changed 57 | 58 | - Allow to use a non-standard socket (for example, `sslsocket` with TLS 59 | support). 60 | - When processing a GET request, the plus sign in the parameter name and 61 | value is now replaced with a space. In order to explicitly pass a "+" 62 | sign it must be represented as "%2B". 63 | 64 | ### Added 65 | 66 | - Add option to control keepalive connection state (#137). 67 | - Add option to control idle connections (#137). 68 | 69 | ## [1.2.0] - 2021-11-10 70 | 71 | ### Changed 72 | 73 | - Disable option display_errors by default. 74 | 75 | ## [1.1.1] - 2021-10-28 76 | 77 | ### Changed 78 | 79 | - Revert all changes related to http v2 (#134). 80 | - Rewrite TAP tests with luatest. 81 | - Create a separate target for running tests in CMake. 82 | - Replace io with fio module. 83 | 84 | ### Added 85 | 86 | - Replace Travis CI with Github Actions. 87 | - Add workflow that publish rockspec. 88 | - Add editorconfig to configure indentation. 89 | - Add luacheck integration. 90 | - Add option to get cookie without escaping. 91 | - Add option to set cookie without escaping and change escaping algorithm. 92 | 93 | ### Fixed 94 | 95 | - Fix FindTarantool.cmake module. 96 | - Fix SEGV_MAPERR when box httpd html escaping. 97 | 98 | ## [2.1.0] - 2020-01-30 99 | 100 | ### Added 101 | 102 | - Return ability to set loggers for a specific route. 103 | - Return ability to server and route to use custom loggers. 104 | 105 | ### Fixed 106 | 107 | - Fix routes overlapping by any pattern in route's path. 108 | - Fix req:redirect_to method. 109 | 110 | ## [2.0.1] - 2019-10-09 111 | 112 | ### Fixed 113 | 114 | - Fix installation paths to not contain extra directories. 115 | 116 | ## [2.0.0] - 2019-10-04 117 | 118 | ### Added 119 | 120 | - Major rewrite since version 1.x. 121 | - Ability to be used with internal http server and an nginx upstream module 122 | (without modifying the backend code). 123 | - Standardized request object (similar to WSGI). 124 | - A new router with route priorities inspired by Mojolicious. 125 | - Middleware support (for e.g. for centrally handling authorization). 126 | 127 | ## [1.1.0] - 2019-05-30 128 | ### Added 129 | - Travis builds for tags. 130 | 131 | ## [1.0.6] - 2019-05-19 132 | ### Added 133 | - Custom logger per route support. 134 | 135 | ### Fixed 136 | - Fixed buffer reading when timeout or disconnect occurs. 137 | - Fixed cookies formatting: stop url-encoding cookie path and quoting cookie expire date. 138 | 139 | ### Changed 140 | - Readme updates: fix setcookie() description, how to use unix socket. 141 | 142 | ## [1.0.5] - 2018-09-03 143 | ### Changed 144 | - Protocol upgrade: detaching mechanism is re-worked. 145 | 146 | ## [1.0.4] - 2018-08-31 147 | ### Added 148 | - Detach callback support for protocol upgrade implementations. 149 | 150 | ## [1.0.3] - 2018-06-29 151 | ### Fixed 152 | - Fixed eof detection. 153 | 154 | ## [1.0.2] - 2018-02-01 155 | ### Fixed 156 | - Fixed request parsing with headers longer than 4096 bytes. 157 | 158 | ## [1.0.1] - 2018-01-22 159 | ### Added 160 | - Added RPM and DEB specs. 161 | - Enabled builds for Tarantool 1.7. 162 | 163 | ### Fixed 164 | - Fixed building on Mac OS X. 165 | - Fixed server handler and before_routes hooks. 166 | - Fixed "data" response body rendering option. 167 | - Fixed crash in uri_escape and uri_unescape when multiple arguments with same name. 168 | - Fixed no distinction between PUT and DELETE methods (#25). 169 | - Fixed compatibility with Tarantool 1.7. 170 | - Fixed empty Content-Type header handling. 171 | - Fixed curl delay: add support for expect=100-Continue. 172 | 173 | ## [1.0] - 2016-11-29 174 | ### Added 175 | - Added requets peer host and port. 176 | - Show tarantool version in HTTP header. 177 | - Support for new Tarantool uri parser. 178 | - Chunked encoding support in responses. 179 | - Chunked encoding support in client. 180 | 181 | ### Changed 182 | - Fedora 23 build is disabled. 183 | - HTTP new sockets API. 184 | - Refactor handler API. 185 | - Cloud build is enabled. 186 | - Update the list of supported OS for tarantool/build. 187 | 188 | ### Fixed 189 | - Fixed build without -std=c99. 190 | - Fixed socket:write() problem: use :write instead :send, use new sockets in http_client. 191 | - Fixed directory traversal attack. 192 | - Fixed routes with dots don't work as expected (#17). 193 | - Fixed truncated rendered template (#18). 194 | 195 | ## [0.0.1] - 2014-05-05 196 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8 FATAL_ERROR) 2 | 3 | project(http C) 4 | if(NOT CMAKE_BUILD_TYPE) 5 | set(CMAKE_BUILD_TYPE Debug) 6 | endif() 7 | set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) 8 | 9 | find_package(LuaTest) 10 | find_package(LuaCheck) 11 | find_package(LuaCov) 12 | find_package(LuaCovCoveralls) 13 | 14 | set(CODE_COVERAGE_REPORT "${PROJECT_SOURCE_DIR}/luacov.report.out") 15 | set(CODE_COVERAGE_STATS "${PROJECT_SOURCE_DIR}/luacov.stats.out") 16 | 17 | # Find Tarantool and Lua dependecies 18 | set(Tarantool_FIND_REQUIRED ON) 19 | find_package(Tarantool) 20 | include_directories(${TARANTOOL_INCLUDE_DIRS}) 21 | 22 | # Set CFLAGS 23 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra") 24 | 25 | string(RANDOM ALPHABET 0123456789 seed) 26 | 27 | add_subdirectory(http) 28 | add_subdirectory(roles) 29 | 30 | add_custom_target(luacheck 31 | COMMAND ${LUACHECK} ${PROJECT_SOURCE_DIR} 32 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 33 | COMMENT "Run luacheck" 34 | ) 35 | 36 | add_custom_target(luatest-coverage 37 | COMMAND ${LUATEST} -v --coverage --shuffle all:${seed} 38 | BYPRODUCTS ${CODE_COVERAGE_STATS} 39 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 40 | COMMENT "Run regression tests with coverage" 41 | ) 42 | 43 | add_custom_target(luatest 44 | COMMAND ${LUATEST} -v --shuffle all:${seed} 45 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 46 | COMMENT "Run regression tests without coverage" 47 | ) 48 | 49 | add_custom_target(coverage 50 | COMMAND ${LUACOV} ${PROJECT_SOURCE_DIR} && grep -A999 '^Summary' ${CODE_COVERAGE_REPORT} 51 | DEPENDS ${CODE_COVERAGE_STATS} 52 | BYPRODUCTS ${CODE_COVERAGE_REPORT} 53 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 54 | COMMENT "Generate code coverage stats" 55 | ) 56 | 57 | if(DEFINED ENV{GITHUB_TOKEN}) 58 | set(COVERALLS_COMMAND ${LUACOVCOVERALLS} --include ^http --verbose --repo-token $ENV{GITHUB_TOKEN}) 59 | else() 60 | set(COVERALLS_COMMAND ${CMAKE_COMMAND} -E echo "Skipped uploading to coveralls.io: no token.") 61 | endif() 62 | 63 | add_custom_target(coveralls 64 | # Replace absolute paths with relative ones. 65 | # In command line: sed -i -e 's@'"$(realpath .)"'/@@'. 66 | COMMAND sed -i -e "\"s@\"'${PROJECT_SOURCE_DIR}'\"/@@\"" ${CODE_COVERAGE_STATS} 67 | COMMAND ${COVERALLS_COMMAND} 68 | DEPENDS ${CODE_COVERAGE_STATS} 69 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 70 | COMMENT "Send code coverage data to the coveralls.io service" 71 | ) 72 | 73 | set (LUA_PATH "LUA_PATH=${PROJECT_SOURCE_DIR}/?.lua\\;${PROJECT_SOURCE_DIR}/?/init.lua\\;\\;") 74 | set (LUA_SOURCE_DIR "LUA_SOURCE_DIR=${PROJECT_SOURCE_DIR}") 75 | set_target_properties(luatest-coverage PROPERTIES ENVIRONMENT "${LUA_PATH};${LUA_SOURCE_DIR}") 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2010-2013 Tarantool AUTHORS: 2 | please see AUTHORS file. 3 | 4 | /* 5 | * Redistribution and use in source and binary forms, with or 6 | * without modification, are permitted provided that the following 7 | * conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above 10 | * copyright notice, this list of conditions and the 11 | * following disclaimer. 12 | * 13 | * 2. Redistributions in binary form must reproduce the above 14 | * copyright notice, this list of conditions and the following 15 | * disclaimer in the documentation and/or other materials 16 | * provided with the distribution. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY AUTHORS ``AS IS'' AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 22 | * AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 23 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 26 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 29 | * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 30 | * SUCH DAMAGE. 31 | */ 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | # HTTP server for Tarantool 1.7.5+ 7 | 8 | [![Static analysis](https://github.com/tarantool/http/actions/workflows/check_on_push.yaml/badge.svg)](https://github.com/tarantool/http/actions/workflows/check_on_push.yaml) 9 | [![Test](https://github.com/tarantool/http/actions/workflows/test.yml/badge.svg)](https://github.com/tarantool/http/actions/workflows/test.yml) 10 | [![Coverage Status](https://coveralls.io/repos/github/tarantool/http/badge.svg?branch=master)](https://coveralls.io/github/tarantool/http?branch=master) 11 | 12 | > **Note:** In Tarantool 1.7.5+, a full-featured HTTP client is available aboard. 13 | > For Tarantool 1.6.5+, both HTTP server and client are available 14 | > [here](https://github.com/tarantool/http/tree/tarantool-1.6). 15 | 16 | ## http v2 has gone 17 | 18 | http v2 that was implemented in 19 | [#90](https://github.com/tarantool/http/issues/90) has been reverted in a 20 | master branch (commits 21 | [01004d7..e7e00ea](https://github.com/tarantool/http/compare/01004d7..e7e00ea)) 22 | and a limited number of reverted commits were reimplemented on top of http v1. 23 | However http v2 changes are still available in a [branch 24 | http-v2-legacy](https://github.com/tarantool/http/tree/http-v2-legacy) as well as Lua 25 | rockspecs available with name `http-v2-legacy` instead of `http`. For reasons of 26 | http v2 revert and decisions regarding each reverted commit see 27 | [#134](https://github.com/tarantool/http/issues/134). 28 | 29 | ## Table of contents 30 | 31 | * [Prerequisites](#prerequisites) 32 | * [Installation](#installation) 33 | * [Usage](#usage) 34 | * [Creating a server](#creating-a-server) 35 | * [Using routes](#using-routes) 36 | * [Contents of app\_dir](#contents-of-app_dir) 37 | * [Route handlers](#route-handlers) 38 | * [Fields and methods of the Request object](#fields-and-methods-of-the-request-object) 39 | * [Fields and methods of the Response object](#fields-and-methods-of-the-response-object) 40 | * [Examples](#examples) 41 | * [Working with stashes](#working-with-stashes) 42 | * [Special stash names](#special-stash-names) 43 | * [Working with cookies](#working-with-cookies) 44 | * [Rendering a template](#rendering-a-template) 45 | * [Template helpers](#template-helpers) 46 | * [Hooks](#hooks) 47 | * [handler(httpd, req)](#handlerhttpd-req) 48 | * [before\_dispatch(httpd, req)](#before_dispatchhttpd-req) 49 | * [after\_dispatch(cx, resp)](#after_dispatchcx-resp) 50 | * [Using a special socket](#using-a-special-socket) 51 | * [Roles](#roles) 52 | * [roles.httpd](#roleshttpd) 53 | * [See also](#see-also) 54 | 55 | ## Prerequisites 56 | 57 | * Tarantool 1.7.5+ with header files (`tarantool` && `tarantool-dev` packages) 58 | 59 | ## Installation 60 | 61 | You can: 62 | 63 | * clone the repository and build the `http` module using CMake: 64 | 65 | ``` bash 66 | git clone https://github.com/tarantool/http.git 67 | cd http && cmake . -DCMAKE_BUILD_TYPE=RelWithDebugInfo 68 | make 69 | make install 70 | ``` 71 | 72 | * install the `http` module using [tt](https://github.com/tarantool/tt): 73 | 74 | ``` bash 75 | tt rocks install http 76 | ``` 77 | 78 | * install the `http` module using LuaRocks 79 | (see [TarantoolRocks](https://github.com/tarantool/rocks) for 80 | LuaRocks configuration details): 81 | 82 | ``` bash 83 | luarocks --server=https://rocks.tarantool.org/ --local install http 84 | ``` 85 | 86 | ## Usage 87 | 88 | The server is an object which is configured with HTTP request 89 | handlers, routes (paths), templates, and a port to bind to. 90 | Unless Tarantool is running under a superuser, port numbers 91 | below 1024 may be unavailable. 92 | 93 | The server can be started and stopped anytime. Multiple 94 | servers can be created. 95 | 96 | To start a server: 97 | 98 | 1. [Create it](#creating-a-server) with `httpd = require('http.server').new(...)`. 99 | 2. [Configure routing](#using-routes) with `httpd:route(...)`. 100 | 3. Start it with `httpd:start()`. 101 | 102 | To stop the server, use `httpd:stop()`. 103 | 104 | ## Creating a server 105 | 106 | ```lua 107 | httpd = require('http.server').new(host, port[, { options } ]) 108 | ``` 109 | 110 | `host` and `port` must contain: 111 | * For tcp socket: the host and port to bind to. 112 | * For unix socket: `unix/` and path to socket (for example `/tmp/http-server.sock`) to bind to. 113 | 114 | `options` may contain: 115 | 116 | * `max_header_size` (default is 4096 bytes) - a limit for 117 | HTTP request header size. 118 | * `header_timeout` (default: 100 seconds) - a timeout until 119 | the server stops reading HTTP headers sent by the client. 120 | The server closes the client connection if the client doesn't 121 | send its headers within the given amount of time. 122 | * `app_dir` (default is '.', the server working directory) - 123 | a path to the directory with HTML templates and controllers. 124 | * `handler` - a Lua function to handle HTTP requests (this is 125 | a handler to use if the module "routing" functionality is not 126 | needed). 127 | * `charset` - the character set for server responses of 128 | type `text/html`, `text/plain` and `application/json`. 129 | * `display_errors` - return application errors and backtraces to the client 130 | (like PHP). Disabled by default (since 1.2.0). 131 | * `log_requests` - log incoming requests. This parameter can receive: 132 | - function value, supporting C-style formatting: log_requests(fmt, ...), where fmt is a format string and ... is Lua Varargs, holding arguments to be replaced in fmt. 133 | - boolean value, where `true` choose default `log.info` and `false` disable request logs at all. 134 | 135 | By default uses `log.info` function for requests logging. 136 | * `log_errors` - same as the `log_requests` option but is used for error messages logging. By default uses `log.error()` function. 137 | * `disable_keepalive` - disables keep-alive connections with misbehaving 138 | clients. Parameter accept a table that contains a user agents names. 139 | By default table is empty. 140 | 141 | Example: 142 | 143 | ```lua 144 | local httpd = http_server.new('127.0.0.1', 8080, { 145 | log_requests = true, 146 | log_errors = true, 147 | disable_keepalive = { 'curl/7.68.0' } 148 | }) 149 | ``` 150 | 151 | * `idle_timeout` - maximum amount of time an idle (keep-alive) connection will 152 | remain idle before closing. When the idle timeout is exceeded, HTTP server 153 | closes the keepalive connection. Default value: 0 seconds (disabled). 154 | * TLS options (to enable it, provide at least one of the following parameters): 155 | * `ssl_cert_file` is a path to the SSL cert file, mandatory; 156 | * `ssl_key_file` is a path to the SSL key file, mandatory; 157 | * `ssl_ca_file` is a path to the SSL CA file, optional; 158 | * `ssl_ciphers` is a colon-separated list of SSL ciphers, optional; 159 | * `ssl_password` is a password for decrypting SSL private key, optional; 160 | * `ssl_password_file` is a SSL file with key for decrypting SSL private key, optional. 161 | 162 | ## Using routes 163 | 164 | It is possible to automatically route requests between different 165 | handlers, depending on the request path. The routing API is inspired 166 | by [Mojolicious](http://mojolicio.us/perldoc/Mojolicious/Guides/Routing) API. 167 | 168 | Routes can be defined using: 169 | 170 | * an exact match (e.g. "index.php") 171 | * simple regular expressions 172 | * extended regular expressions 173 | 174 | Route examples: 175 | 176 | ```text 177 | '/' -- a simple route 178 | '/abc' -- a simple route 179 | '/abc/:cde' -- a route using a simple regular expression 180 | '/abc/:cde/:def' -- a route using a simple regular expression 181 | '/ghi*path' -- a route using an extended regular expression 182 | ``` 183 | 184 | To configure a route, use the `route()` method of the `httpd` object: 185 | 186 | ```lua 187 | httpd:route({ path = '/path/to' }, 'controller#action') 188 | httpd:route({ path = '/', template = 'Hello <%= var %>' }, handle1) 189 | httpd:route({ path = '/:abc/cde', file = 'users.html.el' }, handle2) 190 | httpd:route({ path = '/objects', method = 'GET' }, handle3) 191 | ... 192 | ``` 193 | 194 | To delete a named route, use `delete()` method of the `httpd` object: 195 | 196 | ```lua 197 | httpd:route({ path = '/path/to', name = 'route' }, 'controller#action') 198 | httpd:delete('route') 199 | ``` 200 | 201 | The first argument for `route()` is a Lua table with one or more keys: 202 | 203 | * `file` - a template file name (can be relative to. 204 | `{app_dir}/templates`, where `app_dir` is the path set when creating the 205 | server). If no template file name extension is provided, the extension is 206 | set to ".html.el", meaning HTML with embedded Lua. 207 | * `template` - template Lua variable name, in case the template 208 | is a Lua variable. If `template` is a function, it's called on every 209 | request to get template body. This is useful if template body must be 210 | taken from a database. 211 | * `path` - route path, as described earlier. 212 | * `name` - route name. 213 | * `method` - method on the route like `POST`, `GET`, `PUT`, `DELETE` 214 | * `log_requests` - option that overrides the server parameter of the same name but only for current route. 215 | * `log_errors` - option that overrides the server parameter of the same name but only for current route. 216 | 217 | The second argument is the route handler to be used to produce 218 | a response to the request. 219 | 220 | The typical usage is to avoid passing `file` and `template` arguments, 221 | since they take time to evaluate, but these arguments are useful 222 | for writing tests or defining HTTP servers with just one "route". 223 | 224 | The handler can also be passed as a string of the form 'filename#functionname'. 225 | In that case, the handler body is taken from a file in the 226 | `{app_dir}/controllers` directory. 227 | 228 | ## Contents of `app_dir` 229 | 230 | * `public` - a path to static content. Everything stored on this path 231 | defines a route which matches the file name, and the HTTP server serves this 232 | file automatically, as is. Notice that the server doesn't use `sendfile()`, 233 | and it reads the entire content of the file into the memory before passing 234 | it to the client. ??? Caching is not used, unless turned on. So this is not 235 | suitable for large files, use nginx instead. 236 | * `templates` - a path to templates. 237 | * `controllers` - a path to *.lua files with Lua controllers. For example, 238 | the controller name 'module.submodule#foo' is mapped to 239 | `{app_dir}/controllers/module.submodule.lua`. 240 | 241 | ## Route handlers 242 | 243 | A route handler is a function which accepts one argument (**Request**) and 244 | returns one value (**Response**). 245 | 246 | ```lua 247 | function my_handler(req) 248 | -- req is a Request object 249 | -- resp is a Response object 250 | local resp = req:render({text = req.method..' '..req.path }) 251 | resp.headers['x-test-header'] = 'test'; 252 | resp.status = 201 253 | return resp 254 | end 255 | ``` 256 | 257 | ### Fields and methods of the Request object 258 | 259 | * `req.method` - HTTP request type (`GET`, `POST` etc). 260 | * `req.path` - request path. 261 | * `req.path_raw` - request path without decoding. 262 | * `req.query` - request arguments. 263 | * `req.proto` - HTTP version (for example, `{ 1, 1 }` is `HTTP/1.1`). 264 | * `req.headers` - normalized request headers. A normalized header 265 | is in the lower case, all headers joined together into a single string. 266 | * `req.peer` - a Lua table with information about the remote peer 267 | (like `socket:peer()`). 268 | * `tostring(req)` - returns a string representation of the request. 269 | * `req:request_line()` - returns a first line of the http request (for example, `PUT /path HTTP/1.1`). 270 | * `req:read(delimiter|chunk|{delimiter = x, chunk = x}, timeout)` - reads the 271 | raw request body as a stream (see `socket:read()`). 272 | * `req:json()` - returns a Lua table from a JSON request. 273 | * `req:post_param(name)` - returns a single POST request a parameter value. 274 | If `name` is `nil`, returns all parameters as a Lua table. 275 | * `req:query_param(name)` - returns a single GET request parameter value. 276 | If `name` is `nil`, returns a Lua table with all arguments. 277 | * `req:param(name)` - any request parameter, either GET or POST. 278 | * `req:cookie(name, {raw = true})` | to get a cookie in the request. if `raw` 279 | option was set then cookie will not be unescaped, otherwise cookie's value 280 | will be unescaped. 281 | * `req:stash(name[, value])` - get or set a variable "stashed" 282 | when dispatching a route. 283 | * `req:url_for(name, args, query)` - returns the route's exact URL. 284 | * `req:render({})` - create a **Response** object with a rendered template. 285 | * `req:redirect_to` - create a **Response** object with an HTTP redirect. 286 | 287 | ### Fields and methods of the Response object 288 | 289 | * `resp.status` - HTTP response code. 290 | * `resp.headers` - a Lua table with normalized headers. 291 | * `resp.body` - response body (string|table|wrapped\_iterator). 292 | * `resp:setcookie({ name = 'name', value = 'value', path = '/', expires = '+1y', domain = 'example.com'}, {raw = true})` - 293 | adds `Set-Cookie` headers to `resp.headers`, if `raw` option was set then cookie will not be escaped, 294 | otherwise cookie's value and path will be escaped 295 | 296 | ### Examples 297 | 298 | ```lua 299 | function my_handler(req) 300 | return { 301 | status = 200, 302 | headers = { ['content-type'] = 'text/html; charset=utf8' }, 303 | body = [[ 304 | 305 | Hello, world! 306 | 307 | ]] 308 | } 309 | end 310 | ``` 311 | 312 | ## Working with stashes 313 | 314 | ```lua 315 | function hello(self) 316 | local id = self:stash('id') -- here is :id value 317 | local user = box.space.users:select(id) 318 | if user == nil then 319 | return self:redirect_to('/users_not_found') 320 | end 321 | return self:render({ user = user }) 322 | end 323 | 324 | httpd = httpd.new('127.0.0.1', 8080) 325 | httpd:route( 326 | { path = '/:id/view', template = 'Hello, <%= user.name %>' }, hello) 327 | httpd:start() 328 | ``` 329 | 330 | ### Special stash names 331 | 332 | * `controller` - the controller name. 333 | * `action` - the handler name in the controller. 334 | * `format` - the current output format (e.g. `html`, `txt`). Is 335 | detected automatically based on the request's `path` (for example, `/abc.js` 336 | sets `format` to `js`). When producing a response, `format` is used 337 | to serve the response's 'Content-type:'. 338 | 339 | ## Working with cookies 340 | 341 | To get a cookie, use: 342 | 343 | ```lua 344 | function show_user(self) 345 | local uid = self:cookie('id') 346 | 347 | if uid ~= nil and string.match(uid, '^%d$') ~= nil then 348 | local user = box.select(users, 0, uid) 349 | return self:render({ user = user }) 350 | end 351 | 352 | return self:redirect_to('/login') 353 | end 354 | ``` 355 | 356 | To set a cookie, use the `setcookie()` method of a response object and pass to 357 | it a Lua table defining the cookie to be set: 358 | 359 | ```lua 360 | function user_login(self) 361 | local login = self:param('login') 362 | local password = self:param('password') 363 | 364 | local user = box.select(users, 1, login, password) 365 | if user ~= nil then 366 | local resp = self:redirect_to('/') 367 | resp:setcookie({ name = 'uid', value = user[0], expires = '+1y' }) 368 | return resp 369 | end 370 | 371 | -- to login again and again and again 372 | return self:redirect_to('/login') 373 | end 374 | ``` 375 | 376 | The table must contain the following fields: 377 | 378 | * `name` 379 | * `value` 380 | * `path` (optional; if not set, the current request path is used) 381 | * `domain` (optional) 382 | * `expires` - cookie expire date, or expire offset, for example: 383 | 384 | * `1d` - 1 day 385 | * `+1d` - the same 386 | * `23d` - 23 days 387 | * `+1m` - 1 month (30 days) 388 | * `+1y` - 1 year (365 days) 389 | 390 | ## Rendering a template 391 | 392 | Lua can be used inside a response template, for example: 393 | 394 | ```html 395 | 396 | 397 | <%= title %> 398 | 399 | 400 |
    401 | % for i = 1, 10 do 402 |
  • <%= item[i].key %>: <%= item[i].value %>
  • 403 | % end 404 |
405 | 406 | 407 | ``` 408 | 409 | To embed Lua code into a template, use: 410 | 411 | * `<% lua-here %>` - insert any Lua code, including multi-line. 412 | Can be used anywhere in the template. 413 | * `% lua-here` - a single-line Lua substitution. Can only be 414 | present at the beginning of a line (with optional preceding spaces 415 | and tabs, which are ignored). 416 | 417 | A few control characters may follow `%`: 418 | 419 | * `=` (e.g., `<%= value + 1 %>`) - runs the embedded Lua code 420 | and inserts the result into HTML. Special HTML characters, 421 | such as `<`, `>`, `&`, `"`, are escaped. 422 | * `==` (e.g., `<%== value + 10 %>`) - the same, but without 423 | escaping. 424 | 425 | A Lua statement inside the template has access to the following 426 | environment: 427 | 428 | 1. Lua variables defined in the template, 429 | 1. stashed variables, 430 | 1. variables standing for keys in the `render` table. 431 | 432 | ## Template helpers 433 | 434 | Helpers are special functions that are available in all HTML 435 | templates. These functions must be defined when creating an `httpd` object. 436 | 437 | Setting or deleting a helper: 438 | 439 | ```lua 440 | -- setting a helper 441 | httpd:helper('time', function(self, ...) return box.time() end) 442 | -- deleting a helper 443 | httpd:helper('some_name', nil) 444 | ``` 445 | 446 | Using a helper inside an HTML template: 447 | 448 | ```html 449 |
450 | Current timestamp: <%= time() %> 451 |
452 | ``` 453 | 454 | A helper function can receive arguments. The first argument is 455 | always the current controller. The rest is whatever is 456 | passed to the helper from the template. 457 | 458 | ## Hooks 459 | 460 | It is possible to define additional functions invoked at various 461 | stages of request processing. 462 | 463 | ### `handler(httpd, req)` 464 | 465 | If `handler` is present in `httpd` options, it gets 466 | involved on every HTTP request, and the built-in routing 467 | mechanism is unused (no other hooks are called in this case). 468 | 469 | ### `before_dispatch(httpd, req)` 470 | 471 | Is invoked before a request is routed to a handler. The first 472 | argument of the hook is the HTTP request to be handled. 473 | The return value of the hook is ignored. 474 | 475 | This hook could be used to log a request, or modify request headers. 476 | 477 | ### `after_dispatch(cx, resp)` 478 | 479 | Is invoked after a handler for a route is executed. 480 | 481 | The arguments of the hook are the request passed into the handler, 482 | and the response produced by the handler. 483 | 484 | This hook can be used to modify the response. 485 | The return value of the hook is ignored. 486 | 487 | ## Using a special socket 488 | 489 | To use a special socket, override the `tcp_server_f` field of the HTTP server 490 | object with your own function. The function should return an object similar to 491 | one returned by [socket.tcp_server][socket_ref]. It should call `opts.handler` 492 | when a connection occurs and provide `read`, `write` and `close` methods. 493 | 494 | Example: 495 | 496 | ```lua 497 | local httpd = require('http.server') 498 | local server = httpd.new(settings.host, settings.port) 499 | 500 | -- Use sslsocket. 501 | local sslsocket = require('sslsocket') 502 | server.tcp_server_f = sslsocket.tcp_server 503 | 504 | -- Or use your own handler. 505 | server.tcp_server_f = function(host, port, opts) 506 | assert(type(opts) == 'table') 507 | local name = opts.name 508 | local accept_handler = opts.handler 509 | local http_server = opts.http_server 510 | 511 | <...> 512 | return <..tcp server object..> 513 | end 514 | 515 | server:route() 516 | server:start() 517 | ``` 518 | 519 | [socket_ref]: https://www.tarantool.io/en/doc/latest/reference/reference_lua/socket/#socket-tcp-server 520 | 521 | ## Roles 522 | 523 | Tarantool 3 roles could be accessed from this project. 524 | 525 | ### `roles.httpd` 526 | 527 | It allows configuring one or more HTTP servers. Those servers could be reused 528 | by several other roles. 529 | 530 | Example of the configuration: 531 | 532 | ```yaml 533 | roles_cfg: 534 | roles.httpd: 535 | default: 536 | listen: 8081 537 | additional: 538 | listen: '127.0.0.1:8082' 539 | ``` 540 | 541 | Server address should be provided either as a URI or as a single port 542 | (in this case, `0.0.0.0` address is used). 543 | 544 | User can access every working HTTP server from the configuration by name, 545 | using `require('roles.httpd').get_server(name)` method. 546 | If the `name` argument is `nil`, the default server is returned 547 | (its name should be equal to constant 548 | `require('roles.httpd').DEFAULT_SERVER_NAME`, which is `"default"`). 549 | 550 | Let's look at the example of using this role. Consider a new role 551 | `roles/hello_world.lua`: 552 | ```lua 553 | local M = { dependencies = { 'roles.httpd' } } 554 | local server = {} 555 | 556 | M.validate = function(conf) 557 | if conf == nil or conf.httpd == nil then 558 | error("httpd must be set") 559 | end 560 | local server = require('roles.httpd').get_server(conf.httpd) 561 | if server == nil then 562 | error("the httpd server " .. conf.httpd .. " not found") 563 | end 564 | end 565 | 566 | M.apply = function(conf) 567 | server = require('roles.httpd').get_server(conf.httpd) 568 | 569 | server:route({ 570 | path = '/hello/world', 571 | name = 'greeting', 572 | }, function(tx) 573 | return tx:render({text = 'Hello, world!'}) 574 | end) 575 | end 576 | 577 | M.stop = function() 578 | server:delete('greeting') 579 | end 580 | 581 | return M 582 | ``` 583 | 584 | To enable TLS, provide the following params into roles config (for proper work 585 | it's enough to provide only `ssl_key_file` and `ssl_cert_file`): 586 | 587 | ```yaml 588 | roles_cfg: 589 | roles.httpd: 590 | default: 591 | listen: 8081 592 | ssl_key_file: "path/to/key/file" 593 | ssl_cert_file: "path/to/key/file" 594 | ssl_ca_file: "path/to/key/file" 595 | ssl_ciphers: "cipher1:cipher2" 596 | ssl_password: "password" 597 | ssl_password_file: "path/to/ssl/password" 598 | ``` 599 | 600 | This role accepts a server by name from a config and creates a route to return 601 | `Hello, world!` to every request by this route. 602 | 603 | Then we need to write a simple config to start the Tarantool instance via 604 | `tt`: 605 | ```yaml 606 | app: 607 | file: 'myapp.lua' 608 | 609 | groups: 610 | group001: 611 | replicasets: 612 | replicaset001: 613 | roles: [roles.httpd, roles.hello_world] 614 | roles_cfg: 615 | roles.httpd: 616 | default: 617 | listen: 8081 618 | additional: 619 | listen: '127.0.0.1:8082' 620 | roles.hello_world: 621 | httpd: 'additional' 622 | instances: 623 | instance001: 624 | iproto: 625 | listen: 626 | - uri: '127.0.0.1:3301' 627 | ``` 628 | 629 | Next step, we need to start this instance using `tt start`: 630 | ```bash 631 | $ tt start 632 | • Starting an instance [app:instance001]... 633 | $ tt status 634 | INSTANCE STATUS PID MODE 635 | app:instance001 RUNNING 2499387 RW 636 | ``` 637 | 638 | And then, we can get the greeting by running a simple curl command from a 639 | terminal: 640 | ```bash 641 | $ curl http://127.0.0.1:8082/hello/world 642 | Hello, world! 643 | ``` 644 | 645 | ## See also 646 | 647 | * [Tarantool project][Tarantool] on GitHub 648 | * [Tests][] for the `http` module 649 | 650 | [Tarantool]: http://github.com/tarantool/tarantool 651 | [Tests]: https://github.com/tarantool/http/tree/master/test 652 | -------------------------------------------------------------------------------- /cmake/FindLuaCheck.cmake: -------------------------------------------------------------------------------- 1 | find_program(LUACHECK luacheck 2 | HINTS .rocks/ 3 | PATH_SUFFIXES bin 4 | DOC "Lua static analyzer" 5 | ) 6 | 7 | include(FindPackageHandleStandardArgs) 8 | find_package_handle_standard_args(LuaCheck 9 | REQUIRED_VARS LUACHECK 10 | ) 11 | 12 | mark_as_advanced(LUACHECK) 13 | -------------------------------------------------------------------------------- /cmake/FindLuaCov.cmake: -------------------------------------------------------------------------------- 1 | find_program(LUACOV luacov 2 | HINTS .rocks/ 3 | PATH_SUFFIXES bin 4 | DOC "Lua test coverage analysis" 5 | ) 6 | 7 | include(FindPackageHandleStandardArgs) 8 | find_package_handle_standard_args(LuaCov 9 | REQUIRED_VARS LUACOV 10 | ) 11 | 12 | mark_as_advanced(LUACOV) 13 | -------------------------------------------------------------------------------- /cmake/FindLuaCovCoveralls.cmake: -------------------------------------------------------------------------------- 1 | find_program(LUACOVCOVERALLS luacov-coveralls 2 | HINTS .rocks/ 3 | PATH_SUFFIXES bin 4 | DOC "LuaCov reporter for coveralls.io service" 5 | ) 6 | 7 | include(FindPackageHandleStandardArgs) 8 | find_package_handle_standard_args(LuaCovCoveralls 9 | REQUIRED_VARS LUACOVCOVERALLS 10 | ) 11 | 12 | mark_as_advanced(LUACOVCOVERALLS) 13 | -------------------------------------------------------------------------------- /cmake/FindLuaTest.cmake: -------------------------------------------------------------------------------- 1 | find_program(LUATEST luatest 2 | HINTS .rocks/ 3 | PATH_SUFFIXES bin 4 | DOC "Lua testing framework" 5 | ) 6 | 7 | include(FindPackageHandleStandardArgs) 8 | find_package_handle_standard_args(LuaTest 9 | REQUIRED_VARS LUATEST 10 | ) 11 | 12 | mark_as_advanced(LUATEST) 13 | -------------------------------------------------------------------------------- /cmake/FindTarantool.cmake: -------------------------------------------------------------------------------- 1 | # Define GNU standard installation directories 2 | include(GNUInstallDirs) 3 | 4 | macro(extract_definition name output input) 5 | string(REGEX MATCH "#define[\t ]+${name}[\t ]+\"([^\"]*)\"" 6 | _t "${input}") 7 | string(REGEX REPLACE "#define[\t ]+${name}[\t ]+\"(.*)\"" "\\1" 8 | ${output} "${_t}") 9 | endmacro() 10 | 11 | find_path(TARANTOOL_INCLUDE_DIR tarantool/module.h 12 | HINTS ${TARANTOOL_DIR} ENV TARANTOOL_DIR 13 | PATH_SUFFIXES include 14 | ) 15 | 16 | if(TARANTOOL_INCLUDE_DIR) 17 | set(_config "-") 18 | file(READ "${TARANTOOL_INCLUDE_DIR}/tarantool/module.h" _config0) 19 | string(REPLACE "\\" "\\\\" _config ${_config0}) 20 | unset(_config0) 21 | extract_definition(PACKAGE_VERSION TARANTOOL_VERSION ${_config}) 22 | extract_definition(INSTALL_PREFIX _install_prefix ${_config}) 23 | unset(_config) 24 | endif() 25 | 26 | include(FindPackageHandleStandardArgs) 27 | find_package_handle_standard_args(Tarantool 28 | REQUIRED_VARS TARANTOOL_INCLUDE_DIR VERSION_VAR TARANTOOL_VERSION) 29 | if(TARANTOOL_FOUND) 30 | set(TARANTOOL_INCLUDE_DIRS "${TARANTOOL_INCLUDE_DIR}" 31 | "${TARANTOOL_INCLUDE_DIR}/tarantool/" 32 | CACHE PATH "Include directories for Tarantool") 33 | set(TARANTOOL_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/tarantool" 34 | CACHE PATH "Directory for storing Lua modules written in Lua") 35 | set(TARANTOOL_INSTALL_LUADIR "${CMAKE_INSTALL_DATADIR}/tarantool" 36 | CACHE PATH "Directory for storing Lua modules written in C") 37 | 38 | if (NOT TARANTOOL_FIND_QUIETLY AND NOT FIND_TARANTOOL_DETAILS) 39 | set(FIND_TARANTOOL_DETAILS ON CACHE INTERNAL "Details about TARANTOOL") 40 | message(STATUS "Tarantool LUADIR is ${TARANTOOL_INSTALL_LUADIR}") 41 | message(STATUS "Tarantool LIBDIR is ${TARANTOOL_INSTALL_LIBDIR}") 42 | endif () 43 | endif() 44 | mark_as_advanced(TARANTOOL_INCLUDE_DIRS TARANTOOL_INSTALL_LIBDIR 45 | TARANTOOL_INSTALL_LUADIR) 46 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | tarantool-http/ 2 | files 3 | stamp-* 4 | *.substvars 5 | *.log 6 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | tarantool-http (1.0.0-1) unstable; urgency=medium 2 | 3 | * Initial release. 4 | 5 | -- Roman Tsisyk Thu, 18 Feb 2016 11:19:56 +0300 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: tarantool-http 2 | Priority: optional 3 | Section: database 4 | Maintainer: Roman Tsisyk 5 | Build-Depends: debhelper (>= 9), cdbs, 6 | cmake (>= 2.8), 7 | tarantool-dev (>= 1.7.5.0) 8 | Standards-Version: 3.9.6 9 | Version: 1:1.1.1 10 | Homepage: https://github.com/tarantool/http 11 | Vcs-Git: git://github.com/tarantool/http.git 12 | Vcs-Browser: https://github.com/tarantool/http 13 | 14 | Package: tarantool-http 15 | Architecture: i386 amd64 armhf arm64 16 | Depends: tarantool (>= 1.7.5.0), ${shlibs:Depends}, ${misc:Depends} 17 | Pre-Depends: ${misc:Pre-Depends} 18 | Description: HTTP server for Tarantool 19 | This package provides a HTTP server for Tarantool. 20 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Debianized-By: Roman Tsisyk 3 | Upstream-Name: tarantool-http 4 | Upstream-Contact: roman@tarantool.org 5 | Source: https://github.com/tarantool/http 6 | 7 | Files: * 8 | Copyright: 2010-2013 by Tarantool AUTHORS, please see AUTHORS file. 9 | License: BSD-2-Clause 10 | Redistribution and use in source and binary forms, with or 11 | without modification, are permitted provided that the following 12 | conditions are met: 13 | . 14 | 1. Redistributions of source code must retain the above 15 | copyright notice, this list of conditions and the 16 | following disclaimer. 17 | . 18 | 2. Redistributions in binary form must reproduce the above 19 | copyright notice, this list of conditions and the following 20 | disclaimer in the documentation and/or other materials 21 | provided with the distribution. 22 | . 23 | THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND 24 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 25 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 27 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 30 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 31 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 32 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 34 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 35 | SUCH DAMAGE. 36 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.md 2 | AUTHORS 3 | -------------------------------------------------------------------------------- /debian/prebuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -o pipefail 4 | 5 | curl -LsSf https://www.tarantool.io/release/1.10/installer.sh | sudo bash 6 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | DEB_CMAKE_EXTRA_FLAGS := -DCMAKE_INSTALL_LIBDIR=lib/$(DEB_HOST_MULTIARCH) \ 4 | -DCMAKE_BUILD_TYPE=RelWithDebInfo 5 | 6 | include /usr/share/cdbs/1/rules/debhelper.mk 7 | include /usr/share/cdbs/1/class/cmake.mk 8 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Call this script to install test dependencies. 4 | 5 | set -e 6 | 7 | # Test dependencies: 8 | # Could be replaced with luatest >= 1.1.0 after a release. 9 | tt rocks install luatest 10 | tt rocks install luacheck 0.25.0 11 | tt rocks install luacov 0.13.0 12 | tt rocks install luafilesystem 1.7.0-2 13 | 14 | # cluacov, luacov-coveralls and dependencies 15 | tt rocks install luacov-coveralls 0.2.3-1 --server=https://luarocks.org 16 | tt rocks install cluacov 0.1.2-1 --server=https://luarocks.org 17 | 18 | tt rocks make 19 | -------------------------------------------------------------------------------- /http-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'http' 2 | version = 'scm-1' 3 | source = { 4 | url = 'git+https://github.com/tarantool/http.git', 5 | branch = 'master', 6 | } 7 | description = { 8 | summary = "HTTP server for Tarantool", 9 | homepage = 'https://github.com/tarantool/http/', 10 | license = 'BSD', 11 | } 12 | dependencies = { 13 | 'lua >= 5.1' 14 | } 15 | external_dependencies = { 16 | TARANTOOL = { 17 | header = "tarantool/module.h" 18 | } 19 | } 20 | build = { 21 | type = 'builtin', 22 | 23 | modules = { 24 | ['http.lib'] = { 25 | sources = 'http/lib.c', 26 | incdirs = { 27 | "$(TARANTOOL_INCDIR)" 28 | } 29 | }, 30 | ['http.server'] = 'http/server.lua', 31 | ['http.sslsocket'] = 'http/sslsocket.lua', 32 | ['http.version'] = 'http/version.lua', 33 | ['http.mime_types'] = 'http/mime_types.lua', 34 | ['http.codes'] = 'http/codes.lua', 35 | ['roles.httpd'] = 'roles/httpd.lua', 36 | } 37 | } 38 | 39 | -- vim: syntax=lua 40 | -------------------------------------------------------------------------------- /http/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Build 2 | if (APPLE) 3 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -undefined suppress -flat_namespace") 4 | endif(APPLE) 5 | 6 | add_library(httpd SHARED lib.c) 7 | set_target_properties(httpd 8 | PROPERTIES 9 | PREFIX "" 10 | SUFFIX ".so" 11 | OUTPUT_NAME "lib") 12 | 13 | # Install 14 | install(TARGETS httpd LIBRARY DESTINATION ${TARANTOOL_INSTALL_LIBDIR}/http) 15 | install(FILES server.lua DESTINATION ${TARANTOOL_INSTALL_LUADIR}/http) 16 | install(FILES version.lua DESTINATION ${TARANTOOL_INSTALL_LUADIR}/http) 17 | install(FILES mime_types.lua DESTINATION ${TARANTOOL_INSTALL_LUADIR}/http) 18 | install(FILES codes.lua DESTINATION ${TARANTOOL_INSTALL_LUADIR}/http) 19 | -------------------------------------------------------------------------------- /http/codes.lua: -------------------------------------------------------------------------------- 1 | return { 2 | [101] = 'Switching Protocols', 3 | [200] = 'Ok', 4 | [201] = 'Created', 5 | [202] = 'Accepted', 6 | [203] = 'Non authoritative information', 7 | [204] = 'No content', 8 | [205] = 'Reset content', 9 | [206] = 'Partial content', 10 | [207] = 'Multi status', 11 | [208] = 'Already reported', 12 | [226] = 'IM used', 13 | [300] = 'Multiple choises', 14 | [301] = 'Moved permanently', 15 | [302] = 'Found', 16 | [303] = 'See other', 17 | [304] = 'Not modified', 18 | [305] = 'Use proxy', 19 | [307] = 'Temporary redirect', 20 | [400] = 'Bad request', 21 | [401] = 'Unauthorized', 22 | [402] = 'Payment required', 23 | [403] = 'Forbidden', 24 | [404] = 'Not found', 25 | [405] = 'Method not allowed', 26 | [406] = 'Not acceptable', 27 | [407] = 'Proxy authentification required', 28 | [408] = 'Request timeout', 29 | [409] = 'Conflict', 30 | [410] = 'Gone', 31 | [411] = 'Length required', 32 | [412] = 'Precondition failed', 33 | [413] = 'Request entity too large', 34 | [414] = 'Request uri too large', 35 | [415] = 'Unsupported media type', 36 | [416] = 'Request range not satisfiable', 37 | [417] = 'Expectation failed', 38 | [418] = 'I am a teapot', 39 | [422] = 'Unprocessable entity', 40 | [423] = 'Locked', 41 | [424] = 'Failed dependency', 42 | [425] = 'No code', 43 | [426] = 'Upgrade required', 44 | [428] = 'Precondition required', 45 | [429] = 'Too many requests', 46 | [431] = 'Request header fields too large', 47 | [449] = 'Retry with', 48 | [451] = 'Unavailable for legal reasons', 49 | [456] = 'Unrecoverable error', 50 | [500] = 'Internal server error', 51 | [501] = 'Not implemented', 52 | [502] = 'Bad gateway', 53 | [503] = 'Service unavailable', 54 | [504] = 'Gateway timeout', 55 | [505] = 'Http version not supported', 56 | [506] = 'Variant also negotiates', 57 | [507] = 'Insufficient storage', 58 | [509] = 'Bandwidth limit exceeded', 59 | [510] = 'Not extended', 60 | [511] = 'Network authentication required', 61 | } 62 | -------------------------------------------------------------------------------- /http/httpfast.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | 4 | static inline const char * 5 | httpfast_parse_params(const char *str, size_t str_len, 6 | int (*on_param)(void *uobj, 7 | const char *name, size_t name_len, 8 | const char *value, size_t value_len), void *uobj) 9 | { 10 | enum { 11 | name, 12 | value 13 | } state = name; 14 | 15 | const char *p, *pe; 16 | 17 | const char *nb, *vb; 18 | size_t nl, vl; 19 | 20 | for (nb = p = str, pe = str + str_len; p < pe; p++) { 21 | char c = *p; 22 | switch(state) { 23 | case name: 24 | if (c == '=') { 25 | nl = p - nb; 26 | vb = p + 1; 27 | state = value; 28 | break; 29 | } 30 | 31 | if (c == '&') { 32 | if (p == nb) { 33 | nb = p + 1; 34 | break; 35 | } 36 | nl = p - nb; 37 | if (on_param(uobj, nb, nl, "", 0) != 0) 38 | return p; 39 | nb = p + 1; 40 | break; 41 | } 42 | break; 43 | case value: 44 | if (c != '&') 45 | break; 46 | vl = p - vb; 47 | 48 | if (vl || nl) 49 | if (on_param(uobj, nb, nl, vb, vl) != 0) 50 | return p; 51 | 52 | nb = p + 1; 53 | state = name; 54 | break; 55 | } 56 | } 57 | switch(state) { 58 | case value: 59 | vl = pe - vb; 60 | if (vl || nl) 61 | on_param(uobj, nb, nl, vb, vl); 62 | break; 63 | case name: 64 | nl = pe - nb; 65 | if (nl) 66 | on_param(uobj, nb, nl, "", 0); 67 | break; 68 | 69 | } 70 | 71 | return NULL; 72 | } 73 | 74 | 75 | struct parse_http_events { 76 | void (*on_error)(void *uobj, int code, const char *fmt, va_list ap); 77 | void (*on_warn)(void *uobj, int code, const char *fmt, va_list ap); 78 | 79 | int (*on_header)(void *uobj, 80 | const char *name, size_t name_len, 81 | const char *value, size_t value_len, 82 | int is_continuation); 83 | int (*on_body)(void *uobj, const char *body, size_t body_len); 84 | 85 | int (*on_request_line)( 86 | void *uobj, 87 | const char *method, 88 | size_t method_len, 89 | const char *path, 90 | size_t path_len, 91 | 92 | const char *query, 93 | size_t query_len, 94 | 95 | int http_major, 96 | int http_minor 97 | ); 98 | int (*on_response_line)( 99 | void *uobj, 100 | unsigned code, 101 | const char *reason, 102 | size_t reason_len, 103 | int http_major, 104 | int http_minor 105 | ); 106 | }; 107 | 108 | 109 | enum { 110 | HTTP_PARSER_WRONG_ARGUMENTS = -512, 111 | HTTP_PARSER_IS_NOT_REALIZED_YET, 112 | 113 | HTTP_PARSER_BROKEN_REQUEST_LINE, 114 | HTTP_PARSER_BROKEN_RESPONSE_LINE, 115 | 116 | HTTP_PARSER_BROKEN_LINEDIVIDER, 117 | HTTP_PARSER_BROKEN_HEADER 118 | }; 119 | 120 | 121 | static inline 122 | void emit_errwarn( 123 | void (*cb)(void *uobj, int code, const char *fmt, va_list ap), 124 | 125 | void *uobj, int code, const char *fmt, ...) 126 | { 127 | va_list ap; 128 | va_start(ap, fmt); 129 | if (cb) 130 | cb(uobj, code, fmt, ap); 131 | va_end(ap); 132 | } 133 | 134 | /** 135 | * parse http request (header) 136 | */ 137 | 138 | static inline const char * 139 | httpfast_parse( 140 | const char *str, size_t len, 141 | const struct parse_http_events *event, 142 | void *uobj) 143 | { 144 | #define errorf(code, fmt...) \ 145 | do { \ 146 | emit_errwarn(event->on_error, uobj, code, fmt); \ 147 | return NULL; \ 148 | } while(0) 149 | 150 | #define warnf(code, fmt...) \ 151 | do { \ 152 | emit_errwarn(event->on_warn, uobj, code, fmt); \ 153 | } while(0) 154 | 155 | #define emit_event(name, arg...) \ 156 | do { \ 157 | if (event->name) { \ 158 | if (event->name(uobj, arg) != 0) { \ 159 | return NULL; \ 160 | } \ 161 | } \ 162 | } while(0) 163 | 164 | 165 | 166 | static const char lowcase[] = 167 | "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 168 | "\0\0\0\0\0\0\0\0\0\0\0\0\0-\0\0" "0123456789\0\0\0\0\0\0" 169 | "\0abcdefghijklmnopqrstuvwxyz\0\0\0\0_" 170 | "\0abcdefghijklmnopqrstuvwxyz\0\0\0\0\0" 171 | "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 172 | "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 173 | "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 174 | "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; 175 | 176 | 177 | if (event->on_request_line && event->on_response_line) 178 | errorf( 179 | HTTP_PARSER_WRONG_ARGUMENTS, 180 | "Only one of handlers must be defined: " 181 | "on_request_line or on_response_line" 182 | ); 183 | 184 | if (len == 0) 185 | errorf(HTTP_PARSER_WRONG_ARGUMENTS, "Empty input string"); 186 | 187 | enum { 188 | CR = 13, 189 | LF = 10, 190 | TAB = 9, 191 | }; 192 | 193 | typedef enum { 194 | request_line, 195 | method, 196 | path, 197 | query, 198 | rhttp, 199 | 200 | response_line, 201 | status_sp, 202 | status, 203 | message_sp, 204 | message, 205 | 206 | cr, 207 | lf, 208 | 209 | header_next, 210 | header_name, 211 | header_sp, 212 | header_cl_sp, 213 | header_val, 214 | } pstate; 215 | 216 | pstate state; 217 | if (event->on_request_line) { 218 | state = request_line; 219 | } else if (event->on_response_line) { 220 | state = response_line; 221 | } else { 222 | state = header_next; 223 | } 224 | 225 | const char *p, /* pointer */ 226 | *pe, /* end of data */ 227 | *tb, /* begin of token */ 228 | 229 | *ptb, /* begin of prev token */ 230 | *pptb /* begin of prev prev token */ 231 | ; 232 | size_t tl; /* length of token */ 233 | size_t ptl; /* length of prev token */ 234 | size_t pptl; /* length of prev prev token */ 235 | int h_is_c; /* header is continuation */ 236 | int headers = 0; /* how many headers found */ 237 | char c; 238 | 239 | char major, minor; 240 | unsigned code; 241 | 242 | for (p = str, pe = str + len; p < pe; p++) { 243 | redo: 244 | c = *p; 245 | switch(state) { 246 | /*********************** request line ************************/ 247 | case request_line: 248 | /* 'GET / HTTP/1.0' - min = 14 */ 249 | if (pe - p < 14) { 250 | errorf(HTTP_PARSER_BROKEN_REQUEST_LINE, 251 | "Broken request line" 252 | ); 253 | } 254 | state = method; 255 | tb = p; 256 | goto redo; 257 | 258 | case method: 259 | if (c != ' ' && c != '\t') 260 | break; 261 | pptb = tb; 262 | pptl = p - tb; 263 | tb = p + 1; 264 | state = path; 265 | break; 266 | 267 | 268 | 269 | case path: 270 | if (c == '?') { 271 | state = query; 272 | 273 | ptb = tb; 274 | ptl = p - tb; 275 | 276 | tb = p + 1; 277 | break; 278 | } 279 | 280 | if (c == ' ' || c == '\t') { 281 | state = rhttp; 282 | ptl = p - tb; 283 | ptb = tb; 284 | tb = p; 285 | tl = 0; 286 | break; 287 | } 288 | break; 289 | 290 | case query: 291 | if (c != ' ' && c != '\t') 292 | break; 293 | tl = p - tb; 294 | state = rhttp; 295 | break; 296 | 297 | 298 | case rhttp: 299 | /* H T T P / 1 . 0 */ 300 | /* 0 1 2 3 4 5 6 7 */ 301 | if (pe - p < 8) { 302 | errorf(HTTP_PARSER_BROKEN_REQUEST_LINE, 303 | "Too short request line" 304 | ); 305 | } 306 | if (memcmp(p, "HTTP/", 5) != 0 || p[6] != '.') { 307 | errorf(HTTP_PARSER_BROKEN_REQUEST_LINE, 308 | "Broken protocol section in request line" 309 | ); 310 | } 311 | if (p[5] > '9' || p[5] < '0' || p[7] > '9' || p[7] < '0') { 312 | errorf(HTTP_PARSER_BROKEN_REQUEST_LINE, 313 | "Wrong protocol version in request line" 314 | ); 315 | } 316 | emit_event(on_request_line, 317 | pptb, pptl, 318 | ptb, ptl, 319 | tb, tl, 320 | (int)(p[5] - '0'), 321 | (int)(p[7] - '0') 322 | ); 323 | 324 | p += 7; 325 | 326 | 327 | state = cr; 328 | break; 329 | 330 | 331 | /************************* response line **********************/ 332 | case response_line: 333 | if (pe - p < 15) { 334 | errorf(HTTP_PARSER_BROKEN_RESPONSE_LINE, 335 | "Too short response line" 336 | ); 337 | } 338 | if (memcmp(p, "HTTP/", 5) != 0 && p[6] != '.') { 339 | errorf(HTTP_PARSER_BROKEN_RESPONSE_LINE, 340 | "Protocol section is not valid in response line" 341 | ); 342 | } 343 | if (p[5] > '9' || p[5] < '0' || p[7] > '9' || p[7] < 0) { 344 | errorf(HTTP_PARSER_BROKEN_RESPONSE_LINE, 345 | "Wrong http version number: %c.%c in response line", 346 | p[5], 347 | p[7] 348 | ); 349 | } 350 | 351 | if (p[8] != ' ' && p[8] != '\t') { 352 | errorf(HTTP_PARSER_BROKEN_RESPONSE_LINE, 353 | "Broken protocol section in response line: %*s", 354 | 9, p 355 | ); 356 | } 357 | 358 | major = p[5] - '0'; 359 | minor = p[7] - '0'; 360 | 361 | p += 8 - 1; /* 'HTTP/x.y' - cycle increment */ 362 | tb = p + 1; 363 | state = status_sp; 364 | break; 365 | 366 | case status_sp: 367 | if (c == ' ' || c == '\t') 368 | break; 369 | 370 | state = status; 371 | tb = p; 372 | code = 0; 373 | 374 | case status: 375 | if (c == ' ' || c == '\t') { 376 | tb = p; 377 | state = message_sp; 378 | break; 379 | 380 | } 381 | if (c > '9' || c < '0') { 382 | errorf(HTTP_PARSER_BROKEN_RESPONSE_LINE, 383 | "Non-digit symbol in code in response line: %02X", c); 384 | } 385 | code *= 10; 386 | code += c - '0'; 387 | break; 388 | 389 | case message_sp: 390 | if (c == ' ' || c == '\t') 391 | break; 392 | tb = p; 393 | state = message; 394 | 395 | case message: 396 | if (c != CR && c != LF) 397 | break; 398 | tl = p - tb; 399 | 400 | emit_event(on_response_line, code, tb, tl, major, minor); 401 | state = cr; 402 | goto redo; 403 | 404 | 405 | 406 | /************************ headers *****************************/ 407 | case cr: 408 | if (c == LF) { 409 | state = header_next; 410 | break; 411 | } 412 | 413 | if (c != CR) { 414 | errorf(HTTP_PARSER_BROKEN_LINEDIVIDER, 415 | "Expected CR or LF, received: %02x", 416 | c 417 | ); 418 | } 419 | state = lf; 420 | break; 421 | 422 | case lf: 423 | if (c != LF) { 424 | errorf(HTTP_PARSER_BROKEN_LINEDIVIDER, 425 | "Expected LF, received: %02x", 426 | c 427 | ); 428 | } 429 | state = header_next; 430 | break; 431 | 432 | 433 | case header_next: 434 | if (c == ' ' || c == '\t') { 435 | if (!headers) { 436 | errorf(HTTP_PARSER_BROKEN_HEADER, 437 | "Continuation for header at the first header" 438 | ); 439 | } 440 | state = header_cl_sp; 441 | h_is_c = 1; 442 | break; 443 | } 444 | if (c == LF) { 445 | emit_event(on_body, p + 1, pe - p - 1); 446 | return p + 1; 447 | } 448 | if (c == CR) { 449 | if (p < pe - 1) { 450 | if (p[1] == LF) { 451 | emit_event(on_body, p + 2, pe - p - 2); 452 | } else { 453 | errorf(HTTP_PARSER_BROKEN_HEADER, 454 | "Unexpected sequience: CR, %02X", 455 | p[1] 456 | ); 457 | } 458 | } else { 459 | emit_event(on_body, "", 0); 460 | } 461 | return p + 1; 462 | } 463 | if (!lowcase[(int)c]) { 464 | errorf(HTTP_PARSER_BROKEN_HEADER, 465 | "Broken first symbol of header: %02X", 466 | c 467 | ); 468 | } 469 | headers++; 470 | tb = p; 471 | state = header_name; 472 | break; 473 | 474 | case header_name: 475 | if (lowcase[(int)c]) 476 | break; 477 | 478 | h_is_c = 0; 479 | if (c == ':') { 480 | tl = p - tb; 481 | state = header_cl_sp; 482 | break; 483 | } 484 | 485 | if (c == ' ' || c == TAB) { 486 | tl = p - tb; 487 | state = header_sp; 488 | break; 489 | } 490 | 491 | if (c == CR || c == LF) { 492 | state = header_next; 493 | break; 494 | } 495 | 496 | errorf(HTTP_PARSER_BROKEN_HEADER, 497 | "Unexpected symbol in header name: %02X", 498 | c 499 | ); 500 | /* header value */ 501 | case header_val: 502 | if (c != CR && c != LF) { 503 | break; 504 | } 505 | ptl = p - ptb; 506 | emit_event(on_header, tb, tl, ptb, ptl, h_is_c); 507 | state = cr; 508 | goto redo; 509 | 510 | /* spaces between header ':' and value */ 511 | case header_cl_sp: 512 | if (c == TAB || c == ' ') 513 | break; 514 | 515 | if (c == CR || c == LF) { 516 | emit_event(on_header, tb, tl, "", 0, h_is_c); 517 | state = cr; 518 | goto redo; 519 | } 520 | 521 | 522 | ptb = p; 523 | state = header_val; 524 | break; 525 | 526 | /* spaces between headername and ':' */ 527 | case header_sp: 528 | if (c == TAB || c == ' ') 529 | break; 530 | if (c == ':') { 531 | state = header_cl_sp; 532 | break; 533 | } 534 | errorf(HTTP_PARSER_BROKEN_HEADER, 535 | "Expected ':', received '%c' (%02x)", 536 | c, 537 | c 538 | ); 539 | 540 | } 541 | } 542 | 543 | /* unfinished parsing */ 544 | switch(state) { 545 | case header_val: 546 | ptl = pe - ptb; 547 | emit_event(on_header, tb, tl, ptb, ptl, h_is_c); 548 | break; 549 | 550 | case method: 551 | case path: 552 | errorf(HTTP_PARSER_BROKEN_REQUEST_LINE, 553 | "Unexpected EOF while parsing request line" 554 | ); 555 | 556 | case message: 557 | tl = pe - tb; 558 | emit_event(on_response_line, code, tb, tl, major, minor); 559 | break; 560 | 561 | case message_sp: 562 | emit_event(on_response_line, code, "", 0, major, minor); 563 | break; 564 | 565 | case status_sp: 566 | case status: 567 | errorf(HTTP_PARSER_BROKEN_RESPONSE_LINE, 568 | "Unexpected EOF while parsing response line" 569 | ); 570 | default: 571 | break; 572 | 573 | } 574 | 575 | return str; 576 | 577 | #undef warnf 578 | #undef errorf 579 | #undef emit_event 580 | } 581 | 582 | -------------------------------------------------------------------------------- /http/lib.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Redistribution and use in source and binary forms, with or 4 | * without modification, are permitted provided that the following 5 | * conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above 8 | * copyright notice, this list of conditions and the 9 | * following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials 14 | * provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 18 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 20 | * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 21 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 24 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 27 | * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 28 | * SUCH DAMAGE. 29 | */ 30 | 31 | #include 32 | #include 33 | #include 34 | 35 | #include 36 | #include 37 | #include 38 | 39 | #include "tpleval.h" 40 | #include "httpfast.h" 41 | 42 | static void 43 | tpl_term(int type, const char *str, size_t len, void *data) 44 | { 45 | luaL_Buffer *b = (luaL_Buffer *)data; 46 | size_t i; 47 | 48 | switch(type) { 49 | case TPE_TEXT: 50 | luaL_addstring(b, "_i(\""); 51 | for(i = 0; i < len; i++) { 52 | switch(str[i]) { 53 | case '\n': 54 | luaL_addstring(b, 55 | "\\n\" ..\n\""); 56 | break; 57 | case '\r': 58 | luaL_addstring(b, 59 | "\\r"); 60 | break; 61 | case '"': 62 | luaL_addchar(b, '\\'); 63 | default: 64 | luaL_addchar(b, str[i]); 65 | break; 66 | } 67 | } 68 | luaL_addstring(b, "\") "); 69 | break; 70 | case TPE_LINECODE: 71 | case TPE_MULTILINE_CODE: 72 | /* _i one line */ 73 | if (len > 1 && str[0] == '=' && str[1] == '=') { 74 | luaL_addstring(b, "_i("); 75 | luaL_addlstring(b, str + 2, len - 2); 76 | luaL_addstring(b, ") "); 77 | break; 78 | } 79 | /* _q one line */ 80 | if (len > 0 && str[0] == '=') { 81 | luaL_addstring(b, "_q("); 82 | luaL_addlstring(b, str + 1, len - 1); 83 | luaL_addstring(b, ") "); 84 | break; 85 | } 86 | luaL_addlstring(b, str, len); 87 | luaL_addchar(b, ' '); 88 | break; 89 | default: 90 | abort(); 91 | } 92 | } 93 | 94 | /** 95 | * This function exists because lua_tostring does not use 96 | * __tostring metamethod, and this metamethod has to be used 97 | * if we want to print Lua userdata correctly. 98 | */ 99 | static const char * 100 | luaT_tolstring(lua_State *L, int idx, size_t *len) 101 | { 102 | if (!luaL_callmeta(L, idx, "__tostring")) { 103 | switch (lua_type(L, idx)) { 104 | case LUA_TNUMBER: 105 | case LUA_TSTRING: 106 | lua_pushvalue(L, idx); 107 | break; 108 | case LUA_TBOOLEAN: { 109 | int val = lua_toboolean(L, idx); 110 | lua_pushstring(L, val ? "true" : "false"); 111 | break; 112 | } 113 | case LUA_TNIL: 114 | lua_pushliteral(L, "nil"); 115 | break; 116 | default: 117 | lua_pushfstring(L, "%s: %p", luaL_typename(L, idx), 118 | lua_topointer(L, idx)); 119 | } 120 | } 121 | 122 | return lua_tolstring(L, -1, len); 123 | } 124 | 125 | static int 126 | lbox_httpd_escape_html(struct lua_State *L) 127 | { 128 | int idx = lua_upvalueindex(1); 129 | 130 | int i, top = lua_gettop(L); 131 | lua_rawgeti(L, idx, 1); 132 | 133 | luaL_Buffer b; 134 | luaL_buffinit(L, &b); 135 | 136 | if (lua_isnil(L, -1)) { 137 | luaL_addstring(&b, ""); 138 | } else { 139 | luaL_addvalue(&b); 140 | } 141 | 142 | for (i = 1; i <= top; i++) { 143 | const char *s = luaT_tolstring(L, i, NULL); 144 | int str_idx = lua_gettop(L); 145 | for (; *s; s++) { 146 | switch(*s) { 147 | case '&': 148 | luaL_addstring(&b, "&"); 149 | break; 150 | case '<': 151 | luaL_addstring(&b, "<"); 152 | break; 153 | case '>': 154 | luaL_addstring(&b, ">"); 155 | break; 156 | case '"': 157 | luaL_addstring(&b, """); 158 | break; 159 | case '\'': 160 | luaL_addstring(&b, "'"); 161 | break; 162 | default: 163 | luaL_addchar(&b, *s); 164 | break; 165 | } 166 | } 167 | lua_remove(L, str_idx); 168 | } 169 | 170 | luaL_pushresult(&b); 171 | lua_rawseti(L, idx, 1); 172 | return 0; 173 | } 174 | 175 | static int 176 | lbox_httpd_immediate_html(struct lua_State *L) 177 | { 178 | int idx = lua_upvalueindex(1); 179 | 180 | int k = 0; 181 | int i, top = lua_gettop(L); 182 | 183 | lua_rawgeti(L, idx, 1); 184 | if (lua_isnil(L, -1)) { 185 | lua_pop(L, 1); 186 | } else { 187 | ++k; 188 | } 189 | 190 | lua_checkstack(L, top - 1); 191 | for (i = 1; i <= top; i++) { 192 | if (lua_isnil(L, i)) { 193 | lua_pushliteral(L, "nil"); 194 | ++k; 195 | continue; 196 | } 197 | lua_pushvalue(L, i); 198 | ++k; 199 | } 200 | 201 | lua_concat(L, k); 202 | lua_rawseti(L, idx, 1); 203 | return 0; 204 | } 205 | 206 | static int 207 | lbox_httpd_template(struct lua_State *L) 208 | { 209 | int top = lua_gettop(L); 210 | if (top == 1) 211 | lua_newtable(L); 212 | if (top != 2) 213 | luaL_error(L, "box.httpd.template: absent or spare argument"); 214 | if (!lua_istable(L, 2)) 215 | luaL_error(L, "usage: box.httpd.template(tpl, { var = val })"); 216 | 217 | 218 | lua_newtable(L); /* 3. results (closure table) */ 219 | 220 | lua_pushnil(L); /* 4. place for prepared html */ 221 | 222 | lua_pushnil(L); /* 5. place for process function */ 223 | 224 | lua_pushvalue(L, 3); /* _q */ 225 | lua_pushcclosure(L, lbox_httpd_escape_html, 1); 226 | 227 | lua_pushvalue(L, 3); /* _i */ 228 | lua_pushcclosure(L, lbox_httpd_immediate_html, 1); 229 | 230 | size_t len; 231 | const char *str = lua_tolstring(L, 1, &len); 232 | 233 | luaL_Buffer b; 234 | luaL_buffinit(L, &b); 235 | 236 | luaL_addstring(&b, "return function(_q, _i"); 237 | 238 | lua_pushnil(L); 239 | while(lua_next(L, 2) != 0) { 240 | size_t l; 241 | const char *s = lua_tolstring(L, -2, &l); 242 | 243 | /* TODO: check argument for lua syntax */ 244 | 245 | luaL_addstring(&b, ", "); 246 | luaL_addlstring(&b, s, l); 247 | 248 | lua_pushvalue(L, -2); 249 | lua_remove(L, -3); 250 | } 251 | 252 | luaL_addstring(&b, ") "); 253 | 254 | tpe_parse(str, len, tpl_term, &b); 255 | 256 | luaL_addstring(&b, " end"); 257 | 258 | luaL_pushresult(&b); 259 | 260 | lua_replace(L, 4); 261 | 262 | lua_pushvalue(L, 4); 263 | 264 | /* compile */ 265 | if (luaL_dostring(L, lua_tostring(L, 4)) != 0) 266 | lua_error(L); 267 | 268 | lua_replace(L, 5); /* process function */ 269 | 270 | /* stack: 271 | 1 - user's template, 272 | 2 - user's arglist 273 | 3 - closure table 274 | 4 - prepared html 275 | 5 - compiled function 276 | ... function arguments 277 | */ 278 | 279 | if (lua_pcall(L, lua_gettop(L) - 5, 0, 0) != 0) { 280 | lua_getfield(L, -1, "match"); 281 | 282 | lua_pushvalue(L, -2); 283 | lua_pushliteral(L, ":(%d+):(.*)"); 284 | lua_call(L, 2, 2); 285 | 286 | lua_getfield(L, -1, "format"); 287 | lua_pushliteral(L, "box.httpd.template: users template:%s: %s"); 288 | lua_pushvalue(L, -4); 289 | lua_pushvalue(L, -4); 290 | lua_call(L, 3, 1); 291 | 292 | lua_error(L); 293 | } 294 | 295 | lua_pushnumber(L, 1); 296 | lua_rawget(L, 3); 297 | lua_replace(L, 3); 298 | 299 | return 2; 300 | } 301 | 302 | static void 303 | http_parser_on_error(void *uobj, int code, const char *fmt, va_list ap) 304 | { 305 | struct lua_State *L = (struct lua_State *)uobj; 306 | char estr[256]; 307 | vsnprintf(estr, 256, fmt, ap); 308 | lua_pushliteral(L, "error"); 309 | lua_pushstring(L, estr); 310 | lua_rawset(L, -4); 311 | 312 | (void)code; 313 | } 314 | 315 | static int 316 | http_parser_on_header(void *uobj, const char *name, size_t name_len, 317 | const char *value, size_t value_len, int is_continuation) 318 | { 319 | struct lua_State *L = (struct lua_State *)uobj; 320 | 321 | 322 | luaL_Buffer b; 323 | luaL_buffinit(L, &b); 324 | size_t i; 325 | for (i = 0; i < name_len; i++) { 326 | switch(name[i]) { 327 | case 'A' ... 'Z': 328 | luaL_addchar(&b, name[i] - 'A' + 'a'); 329 | break; 330 | default: 331 | luaL_addchar(&b, name[i]); 332 | } 333 | } 334 | luaL_pushresult(&b); 335 | 336 | if (is_continuation) { 337 | lua_pushvalue(L, -1); 338 | lua_rawget(L, -3); 339 | 340 | luaL_Buffer b; 341 | luaL_buffinit(L, &b); 342 | luaL_addvalue(&b); 343 | luaL_addchar(&b, ' '); 344 | luaL_addlstring(&b, value, value_len); 345 | luaL_pushresult(&b); 346 | 347 | } else { 348 | lua_pushvalue(L, -1); 349 | lua_rawget(L, -3); 350 | 351 | if (lua_isnil(L, -1)) { 352 | lua_pop(L, 1); 353 | lua_pushlstring(L, value, value_len); 354 | } else { 355 | luaL_Buffer b; 356 | luaL_buffinit(L, &b); 357 | luaL_addvalue(&b); 358 | luaL_addchar(&b, ','); 359 | luaL_addchar(&b, ' '); 360 | luaL_addlstring(&b, value, value_len); 361 | luaL_pushresult(&b); 362 | } 363 | } 364 | 365 | lua_rawset(L, -3); 366 | 367 | return 0; 368 | } 369 | 370 | static int 371 | http_parser_on_body(void *uobj, const char *body, size_t body_len) 372 | { 373 | struct lua_State *L = (struct lua_State *)uobj; 374 | lua_pushliteral(L, "body"); 375 | lua_pushlstring(L, body, body_len); 376 | lua_rawset(L, -4); 377 | return 0; 378 | } 379 | 380 | static int 381 | http_parser_on_request_line(void *uobj, const char *method, size_t method_len, 382 | const char *path, size_t path_len, 383 | const char *query, size_t query_len, 384 | int http_major, int http_minor) 385 | { 386 | struct lua_State *L = (struct lua_State *)uobj; 387 | 388 | lua_pushliteral(L, "method"); 389 | lua_pushlstring(L, method, method_len); 390 | lua_rawset(L, -4); 391 | 392 | lua_pushliteral(L, "path"); 393 | lua_pushlstring(L, path, path_len); 394 | lua_rawset(L, -4); 395 | 396 | lua_pushliteral(L, "query"); 397 | lua_pushlstring(L, query, query_len); 398 | lua_rawset(L, -4); 399 | 400 | lua_pushliteral(L, "proto"); 401 | lua_newtable(L); 402 | 403 | lua_pushnumber(L, 1); 404 | lua_pushnumber(L, http_major); 405 | lua_rawset(L, -3); 406 | 407 | lua_pushnumber(L, 2); 408 | lua_pushnumber(L, http_minor); 409 | lua_rawset(L, -3); 410 | 411 | lua_rawset(L, -4); 412 | 413 | return 0; 414 | } 415 | 416 | static int 417 | http_parser_on_response_line(void *uobj, unsigned code, 418 | const char *reason, size_t reason_len, 419 | int http_major, int http_minor) 420 | { 421 | struct lua_State *L = (struct lua_State *)uobj; 422 | 423 | lua_pushliteral(L, "proto"); 424 | lua_newtable(L); 425 | 426 | lua_pushnumber(L, 1); 427 | lua_pushnumber(L, http_major); 428 | lua_rawset(L, -3); 429 | 430 | lua_pushnumber(L, 2); 431 | lua_pushnumber(L, http_minor); 432 | lua_rawset(L, -3); 433 | 434 | lua_rawset(L, -4); 435 | 436 | lua_pushliteral(L, "reason"); 437 | lua_pushlstring(L, reason, reason_len); 438 | lua_rawset(L, -4); 439 | 440 | lua_pushliteral(L, "status"); 441 | lua_pushnumber(L, code); 442 | lua_rawset(L, -4); 443 | 444 | return 0; 445 | } 446 | 447 | static int 448 | lbox_http_parse_response(struct lua_State *L) 449 | { 450 | int top = lua_gettop(L); 451 | 452 | if (!top) 453 | luaL_error(L, "bad arguments"); 454 | 455 | size_t len; 456 | const char *s = lua_tolstring(L, 1, &len); 457 | 458 | struct parse_http_events ev; 459 | memset(&ev, 0, sizeof(ev)); 460 | ev.on_error = http_parser_on_error; 461 | ev.on_header = http_parser_on_header; 462 | ev.on_body = http_parser_on_body; 463 | ev.on_response_line = http_parser_on_response_line; 464 | 465 | lua_newtable(L); /* results */ 466 | 467 | lua_newtable(L); /* headers */ 468 | lua_pushstring(L, "headers"); 469 | lua_pushvalue(L, -2); 470 | lua_rawset(L, -4); 471 | 472 | httpfast_parse(s, len, &ev, L); 473 | 474 | lua_pop(L, 1); 475 | return 1; 476 | } 477 | 478 | static int 479 | lbox_httpd_parse_request(struct lua_State *L) 480 | { 481 | int top = lua_gettop(L); 482 | 483 | if (!top) 484 | luaL_error(L, "bad arguments"); 485 | 486 | size_t len; 487 | const char *s = lua_tolstring(L, 1, &len); 488 | 489 | struct parse_http_events ev; 490 | memset(&ev, 0, sizeof(ev)); 491 | ev.on_error = http_parser_on_error; 492 | ev.on_header = http_parser_on_header; 493 | ev.on_body = http_parser_on_body; 494 | ev.on_request_line = http_parser_on_request_line; 495 | 496 | lua_newtable(L); /* results */ 497 | 498 | lua_newtable(L); /* headers */ 499 | lua_pushstring(L, "headers"); 500 | lua_pushvalue(L, -2); 501 | lua_rawset(L, -4); 502 | 503 | httpfast_parse(s, len, &ev, L); 504 | 505 | lua_pop(L, 1); 506 | return 1; 507 | } 508 | 509 | static inline int 510 | httpd_on_param(void *uobj, const char *name, size_t name_len, 511 | const char *value, size_t value_len) 512 | { 513 | struct lua_State *L = (struct lua_State *)uobj; 514 | 515 | lua_pushlstring(L, name, name_len); 516 | lua_pushvalue(L, -1); 517 | lua_rawget(L, -3); 518 | if (lua_isnil(L, -1)) { 519 | lua_pop(L, 1); 520 | lua_pushlstring(L, value, value_len); 521 | lua_rawset(L, -3); 522 | return 0; 523 | } 524 | if (lua_istable(L, -1)) { 525 | lua_pushnumber(L, lua_objlen(L, -1) + 1); 526 | lua_pushlstring(L, value, value_len); 527 | lua_rawset(L, -3); 528 | lua_pop(L, 2); /* table and name */ 529 | return 0; 530 | } 531 | lua_newtable(L); 532 | lua_pushvalue(L, -2); 533 | lua_rawseti(L, -2, 1); 534 | lua_remove(L, -2); 535 | 536 | lua_pushlstring(L, value, value_len); 537 | lua_rawseti(L, -2, 2); 538 | 539 | lua_rawset(L, -3); 540 | return 0; 541 | } 542 | 543 | static int 544 | lbox_httpd_params(struct lua_State *L) 545 | { 546 | lua_newtable(L); 547 | if (!lua_gettop(L)) 548 | return 1; 549 | 550 | const char *s; 551 | size_t len; 552 | s = lua_tolstring(L, 1, &len); 553 | httpfast_parse_params(s, len, httpd_on_param, L); 554 | return 1; 555 | } 556 | 557 | LUA_API int 558 | luaopen_http_lib(lua_State *L) 559 | { 560 | static const struct luaL_Reg reg[] = { 561 | {"parse_response", lbox_http_parse_response}, 562 | {"template", lbox_httpd_template}, 563 | {"_parse_request", lbox_httpd_parse_request}, 564 | {"params", lbox_httpd_params}, 565 | {NULL, NULL} 566 | }; 567 | 568 | luaL_register(L, "box._lib", reg); 569 | return 1; 570 | } 571 | -------------------------------------------------------------------------------- /http/sslsocket.lua: -------------------------------------------------------------------------------- 1 | local TIMEOUT_INFINITY = 500 * 365 * 86400 2 | local LIMIT_INFINITY = 2147483647 3 | 4 | local log = require('log') 5 | local ffi = require('ffi') 6 | local fio = require('fio') 7 | local socket = require('socket') 8 | local buffer = require('buffer') 9 | local clock = require('clock') 10 | local errno = require('errno') 11 | 12 | pcall( 13 | function() 14 | ffi.cdef[[ 15 | typedef struct SSL_METHOD {} SSL_METHOD; 16 | typedef struct SSL_CTX {} SSL_CTX; 17 | typedef struct SSL {} SSL; 18 | 19 | const SSL_METHOD *TLS_server_method(void); 20 | const SSL_METHOD *TLS_client_method(void); 21 | 22 | SSL_CTX *SSL_CTX_new(const SSL_METHOD *method); 23 | void SSL_CTX_free(SSL_CTX *); 24 | 25 | int SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type); 26 | int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type); 27 | void SSL_CTX_set_default_passwd_cb_userdata(SSL_CTX *ctx, void *u); 28 | typedef int (*pem_passwd_cb)(char *buf, int size, int rwflag, void *userdata); 29 | 30 | void SSL_CTX_set_default_passwd_cb(SSL_CTX *ctx, pem_passwd_cb cb); 31 | 32 | int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, 33 | const char *CApath); 34 | int SSL_CTX_set_cipher_list(SSL_CTX *ctx, const char *str); 35 | void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, 36 | int (*verify_callback)(int, void *)); 37 | 38 | SSL *SSL_new(SSL_CTX *ctx); 39 | void SSL_free(SSL *ssl); 40 | 41 | int SSL_set_fd(SSL *s, int fd); 42 | 43 | void SSL_set_connect_state(SSL *s); 44 | void SSL_set_accept_state(SSL *s); 45 | 46 | int SSL_write(SSL *ssl, const void *buf, int num); 47 | int SSL_read(SSL *ssl, void *buf, int num); 48 | 49 | int SSL_pending(const SSL *ssl); 50 | 51 | void ERR_clear_error(void); 52 | char *ERR_error_string(unsigned long e, char *buf); 53 | unsigned long ERR_peek_last_error(void); 54 | 55 | int SSL_get_error(const SSL *s, int ret_code); 56 | 57 | void *memmem(const void *haystack, size_t haystacklen, 58 | const void *needle, size_t needlelen); 59 | ]] 60 | end) 61 | 62 | local function slice_wait(timeout, starttime) 63 | if timeout == nil then 64 | return nil 65 | end 66 | 67 | return timeout - (clock.time() - starttime) 68 | end 69 | 70 | local X509_FILETYPE_PEM = 1 71 | 72 | local function tls_server_method() 73 | return ffi.C.TLS_server_method() 74 | end 75 | 76 | local function ctx(method) 77 | ffi.C.ERR_clear_error() 78 | 79 | return ffi.gc(ffi.C.SSL_CTX_new(method), ffi.C.SSL_CTX_free) 80 | end 81 | 82 | local function ctx_use_private_key_file(ctx, pem_file, password, password_file) 83 | ffi.C.SSL_CTX_set_default_passwd_cb(ctx, box.NULL); 84 | 85 | if password ~= nil then 86 | log.info('set private key password') 87 | 88 | ffi.C.SSL_CTX_set_default_passwd_cb_userdata(ctx, 89 | ffi.cast('void*', ffi.cast('char*', password))) 90 | local rc = ffi.C.SSL_CTX_use_PrivateKey_file(ctx, pem_file, X509_FILETYPE_PEM) 91 | ffi.C.SSL_CTX_set_default_passwd_cb_userdata(ctx, box.NULL); 92 | 93 | if rc == 1 then 94 | return true 95 | end 96 | ffi.C.ERR_clear_error() 97 | end 98 | 99 | if password_file ~= nil then 100 | local fh = fio.open(password_file, {'O_RDONLY'}) 101 | if fh == nil then 102 | return false 103 | end 104 | 105 | local is_loaded = false 106 | local password_from_file = "" 107 | local char 108 | while char ~= '' do 109 | while true do 110 | char = fh:read(1) 111 | if char == '\n' or char == '' then 112 | break 113 | end 114 | password_from_file = password_from_file .. char 115 | end 116 | 117 | ffi.C.SSL_CTX_set_default_passwd_cb_userdata(ctx, 118 | ffi.cast('void*', ffi.cast('char*', password_from_file))) 119 | local rc = ffi.C.SSL_CTX_use_PrivateKey_file(ctx, pem_file, X509_FILETYPE_PEM) 120 | ffi.C.SSL_CTX_set_default_passwd_cb_userdata(ctx, box.NULL); 121 | 122 | if rc == 1 then 123 | is_loaded = true 124 | break 125 | end 126 | 127 | ffi.C.ERR_clear_error() 128 | password_from_file = '' 129 | end 130 | 131 | fh:close() 132 | 133 | return is_loaded 134 | end 135 | 136 | local rc = ffi.C.SSL_CTX_use_PrivateKey_file(ctx, pem_file, X509_FILETYPE_PEM) 137 | if rc ~= 1 then 138 | ffi.C.ERR_clear_error() 139 | return false 140 | end 141 | 142 | return true 143 | end 144 | 145 | local function ctx_use_certificate_file(ctx, pem_file) 146 | if ffi.C.SSL_CTX_use_certificate_file(ctx, pem_file, X509_FILETYPE_PEM) ~= 1 then 147 | ffi.C.ERR_clear_error() 148 | return false 149 | end 150 | return true 151 | end 152 | 153 | local function ctx_load_verify_locations(ctx, ca_file) 154 | if ffi.C.SSL_CTX_load_verify_locations(ctx, ca_file, box.NULL) ~= 1 then 155 | ffi.C.ERR_clear_error() 156 | return false 157 | end 158 | return true 159 | end 160 | 161 | local function ctx_set_cipher_list(ctx, str) 162 | if ffi.C.SSL_CTX_set_cipher_list(ctx, str) ~= 1 then 163 | return false 164 | end 165 | return true 166 | end 167 | 168 | local function ctx_set_verify(ctx, flags) 169 | ffi.C.SSL_CTX_set_verify(ctx, flags, box.NULL) 170 | end 171 | 172 | local default_ctx = ctx(ffi.C.TLS_server_method()) 173 | 174 | local SSL_ERROR_WANT_READ = 2 175 | local SSL_ERROR_WANT_WRITE = 3 176 | local SSL_ERROR_SYSCALL = 5 -- Look at error stack/return value/errno. 177 | local SSL_ERROR_ZERO_RETURN = 6 178 | 179 | local sslsocket = { 180 | } 181 | sslsocket.__index = sslsocket 182 | 183 | local WAIT_FOR_READ =1 184 | local WAIT_FOR_WRITE =2 185 | 186 | function sslsocket.write(self, data, timeout) 187 | local start = clock.time() 188 | 189 | local size = #data 190 | local s = ffi.cast('const char *', data) 191 | 192 | local mode = WAIT_FOR_WRITE 193 | 194 | while true do 195 | local rc = nil 196 | if mode == WAIT_FOR_READ then 197 | rc = self.sock:readable(slice_wait(timeout, start)) 198 | elseif mode == WAIT_FOR_WRITE then 199 | rc = self.sock:writable(slice_wait(timeout, start)) 200 | else 201 | assert(false) 202 | end 203 | 204 | if not rc then 205 | self.sock._errno = errno.ETIMEDOUT 206 | return nil, 'Timeout exceeded' 207 | end 208 | 209 | ffi.C.ERR_clear_error() 210 | local num = ffi.C.SSL_write(self.ssl, s, size); 211 | if num <= 0 then 212 | local ssl_error = ffi.C.SSL_get_error(self.ssl, num); 213 | if ssl_error == SSL_ERROR_WANT_WRITE then 214 | mode = WAIT_FOR_WRITE 215 | elseif ssl_error == SSL_ERROR_WANT_READ then 216 | mode = WAIT_FOR_READ 217 | elseif ssl_error == SSL_ERROR_SYSCALL then 218 | return nil, self.sock:error() 219 | elseif ssl_error == SSL_ERROR_ZERO_RETURN then 220 | return 0 221 | else 222 | local error_string = ffi.string(ffi.C.ERR_error_string(ssl_error, nil)) 223 | log.info(error_string) 224 | return nil, error_string 225 | end 226 | else 227 | return num 228 | end 229 | end 230 | end 231 | 232 | function sslsocket.close(self) 233 | return self.sock:close() 234 | end 235 | 236 | function sslsocket.error(self) 237 | local error_string = 238 | ffi.string(ffi.C.ERR_error_string(ffi.C.ERR_peek_last_error(), nil)) 239 | 240 | return self.sock:error() or error_string 241 | end 242 | 243 | function sslsocket.errno(self) 244 | return self.sock:errno() or ffi.C.ERR_peek_last_error() 245 | end 246 | 247 | function sslsocket.fd(self) 248 | return self.sock:fd() 249 | end 250 | 251 | function sslsocket.nonblock(self, nb) 252 | return self.sock:nonblock(nb) 253 | end 254 | 255 | local function sysread(self, charptr, size, timeout) 256 | local start = clock.time() 257 | 258 | local mode = rawget(self, 'first_state') or WAIT_FOR_READ 259 | rawset(self, 'first_state', nil) 260 | 261 | while true do 262 | local rc = nil 263 | if mode == WAIT_FOR_READ then 264 | if ffi.C.SSL_pending(self.ssl) > 0 then 265 | rc = true 266 | else 267 | rc = self.sock:readable(slice_wait(timeout, start)) 268 | end 269 | elseif mode == WAIT_FOR_WRITE then 270 | rc = self.sock:writable(slice_wait(timeout, start)) 271 | else 272 | assert(false) 273 | end 274 | 275 | if not rc then 276 | self.sock._errno = errno.ETIMEDOUT 277 | return nil, 'Timeout exceeded' 278 | end 279 | 280 | ffi.C.ERR_clear_error() 281 | local num = ffi.C.SSL_read(self.ssl, charptr, size); 282 | if num <= 0 then 283 | local ssl_error = ffi.C.SSL_get_error(self.ssl, num); 284 | if ssl_error == SSL_ERROR_WANT_WRITE then 285 | mode = WAIT_FOR_WRITE 286 | elseif ssl_error == SSL_ERROR_WANT_READ then 287 | mode = WAIT_FOR_READ 288 | elseif ssl_error == SSL_ERROR_SYSCALL then 289 | return nil, self.sock:error() 290 | elseif ssl_error == SSL_ERROR_ZERO_RETURN then 291 | return 0 292 | else 293 | local error_string = ffi.string(ffi.C.ERR_error_string(ssl_error, nil)) 294 | log.info(error_string) 295 | return nil, error_string 296 | end 297 | else 298 | return num 299 | end 300 | end 301 | end 302 | 303 | local function read(self, limit, timeout, check, ...) 304 | assert(limit >= 0) 305 | 306 | local start = clock.time() 307 | 308 | limit = math.min(limit, LIMIT_INFINITY) 309 | local rbuf = self.rbuf 310 | if rbuf == nil then 311 | rbuf = buffer.ibuf() 312 | rawset(self, 'rbuf', rbuf) 313 | end 314 | 315 | local len = check(self, limit, ...) 316 | if len ~= nil then 317 | local data = ffi.string(rbuf.rpos, len) 318 | rbuf.rpos = rbuf.rpos + len 319 | return data 320 | end 321 | 322 | while true do 323 | assert(rbuf:size() < limit) 324 | local to_read = math.min(limit - rbuf:size(), buffer.READAHEAD) 325 | local data = rbuf:reserve(to_read) 326 | assert(rbuf:unused() >= to_read) 327 | 328 | local res, err = sysread(self, data, rbuf:unused(), slice_wait(timeout, start)) 329 | if res == 0 then -- EOF. 330 | local len = rbuf:size() 331 | local data = ffi.string(rbuf.rpos, len) 332 | rbuf.rpos = rbuf.rpos + len 333 | return data 334 | elseif res ~= nil then 335 | rbuf.wpos = rbuf.wpos + res 336 | local len = check(self, limit, ...) 337 | if len ~= nil then 338 | local data = ffi.string(rbuf.rpos, len) 339 | rbuf.rpos = rbuf.rpos + len 340 | return data 341 | end 342 | else 343 | return nil, err 344 | end 345 | end 346 | 347 | -- Not reached. 348 | end 349 | 350 | local function check_limit(self, limit) 351 | if self.rbuf:size() >= limit then 352 | return limit 353 | end 354 | return nil 355 | end 356 | 357 | local function check_delimiter(self, limit, eols) 358 | if limit == 0 then 359 | return 0 360 | end 361 | local rbuf = self.rbuf 362 | if rbuf:size() == 0 then 363 | return nil 364 | end 365 | 366 | local shortest 367 | for _, eol in ipairs(eols) do 368 | local data = ffi.C.memmem(rbuf.rpos, rbuf:size(), eol, #eol) 369 | if data ~= nil then 370 | local len = ffi.cast('char *', data) - rbuf.rpos + #eol 371 | if shortest == nil or shortest > len then 372 | shortest = len 373 | end 374 | end 375 | end 376 | if shortest ~= nil and shortest <= limit then 377 | return shortest 378 | elseif limit <= rbuf:size() then 379 | return limit 380 | end 381 | return nil 382 | end 383 | 384 | function sslsocket.read(self, opts, timeout) 385 | timeout = timeout or TIMEOUT_INFINITY 386 | if type(opts) == 'number' then 387 | return read(self, opts, timeout, check_limit) 388 | elseif type(opts) == 'string' then 389 | return read(self, LIMIT_INFINITY, timeout, check_delimiter, { opts }) 390 | elseif type(opts) == 'table' then 391 | local chunk = opts.chunk or opts.size or LIMIT_INFINITY 392 | local delimiter = opts.delimiter or opts.line 393 | if delimiter == nil then 394 | return read(self, chunk, timeout, check_limit) 395 | elseif type(delimiter) == 'string' then 396 | return read(self, chunk, timeout, check_delimiter, { delimiter }) 397 | elseif type(delimiter) == 'table' then 398 | return read(self, chunk, timeout, check_delimiter, delimiter) 399 | end 400 | end 401 | error('Usage: s:read(delimiter|chunk|{delimiter = x, chunk = x}, timeout)') 402 | end 403 | 404 | function sslsocket.readable(self, timeout) 405 | return self.sock:readable(timeout) 406 | end 407 | 408 | local function wrap_accepted_socket(sock, sslctx) 409 | sslctx = sslctx or default_ctx 410 | 411 | ffi.C.ERR_clear_error() 412 | local ssl = ffi.gc(ffi.C.SSL_new(sslctx), 413 | ffi.C.SSL_free) 414 | if ssl == nil then 415 | sock:close() 416 | return nil, 'SSL_new failed' 417 | end 418 | 419 | sock:nonblock(true) 420 | 421 | ffi.C.ERR_clear_error() 422 | local rc = ffi.C.SSL_set_fd(ssl, sock:fd()) 423 | if rc == 0 then 424 | sock:close() 425 | return nil, 'SSL_set_fd failed' 426 | end 427 | 428 | ffi.C.ERR_clear_error() 429 | ffi.C.SSL_set_accept_state(ssl); 430 | 431 | local self = setmetatable({}, sslsocket) 432 | rawset(self, 'sock', sock) 433 | rawset(self, 'ctx', sslctx) 434 | rawset(self, 'ssl', ssl) 435 | return self 436 | end 437 | 438 | local function tcp_server(host, port, handler, timeout, sslctx) 439 | sslctx = sslctx or default_ctx 440 | 441 | local handler_function = handler.handler 442 | 443 | local wrapper = function(sock, from) 444 | local self, err = wrap_accepted_socket(sock, sslctx) 445 | if not self then 446 | log.info('sslsocket.tcp_server error: %s ', err) 447 | else 448 | handler_function(self, from) 449 | end 450 | end 451 | 452 | handler.handler = wrapper 453 | 454 | return socket.tcp_server(host, port, handler, timeout) 455 | end 456 | 457 | return { 458 | tls_server_method = tls_server_method, 459 | 460 | ctx = ctx, 461 | ctx_use_private_key_file = ctx_use_private_key_file, 462 | ctx_use_certificate_file = ctx_use_certificate_file, 463 | ctx_load_verify_locations = ctx_load_verify_locations, 464 | ctx_set_cipher_list = ctx_set_cipher_list, 465 | ctx_set_verify = ctx_set_verify, 466 | 467 | tcp_server = tcp_server, 468 | 469 | wrap_accepted_socket = wrap_accepted_socket, 470 | } 471 | -------------------------------------------------------------------------------- /http/tpleval.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Redistribution and use in source and binary forms, with or 3 | * without modification, are permitted provided that the following 4 | * conditions are met: 5 | * 6 | * 1. Redistributions of source code must retain the above 7 | * copyright notice, this list of conditions and the 8 | * following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above 11 | * copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials 13 | * provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND 16 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 17 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 19 | * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 23 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 26 | * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | * SUCH DAMAGE. 28 | */ 29 | #ifndef TPL_EVAL_H_INCLUDED 30 | #define TPL_EVAL_H_INCLUDED 31 | 32 | #include 33 | #include 34 | 35 | enum { 36 | TPE_TEXT, 37 | TPE_LINECODE, 38 | TPE_MULTILINE_CODE 39 | }; 40 | 41 | static inline void 42 | tpe_parse(const char *p, size_t len, 43 | void(*term)(int type, const char *str, size_t len, void *data), 44 | void *data) 45 | { 46 | int bl = 1; 47 | size_t i, be; 48 | int type = TPE_TEXT; 49 | 50 | for (be = i = 0; i < len; i++) { 51 | if (type == TPE_TEXT) { 52 | switch(p[i]) { 53 | case ' ': 54 | case '\t': 55 | break; 56 | case '%': 57 | if (bl) { 58 | if (be < i) 59 | term(type, 60 | p + be, 61 | i - be, 62 | data); 63 | 64 | be = i + 1; 65 | bl = 0; 66 | 67 | type = TPE_LINECODE; 68 | break; 69 | } 70 | 71 | if (i == 0 || p[i - 1] != '<') 72 | break; 73 | 74 | if (be < i - 1) 75 | term(type, 76 | p + be, 77 | i - be - 1, 78 | data); 79 | be = i + 1; 80 | bl = 0; 81 | 82 | type = TPE_MULTILINE_CODE; 83 | break; 84 | 85 | case '\n': 86 | if (be <= i) 87 | term(type, 88 | p + be, 89 | i - be + 1, 90 | data); 91 | be = i + 1; 92 | bl = 1; 93 | break; 94 | default: 95 | bl = 0; 96 | break; 97 | } 98 | continue; 99 | } 100 | 101 | if (type == TPE_LINECODE) { 102 | switch(p[i]) { 103 | case '\n': 104 | if (be < i) 105 | term(type, 106 | p + be, i - be, data); 107 | be = i; 108 | type = TPE_TEXT; 109 | bl = 1; 110 | break; 111 | default: 112 | break; 113 | } 114 | continue; 115 | } 116 | 117 | if (type == TPE_MULTILINE_CODE) { 118 | switch(p[i]) { 119 | case '%': 120 | if (i == len - 1 || p[i + 1] != '>') 121 | continue; 122 | if (be < i) 123 | term(type, 124 | p + be, i - be, data); 125 | be = i + 2; 126 | i++; 127 | bl = 0; 128 | type = TPE_TEXT; 129 | break; 130 | default: 131 | break; 132 | } 133 | continue; 134 | } 135 | 136 | abort(); 137 | } 138 | 139 | if (len == 0 || be >= len) 140 | return; 141 | 142 | switch(type) { 143 | /* unclosed multiline tag as text */ 144 | case TPE_MULTILINE_CODE: 145 | if (be >= 2) 146 | be -= 2; 147 | type = TPE_TEXT; 148 | 149 | case TPE_LINECODE: 150 | case TPE_TEXT: 151 | term(type, p + be, len - be, data); 152 | break; 153 | default: 154 | break; 155 | } 156 | } 157 | 158 | 159 | #endif /* TPL_EVAL_H_INCLUDED */ 160 | -------------------------------------------------------------------------------- /http/version.lua: -------------------------------------------------------------------------------- 1 | -- Сontains the module version. 2 | -- Requires manual update in case of release commit. 3 | 4 | return '1.7.0' 5 | -------------------------------------------------------------------------------- /roles/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Install. 2 | install(FILES httpd.lua DESTINATION ${TARANTOOL_INSTALL_LUADIR}/roles) 3 | -------------------------------------------------------------------------------- /roles/httpd.lua: -------------------------------------------------------------------------------- 1 | local checks = require('checks') 2 | local urilib = require('uri') 3 | local http_server = require('http.server') 4 | 5 | local M = { 6 | DEFAULT_SERVER_NAME = 'default', 7 | } 8 | local servers = {} 9 | 10 | local function parse_listen(listen) 11 | if listen == nil then 12 | return nil, nil, "must exist" 13 | end 14 | if type(listen) ~= "string" and type(listen) ~= "number" then 15 | return nil, nil, "must be a string or a number, got " .. type(listen) 16 | end 17 | 18 | local host 19 | local port 20 | if type(listen) == "string" then 21 | local uri, err = urilib.parse(listen) 22 | if err ~= nil then 23 | return nil, nil, "failed to parse URI: " .. err 24 | end 25 | 26 | if uri.scheme ~= nil then 27 | if uri.scheme == "unix" then 28 | uri.unix = uri.path 29 | else 30 | return nil, nil, "URI scheme is not supported" 31 | end 32 | end 33 | 34 | if uri.login ~= nil or uri.password then 35 | return nil, nil, "URI login and password are not supported" 36 | end 37 | 38 | if uri.query ~= nil then 39 | return nil, nil, "URI query component is not supported" 40 | end 41 | 42 | if uri.unix ~= nil then 43 | host = "unix/" 44 | port = uri.unix 45 | else 46 | if uri.service == nil then 47 | return nil, nil, "URI must contain a port" 48 | end 49 | 50 | port = tonumber(uri.service) 51 | if port == nil then 52 | return nil, nil, "URI port must be a number" 53 | end 54 | if uri.host ~= nil then 55 | host = uri.host 56 | elseif uri.ipv4 ~= nil then 57 | host = uri.ipv4 58 | elseif uri.ipv6 ~= nil then 59 | host = uri.ipv6 60 | else 61 | host = "0.0.0.0" 62 | end 63 | end 64 | elseif type(listen) == "number" then 65 | host = "0.0.0.0" 66 | port = listen 67 | end 68 | 69 | if type(port) == "number" and (port < 1 or port > 65535) then 70 | return nil, nil, "port must be in the range [1, 65535]" 71 | end 72 | return host, port, nil 73 | end 74 | 75 | -- parse_params returns table with set options from config to pass 76 | -- it into new() function. 77 | local function parse_params(node) 78 | return { 79 | ssl_cert_file = node.ssl_cert_file, 80 | ssl_key_file = node.ssl_key_file, 81 | ssl_password = node.ssl_password, 82 | ssl_password_file = node.ssl_password_file, 83 | ssl_ca_file = node.ssl_ca_file, 84 | ssl_ciphers = node.ssl_ciphers, 85 | } 86 | end 87 | 88 | local function apply_http(name, node) 89 | local host, port, err = parse_listen(node.listen) 90 | if err ~= nil then 91 | error("failed to parse URI: " .. err) 92 | end 93 | 94 | if servers[name] == nil then 95 | local httpd = http_server.new(host, port, parse_params(node)) 96 | 97 | httpd:start() 98 | servers[name] = { 99 | httpd = httpd, 100 | routes = {}, 101 | } 102 | end 103 | end 104 | 105 | M.validate = function(conf) 106 | if conf ~= nil and type(conf) ~= "table" then 107 | error("configuration must be a table, got " .. type(conf)) 108 | end 109 | conf = conf or {} 110 | 111 | for name, node in pairs(conf) do 112 | if type(name) ~= 'string' then 113 | error("name of the server must be a string") 114 | end 115 | 116 | local host, port, err = parse_listen(node.listen) 117 | if err ~= nil then 118 | error("failed to parse http 'listen' param: " .. err) 119 | end 120 | 121 | local ok, err = pcall(http_server.new, host, port, parse_params(node)) 122 | if not ok then 123 | error("failed to parse params in " .. name .. " server: " .. tostring(err)) 124 | end 125 | end 126 | end 127 | 128 | M.apply = function(conf) 129 | -- This should be called on the role's lifecycle, but it's better to give 130 | -- a meaningful error if something goes wrong. 131 | M.validate(conf) 132 | 133 | for name, node in pairs(conf or {}) do 134 | apply_http(name, node) 135 | end 136 | end 137 | 138 | M.stop = function() 139 | for _, server in pairs(servers) do 140 | server.httpd:stop() 141 | end 142 | servers = {} 143 | end 144 | 145 | M.get_server = function(name) 146 | checks('?string') 147 | 148 | name = name or M.DEFAULT_SERVER_NAME 149 | return (servers[name] or {}).httpd 150 | end 151 | 152 | return M 153 | -------------------------------------------------------------------------------- /rpm/prebuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -o pipefail 4 | 5 | curl -LsSf https://www.tarantool.io/release/1.10/installer.sh | sudo bash 6 | -------------------------------------------------------------------------------- /rpm/tarantool-http.spec: -------------------------------------------------------------------------------- 1 | Name: tarantool-http 2 | Version: 1.1.1 3 | Release: 1%{?dist} 4 | Epoch: 1 5 | Summary: HTTP server for Tarantool 6 | Group: Applications/Databases 7 | License: BSD 8 | URL: https://github.com/tarantool/http 9 | Source0: https://github.com/tarantool/%{name}/archive/%{version}/%{name}-%{version}.tar.gz 10 | BuildRequires: cmake >= 2.8 11 | BuildRequires: gcc >= 4.5 12 | BuildRequires: tarantool-devel >= 1.7.5.0 13 | BuildRequires: /usr/bin/prove 14 | Requires: tarantool >= 1.7.5.0 15 | 16 | %description 17 | This package provides a HTTP server for Tarantool. 18 | 19 | %prep 20 | %setup -q -n %{name}-%{version} 21 | 22 | %build 23 | %cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo 24 | %if 0%{?fedora} >= 33 || 0%{?rhel} >= 8 25 | %cmake_build 26 | %else 27 | %make_build 28 | %endif 29 | 30 | %install 31 | %if 0%{?fedora} >= 33 || 0%{?rhel} >= 8 32 | %cmake_install 33 | %else 34 | %make_install 35 | %endif 36 | 37 | %files 38 | %{_libdir}/tarantool/*/ 39 | %{_datarootdir}/tarantool/*/ 40 | %doc README.md 41 | %{!?_licensedir:%global license %doc} 42 | %license LICENSE AUTHORS 43 | 44 | %changelog 45 | * Thu Feb 18 2016 Roman Tsisyk 1.0.0-1 46 | - Initial version of the RPM spec 47 | -------------------------------------------------------------------------------- /test/controllers/module/controller.lua: -------------------------------------------------------------------------------- 1 | return { 2 | action = function(self) return self:render() end 3 | } 4 | -------------------------------------------------------------------------------- /test/helpers.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local http_server = require('http.server') 3 | local socket = require('socket') 4 | 5 | local helpers = table.copy(require('luatest').helpers) 6 | 7 | local luatest = require('luatest') 8 | local luatest_utils = require('luatest.utils') 9 | 10 | helpers.base_port = 12345 11 | helpers.base_host = '127.0.0.1' 12 | helpers.base_uri = ('http://%s:%s'):format(helpers.base_host, helpers.base_port) 13 | helpers.tls_uri = ('https://%s:%s'):format('localhost', helpers.base_port) 14 | 15 | helpers.get_testdir_path = function() 16 | local path = os.getenv('LUA_SOURCE_DIR') or './' 17 | return fio.pathjoin(path, 'test') 18 | end 19 | 20 | helpers.cfgserv = function(opts) 21 | local path = helpers.get_testdir_path() 22 | 23 | local opts = opts or {} 24 | local opts = http_server.internal.extend({ 25 | app_dir = path, 26 | log_requests = false, 27 | log_errors = false 28 | }, opts) 29 | 30 | local httpd = http_server.new(helpers.base_host, helpers.base_port, opts) 31 | :route({path = '/abc/:cde/:def', name = 'test'}, function() end) 32 | :route({path = '/abc'}, function() end) 33 | :route({path = '/ctxaction'}, 'module.controller#action') 34 | :route({path = '/absentaction'}, 'module.controller#absent') 35 | :route({path = '/absent'}, 'module.absent#action') 36 | :route({path = '/abc/:cde'}, function() end) 37 | :route({path = '/abc_:cde_def'}, function() end) 38 | :route({path = '/abc-:cde-def'}, function() end) 39 | :route({path = '/aba*def'}, function() end) 40 | :route({path = '/abb*def/cde', name = 'star'}, function() end) 41 | :route({path = '/banners/:token'}) 42 | :helper('helper_title', function(self, a) return 'Hello, ' .. a end) 43 | :route({path = '/helper', file = 'helper.html.el'}) 44 | :route({path = '/test', file = 'test.html.el' }, 45 | function(cx) return cx:render({ title = 'title: 123' }) end) 46 | 47 | return httpd 48 | end 49 | 50 | local log_queue = {} 51 | 52 | helpers.clear_log_queue = function() 53 | log_queue = {} 54 | end 55 | 56 | helpers.custom_logger = { 57 | debug = function() end, 58 | verbose = function() 59 | table.insert(log_queue, { 60 | log_lvl = 'verbose', 61 | }) 62 | end, 63 | info = function(...) 64 | table.insert(log_queue, { 65 | log_lvl = 'info', 66 | msg = string.format(...) 67 | }) 68 | end, 69 | warn = function(...) 70 | table.insert(log_queue, { 71 | log_lvl = 'warn', 72 | msg = string.format(...) 73 | }) 74 | end, 75 | error = function(...) 76 | table.insert(log_queue, { 77 | log_lvl = 'error', 78 | msg = string.format(...) 79 | }) 80 | end 81 | } 82 | 83 | helpers.find_msg_in_log_queue = function(msg, strict) 84 | for _, log in ipairs(log_queue) do 85 | if not strict then 86 | if log.msg:match(msg) then 87 | return log 88 | end 89 | else 90 | if log.msg == msg then 91 | return log 92 | end 93 | end 94 | end 95 | end 96 | 97 | helpers.teardown = function(httpd) 98 | local host = httpd.host 99 | local port = httpd.port 100 | httpd:stop() 101 | helpers.retrying({ 102 | timeout = 1, 103 | }, function() 104 | local s, _ = socket.tcp_connect(host, port) 105 | if s ~= nil then 106 | s:close() 107 | end 108 | assert(s == nil, 'http server is stopped') 109 | end) 110 | end 111 | 112 | helpers.is_tarantool3 = function() 113 | local tarantool_version = luatest_utils.get_tarantool_version() 114 | return luatest_utils.version_ge(tarantool_version, luatest_utils.version(3, 0, 0)) 115 | end 116 | 117 | helpers.skip_if_not_tarantool3 = function() 118 | luatest.skip_if(not helpers.is_tarantool3(), 'Only Tarantool 3 is supported') 119 | end 120 | 121 | helpers.update_lua_env_variables = function(server) 122 | local ROOT = fio.dirname(fio.dirname(fio.abspath(package.search('http.server')))) 123 | 124 | server.env.LUA_PATH = (server.env.LUA_PATH or '') .. 125 | ROOT .. '/?.lua;' .. ROOT .. '/?/?.lua;' .. 126 | ROOT .. '/.rocks/share/tarantool/?.lua;' .. 127 | ROOT .. '/.rocks/share/tarantool/?/init.lua;' 128 | server.env.LUA_CPATH = (server.env.LUA_CPATH or '') .. 129 | ROOT .. '/.rocks/lib/tarantool/?.so;' .. 130 | ROOT .. '/.rocks/lib/tarantool/?/?.so;' 131 | end 132 | 133 | return helpers 134 | -------------------------------------------------------------------------------- /test/integration/http_log_requests_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local http_client = require('http.client') 3 | local http_server = require('http.server') 4 | 5 | local helpers = require('test.helpers') 6 | 7 | local g = t.group() 8 | 9 | g.before_test('test_server_custom_logger', function() 10 | g.httpd = http_server.new(helpers.base_host, helpers.base_port, { 11 | log_requests = helpers.custom_logger.info, 12 | log_errors = helpers.custom_logger.error 13 | }) 14 | g.httpd:route({ 15 | path='/' 16 | }, function(_) end) 17 | g.httpd:route({ 18 | path='/error' 19 | }, function(_) error('Some error...') end) 20 | g.httpd:start() 21 | end) 22 | 23 | g.after_test('test_server_custom_logger', function() 24 | helpers.teardown(g.httpd) 25 | end) 26 | 27 | g.before_test('test_log_errors_off', function() 28 | g.httpd = http_server.new(helpers.base_host, helpers.base_port, { 29 | log_errors = false 30 | }) 31 | g.httpd:start() 32 | end) 33 | 34 | g.after_test('test_log_errors_off', function() 35 | helpers.teardown(g.httpd) 36 | end) 37 | 38 | g.before_test('test_route_custom_logger', function() 39 | g.httpd = http_server.new(helpers.base_host, helpers.base_port, { 40 | log_requests = true, 41 | log_errors = true 42 | }) 43 | g.httpd:start() 44 | end) 45 | 46 | g.after_test('test_route_custom_logger', function() 47 | helpers.teardown(g.httpd) 48 | end) 49 | 50 | g.before_test('test_log_requests_off', function() 51 | g.httpd = http_server.new(helpers.base_host, helpers.base_port, { 52 | log_requests = false 53 | }) 54 | g.httpd:start() 55 | end) 56 | 57 | g.after_test('test_log_requests_off', function() 58 | helpers.teardown(g.httpd) 59 | end) 60 | 61 | -- Setting log option for server instance. 62 | g.test_server_custom_logger = function() 63 | http_client.get(helpers.base_uri) 64 | t.assert_equals(helpers.find_msg_in_log_queue('GET /'), { 65 | log_lvl = 'info', 66 | msg = 'GET /' 67 | }, "Route should logging requests in custom logger if it's presents") 68 | helpers.clear_log_queue() 69 | 70 | http_client.get(helpers.base_uri .. '/error') 71 | --[[ 72 | t.assert_str_contains(helpers.find_msg_in_log_queue('Some error...', false), 73 | "Route should logging error in custom logger if it's presents") 74 | ]] 75 | helpers.clear_log_queue() 76 | end 77 | 78 | -- Setting log options for route. 79 | g.test_log_options = function() 80 | local httpd = http_server.new(helpers.base_host, helpers.base_port, { 81 | log_requests = true, 82 | log_errors = false 83 | }) 84 | local dummy_logger = function() end 85 | 86 | local ok, err = pcall(httpd.route, httpd, { 87 | path = '/', 88 | log_requests = 3 89 | }) 90 | t.assert_equals(ok, false, "Route logger can't be a log_level digit") 91 | t.assert_str_contains(err, "'log_requests' option should be a function", 92 | 'route() should return error message in case of incorrect logger option') 93 | 94 | ok, err = pcall(httpd.route, httpd, { 95 | path = '/', 96 | log_requests = { 97 | info = dummy_logger 98 | } 99 | }) 100 | t.assert_equals(ok, false, "Route logger can't be a table") 101 | t.assert_str_contains(err, "'log_requests' option should be a function", 102 | 'route() should return error message in case of incorrect logger option') 103 | 104 | local ok, err = pcall(httpd.route, httpd, { 105 | path = '/', 106 | log_errors = 3 107 | }) 108 | t.assert_equals(ok, false, "Route error logger can't be a log_level digit") 109 | t.assert_str_contains(err, "'log_errors' option should be a function", 110 | "route() should return error message in case of incorrect logger option") 111 | 112 | ok, err = pcall(httpd.route, httpd, { 113 | path = '/', 114 | log_errors = { 115 | error = dummy_logger 116 | } 117 | }) 118 | t.assert_equals(ok, false, "Route error logger can't be a table") 119 | t.assert_str_contains(err, "'log_errors' option should be a function", 120 | 'route() should return error message in case of incorrect log_errors option') 121 | end 122 | 123 | -- Log output with custom loggers on route. 124 | g.test_route_custom_logger = function() 125 | local httpd = g.httpd 126 | httpd:route({ 127 | path = '/', 128 | log_requests = helpers.custom_logger.info, 129 | log_errors = helpers.custom_logger.error 130 | }, function(_) end) 131 | http_client.get(helpers.base_uri) 132 | t.assert_equals(helpers.find_msg_in_log_queue('GET /'), { 133 | log_lvl = 'info', 134 | msg = 'GET /' 135 | }, "Route should logging requests in custom logger if it's presents") 136 | helpers.clear_log_queue() 137 | 138 | httpd.routes = {} 139 | httpd:route({ 140 | path = '/', 141 | log_requests = helpers.custom_logger.info, 142 | log_errors = helpers.custom_logger.error 143 | }, function(_) 144 | error('User business logic exception...') 145 | end) 146 | http_client.get('127.0.0.1:12345') 147 | --test:is_deeply(helpers.find_msg_in_log_queue('GET /'), { 148 | -- log_lvl = 'info', 149 | -- msg = 'GET /' 150 | --}, "Route should logging request and error in case of route exception") 151 | --test:ok(helpers.find_msg_in_log_queue('User business logic exception...', false), 152 | -- "Route should logging error custom logger if it's presents in case of route exception") 153 | helpers.clear_log_queue() 154 | end 155 | 156 | -- Log route requests with turned off 'log_requests' option. 157 | g.test_log_requests_off = function() 158 | local httpd = g.httpd 159 | httpd:route({ 160 | path = '/', 161 | log_requests = helpers.custom_logger.info 162 | }, function(_) end) 163 | http_client.get(helpers.base_uri) 164 | --test:is_deeply(helpers.find_msg_in_log_queue('GET /'), { 165 | -- log_lvl = 'info', 166 | -- msg = 'GET /' 167 | --}, "Route can override logging requests if the http server have turned off 'log_requests' option") 168 | helpers.clear_log_queue() 169 | end 170 | 171 | -- Log route requests with turned off 'log_errors' option. 172 | g.test_log_errors_off = function() 173 | local httpd = g.httpd 174 | httpd:route({ 175 | path = '/', 176 | log_errors = helpers.custom_logger.error 177 | }, function(_) 178 | error('User business logic exception...') 179 | end) 180 | http_client.get(helpers.base_uri) 181 | --t.assert_str_contains(helpers.find_msg_in_log_queue('User business logic exception...', false), 182 | -- "Route can override logging requests if the http server have turned off 'log_errors' option") 183 | helpers.clear_log_queue() 184 | end 185 | -------------------------------------------------------------------------------- /test/integration/http_server_delete_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local http_client = require('http.client') 3 | 4 | local helpers = require('test.helpers') 5 | 6 | local g = t.group() 7 | 8 | g.before_each(function() 9 | g.httpd = helpers.cfgserv({ 10 | display_errors = true, 11 | }) 12 | g.httpd:start() 13 | end) 14 | 15 | g.after_each(function() 16 | helpers.teardown(g.httpd) 17 | end) 18 | 19 | g.test_delete = function() 20 | local httpd = g.httpd 21 | httpd:route({ 22 | path = '/content_type', 23 | name = 'content_type', 24 | }, function() 25 | return { 26 | status = 200, 27 | } 28 | end) 29 | 30 | local r = http_client.get(helpers.base_uri .. '/content_type') 31 | t.assert_equals(r.status, 200) 32 | 33 | httpd:delete('content_type') 34 | 35 | r = http_client.get(helpers.base_uri .. '/content_type') 36 | t.assert_equals(r.status, 404) 37 | end 38 | -------------------------------------------------------------------------------- /test/integration/http_server_options_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local http_client = require('http.client') 3 | 4 | local helpers = require('test.helpers') 5 | 6 | local g = t.group() 7 | 8 | g.after_each(function() 9 | helpers.teardown(g.httpd) 10 | end) 11 | 12 | g.before_test('test_keepalive_is_allowed', function() 13 | g.httpd = helpers.cfgserv() 14 | g.httpd:start() 15 | end) 16 | 17 | g.test_keepalive_is_allowed = function() 18 | local conn_is_opened = false 19 | local conn_is_closed = false 20 | g.httpd.internal.preprocess_client_handler = function() conn_is_opened = true end 21 | g.httpd.internal.postprocess_client_handler = function() conn_is_closed = true end 22 | 23 | -- Set HTTP keepalive headers: Connection:Keep-Alive and 24 | -- Keep-Alive:timeout=. Otherwise HTTP client will send 25 | -- "Connection:close". 26 | local opts = { 27 | keepalive_idle = 3600, 28 | keepalive_interval = 3600, 29 | } 30 | local r = http_client.new():request('GET', helpers.base_uri .. '/test', nil, opts) 31 | t.assert_equals(r.status, 200) 32 | t.assert_equals(r.headers.connection, 'keep-alive') 33 | t.assert_equals(conn_is_opened, true) 34 | t.assert_equals(conn_is_closed, false) -- Connection is alive. 35 | end 36 | 37 | g.before_test('test_keepalive_is_disallowed', function() 38 | g.useragent = 'Mozilla/4.0' 39 | g.httpd = helpers.cfgserv({ 40 | disable_keepalive = { g.useragent }, 41 | }) 42 | g.httpd:start() 43 | end) 44 | 45 | g.test_keepalive_is_disallowed = function() 46 | local conn_is_opened = false 47 | local conn_is_closed = false 48 | g.httpd.internal.preprocess_client_handler = function() conn_is_opened = true end 49 | g.httpd.internal.postprocess_client_handler = function() conn_is_closed = true end 50 | 51 | -- Set HTTP keepalive headers: Connection:Keep-Alive and 52 | -- Keep-Alive:timeout=. Otherwise HTTP client will send 53 | -- "Connection:close". 54 | local opts = { 55 | headers = { 56 | ['user-agent'] = g.useragent, 57 | }, 58 | keepalive_idle = 3600, 59 | keepalive_interval = 3600, 60 | } 61 | local r = http_client.new():request('GET', helpers.base_uri .. '/test', nil, opts) 62 | t.assert_equals(r.status, 200) 63 | t.assert_equals(r.headers.connection, 'close') 64 | t.assert_equals(conn_is_opened, true) 65 | t.assert_equals(conn_is_closed, true) -- Connection is closed. 66 | end 67 | 68 | g.before_test('test_disable_keepalive_is_set', function() 69 | g.useragent = 'Mozilla/5.0' 70 | g.httpd = helpers.cfgserv({ 71 | disable_keepalive = { g.useragent }, 72 | }) 73 | g.httpd:start() 74 | end) 75 | 76 | g.test_disable_keepalive_is_set = function(g) 77 | local httpd = g.httpd 78 | t.assert_equals(httpd.disable_keepalive[g.useragent], true) 79 | end 80 | 81 | g.before_test('test_disable_keepalive_default', function() 82 | g.httpd = helpers.cfgserv() 83 | g.httpd:start() 84 | end) 85 | 86 | g.test_disable_keepalive_default = function(g) 87 | local httpd = g.httpd 88 | t.assert_equals(table.getn(httpd.options.disable_keepalive), 0) 89 | end 90 | 91 | g.before_test('test_idle_timeout_default', function() 92 | g.httpd = helpers.cfgserv() 93 | g.httpd:start() 94 | end) 95 | 96 | g.test_idle_timeout_default = function(g) 97 | local httpd = g.httpd 98 | t.assert_equals(httpd.options.idle_timeout, 0) 99 | end 100 | 101 | g.before_test('test_idle_timeout_is_set', function() 102 | g.idle_timeout = 0.5 103 | g.httpd = helpers.cfgserv({ 104 | idle_timeout = g.idle_timeout, 105 | }) 106 | g.httpd:start() 107 | end) 108 | 109 | g.test_idle_timeout_is_set = function(g) 110 | local conn_is_opened = false 111 | local conn_is_closed = false 112 | local httpd = g.httpd 113 | g.httpd.internal.preprocess_client_handler = function() conn_is_opened = true end 114 | g.httpd.internal.postprocess_client_handler = function() conn_is_closed = true end 115 | 116 | t.assert_equals(httpd.options.idle_timeout, g.idle_timeout) 117 | 118 | -- Set HTTP keepalive headers: Connection:Keep-Alive and 119 | -- Keep-Alive:timeout=. Otherwise HTTP client will send 120 | -- "Connection:close". 121 | local opts = { 122 | keepalive_idle = 3600, 123 | keepalive_interval = 3600, 124 | } 125 | local r = http_client.new():request('GET', helpers.base_uri .. '/test', nil, opts) 126 | t.assert_equals(r.status, 200) 127 | t.assert_equals(r.headers.connection, 'keep-alive') 128 | t.assert_equals(conn_is_opened, true) 129 | 130 | helpers.retrying({ 131 | timeout = g.idle_timeout * 2, 132 | }, function() 133 | assert(conn_is_closed, 'connection is closed') 134 | end) 135 | t.assert_equals(conn_is_closed, true) -- Connection is closed. 136 | end 137 | 138 | g.before_test('test_idle_timeout_is_unset', function() 139 | g.httpd = helpers.cfgserv({}) 140 | g.httpd:start() 141 | end) 142 | 143 | g.test_idle_timeout_is_unset = function(g) 144 | local conn_is_opened = false 145 | local conn_is_closed = false 146 | g.httpd.internal.preprocess_client_handler = function() conn_is_opened = true end 147 | g.httpd.internal.postprocess_client_handler = function() conn_is_closed = true end 148 | 149 | -- Set HTTP keepalive headers: Connection:Keep-Alive and 150 | -- Keep-Alive:timeout=. Otherwise HTTP client will send 151 | -- "Connection:close". 152 | local opts = { 153 | keepalive_idle = 3600, 154 | keepalive_interval = 3600, 155 | } 156 | local r = http_client.new():request('GET', helpers.base_uri .. '/test', nil, opts) 157 | 158 | -- The server should not close the connection during the keepalive_idle 159 | -- seconds. We could check that server will close connection after 160 | -- keepalive_idle seconds, but HTTP server under test does not support 161 | -- "Keep-Alive: timeout=NUM" header. 162 | 163 | t.assert_equals(r.status, 200) 164 | t.assert_equals(r.headers.connection, 'keep-alive') 165 | t.assert_equals(conn_is_opened, true) 166 | t.assert_equals(conn_is_closed, false) -- Connection is alive. 167 | end 168 | -------------------------------------------------------------------------------- /test/integration/http_server_requests_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local http_client = require('http.client') 3 | local json = require('json') 4 | 5 | local helpers = require('test.helpers') 6 | 7 | local g = t.group() 8 | 9 | g.before_each(function() 10 | g.httpd = helpers.cfgserv({ 11 | display_errors = true, 12 | }) 13 | g.httpd:start() 14 | end) 15 | 16 | g.after_each(function() 17 | helpers.teardown(g.httpd) 18 | end) 19 | 20 | g.test_test = function() 21 | local r = http_client.get(helpers.base_uri .. '/test') 22 | t.assert_equals(r.status, 200, '/test code') 23 | 24 | t.assert_equals(r.proto[1], 1, '/test http 1.1') 25 | t.assert_equals(r.proto[2], 1, '/test http 1.1') 26 | t.assert_equals(r.reason, 'Ok', '/test reason') 27 | t.assert_equals(string.match(r.body, 'title: 123'), 'title: 123', '/test body') 28 | end 29 | 30 | g.test_404 = function() 31 | local r = http_client.get(helpers.base_uri .. '/test404') 32 | t.assert_equals(r.status, 404, '/test404 code') 33 | -- broken in built-in tarantool/http 34 | --t.assert_equals(r.reason, 'Not found', '/test404 reason') 35 | end 36 | 37 | g.test_absent = function() 38 | local r = http_client.get(helpers.base_uri .. '/absent') 39 | t.assert_equals(r.status, 500, '/absent code') 40 | --t.assert_equals(r.reason, 'Internal server error', '/absent reason') 41 | t.assert_equals(string.match(r.body, 'load module'), 'load module', '/absent body') 42 | end 43 | 44 | g.test_ctx_action = function() 45 | local r = http_client.get(helpers.base_uri .. '/ctxaction') 46 | t.assert_equals(r.status, 200, '/ctxaction code') 47 | t.assert_equals(r.reason, 'Ok', '/ctxaction reason') 48 | t.assert_equals(string.match(r.body, 'Hello, Tarantool'), 'Hello, Tarantool', 49 | '/ctxaction body') 50 | t.assert_equals(string.match(r.body, 'action: action'), 'action: action', 51 | '/ctxaction body action') 52 | t.assert_equals(string.match(r.body, 'controller: module[.]controller'), 53 | 'controller: module.controller', '/ctxaction body controller') 54 | end 55 | 56 | g.test_ctx_action_invalid = function() 57 | local r = http_client.get(helpers.base_uri .. '/ctxaction.invalid') 58 | t.assert_equals(r.status, 404, '/ctxaction.invalid code') -- WTF? 59 | --t.assert_equals(r.reason, 'Ok', '/ctxaction.invalid reason') 60 | t.assert_equals(r.body, nil, '/ctxaction.invalid body') 61 | end 62 | 63 | g.test_static_file = function() 64 | local r = http_client.get(helpers.base_uri .. '/hello.html') 65 | t.assert_equals(r.status, 200, '/hello.html code') 66 | t.assert_equals(r.reason, 'Ok', '/hello.html reason') 67 | t.assert_equals(string.match(r.body, 'static html'), 'static html', 68 | '/hello.html body') 69 | end 70 | 71 | g.test_absent_action = function() 72 | local r = http_client.get(helpers.base_uri .. '/absentaction') 73 | t.assert_equals(r.status, 500, '/absentaction 500') 74 | --t.assert_equals(r.reason, 'Unknown', '/absentaction reason') 75 | t.assert_equals(string.match(r.body, 'contain function'), 'contain function', 76 | '/absentaction body') 77 | end 78 | 79 | g.test_helper = function() 80 | local r = http_client.get(helpers.base_uri .. '/helper') 81 | t.assert_equals(r.status, 200, 'helper 200') 82 | t.assert_equals(r.reason, 'Ok', 'helper reason') 83 | t.assert_equals(string.match(r.body, 'Hello, world'), 'Hello, world', 'helper body') 84 | end 85 | 86 | g.test_500 = function() 87 | local httpd = g.httpd 88 | local r = http_client.get(helpers.base_uri .. '/helper?abc') 89 | t.assert_equals(r.status, 200, 'helper?abc 200') 90 | t.assert_equals(r.reason, 'Ok', 'helper?abc reason') 91 | t.assert_equals(string.match(r.body, 'Hello, world'), 'Hello, world', 'helper body') 92 | 93 | httpd:route({ 94 | path = '/die', 95 | file = 'helper.html.el' 96 | }, function() 97 | error(123) 98 | end) 99 | 100 | local r = http_client.get(helpers.base_uri .. '/die') 101 | t.assert_equals(r.status, 500, 'die 500') 102 | --t.assert_equals(r.reason, 'Unknown', 'die reason') 103 | end 104 | 105 | g.test_server_request_10 = function() 106 | local httpd = g.httpd 107 | httpd:route({ 108 | path = '/info' 109 | }, function(cx) 110 | return cx:render({ json = cx.peer }) 111 | end) 112 | 113 | local r = json.decode(http_client.get(helpers.base_uri .. '/info').body) 114 | t.assert_equals(r.host, helpers.base_host, 'peer.host') 115 | t.assert_type(r.port, 'number', 'peer.port') 116 | end 117 | 118 | g.test_POST = function() 119 | local httpd = g.httpd 120 | local r = httpd:route({ 121 | method = 'POST', 122 | path = '/dit', 123 | file = 'helper.html.el' 124 | }, function(tx) 125 | return tx:render({text = 'POST = ' .. tx:read()}) 126 | end) 127 | t.assert_type(r, 'table', ':route') 128 | end 129 | 130 | -- GET/POST at one route. 131 | g.test_GET_and_POST = function() 132 | local httpd = g.httpd 133 | local r = httpd:route({ 134 | method = 'POST', 135 | path = '/dit', 136 | file = 'helper.html.el' 137 | }, function(tx) 138 | return tx:render({text = 'POST = ' .. tx:read()}) 139 | end) 140 | t.assert_type(r, 'table', 'add POST method') 141 | 142 | r = httpd:route({ 143 | method = 'GET', 144 | path = '/dit', 145 | file = 'helper.html.el' 146 | }, function(tx) 147 | return tx:render({text = 'GET = ' .. tx:read()}) 148 | end) 149 | t.assert_type(r, 'table', 'add GET method') 150 | 151 | r = http_client.request('POST', helpers.base_uri .. '/dit', 'test') 152 | t.assert_equals(r.body, 'POST = test', 'POST reply') 153 | r = http_client.request('GET', helpers.base_uri .. '/dit') 154 | t.assert_equals(r.body, 'GET = ', 'GET reply') 155 | 156 | local r = http_client.request('GET', helpers.base_uri .. '/dit') 157 | t.assert_equals(r.body, 'GET = ', 'GET reply') 158 | 159 | local r = http_client.request('POST', helpers.base_uri .. '/dit', 'test') 160 | t.assert_equals(r.body, 'POST = test', 'POST reply') 161 | end 162 | 163 | -- test GET parameters. 164 | g.test_GET_params = function() 165 | local httpd = g.httpd 166 | 167 | local r = httpd:route({ 168 | method = 'GET', 169 | path = '/param', 170 | file = 'helper.html.el' 171 | }, function(tx) 172 | local params = "" 173 | for k,v in pairs(tx:param()) do 174 | params = params .. k .. "=" .. v 175 | end 176 | return tx:render({text = params .. tx:read()}) 177 | end) 178 | t.assert_type(r, 'table', 'add GET method') 179 | 180 | r = http_client.request('GET', helpers.base_uri .. '/param?a=1') 181 | t.assert_equals(r.body, 'a=1', 'GET reply parameter') 182 | 183 | r = http_client.request('GET', helpers.base_uri .. '/param?a+a=1') 184 | t.assert_equals(r.body, 'a a=1', 'GET reply parameter name with plus') 185 | 186 | r = http_client.request('GET', helpers.base_uri .. '/param?a=1+1') 187 | t.assert_equals(r.body, 'a=1 1', 'GET reply parameter value with plus') 188 | end 189 | 190 | g.test_DELETE = function() 191 | local httpd = g.httpd 192 | local r = httpd:route({ 193 | method = 'DELETE', 194 | path = '/dit', 195 | file = 'helper.html.el' 196 | }, function(tx) 197 | return tx:render({text = 'DELETE = ' .. tx:read()}) 198 | end) 199 | t.assert_type(r, 'table', 'add DELETE method') 200 | 201 | local r = http_client.request('DELETE', helpers.base_uri .. '/dit', 'test1') 202 | t.assert_equals(r.body, 'DELETE = test1', 'DELETE reply') 203 | end 204 | 205 | g.test_PATCH = function() 206 | local httpd = g.httpd 207 | local r = httpd:route({ 208 | method = 'PATCH', 209 | path = '/dit', 210 | file = 'helper.html.el' 211 | }, function(tx) 212 | return tx:render({text = 'PATCH = ' .. tx:read()}) 213 | end ) 214 | t.assert_type(r, 'table', 'add PATCH method') 215 | 216 | local r = http_client.request('PATCH', helpers.base_uri .. '/dit', 'test2') 217 | t.assert_equals(r.body, 'PATCH = test2', 'PATCH reply') 218 | end 219 | 220 | g.test_chunked_encoding = function() 221 | local httpd = g.httpd 222 | httpd:route({ 223 | path = '/chunked' 224 | }, function(self) 225 | return self:iterate(ipairs({'chunked', 'encoding', 't\r\nest'})) 226 | end) 227 | 228 | -- HTTP client currently doesn't support chunked encoding. 229 | local r = http_client.get(helpers.base_uri .. '/chunked') 230 | t.assert_equals(r.status, 200, 'chunked 200') 231 | t.assert_equals(r.headers['transfer-encoding'], 'chunked', 'chunked headers') 232 | t.assert_equals(r.body, 'chunkedencodingt\r\nest', 'chunked body') 233 | end 234 | 235 | -- Get raw cookie value (Günter -> Günter). 236 | g.test_get_cookie = function() 237 | local cookie = 'Günter' 238 | local httpd = g.httpd 239 | httpd:route({ 240 | path = '/receive_cookie' 241 | }, function(req) 242 | local name = req:cookie('name', { 243 | raw = true 244 | }) 245 | return req:render({ 246 | text = ('name=%s'):format(name) 247 | }) 248 | end) 249 | 250 | local r = http_client.get(helpers.base_uri .. '/receive_cookie', { 251 | headers = { 252 | cookie = 'name=' .. cookie, 253 | } 254 | }) 255 | 256 | t.assert_equals(r.status, 200, 'response status') 257 | t.assert_equals(r.body, 'name=' .. cookie, 'body with raw cookie') 258 | end 259 | 260 | -- Get escaped cookie (G%C3%BCnter -> Günter). 261 | g.test_get_escaped_cookie = function() 262 | local str_escaped = 'G%C3%BCnter' 263 | local str_non_escaped = 'Günter' 264 | local httpd = g.httpd 265 | httpd:route({ 266 | path = '/receive_cookie' 267 | }, function(req) 268 | local name = req:cookie('name') 269 | return req:render({ 270 | text = ('name=%s'):format(name) 271 | }) 272 | end) 273 | 274 | local r = http_client.get(helpers.base_uri .. '/receive_cookie', { 275 | headers = { 276 | cookie = 'name=' .. str_escaped, 277 | } 278 | }) 279 | 280 | t.assert_equals(r.status, 200, 'response status') 281 | t.assert_equals(r.body, 'name=' .. str_non_escaped, 'body with escaped cookie') 282 | end 283 | 284 | -- Set escaped cookie (Günter -> G%C3%BCnter). 285 | g.test_set_escaped_cookie = function(g) 286 | local str_escaped = 'G%C3%BCnter' 287 | local str_non_escaped = 'Günter' 288 | 289 | local httpd = g.httpd 290 | httpd:route({ 291 | path = '/cookie' 292 | }, function(req) 293 | local resp = req:render({ 294 | text = '' 295 | }) 296 | resp:setcookie({ 297 | name = 'name', 298 | value = str_non_escaped 299 | }) 300 | return resp 301 | end) 302 | 303 | local r = http_client.get(helpers.base_uri .. '/cookie') 304 | t.assert_equals(r.status, 200, 'response status') 305 | t.assert_equals(r.headers['set-cookie'], 'name=' .. str_escaped, 'header with escaped cookie') 306 | end 307 | 308 | -- Set raw cookie (Günter -> Günter). 309 | g.test_set_raw_cookie = function(g) 310 | local cookie = 'Günter' 311 | local httpd = g.httpd 312 | httpd:route({ 313 | path = '/cookie' 314 | }, function(req) 315 | local resp = req:render({ 316 | text = '' 317 | }) 318 | resp:setcookie({ 319 | name = 'name', 320 | value = cookie 321 | }, { 322 | raw = true 323 | }) 324 | return resp 325 | end) 326 | 327 | local r = http_client.get(helpers.base_uri .. '/cookie') 328 | t.assert_equals(r.status, 200, 'response status') 329 | t.assert_equals(r.headers['set-cookie'], 'name=' .. cookie, 'header with raw cookie') 330 | end 331 | 332 | -- Request object methods. 333 | g.test_request_object_methods = function() 334 | local httpd = g.httpd 335 | httpd:route({ 336 | path = '/check_req_methods_for_json', 337 | method = 'POST' 338 | }, function(req) 339 | return { 340 | headers = {}, 341 | body = json.encode({ 342 | request_line = req:request_line(), 343 | read_cached = req:read_cached(), 344 | json = req:json(), 345 | post_param_for_kind = req:post_param('kind'), 346 | }), 347 | status = 200, 348 | } 349 | end) 350 | 351 | httpd:route({ 352 | path = '/check_req_methods', 353 | method = 'POST' 354 | }, function(req) 355 | return { 356 | headers = {}, 357 | body = json.encode({ 358 | request_line = req:request_line(), 359 | read_cached = req:read_cached(), 360 | }), 361 | status = 200, 362 | } 363 | end) 364 | 365 | local r = http_client.post( 366 | helpers.base_uri .. '/check_req_methods_for_json', 367 | '{"kind": "json"}', { 368 | headers = { 369 | ['Content-type'] = 'application/json', 370 | ['X-test-header'] = 'test-value' 371 | } 372 | }) 373 | t.assert_equals(r.status, 200, 'status') 374 | 375 | local parsed_body = json.decode(r.body) 376 | t.assert_equals(parsed_body.request_line, 377 | 'POST /check_req_methods_for_json? HTTP/1.1', 'req.request_line') 378 | t.assert_equals(parsed_body.read_cached, 379 | '{"kind": "json"}', 'json req:read_cached()') 380 | t.assert_equals(parsed_body.json, { 381 | kind = 'json' 382 | }, 'req:json()') 383 | t.assert_equals(parsed_body.post_param_for_kind, 384 | 'json', 'req:post_param()') 385 | 386 | local r = http_client.post( 387 | helpers.base_uri .. '/check_req_methods', 388 | 'hello mister' 389 | ) 390 | t.assert_equals(r.status, 200, 'status') 391 | local parsed_body = json.decode(r.body) 392 | t.assert_equals(parsed_body.read_cached, 'hello mister', 393 | 'non-json req:read_cached()') 394 | end 395 | 396 | g.test_content_type_header_with_render = function() 397 | local httpd = g.httpd 398 | httpd:route({ 399 | method = 'GET', 400 | path = '/content_type', 401 | file = 'helper.html.el' 402 | }, function(tx) 403 | return tx:render() 404 | end) 405 | 406 | local r = http_client.get(helpers.base_uri .. '/content_type') 407 | t.assert_equals(r.status, 200) 408 | t.assert_equals(r.headers['content-type'], 'text/html; charset=utf-8', 'content-type header') 409 | end 410 | 411 | g.test_content_type_header_without_render = function() 412 | local httpd = g.httpd 413 | httpd:route({ 414 | path = '/content_type' 415 | }, function() end) 416 | local r = http_client.get(helpers.base_uri .. '/content_type') 417 | t.assert_equals(r.status, 200) 418 | t.assert_equals(r.headers['content-type'], 'text/plain; charset=utf-8', 'content-type header') 419 | end 420 | 421 | g.test_get_dot_slash = function() 422 | local httpd = g.httpd 423 | httpd:route({ 424 | path = '/*dot_slash' 425 | }, function() end) 426 | local r = http_client.get(helpers.base_uri .. '/dot_slash.') 427 | t.assert_equals(r.status, 200) 428 | end 429 | 430 | g.test_unwanted_content_type = function() 431 | local httpd = g.httpd 432 | httpd:route({ 433 | path = '/unwanted-content-type' 434 | }, function(req) 435 | local response = req:render{ json = req:param() } 436 | response.status = 200 437 | return response 438 | end) 439 | 440 | local opt = { 441 | headers = { 442 | ['Content-Type'] = 'application/json' 443 | } 444 | } 445 | local r = http_client.get(helpers.base_uri .. '/unwanted-content-type', opt) 446 | t.assert_equals(r.status, 200) 447 | t.assert_equals(r.body, '[]') 448 | end 449 | -------------------------------------------------------------------------------- /test/integration/http_server_url_for_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | 3 | local helpers = require('test.helpers') 4 | 5 | local g = t.group() 6 | 7 | g.before_each(function() 8 | g.httpd = helpers.cfgserv() 9 | g.httpd:start() 10 | end) 11 | 12 | g.after_each(function() 13 | helpers.teardown(g.httpd) 14 | end) 15 | 16 | g.test_server_url_for = function() 17 | local httpd = g.httpd 18 | t.assert_equals(httpd:url_for('abcdef'), '/abcdef', '/abcdef') 19 | t.assert_equals(httpd:url_for('test'), '/abc//', '/abc//') 20 | t.assert_equals(httpd:url_for('test', { cde = 'cde_v', def = 'def_v' }), 21 | '/abc/cde_v/def_v', '/abc/cde_v/def_v') 22 | t.assert_equals(httpd:url_for('star', { def = '/def_v' }), 23 | '/abb/def_v/cde', '/abb/def_v/cde') 24 | t.assert_equals(httpd:url_for('star', { def = '/def_v' }, { a = 'b', c = 'd' }), 25 | '/abb/def_v/cde?a=b&c=d', '/abb/def_v/cde?a=b&c=d') 26 | end 27 | -------------------------------------------------------------------------------- /test/integration/http_server_url_match_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | 3 | local helpers = require('test.helpers') 4 | 5 | local g = t.group() 6 | 7 | g.before_each(function() 8 | g.httpd = helpers.cfgserv() 9 | g.httpd:start() 10 | end) 11 | 12 | g.after_each(function() 13 | helpers.teardown(g.httpd) 14 | end) 15 | 16 | g.test_server_url_match = function() 17 | local httpd = g.httpd 18 | t.assert_type(httpd, 'table', 'httpd object') 19 | t.assert_not_equals(httpd, nil) 20 | t.assert_is(httpd:match('GET', '/'), nil) 21 | t.assert_equals(httpd:match('GET', '/abc').endpoint.path, '/abc', '/abc') 22 | t.assert_equals(#httpd:match('GET', '/abc').stash, 0, '/abc') 23 | t.assert_equals(httpd:match('GET', '/abc/123').endpoint.path, '/abc/:cde', '/abc/123') 24 | t.assert_equals(httpd:match('GET', '/abc/123').stash.cde, '123', '/abc/123') 25 | t.assert_equals(httpd:match('GET', '/abc/123/122').endpoint.path, '/abc/:cde/:def', 26 | '/abc/123/122') 27 | t.assert_equals(httpd:match('GET', '/abc/123/122').stash.def, '122', 28 | '/abc/123/122') 29 | t.assert_equals(httpd:match('GET', '/abc/123/122').stash.cde, '123', 30 | '/abc/123/122') 31 | t.assert_equals(httpd:match('GET', '/abc_123-122').endpoint.path, '/abc_:cde_def', 32 | '/abc_123-122') 33 | t.assert_equals(httpd:match('GET', '/abc_123-122').stash.cde_def, '123-122', 34 | '/abc_123-122') 35 | t.assert_equals(httpd:match('GET', '/abc-123-def').endpoint.path, '/abc-:cde-def', 36 | '/abc-123-def') 37 | t.assert_equals(httpd:match('GET', '/abc-123-def').stash.cde, '123', 38 | '/abc-123-def') 39 | t.assert_equals(httpd:match('GET', '/aba-123-dea/1/2/3').endpoint.path, 40 | '/aba*def', '/aba-123-dea/1/2/3') 41 | t.assert_equals(httpd:match('GET', '/aba-123-dea/1/2/3').stash.def, 42 | '-123-dea/1/2/3', '/aba-123-dea/1/2/3') 43 | t.assert_equals(httpd:match('GET', '/abb-123-dea/1/2/3/cde').endpoint.path, 44 | '/abb*def/cde', '/abb-123-dea/1/2/3/cde') 45 | t.assert_equals(httpd:match('GET', '/abb-123-dea/1/2/3/cde').stash.def, 46 | '-123-dea/1/2/3', '/abb-123-dea/1/2/3/cde') 47 | t.assert_equals(httpd:match('GET', '/banners/1wulc.z8kiy.6p5e3').stash.token, 48 | '1wulc.z8kiy.6p5e3', 'stash with dots') 49 | end 50 | -------------------------------------------------------------------------------- /test/integration/http_tls_enabled_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local http_server = require('http.server') 3 | local http_client = require('http.client').new() 4 | local fio = require('fio') 5 | 6 | local helpers = require('test.helpers') 7 | 8 | local g = t.group('ssl') 9 | 10 | local ssl_data_dir = fio.pathjoin(helpers.get_testdir_path(), "ssl_data") 11 | 12 | local server_test_cases = { 13 | test_key_password_missing = { 14 | ssl_opts = { 15 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key'), 16 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 17 | }, 18 | expected_err_msg = 'Private key is invalid or password mismatch', 19 | }, 20 | test_incorrect_key_password = { 21 | ssl_opts = { 22 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key'), 23 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 24 | ssl_password = 'incorrect_password', 25 | }, 26 | expected_err_msg = 'Private key is invalid or password mismatch', 27 | }, 28 | test_invalid_ciphers_server = { 29 | ssl_opts = { 30 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 31 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 32 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 33 | ssl_ciphers = "INVALID", 34 | }, 35 | expected_err_msg = "Ciphers are invalid", 36 | }, 37 | test_invalid_ca = { 38 | ssl_opts = { 39 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 40 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 41 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.key'), 42 | }, 43 | expected_err_msg = "CA file is invalid", 44 | }, 45 | } 46 | 47 | for name, tc in pairs(server_test_cases) do 48 | g.before_test(name, function() 49 | g.httpd = helpers.cfgserv(tc.ssl_opts) 50 | end) 51 | 52 | g.after_test(name, function() 53 | if g.httpd.is_run then 54 | helpers.teardown(g.httpd) 55 | end 56 | end) 57 | 58 | g[name] = function() 59 | t.assert_error_msg_contains(tc.expected_err_msg, function() 60 | g.httpd:start() 61 | end) 62 | end 63 | end 64 | 65 | local client_test_cases = { 66 | test_basic = { 67 | ssl_opts = { 68 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 69 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 70 | }, 71 | }, 72 | test_encrypted_key_ok = { 73 | ssl_opts = { 74 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key'), 75 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 76 | ssl_password = '1q2w3e', 77 | }, 78 | }, 79 | test_encrypted_key_password_file = { 80 | ssl_opts = { 81 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key'), 82 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 83 | ssl_password_file = fio.pathjoin(ssl_data_dir, 'passwd'), 84 | }, 85 | }, 86 | test_encrypted_key_many_passwords_file = { 87 | ssl_opts = { 88 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key'), 89 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 90 | ssl_password_file = fio.pathjoin(ssl_data_dir, 'passwords'), 91 | }, 92 | }, 93 | test_key_crt_ca_server_key_crt_client = { 94 | ssl_opts = { 95 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 96 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 97 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 98 | }, 99 | request_opts = { 100 | ssl_cert = fio.pathjoin(ssl_data_dir, 'client.crt'), 101 | ssl_key = fio.pathjoin(ssl_data_dir, 'client.key'), 102 | }, 103 | }, 104 | test_client_password_key_missing = { 105 | ssl_opts = { 106 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 107 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 108 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 109 | }, 110 | request_opts = { 111 | ssl_cert = fio.pathjoin(ssl_data_dir, 'client.crt'), 112 | ssl_key = fio.pathjoin(ssl_data_dir, 'client.enc.key'), 113 | }, 114 | expected_err_msg = "curl: Problem with the local SSL certificate", 115 | }, 116 | test_ciphers_server = { 117 | ssl_opts = { 118 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 119 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 120 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 121 | ssl_ciphers = "ECDHE-RSA-AES256-GCM-SHA384", 122 | }, 123 | request_opts = { 124 | ssl_cert = fio.pathjoin(ssl_data_dir, 'client.crt'), 125 | ssl_key = fio.pathjoin(ssl_data_dir, 'client.key'), 126 | }, 127 | }, 128 | test_invalid_key_path = { 129 | ssl_opts = { 130 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 131 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 132 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 133 | }, 134 | request_opts = { 135 | ssl_cert = fio.pathjoin(ssl_data_dir, 'client.crt'), 136 | ssl_key = fio.pathjoin(ssl_data_dir, 'invalid.key'), 137 | }, 138 | expected_err_msg = "curl: Problem with the local SSL certificate", 139 | }, 140 | test_invalid_cert_path = { 141 | ssl_opts = { 142 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 143 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 144 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 145 | }, 146 | request_opts = { 147 | ssl_cert = fio.pathjoin(ssl_data_dir, 'invalid.crt'), 148 | ssl_key = fio.pathjoin(ssl_data_dir, 'client.key'), 149 | }, 150 | expected_err_msg = "curl: Problem with the local SSL certificate", 151 | }, 152 | } 153 | 154 | for name, tc in pairs(client_test_cases) do 155 | g.before_test(name, function() 156 | g.httpd = helpers.cfgserv(tc.ssl_opts) 157 | g.httpd:start() 158 | end) 159 | 160 | g.after_test(name, function() 161 | helpers.teardown(g.httpd) 162 | end) 163 | 164 | g[name] = function() 165 | local req_opts = http_server.internal.extend({ 166 | -- We need to provide ca_file by default because curl uses the 167 | -- system native CA store for verification. 168 | -- See: https://curl.se/docs/sslcerts.html 169 | ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 170 | verbose = true, 171 | }, tc.request_opts or {}) 172 | 173 | if tc.expected_err_msg ~= nil then 174 | t.assert_error_msg_contains(tc.expected_err_msg, function() 175 | http_client:get(helpers.tls_uri .. '/test', req_opts) 176 | end) 177 | else 178 | local r = http_client:get(helpers.tls_uri .. '/test', req_opts) 179 | t.assert_equals(r.status, 200, 'response not 200') 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /test/integration/http_tls_enabled_validate_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local fio = require("fio") 3 | 4 | local http_server = require('http.server') 5 | local helpers = require('test.helpers') 6 | 7 | local g = t.group() 8 | 9 | local ssl_data_dir = fio.pathjoin(helpers.get_testdir_path(), "ssl_data") 10 | 11 | local test_cases = { 12 | ssl_cert_file_missing = { 13 | opts = { 14 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 15 | }, 16 | expected_err_msg = "ssl_key_file and ssl_cert_file must be set to enable TLS", 17 | }, 18 | ssl_cert_file_incorrect_type = { 19 | opts = { 20 | ssl_cert_file = 1, 21 | }, 22 | expected_err_msg = "ssl_cert_file option must be a string", 23 | }, 24 | cert_file_not_exists = { 25 | opts = { 26 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 27 | ssl_cert_file = "some/path", 28 | }, 29 | expected_err_msg = 'file "some/path" not exists', 30 | }, 31 | ssl_key_file_missing = { 32 | opts = { 33 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 34 | }, 35 | expected_err_msg = "ssl_key_file and ssl_cert_file must be set to enable TLS", 36 | }, 37 | ssl_key_file_incorrect_type = { 38 | opts = { 39 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 40 | ssl_key_file = 1, 41 | }, 42 | expected_err_msg = "ssl_key_file option must be a string", 43 | }, 44 | ssl_key_file_not_exists = { 45 | opts = { 46 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 47 | ssl_key_file = "some/path", 48 | }, 49 | expected_err_msg = 'file "some/path" not exists', 50 | }, 51 | ssl_password_incorrect_type = { 52 | opts = { 53 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 54 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 55 | ssl_password = 1, 56 | }, 57 | expected_err_msg = "ssl_password option must be a string", 58 | }, 59 | ssl_password_file_incorrect_type = { 60 | opts = { 61 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 62 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 63 | ssl_password = "password", 64 | ssl_password_file = 1, 65 | }, 66 | expected_err_msg = "ssl_password_file option must be a string", 67 | }, 68 | ssl_password_file_not_exists = { 69 | opts = { 70 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 71 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 72 | ssl_password = "password", 73 | ssl_password_file = "some/path", 74 | }, 75 | expected_err_msg = 'file "some/path" not exists', 76 | }, 77 | ssl_ca_file_incorrect_type = { 78 | opts = { 79 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 80 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 81 | ssl_password = "password", 82 | ssl_password_file = fio.pathjoin(ssl_data_dir, 'passwords'), 83 | ssl_ca_file = 1, 84 | }, 85 | expected_err_msg = "ssl_ca_file option must be a string", 86 | }, 87 | ssl_ca_file_not_exists = { 88 | opts = { 89 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 90 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 91 | ssl_password = "password", 92 | ssl_password_file = fio.pathjoin(ssl_data_dir, 'passwords'), 93 | ssl_ca_file = "some/path", 94 | }, 95 | expected_err_msg = 'file "some/path" not exists', 96 | }, 97 | ssl_ciphers_incorrect_type = { 98 | opts = { 99 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 100 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 101 | ssl_password = "password", 102 | ssl_password_file = fio.pathjoin(ssl_data_dir, 'passwords'), 103 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 104 | ssl_ciphers = 1, 105 | }, 106 | expected_err_msg = "ssl_ciphers option must be a string", 107 | }, 108 | } 109 | 110 | for name, case in pairs(test_cases) do 111 | g['test_ssl_option_' .. name] = function() 112 | t.assert_error_msg_contains(case.expected_err_msg, function() 113 | http_server.new('host', 8080, case.opts) 114 | end) 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/integration/httpd_role_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local treegen = require('luatest.treegen') 3 | local server = require('luatest.server') 4 | local fun = require('fun') 5 | local yaml = require('yaml') 6 | local fio = require('fio') 7 | local http_client = require('http.client').new() 8 | 9 | 10 | local helpers = require('test.helpers') 11 | 12 | local g = t.group(nil, t.helpers.matrix({use_tls = {true, false}})) 13 | 14 | local ssl_data_dir = fio.abspath(fio.pathjoin(helpers.get_testdir_path(), "ssl_data")) 15 | 16 | local config = { 17 | credentials = { 18 | users = { 19 | guest = { 20 | roles = {'super'}, 21 | }, 22 | }, 23 | }, 24 | iproto = { 25 | listen = {{uri = 'unix/:./{{ instance_name }}.iproto'}}, 26 | }, 27 | groups = { 28 | ['group-001'] = { 29 | replicasets = { 30 | ['replicaset-001'] = { 31 | roles = { 32 | 'roles.httpd', 33 | 'test.mocks.mock_role', 34 | }, 35 | roles_cfg = { 36 | ['roles.httpd'] = { 37 | default = { 38 | listen = 13000, 39 | }, 40 | additional = { 41 | listen = 13001, 42 | } 43 | }, 44 | ['test.mocks.mock_role'] = { 45 | { 46 | id = 1, 47 | }, 48 | { 49 | id = 2, 50 | name = 'additional', 51 | }, 52 | }, 53 | }, 54 | instances = { 55 | ['instance-001'] = {}, 56 | }, 57 | }, 58 | }, 59 | }, 60 | }, 61 | } 62 | 63 | local tls_config = table.deepcopy(config) 64 | tls_config.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default 65 | .ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt') 66 | 67 | tls_config.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default 68 | .ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key') 69 | 70 | tls_config.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default 71 | .ssl_password_file = fio.pathjoin(ssl_data_dir, 'passwords') 72 | 73 | g.before_each(function(cg) 74 | helpers.skip_if_not_tarantool3() 75 | 76 | local dir = treegen.prepare_directory({}, {}) 77 | 78 | local cfg = config 79 | if cg.params.use_tls then 80 | cfg = tls_config 81 | end 82 | 83 | local config_file = treegen.write_file(dir, 'config.yaml', 84 | yaml.encode(cfg)) 85 | local opts = {config_file = config_file, chdir = dir} 86 | cg.server = server:new(fun.chain(opts, {alias = 'instance-001'}):tomap()) 87 | helpers.update_lua_env_variables(cg.server) 88 | 89 | cg.server:start() 90 | end) 91 | 92 | g.after_each(function(cg) 93 | helpers.teardown(cg.server) 94 | end) 95 | 96 | g.test_httpd_role_usage = function(cg) 97 | if cg.params.use_tls then 98 | local resp = http_client:get('https://localhost:13000/ping', { 99 | ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt') 100 | }) 101 | t.assert_equals(resp.status, 200, 'response not 200') 102 | t.assert_equals(resp.body, 'pong') 103 | end 104 | 105 | -- We can use https only for one endpoind due to we haven't publish separate 106 | -- certificates for it. 107 | local resp = http_client:get('http://localhost:13001/ping') 108 | t.assert_equals(resp.status, 200, 'response not 200') 109 | t.assert_equals(resp.body, 'pong') 110 | 111 | t.assert_equals(cg.server:eval( 112 | 'return require("test.mocks.mock_role").get_server_port(1)' 113 | ), 13000) 114 | t.assert_equals(cg.server:eval( 115 | 'return require("test.mocks.mock_role").get_server_port(2)' 116 | ), 13001) 117 | end 118 | -------------------------------------------------------------------------------- /test/mocks/mock_role.lua: -------------------------------------------------------------------------------- 1 | local M = {dependencies = { 'roles.httpd' }} 2 | 3 | local servers = {} 4 | 5 | M.validate = function() end 6 | 7 | M.apply = function(conf) 8 | for _, server in pairs(conf) do 9 | servers[server.id] = require('roles.httpd').get_server(server.name) 10 | 11 | servers[server.id]:route({ 12 | path = '/ping', 13 | }, function(tx) 14 | return tx:render({text = 'pong'}) 15 | end) 16 | end 17 | end 18 | 19 | M.stop = function() end 20 | 21 | M.get_server_port = function(id) 22 | return servers[id].port 23 | end 24 | 25 | return M 26 | -------------------------------------------------------------------------------- /test/public/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello, tarantool 4 | 5 | 6 | static html file 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/public/lorem.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ut nisl pulvinar, ornare justo nec, vulputate enim. Phasellus porta erat quis tortor volutpat, sed semper dui imperdiet. Nullam efficitur ornare urna, volutpat dignissim augue efficitur ac. Nunc dignissim consectetur sapien vel dignissim. Aliquam a lorem urna. Phasellus mollis faucibus dictum. Phasellus tristique lacus et dui sollicitudin, in hendrerit risus tincidunt. Aliquam massa leo, semper et enim at, iaculis gravida orci. Praesent eu odio quis nisi malesuada congue. 2 | Nam in ipsum magna. Ut convallis arcu eget eleifend egestas. Mauris commodo ac metus quis vehicula. Proin mi nisl, bibendum sed justo vel, dictum auctor libero. In aliquet ex sit amet enim varius, in eleifend arcu dapibus. Quisque pellentesque mauris a ipsum iaculis, sit amet porta felis fermentum. Nunc mattis nibh et placerat fringilla. Nullam porta, lorem at facilisis scelerisque, felis dolor commodo turpis, ultrices dapibus ipsum dui vitae tortor. Donec a interdum erat. 3 | Sed elementum enim vel egestas auctor. Proin hendrerit erat a erat viverra, id accumsan ligula porttitor. Maecenas rutrum eget risus eget fringilla. Aenean at mauris posuere, lacinia massa at, laoreet urna. Sed varius tortor ut massa tristique, id lobortis risus venenatis. Praesent vel felis sit amet orci interdum lacinia. Mauris id cursus lectus. Cras consequat facilisis justo in hendrerit. Morbi faucibus convallis tellus, non pulvinar felis mattis vitae. Aliquam fringilla a nisi et hendrerit. Sed tincidunt mauris massa, non consectetur erat tempus ut. Suspendisse a libero nulla. 4 | Nunc magna enim, efficitur vel urna et, volutpat pretium neque. Aliquam nec mauris nec eros eleifend vestibulum at et purus. Mauris blandit sem massa, id placerat neque volutpat sit amet. Nullam gravida sollicitudin enim quis fringilla. Donec ante justo, placerat nec tortor ut, ullamcorper dignissim ligula. Morbi ac arcu odio. Pellentesque laoreet mollis urna, sit amet ultrices nisl dapibus laoreet. Nunc facilisis et urna at ullamcorper. 5 | Proin sed mollis orci. In pulvinar velit varius lorem pretium sagittis. Morbi non erat est. Nullam at quam magna. In ac vehicula nulla. Praesent vel euismod turpis. Integer erat enim, accumsan nec eleifend nec, congue sit amet magna. Phasellus non massa id lectus dictum convallis ultricies ac augue. Ut nec ante quis justo placerat efficitur. 6 | -------------------------------------------------------------------------------- /test/ssl_data/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test/ssl_data/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | 4TlYf5C8oriuXlIl/xjIG4HwVqCadYY= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /test/ssl_data/ca.srl: -------------------------------------------------------------------------------- 1 | 74C2F45BD69A6CF5D81316DE585538739B3BFD95 2 | -------------------------------------------------------------------------------- /test/ssl_data/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFkDCCA3igAwIBAgIUdML0W9aabPXYExbeWFU4c5s7/ZUwDQYJKoZIhvcNAQEL 3 | BQAwVTEQMA4GA1UECwwHVW5rbm93bjEQMA4GA1UECgwHVW5rbm93bjEQMA4GA1UE 4 | BwwHVW5rbm93bjEQMA4GA1UECAwHdW5rbm93bjELMAkGA1UEBhMCQVUwIBcNMjQx 5 | MTA4MDczODMwWhgPMjEyNDEwMTUwNzM4MzBaMGkxEjAQBgNVBAMMCWxvY2FsaG9z 6 | dDEQMA4GA1UECwwHVW5rbm93bjEQMA4GA1UECgwHVW5rbm93bjEQMA4GA1UEBwwH 7 | VW5rbm93bjEQMA4GA1UECAwHdW5rbm93bjELMAkGA1UEBhMCQVUwggIiMA0GCSqG 8 | SIb3DQEBAQUAA4ICDwAwggIKAoICAQDSboDBNNB1tNcBZabnihhzScKykxhhToA+ 9 | 3vAkyOslp7Z1DeQGZ/jt0IFyyDXTLKRFRu2U8vMlxnxKrpwUD3MMFyJFDg9p7oHF 10 | ds6wNGlm79cF+WsTzSYg3Ep0egku8SpvnvMxRS/gJXn3w/QCK3sUj7J8f81fh4Dp 11 | zmzBB6+vf3Roryp745GTeYuJaV0pKZ/cZexpTPw4kvHWgq1TDeIV76rTyPXY0dYn 12 | dtXqzCEm2TCMj2Rek/YPHlM219p6XWisuOMHqIH4/LZOMPElTkccajPxWy2C/kUC 13 | USKA5A98011xXUn+BnNs2HEIOFJQhJDL+xEizNFbP2Ca/xhRdDt+0xX0I3313gmy 14 | qfnof6mivWUsb6EG8l1Py9Nrr4MEzU8917i4hCU9WitL8XJbGjJi8O2n9KuNoRPs 15 | skHkbtJ1Fv2FAVt5pKLNYYolknJVQ2xSlgEaBbQqhXM4xM23hCAe5jTk6MqwUeYM 16 | HZri9LnQ3Chvfw/qWGGt82mQewcueMZV0FPwOomKctnTwCYdImam/YDV9JE8dAXh 17 | t7mLNJufPXRXZt6Tp4XWDc8img8o9d1ExapqyCXxxb4h/BptwTeZMSNH0RD+nvDw 18 | T82CGRIzuN6gODqesUu8BTB84W1xIM+yylfeOFiXIGJD0oo+cBLn9QDHLqDrFgJ2 19 | ZChOH9/lqQIDAQABo0IwQDAdBgNVHQ4EFgQU1mlkt5xLVzigVk5d78ClvrPTfu0w 20 | HwYDVR0jBBgwFoAUddXCOeipTENVQsf93cam7mZIW08wDQYJKoZIhvcNAQELBQAD 21 | ggIBAGte0IqifuuwfHjhLS6mypTBiVMcrotwHPHguZL1gCg5xPpVRre8MhE802BN 22 | e4Uz1dH7Ri2IvW7QQ2XmeZVKs6wxrnD6Ubt8Hx6HlumdesJKRTlXzOUUqMUZM/um 23 | 18wjIo5i+sIzsFTUicXNwc0SpeT1k8J+lWW2QxAIVxdv9HRI+l+RRRJbxZWjUoDK 24 | nOKRCbDN125f1NcpU1Nqh6dLwg0hv86T+CJmGT8GADmJ/CakcHZw8PFWoWAGVRep 25 | HlaRjyD/nHDmkYFmJCFyYI4gDThnbb7Ke1EKXlxc6gWpCMhLvWsGiZwFqGauK3Mp 26 | Bh8LE1XCXJ7d510Ea/7fkiz4qdhVs3wDIWg9TmydVLPd7/VmTa0pHmrldaiCmQhu 27 | JoxXTTpY8zRRe5kd7mdqoSZB0qwfn3kouT8FK5cOghXGaSqqskhzEGr/RuyAsYMA 28 | jx0PW6PLZ1+pGG2d7kAL1ReKwmz1UaGyTl7ntGle1doWwpMPQGf9TlXvscNhCDoP 29 | cDBYOJkyEfFVDYYhHUg9GpDSpFONWNKxMFynO8I5NPovnY1j4VJ5GmvnQPXmTvt8 30 | KfbPFUuaTsywMoyPzwcMPxRDdcz6QSLaWHXPqoa7sVToC3zn5ra8/PFeU/JoPjKS 31 | U0EpGjnWv3QqEz19YkejePKguVqGaa9AMsMAe0ZT4UqTIj3N 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test/ssl_data/client.enc.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | 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 54 | -----END ENCRYPTED PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /test/ssl_data/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | 9eIgheeP8x6NoIy/1o+oiZGoFOlitIY= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /test/ssl_data/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Significant part of this script came from 3 | # https://github.com/tarantool/tarantool-ee/blob/master/test/enterprise-luatest/ssl_cert/gen.sh 4 | # 5 | # This script generates SSL keys and certificates used for testing. 6 | # The version of OpenSSL from which files was generated is 3.4.0. 7 | 8 | HOST=localhost 9 | NEWKEY_ARG=rsa:4096 10 | DAYS_ARG=36500 11 | 12 | # 13 | # Generates new CA. 14 | # 15 | # The new private key and certificate are written to files "${ca}.key" and 16 | # "${ca}.crt" respectively where "${ca}" is the name of the new CA as given 17 | # in the first argument. 18 | # 19 | gen_ca() 20 | { 21 | local ca="${1}" 22 | openssl req -new -nodes -newkey "${NEWKEY_ARG}" -days "${DAYS_ARG}" -x509 \ 23 | -subj "/OU=Unknown/O=Unknown/L=Unknown/ST=unknown/C=AU" \ 24 | -keyout "${ca}.key" -out "${ca}.crt" 25 | } 26 | 27 | # 28 | # Generates new certificate and private key signed by the given CA. 29 | # 30 | # The new private key and certificate are written to files "${cert}.key" and 31 | # "${cert}.crt" respectively where "${cert}" is the certificate name as given 32 | # in the first argument. The CA and private key used for signing the new 33 | # certificate should be located in "${ca}.cert" and "${ca}.key" where "${ca}" 34 | # is the value of the second argument. 35 | # 36 | gen_cert() 37 | { 38 | local cert="${1}" 39 | local ca="${2}" 40 | openssl req -new -nodes -newkey "${NEWKEY_ARG}" \ 41 | -subj "/CN=${HOST}/OU=Unknown/O=Unknown/L=Unknown/ST=unknown/C=AU" \ 42 | -keyout "${cert}.key" -out "${cert}.csr" 43 | openssl x509 -req -days "${DAYS_ARG}" \ 44 | -CAcreateserial -CA "${ca}.crt" -CAkey "${ca}.key" \ 45 | -in "${cert}.csr" -out "${cert}.crt" 46 | rm -f "${cert}.csr" 47 | } 48 | 49 | # 50 | # Encrypt private key file. 51 | # $1 - file name without extension 52 | # $2 - pass phrase 53 | # 54 | # Encrypted key is written to ${1}.enc.key 55 | # 56 | encrypt_key() 57 | { 58 | local key="${1}" 59 | local pass="${2}" 60 | openssl rsa -aes256 -passout "pass:${pass}" \ 61 | -in "${key}.key" -out "${key}.enc.key" 62 | } 63 | 64 | gen_ca ca 65 | gen_cert server ca 66 | gen_cert client ca 67 | encrypt_key server 1q2w3e 68 | encrypt_key client 123qwe 69 | 70 | echo '1q2w3e' > passwd 71 | echo $'incorrect_password\n1q2w3e' > passwords 72 | -------------------------------------------------------------------------------- /test/ssl_data/passwd: -------------------------------------------------------------------------------- 1 | 1q2w3e 2 | -------------------------------------------------------------------------------- /test/ssl_data/passwords: -------------------------------------------------------------------------------- 1 | incorrect_password 2 | 1q2w3e 3 | -------------------------------------------------------------------------------- /test/ssl_data/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test/ssl_data/server.enc.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIJtTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQ7I2+/mVx6yS90HEJ 3 | MStG9AICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEHW3dsgTgVZTsfbs 4 | ngJ5IMIEgglQMAYWO0+LPbnBDhg5lhZBsoOzuW3BiobXeOH+NhlXYNV1vJEiACh4 5 | NiV3+756ZSq1Vcd0tLjT7TgQ7jWjDcOuPxtyC9zVLi2pVYYMI2K9yX2j2vSObPYV 6 | t/LPI+i7cBNbUK7RI78LDmw2R9w3DooxvA7RZrkvHOn387NMEIdH/snU9LwpuXbH 7 | 4D5ejIxTeZJe7jWS2jDjMh+jGobVFMPHQGlL7/wr0eabpUfCyzqt58W1Fo8YpoFT 8 | ytq2aGZ23++Joh20yirWx45jO0dCmhaQQ+5DwYC3nYw03HAqxElA3Gmf4FZHzWZP 9 | g3Fn43IOPeSVBnXdQDcgLWpVyrYlVbVAoTbHNR7CVocFXHY6PLTTrpIhU42J9i2A 10 | /p+kNNKVqdJ7MNhYS/zq4k/A4osWj8HpgtijkdpvhB+XEP5aeomFAqcgj7dlExKZ 11 | tIdCNeFygILg+xCy9LtqgWKkYWunaBYrqMFPB6va/ytqcZZinu6nFsxlYshwI01s 12 | rojWZYepRr9Ry+AGqfdug9shoS8Cz2T5VB2ea8FB667H09PlzdmPC6I+9whKege1 13 | KTmzSMdebHV6EUQ/vlUwqfrW7geIg/mKGWLfdveSqp16T2T+spyiVMETaCCHVkah 14 | uFs8C+7sRqzUP/UZm6WZCgr9RMsHuAOlUp2+GJ2cwSsD/vVuniJYbz0U4iKX5g8z 15 | Yw+/KamkNBn77D9nIUnMV/aS6XZc//HwbVgOzt0K2vK1aPU683Sw9wyTzXxzArPm 16 | FFl4NbqmmkpFwq+jmDkMmmWbLqtn4fXisswn/R+h0KUoDpcJcTgz+TrCVCViEkz8 17 | Kmpchvp3xENkZbsoZBKZLNljPpHx0zgvgVZRAdkzf9pMGBiblchfuBeNvBhDDHmW 18 | DMa73cU/xmuDkfHkRUdMmdbD3+50vrunPue16wYpVJe5vSihzys++i6OLPqIBA89 19 | fXcX6HyAgtMlLv6HfOiSTH4SVtsKJ5/u6uNorRlTuGjpMOZG6E26tHSs+h8OgUFr 20 | 7xIZmnhT0J/DkepEDBmsV6FT0/B+C8Cl1fCH+/b/SH5ca23xqgs9h1dqCk3+Cy0K 21 | XJrehiJU85u36/R/cHQnaxk3yVDIvAcHJdZfbuFMk759vBh1SkhDEtDtpFmoQCRe 22 | iksGPo46crZuQD9ltqmuPWr+Ck/ejwwHDYx45HjdrOq1PGorAHifi63jXf9A7L0+ 23 | Ot4oqCELySVkC0JVDIXXid1ibLFVsqNwplAipzj7zLxWDkkGZpC1Dbhmv2wKoy86 24 | i7AMd6pRK/F/hFtEvI7mq8OA9RqTiZh3qSMxcw0/Ev6cMjD9VY5ZQNLcpxKGuyMi 25 | YC7DlsKL2wm0XV0smVY9OlnqAVBqP0tf4J7NX68aZ72XLzSbR4XHs6Z5p2LBnDwV 26 | fNwJqVyRIYwCDOL2oaxzyk6lXwVAO8b/Mld152Iv3fc2ZBsIe4AJiBtMDlju+V7H 27 | KCTleuknCRUcJ/1vRNnQLOON+9Q8T9FBQ8PeiPKkik/9Mo/8GqAmRhPkfM48Jath 28 | vZkDEj9nVTnYoW7KdlR9IP/6Ta2RO7BIub84uFxopiedAtx3p8zECqvqNUqfO9c2 29 | LAwYrNPiOMHgMp9FsDSIngnkhX5oOUDmaJFBxbVt745kXboX5KcRX8yTI7U2adeu 30 | SppOw8c7WB1rp12TUDSxxHQ5Qh0+nq6nNqFNZlLVZKjiMCKeM0nAkyVQBx6KHS4A 31 | 3jCHPVfreJz7S9qqGjYb/aIHsZGYRRx1JOkE+LRyi2kzune6TQTD5xwBECWJ4jRV 32 | I+3FGH9Ki489s3ItLwaFRO13xz2QF0cM0KWpDKgYaDvraCFG13xXAzHd1xR4PgHW 33 | SrundzjyFpBDQt/f0u7Jvg7u5Ai6T+WX4DkfwzS18rnnCH7HDgzNaVDI35QGb6Ro 34 | OCrgfCX8i4dApc9va9D7Uld7DB+rnhbALS8YDgkb4IRyNirCssCD24d8r2vKWZop 35 | 1hkB9RmJoGXmmBhRjpFUaOzOCaH3JRZ85kQn3bCQteqbq2uWxznSstJsiLapel8R 36 | F6AJ3cRxMUqrOcZCVdQcjtZ2yJ9ps2BV39o7gSLn+OBKoM3czsfxB49EpoWcN8VS 37 | hF32HMPYcOMJqTAuRkykBf45f+yULWY1R6nQXVVemzDkKcwIEnnZZ3mWs5ekLUD5 38 | lFBP82HI3V1saZIOBvXYIW20cmukvxKJ65KDO9ZYvOOTQ6fpsjqySCOQc1at/6D0 39 | seH0uE+QqhaKR4Bt6tk12eoRlTTWf25Vd8Qu2ydknJPitSKbQXrqSFBuKxNcXOOn 40 | DH4PXWlUI3mrPZKotWsiMM7Bz5kLklSunWPKJhwnx/V2TsqLk8vnZt6CtGSs1Kcw 41 | ZUtHXkv1DnKzydPgn4KQAUfPT0CnzFYRKQW2/6L0RpgL+wgcyCx+S8+PBEe7Hgke 42 | 2u6AqaFc2DsB78de92LQR7VCmATHAKv8FZf6kFoGUmag4/nhtWvJ9InWWjz8Q2mr 43 | EQsydhwjho46JjXAHuOPa1KqfTcBg8e5A0mZ/o/yUoTQ2MAD8+cckaJo0WSJk1ZF 44 | sRHzs2mmq7yEFFxWTd1ePB1fSPlf4kGs6phJTR+5luitpd6Qcclx3GW/FkIaX2Dw 45 | y3JK0nIYmxkMqS0WMpObYLnzw7yes6FbkZQcDsmEHVC/g21L80coRR4BgtMvJeta 46 | +0siRcgXBAucRJkcgYGNHXtrD4xh7larwPPVJ/92b7VNgOJ+YnjwlyG65lH+P5ab 47 | 1kmVH+n/oFnuFxlMcFcTYGLnj81tYgSjP1HRXIaF96Rqh1h7ArILoCCumw6Vv4Of 48 | 3zMQ69LRdOa1DmYJFrP/o9DEd9CWFe/MnF1UG5c8waB/GoSu39mFT2FA7zYIGovA 49 | 6Vct6eIzYpOOolrX0bW6iCctccK7bxf8RJVZEdcY9Jp+gnMvBd92UbBsBrBi7ufh 50 | O83O2pZxlbhzJSBUpL3DAjEm69Qzp2XEPaw8HlpxnDg1q2o+HVjcZJlbm3giCl/T 51 | 4jcvoOaXEK0+RV3SBhZgYTfgWeVy4ILVoAfFq2TYWDa+Y4dqjN0PGe/i8l7eynAu 52 | EpdNdT1GDtBJmu7ig1sSmMGdhsqpT02XuGYMPX5jTzZZsh+Fpq0eLREmgGc+TM3M 53 | s5/RmpDGsdSyOtaMw91kTGtGcbD7tDMEy1UtDO1z0xc/d7wKaXdz0Ks= 54 | -----END ENCRYPTED PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /test/ssl_data/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | OKeTgVk//S5ZTzfk3kynPNbepCk9Gbo= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /test/templates/helper.html.el: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= helper_title('world') %> 4 | 5 | 6 | Helpers test 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/templates/module/controller/action.html.el: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello, Tarantool httpd! 4 | 5 | 6 | action: <%= action %> 7 | controller: <%= controller %> 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/templates/module/controller/action.js.el: -------------------------------------------------------------------------------- 1 | { 2 | "hello":"json template <%= format %>" 3 | } 4 | -------------------------------------------------------------------------------- /test/templates/test.html.el: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= title %> 4 | 5 | 6 | Tarantool is an application server 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/unit/http_custom_socket_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local http_server = require('http.server') 3 | 4 | local g = t.group() 5 | 6 | g.test_custom_socket = function() 7 | local is_listening = false 8 | 9 | local function tcp_server(host, port, opts) 10 | assert(type(opts) == 'table') 11 | local name = opts.name 12 | local accept_handler = opts.handler 13 | local http_server = opts.http_server 14 | 15 | -- Verify arguments. 16 | t.assert_equals(host, 'host', 'check host') 17 | t.assert_equals(port, 123, 'check port') 18 | 19 | -- Verify options. 20 | t.assert_equals(name, 'http', 'check server name') 21 | t.assert_type(accept_handler, 'function', 'check accept handler') 22 | t.assert_type(http_server.routes, 'table', 23 | 'http server object is accessible') 24 | 25 | is_listening = true 26 | return { 27 | close = function(self) 28 | is_listening = false 29 | end 30 | } 31 | end 32 | 33 | local myhttp = http_server.new('host', 123) 34 | myhttp.tcp_server_f = tcp_server 35 | myhttp:route({path = '/abc'}, function(_) end) 36 | myhttp:start() 37 | t.assert_equals(is_listening, true, 'custom socket is actually used') 38 | 39 | -- The key reason why the field is called `tcp_server_f` is 40 | -- that the HTTP server sets `myhttp.tcp_server` to the TCP 41 | -- server object. The name clash is harmless for the module, 42 | -- but may confuse a user, so the `tcp_server_f` was chosen 43 | -- instead of just `tcp_server`. The `tcp_server_f` field is 44 | -- guaranteed to remains the same after `myhttp:start()`. 45 | t.assert_equals(myhttp.tcp_server_f, tcp_server, 46 | 'tcp_server_f field was not changed after :start()') 47 | 48 | myhttp:stop() 49 | t.assert_equals(is_listening, false, 'custom socket was closed') 50 | end 51 | -------------------------------------------------------------------------------- /test/unit/http_params_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local http_lib = require('http.lib') 3 | 4 | local pgroup = t.group('http_params', { 5 | { params = nil, t = {}, comment = 'nil string' }, 6 | { params = '', t = {}, comment = 'empty string' }, 7 | { params = 'a', t = { a = '' }, comment = 'separate literal' }, 8 | { params = 'a=b', t = { a = 'b' }, comment = 'one variable' }, 9 | { params = 'a=b&b=cde', t = {a = 'b', b = 'cde'}, comment = 'some'}, 10 | { params = 'a=b&b=cde&a=1', t = {a = { 'b', '1' }, b = 'cde'}, comment = 'array'} 11 | }) 12 | 13 | pgroup.test_params = function(g) 14 | t.assert_equals(http_lib.params(g.params.params), g.params.t, g.params.comment) 15 | end 16 | -------------------------------------------------------------------------------- /test/unit/http_parse_request_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local http_lib = require('http.lib') 3 | 4 | local g = t.group() 5 | 6 | g.test_parse_request = function() 7 | t.assert_equals(http_lib._parse_request('abc'), { 8 | error = 'Broken request line', 9 | headers = {} 10 | }, 'broken request') 11 | 12 | t.assert_equals( 13 | http_lib._parse_request('GET / HTTP/1.1\nHost: s.com\r\n\r\n').path, 14 | '/', 15 | 'path' 16 | ) 17 | t.assert_equals( 18 | http_lib._parse_request('GET / HTTP/1.1\nHost: s.com\r\n\r\n').proto, 19 | {1, 1}, 20 | 'proto' 21 | ) 22 | t.assert_equals( 23 | http_lib._parse_request('GET / HTTP/1.1\nHost: s.com\r\n\r\n').headers, 24 | {host = 's.com'}, 25 | 'host' 26 | ) 27 | t.assert_equals( 28 | http_lib._parse_request('GET / HTTP/1.1\nHost: s.com\r\n\r\n').method, 29 | 'GET', 30 | 'method' 31 | ) 32 | t.assert_equals( 33 | http_lib._parse_request('GET / HTTP/1.1\nHost: s.com\r\n\r\n').query, 34 | '', 35 | 'query' 36 | ) 37 | end 38 | -------------------------------------------------------------------------------- /test/unit/http_setcookie_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | 3 | local http_server = require('http.server') 4 | 5 | local g = t.group() 6 | 7 | local function get_object() 8 | return setmetatable({}, http_server.internal.response_mt) 9 | end 10 | 11 | g.test_values_escaping = function() 12 | local test_table = { 13 | whitespace = { 14 | value = "f f", 15 | result = 'f%20f', 16 | }, 17 | dquote = { 18 | value = 'f"f', 19 | result = 'f%22f', 20 | }, 21 | comma = { 22 | value = "f,f", 23 | result = "f%2Cf", 24 | }, 25 | semicolon = { 26 | value = "f;f", 27 | result = "f%3Bf", 28 | }, 29 | backslash = { 30 | value = "f\\f", 31 | result = "f%5Cf", 32 | }, 33 | unicode = { 34 | value = "fюf", 35 | result = "f%D1%8Ef" 36 | }, 37 | unprintable_ascii = { 38 | value = string.char(15), 39 | result = "%0F" 40 | } 41 | } 42 | 43 | for byte = 33, 126 do 44 | if byte ~= string.byte('"') and 45 | byte ~= string.byte(",") and 46 | byte ~= string.byte(";") and 47 | byte ~= string.byte("\\") then 48 | test_table[byte] = { 49 | value = "f" .. string.char(byte) .. "f", 50 | result = "f" .. string.char(byte) .. "f", 51 | } 52 | end 53 | end 54 | 55 | for case_name, case in pairs(test_table) do 56 | local resp = get_object() 57 | resp:setcookie({ 58 | name='name', 59 | value = case.value 60 | }) 61 | t.assert_equals(resp.headers['set-cookie'], { 62 | "name=" .. case.result 63 | }, case_name) 64 | end 65 | end 66 | 67 | g.test_values_raw = function() 68 | local test_table = {} 69 | for byte = 0, 127 do 70 | test_table[byte] = { 71 | value = "f" .. string.char(byte) .. "f", 72 | result = "f" .. string.char(byte) .. "f", 73 | } 74 | end 75 | 76 | test_table.unicode = { 77 | value = "fюf", 78 | result = "fюf" 79 | } 80 | 81 | for case_name, case in pairs(test_table) do 82 | local resp = get_object() 83 | resp:setcookie({ 84 | name='name', 85 | value = case.value 86 | }, { 87 | raw = true 88 | }) 89 | t.assert_equals(resp.headers['set-cookie'], { 90 | "name=" .. case.result 91 | }, case_name) 92 | end 93 | end 94 | 95 | g.test_path_escaping = function() 96 | local test_table = { 97 | semicolon = { 98 | path = "f;f", 99 | result = "f%3Bf", 100 | }, 101 | unicode = { 102 | path = "fюf", 103 | result = "f%D1%8Ef" 104 | }, 105 | unprintable_ascii = { 106 | path = string.char(15), 107 | result = "%0F" 108 | } 109 | } 110 | 111 | for byte = 32, 126 do 112 | if byte ~= string.byte(";") then 113 | test_table[byte] = { 114 | path = "f" .. string.char(byte) .. "f", 115 | result = "f" .. string.char(byte) .. "f", 116 | } 117 | end 118 | end 119 | 120 | for case_name, case in pairs(test_table) do 121 | local resp = get_object() 122 | resp:setcookie({ 123 | name='name', 124 | value = 'value', 125 | path = case.path 126 | }) 127 | t.assert_equals(resp.headers['set-cookie'], { 128 | "name=value;" .. 'path=' .. case.result 129 | }, case_name) 130 | end 131 | end 132 | 133 | g.test_path_raw = function() 134 | local test_table = {} 135 | for byte = 0, 127 do 136 | test_table[byte] = { 137 | path = "f" .. string.char(byte) .. "f", 138 | result = "f" .. string.char(byte) .. "f", 139 | } 140 | end 141 | 142 | test_table.unicode = { 143 | path = "fюf", 144 | result = "fюf" 145 | } 146 | 147 | for case_name, case in pairs(test_table) do 148 | local resp = get_object() 149 | resp:setcookie({ 150 | name='name', 151 | value = 'value', 152 | path = case.path 153 | }, { 154 | raw = true 155 | }) 156 | t.assert_equals(resp.headers['set-cookie'], { 157 | "name=value;" .. 'path=' .. case.result 158 | }, case_name) 159 | end 160 | end 161 | 162 | g.test_set_header = function() 163 | local test_table = { 164 | name_value = { 165 | cookie = { 166 | name = 'name', 167 | value = 'value' 168 | }, 169 | result = {"name=value"}, 170 | }, 171 | name_value_path = { 172 | cookie = { 173 | name = 'name', 174 | value = 'value', 175 | path = 'path' 176 | }, 177 | result = {"name=value;path=path"}, 178 | }, 179 | name_value_path_domain = { 180 | cookie = { 181 | name = 'name', 182 | value = 'value', 183 | path = 'path', 184 | domain = 'domain', 185 | }, 186 | result = {"name=value;path=path;domain=domain"}, 187 | }, 188 | name_value_path_domain_expires = { 189 | cookie = { 190 | name = 'name', 191 | value = 'value', 192 | path = 'path', 193 | domain = 'domain', 194 | expires = 'expires' 195 | }, 196 | result = {"name=value;path=path;domain=domain;expires=expires"}, 197 | }, 198 | } 199 | 200 | for case_name, case in pairs(test_table) do 201 | local resp = get_object() 202 | resp:setcookie(case.cookie) 203 | t.assert_equals(resp.headers["set-cookie"], case.result, case_name) 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /test/unit/http_split_uri_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local urilib = require('uri') 3 | 4 | local g = t.group() 5 | 6 | local function check(uri, rhs) 7 | local lhs = urilib.parse(uri) 8 | local extra = { lhs = lhs, rhs = rhs } 9 | if lhs.query == '' then 10 | lhs.query = nil 11 | end 12 | 13 | t.assert_equals(lhs.scheme, rhs.scheme, uri..' scheme', extra) 14 | t.assert_equals(lhs.host, rhs.host, uri..' host', extra) 15 | t.assert_equals(lhs.service, rhs.service, uri..' service', extra) 16 | t.assert_equals(lhs.path, rhs.path, uri..' path', extra) 17 | t.assert_equals(lhs.query, rhs.query, uri..' query', extra) 18 | end 19 | 20 | g.test_split_uri = function() 21 | check('http://abc', { 22 | scheme = 'http', 23 | host = 'abc' 24 | }) 25 | check('http://abc/', { 26 | scheme = 'http', 27 | host = 'abc', 28 | path ='/' 29 | }) 30 | check('http://abc?', { 31 | scheme = 'http', 32 | host = 'abc' 33 | }) 34 | check('http://abc/?', { 35 | scheme = 'http', 36 | host = 'abc', 37 | path ='/' 38 | }) 39 | check('http://abc/?', { 40 | scheme = 'http', 41 | host = 'abc', 42 | path ='/' 43 | }) 44 | check('http://abc:123', { 45 | scheme = 'http', 46 | host = 'abc', 47 | service = '123' 48 | }) 49 | check('http://abc:123?', { 50 | scheme = 'http', 51 | host = 'abc', 52 | service = '123' 53 | }) 54 | check('http://abc:123?query', { 55 | scheme = 'http', 56 | host = 'abc', 57 | service = '123', 58 | query = 'query' 59 | }) 60 | check('http://domain.subdomain.com:service?query', { 61 | scheme = 'http', 62 | host = 'domain.subdomain.com', 63 | service = 'service', 64 | query = 'query' 65 | }) 66 | check('google.com', { 67 | host = 'google.com' 68 | }) 69 | check('google.com?query', { 70 | host = 'google.com', 71 | query = 'query' 72 | }) 73 | check('google.com/abc?query', { 74 | host = 'google.com', 75 | path = '/abc', 76 | query = 'query' 77 | }) 78 | check('https://google.com:443/abc?query', { 79 | scheme = 'https', 80 | host = 'google.com', 81 | service = '443', 82 | path = '/abc', 83 | query = 'query' 84 | }) 85 | end 86 | -------------------------------------------------------------------------------- /test/unit/http_template_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local http_lib = require('http.lib') 3 | 4 | local g = t.group() 5 | 6 | g.test_template_1 = function() 7 | t.assert_equals(http_lib.template("<% for i = 1, cnt do %> <%= abc %> <% end %>", 8 | {abc = '1 <3>&" ', cnt = 3}), 9 | ' 1 <3>&" 1 <3>&" 1 <3>&" ', 10 | 'tmpl1') 11 | end 12 | 13 | -- The strict module requires a global variable to be explicitly defined 14 | _G.ab = nil 15 | 16 | g.test_template_2 = function() 17 | t.assert_equals(http_lib.template('<% for i = 1, cnt do %> <%= ab %> <% end %>', 18 | {abc = '1 <3>&" ', cnt = 3}), 19 | ' nil nil nil ', 'tmpl2') 20 | end 21 | 22 | g.test_broken_template = function() 23 | local r, msg = pcall(http_lib.template, '<% ab() %>', {ab = '1'}) 24 | t.assert(r == false and msg:match("call local 'ab'") ~= nil, 'bad template') 25 | end 26 | 27 | g.test_rendered_template_truncated_gh_18 = function() 28 | local template = [[ 29 | 30 | 31 |
32 | % for i,v in pairs(t) do 33 | 34 | 35 | 36 | 37 | % end 38 |
<%= i %><%= v %>
39 | 40 | 41 | ]] 42 | 43 | local tt = {} 44 | for i=1, 100 do 45 | tt[i] = string.rep('#', i) 46 | end 47 | 48 | local rendered, _ = http_lib.template(template, { t = tt }) 49 | t.assert(#rendered > 10000, 'rendered size') 50 | t.assert_equals(rendered:sub(#rendered - 7, #rendered - 1), '', 'rendered eof') 51 | end 52 | 53 | g.test_incorrect_arguments_escaping_leads_to_segfault_gh_51 = function() 54 | local template = [[<%= {{continue}} %>"]] 55 | local result = http_lib.template(template, {continue = '/'}) 56 | t.assert(result:find('\"') ~= nil) 57 | end 58 | -------------------------------------------------------------------------------- /test/unit/httpd_role_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local httpd_role = require('roles.httpd') 5 | local helpers = require('test.helpers') 6 | local fio = require('fio') 7 | 8 | local ssl_data_dir = fio.abspath(fio.pathjoin(helpers.get_testdir_path(), "ssl_data")) 9 | 10 | g.after_each(function() 11 | httpd_role.stop() 12 | end) 13 | 14 | local validation_cases = { 15 | ["not_table"] = { 16 | cfg = 42, 17 | err = "configuration must be a table, got number", 18 | }, 19 | ["name_not_string"] = { 20 | cfg = { 21 | [42] = { 22 | listen = 8081, 23 | }, 24 | }, 25 | err = "name of the server must be a string", 26 | }, 27 | ["listen_not_exist"] = { 28 | cfg = { 29 | server = { 30 | listen = nil, 31 | }, 32 | }, 33 | err = "failed to parse http 'listen' param: must exist", 34 | }, 35 | ["listen_not_string_and_not_number"] = { 36 | cfg = { 37 | server = { 38 | listen = {}, 39 | }, 40 | }, 41 | err = "failed to parse http 'listen' param: must be a string or a number, got table", 42 | }, 43 | ["listen_port_too_small"] = { 44 | cfg = { 45 | server = { 46 | listen = 0, 47 | }, 48 | }, 49 | err = "failed to parse http 'listen' param: port must be in the range [1, 65535]", 50 | }, 51 | ["listen_port_in_range"] = { 52 | cfg = { 53 | server = { 54 | listen = 8081, 55 | }, 56 | }, 57 | }, 58 | ["listen_port_too_big"] = { 59 | cfg = { 60 | server = { 61 | listen = 65536, 62 | }, 63 | }, 64 | err = "failed to parse http 'listen' param: port must be in the range [1, 65535]", 65 | }, 66 | ["listen_uri_no_port"] = { 67 | cfg = { 68 | server = { 69 | listen = "localhost", 70 | }, 71 | }, 72 | err = "failed to parse http 'listen' param: URI must contain a port", 73 | }, 74 | ["listen_uri_port_too_small"] = { 75 | cfg = { 76 | server = { 77 | listen = "localhost:0", 78 | }, 79 | }, 80 | err = "failed to parse http 'listen' param: port must be in the range [1, 65535]", 81 | }, 82 | ["listen_uri_with_port_in_range"] = { 83 | cfg = { 84 | server = { 85 | listen = "localhost:8081", 86 | }, 87 | }, 88 | }, 89 | ["listen_uri_port_too_big"] = { 90 | cfg = { 91 | server = { 92 | listen = "localhost:65536", 93 | }, 94 | }, 95 | err = "failed to parse http 'listen' param: port must be in the range [1, 65535]", 96 | }, 97 | ["listen_uri_port_not_number"] = { 98 | cfg = { 99 | server = { 100 | listen = "localhost:foo", 101 | }, 102 | }, 103 | err = "failed to parse http 'listen' param: URI port must be a number", 104 | }, 105 | ["listen_uri_non_unix_scheme"] = { 106 | cfg = { 107 | server = { 108 | listen = "http://localhost:123", 109 | }, 110 | }, 111 | err = "failed to parse http 'listen' param: URI scheme is not supported", 112 | }, 113 | ["listen_uri_login_password"] = { 114 | cfg = { 115 | server = { 116 | listen = "login:password@localhost:123", 117 | }, 118 | }, 119 | err = "failed to parse http 'listen' param: URI login and password are not supported", 120 | }, 121 | ["listen_uri_query"] = { 122 | cfg = { 123 | server = { 124 | listen = "localhost:123/?foo=bar", 125 | }, 126 | }, 127 | err = "failed to parse http 'listen' param: URI query component is not supported", 128 | }, 129 | ["ssl_ok_minimal"] = { 130 | cfg = { 131 | server = { 132 | listen = "localhost:123", 133 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 134 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt') 135 | }, 136 | }, 137 | }, 138 | ["ssl_ok_full"] = { 139 | cfg = { 140 | server = { 141 | listen = 123, 142 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 143 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 144 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 145 | ssl_ciphers = "ECDHE-RSA-AES256-GCM-SHA384", 146 | }, 147 | }, 148 | }, 149 | ["ssl_key_file_not_string"] = { 150 | cfg = { 151 | server = { 152 | listen = "localhost:123", 153 | ssl_key_file = 123, 154 | }, 155 | }, 156 | err = "ssl_key_file option must be a string", 157 | }, 158 | ["ssl_cert_file_not_string"] = { 159 | cfg = { 160 | server = { 161 | listen = "localhost:123", 162 | ssl_cert_file = 123, 163 | }, 164 | }, 165 | err = "ssl_cert_file option must be a string", 166 | }, 167 | ["ssl_password_not_string"] = { 168 | cfg = { 169 | server = { 170 | listen = "localhost:123", 171 | ssl_password = 123, 172 | }, 173 | }, 174 | err = "ssl_password option must be a string", 175 | }, 176 | ["ssl_password_file_not_string"] = { 177 | cfg = { 178 | server = { 179 | listen = "localhost:123", 180 | ssl_password_file = 123, 181 | }, 182 | }, 183 | err = "ssl_password_file option must be a string", 184 | }, 185 | ["ssl_ca_file_not_string"] = { 186 | cfg = { 187 | server = { 188 | listen = "localhost:123", 189 | ssl_ca_file = 123, 190 | }, 191 | }, 192 | err = "ssl_ca_file option must be a string", 193 | }, 194 | ["ssl_ciphers_not_string"] = { 195 | cfg = { 196 | server = { 197 | listen = "localhost:123", 198 | ssl_ciphers = 123, 199 | }, 200 | }, 201 | err = "ssl_ciphers option must be a string", 202 | }, 203 | ["ssl_key_and_cert_must_exist"] = { 204 | cfg = { 205 | server = { 206 | listen = "localhost:123", 207 | ssl_ciphers = 'cipher1:cipher2', 208 | }, 209 | }, 210 | err = "ssl_key_file and ssl_cert_file must be set to enable TLS", 211 | }, 212 | } 213 | 214 | for name, case in pairs(validation_cases) do 215 | local test_name = ('test_validate_http_%s%s'):format( 216 | (case.err ~= nil) and 'fails_on_' or 'success_for_', 217 | name 218 | ) 219 | 220 | g[test_name] = function() 221 | local ok, res = pcall(httpd_role.validate, case.cfg) 222 | 223 | if case.err ~= nil then 224 | t.assert_not(ok) 225 | t.assert_str_contains(res, case.err) 226 | else 227 | t.assert(ok) 228 | t.assert_is(res, nil) 229 | end 230 | end 231 | end 232 | 233 | g['test_get_default_without_apply'] = function() 234 | local result = httpd_role.get_server() 235 | t.assert_is(result, nil) 236 | end 237 | 238 | g['test_get_default_no_default'] = function() 239 | local cfg = { 240 | not_a_default = { 241 | listen = 13000, 242 | }, 243 | } 244 | 245 | httpd_role.apply(cfg) 246 | 247 | local result = httpd_role.get_server() 248 | t.assert_is(result, nil) 249 | end 250 | 251 | g['test_get_default'] = function() 252 | local cfg = { 253 | [httpd_role.DEFAULT_SERVER_NAME] = { 254 | listen = 13001, 255 | }, 256 | } 257 | 258 | httpd_role.apply(cfg) 259 | 260 | local result = httpd_role.get_server() 261 | t.assert_not_equals(result, nil) 262 | t.assert_is(result.port, 13001) 263 | end 264 | 265 | g['test_get_server_bad_type'] = function() 266 | local ok, res = pcall(httpd_role.get_server, {}) 267 | 268 | t.assert_not(ok) 269 | t.assert_str_contains(res, '?string expected, got table') 270 | end 271 | --------------------------------------------------------------------------------