├── debian ├── compat ├── docs ├── source │ └── format ├── .gitignore ├── prebuild.sh ├── changelog ├── rules ├── control └── copyright ├── test ├── ssl_data │ ├── passwd │ ├── passwords │ ├── ca.srl │ ├── ca.crt │ ├── client.crt │ ├── server.crt │ ├── bad_client.crt │ ├── generate.sh │ ├── ca.key │ ├── client.key │ ├── server.key │ ├── bad_client.key │ ├── client.enc.key │ └── server.enc.key ├── templates │ ├── module │ │ └── controller │ │ │ ├── action.js.el │ │ │ └── action.html.el │ ├── helper.html.el │ └── test.html.el ├── controllers │ └── module │ │ └── controller.lua ├── public │ ├── hello.html │ └── lorem.txt ├── unit │ ├── http_params_test.lua │ ├── http_parse_request_test.lua │ ├── http_template_test.lua │ ├── http_custom_socket_test.lua │ ├── http_split_uri_test.lua │ ├── http_setcookie_test.lua │ └── httpd_role_test.lua ├── integration │ ├── http_server_delete_test.lua │ ├── http_server_url_for_test.lua │ ├── http_server_url_match_test.lua │ ├── http_tls_enabled_validate_test.lua │ ├── http_server_options_test.lua │ ├── http_log_requests_test.lua │ ├── http_tls_enabled_test.lua │ ├── httpd_role_test.lua │ └── http_server_requests_test.lua ├── mocks │ └── mock_role.lua └── helpers.lua ├── roles ├── CMakeLists.txt └── httpd.lua ├── http ├── version.lua ├── CMakeLists.txt ├── codes.lua ├── tpleval.h ├── lib.c ├── sslsocket.lua └── httpfast.h ├── rpm ├── prebuild.sh └── tarantool-http.spec ├── AUTHORS ├── cmake ├── FindLuaCov.cmake ├── FindLuaCheck.cmake ├── FindLuaTest.cmake ├── FindLuaCovCoveralls.cmake └── FindTarantool.cmake ├── .gitignore ├── .editorconfig ├── deps.sh ├── .github └── workflows │ ├── check_on_push.yaml │ ├── test.yml │ ├── publish.yaml │ └── packaging.yml ├── .luacheckrc ├── http-scm-1.rockspec ├── LICENSE ├── CMakeLists.txt └── CHANGELOG.md /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /test/ssl_data/passwd: -------------------------------------------------------------------------------- 1 | 1q2w3e 2 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.md 2 | AUTHORS 3 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /test/ssl_data/passwords: -------------------------------------------------------------------------------- 1 | incorrect_password 2 | 1q2w3e 3 | -------------------------------------------------------------------------------- /test/ssl_data/ca.srl: -------------------------------------------------------------------------------- 1 | 74C2F45BD69A6CF5D81316DE585538739B3BFD95 2 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | tarantool-http/ 2 | files 3 | stamp-* 4 | *.substvars 5 | *.log 6 | -------------------------------------------------------------------------------- /test/templates/module/controller/action.js.el: -------------------------------------------------------------------------------- 1 | { 2 | "hello":"json template <%= format %>" 3 | } 4 | -------------------------------------------------------------------------------- /roles/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Install. 2 | install(FILES httpd.lua DESTINATION ${TARANTOOL_INSTALL_LUADIR}/roles) 3 | -------------------------------------------------------------------------------- /test/controllers/module/controller.lua: -------------------------------------------------------------------------------- 1 | return { 2 | action = function(self) return self:render() end 3 | } 4 | -------------------------------------------------------------------------------- /http/version.lua: -------------------------------------------------------------------------------- 1 | -- Сontains the module version. 2 | -- Requires manual update in case of release commit. 3 | 4 | return '1.9.0' 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/public/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello, tarantool 4 | 5 | 6 | static html file 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/templates/helper.html.el: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= helper_title('world') %> 4 | 5 | 6 | Helpers test 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/templates/test.html.el: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= title %> 4 | 5 | 6 | Tarantool is an application server 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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_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/mocks/mock_role.lua: -------------------------------------------------------------------------------- 1 | local M = {dependencies = { 'roles.httpd' }} 2 | 3 | local servers = {} 4 | local applied = {} 5 | 6 | M.validate = function() end 7 | 8 | M.apply = function(conf) 9 | for _, server in pairs(conf) do 10 | servers[server.id] = require('roles.httpd').get_server(server.name) 11 | 12 | if servers[server.id] ~= nil then 13 | servers[server.id]:route({ 14 | path = '/ping', 15 | }, function(tx) 16 | return tx:render({text = 'pong'}) 17 | end) 18 | 19 | if applied[server.id] == nil then 20 | servers[server.id]:route({ 21 | path = '/ping_once', 22 | }, function(tx) 23 | return tx:render({text = 'pong once'}) 24 | end) 25 | applied[server.id] = {} 26 | end 27 | end 28 | end 29 | end 30 | 31 | M.stop = function() end 32 | 33 | M.get_server_port = function(id) 34 | return servers[id].port 35 | end 36 | 37 | return M 38 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | static-build: [false] 16 | include: 17 | - tarantool: '3.5' 18 | coveralls: true 19 | static-build: false 20 | - tarantool: '3.5' 21 | coveralls: false 22 | static-build: true 23 | runs-on: [ubuntu-22.04] 24 | steps: 25 | - uses: actions/checkout@master 26 | - uses: tarantool/setup-tarantool@v3 27 | with: 28 | tarantool-version: ${{ matrix.tarantool }} 29 | 30 | - name: Prepare the repo 31 | run: curl -L https://tarantool.io/release/2/installer.sh | bash 32 | env: 33 | DEBIAN_FRONTEND: noninteractive 34 | 35 | - name: Install tt cli 36 | run: sudo apt install -y tt=2.5.2 37 | env: 38 | DEBIAN_FRONTEND: noninteractive 39 | 40 | - name: Install dynamic Tarantool 41 | if: matrix.static-build == false 42 | run: tt install tarantool ${{ matrix.tarantool }} --dynamic 43 | 44 | - name: Cache rocks 45 | uses: actions/cache@v3 46 | id: cache-rocks 47 | with: 48 | path: .rocks/ 49 | key: cache-rocks-${{ matrix.tarantool }}-05 50 | 51 | - run: echo $PWD/.rocks/bin >> $GITHUB_PATH 52 | 53 | - run: ./deps.sh 54 | 55 | - name: Build module 56 | run: | 57 | cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -S . -B build 58 | make -C build 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | - name: Run tests without code coverage analysis 63 | run: make -C build luatest 64 | if: matrix.coveralls != true 65 | 66 | - name: Send code coverage to coveralls.io 67 | run: make -C build coveralls 68 | if: ${{ matrix.coveralls }} 69 | -------------------------------------------------------------------------------- /test/ssl_data/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test/ssl_data/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test/ssl_data/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test/ssl_data/bad_client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 --include ^roles --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 | -------------------------------------------------------------------------------- /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/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | 9eIgheeP8x6NoIy/1o+oiZGoFOlitIY= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /test/ssl_data/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | OKeTgVk//S5ZTzfk3kynPNbepCk9Gbo= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /test/ssl_data/bad_client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQC9AKdee4oztHzN 3 | rfDvf0wrijKPaHCKtrwz7vegGUiP6cQRLn9RO+OHnvZOxii/QdCtFAZHDLyhd5pw 4 | zyiJxh6iup2teOkmE9jQRdkFkZifJe5hzuRvZKj0Wdqe0T2bNzlNH2ejDrTD+I2n 5 | 1N33pDcg7OIzm7w/FyV7HdXOU52cOrwcCv+5OdG8qxr7KunrD/Es5HMr3YkNeEk6 6 | PNAZeKIFHEmiIZoavfcZv1Ks78jRNh1/FehgM1lrCvhAF7S+u3NTKoMRutLMMNJ6 7 | 7ag9bwVbeYgxtXFLwFr/GBx+K1xnG9rsI6TiC48OoYBKSgFXmDLu27scgtIlbdlc 8 | MJBX4ElpTiLcPvo62UoCTaImArBFaiFsO3QeG4Db6i20zXrlpTWJMDTq06Uk9zSc 9 | pHGlFDLsLt3Ptk7hw9wfkU/vMO/GAgU/ShQbTK/Cw0ZodTpAcCyyH60owx4ynBd+ 10 | XgHEa3jbG2MAOsPtbgULOnboFUkwtwvKN+M647aD8OLQWGCgNOQM05MDe4BJnFf9 11 | yQEU0gyWQVT6n3hhgV3ZRWZ4nrEz7qJf4ay+kvLrvP7jdELMmb2p+HATdzeiAb6j 12 | pIsmse6x/DfL96qc1j8GH9P60W9oE0ISR+7Fy15Y+Wqov/WnrpbCIA/yw6JdjDRb 13 | +4qjY2+BmpGIwKWKND5IC33s6oCh5wIDAQABAoICAEYWyXp8xtYAzy15HTm7k9Qr 14 | pi9PVDjkpit+KX9KEQIpdwfGHfnSg0CmfwHcc3zlm8yred58RzF7yJ6f/BEHkxHW 15 | saWEirWPs54c4OuzQA14xAuqbUUv54XiEnRF9Ror4wiKJmUuDXQFJwb/pibxU25W 16 | 2lW4IZml7ETZXhHrKS4oC908aPPYEMLuEw3krqV4nn/+4gT43RvNKR67MZLYjQDn 17 | KhlBa8QSAWIfdLnkHC0Va9/WkHuoXzcWdNRT1jfLDOvg/oUjKowFaPCkVHkfxDVV 18 | ft+sQS0N0tD5sItLajNkfY2HdFxNXApZctlZ02CX9P9mJd/fVa4CrBIHgmfMKXyL 19 | gJWBH52xmiBmoHJfXsirXW9zgVOuMSLam4pu5aXj/iSWnT8jdTI6GWLN+D8geft8 20 | ouTMocXqZTJlyFAam2DwEkgz5JOfDDPO/+biefnLVA9g/D+jalz5lPD6PfIE5/9A 21 | Hvmkh6t3KDyLWGFGxhqHJoBODn3oN4tOfwfH4vxDV9rLE7KW5m+das1znqiRsQ+/ 22 | kkH0HiYSzNIaxqQMuM+MhJl0v5ZYcHJZsP0MWXZU10oph7UHjspclUYuMrr2K7p2 23 | 6YcTa2rqv+T1elo4dKj7oqLHM4U1iShmraJp4tIvTAQMcI5f4uYIgbVcEDwHDxz0 24 | WYlgY1uEFO91FEoFCYdFAoIBAQDvOmggAXEi/JcSTcjCpit/zwdAE3G3Ep1om1Za 25 | BrhMc1ObGWGUVX1DYuY57jwcbp2UEp41oGtOSZdbKx3zsRsjcfJLNQ4JHbbHsMBr 26 | KE/QidmVojl6qeZ3w/8SO2KV4B/Bxqdgq86wHDYrMGNw2n43OYFLd9DIoBLN/XhF 27 | hLmHp1gzBDLpgDRP4Va+l0G2vUAnHdBz6KAOndQH6ifMtSVC6r+PqiTmVt0cilBK 28 | 5jzJoIiEzcZ3zoZDM2z7iDAY322nEMKWR4ZJKxutPlezoXIzgU/tr9pN5D3zCsUZ 29 | L+6njNEEfl3znS6vcLt0L8qbsdr8csITKYKBLHZNFFmiJuxdAoIBAQDKQNA/7gT3 30 | P5VIsAVskBC9NhEP/nPCSn48fTj7715DO6+D3bO4qeSwJE5zyvJwDXOo/nybIVhu 31 | jlDLkGQ6siU0oaetI6Pq4BXhz/Bz8YEqcpcClL95pUIT58azTcrWt99TaihZvZ3b 32 | ML35h6k6inBw+Aw7/yxJuZ7uG/D6cIrebVpZ0gxN9uSBBAtngcfwAWZetDkWPEf+ 33 | IUWLwy/J7DZOApJ1vTyIJnQH7WFvTxURjpIdPsOiH3Q4sZQxPz6dwBMumEbZV5pi 34 | 2ePZh7BT0O0fdUWksBj9JNkv7b9pLkRB70Bs1U7jBQ1fSXooYQCtiuk4m/1/94ky 35 | 0eg42HMSuAMTAoIBAQDhWJ1Y+MK/+Du+bDMe2DTFkhj8TNSjZQ+NyDWRXB8jNMee 36 | pEv81ILIhVLlYvqQtcoN/3O0hEZQWpYOtRDjywMLYnygR3vPLoRMmrzGtBRrFk81 37 | 2rhWSdDlJGUToYj+MT74484rC+wIjKqiCFTDq62VC8A1fMnZEqBkFc3DfoDdvc8h 38 | T2U9+xxL2rJBmm22W5Mgxb7kUE7lNdrTEckn1cMhw8tq4xUbPNvP1KJJy5ObQnMW 39 | 1leL56kliD2yutjDtUOvSeRid0GRjt/lU4J9nSjcR4UpGquDD+sjFBQR48rlXYpO 40 | t1J89qVRcdnCWnp6KxFjGB6kukdKsr1FYlQEoLGpAoIBAQDGastizHlmrqQfyT+o 41 | /7TMS0x16mVaSIaLhTXwQyawws8viMKV+WZ3P0cP5hvtveSn9/H6pr4Ax/GPozoR 42 | M0+40JaVDw/yjqApBjyZImZbZEutpowqJOwsZwfSRBEokP6w8MZhM9q3fJwDPwnQ 43 | epxQ16f4/B9QvJ+kbRj+OIakK5el4qFbo0kNIRCnHPUvCdCKPDh9Dep6790wfe5W 44 | JDwqT++rPlkyILdYR5N9BZJfxQSnWDnIxR7Zt6zwm2EslZC793waIQ0+yQ/1Cl77 45 | +02FvSDzrib1wb6ofI95+n/QR41mt+VKZlx2DLmg/3kQx+SBOtd5QTkB+Fff3MkX 46 | phqtAoIBAQDKKd9fGGQJ1snfa8Gut3SKqsDGzT3DDc7uuka+zpWHEdL8teHHKcJw 47 | WWnp+4VulTPtLsU9WkaMEg0EaEJmMlo2tIRrR4JjiP+bwkvh0K3rfcMd0kfQl0Iw 48 | AmK37WzIe1WQGWuwulWcBkfoHY4eLyPbAG9SNwJ5uNrW7x1qm5hsJoi2XCmqv5Xi 49 | AV933BMGw7Lvd9rbW4PkwZnJ4LGCz41XE4QYIjWylSH0aAbraNSYlu5df/fysr2v 50 | o15bk6DylnQ+9EQ1fxnAbqYwdPP2e32WD5sxDijebxN30gVP8vbWY9VcMnmnZk6i 51 | 4xz9jwcKbEzsHdwVB+OoRs47xcWsd923 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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: 'bullseye' } 34 | - { os: 'el', dist: '7' } 35 | - { os: 'el', dist: '8' } 36 | - { os: 'fedora', dist: '30' } 37 | - { os: 'fedora', dist: '31' } 38 | - { os: 'fedora', dist: '32' } 39 | - { os: 'fedora', dist: '33' } 40 | - { os: 'fedora', dist: '34' } 41 | - { os: 'fedora', dist: '35' } 42 | - { os: 'fedora', dist: '36' } 43 | - { os: 'ubuntu', dist: 'xenial' } 44 | - { os: 'ubuntu', dist: 'bionic' } 45 | - { os: 'ubuntu', dist: 'focal' } 46 | - { os: 'ubuntu', dist: 'groovy' } 47 | - { os: 'ubuntu', dist: 'jammy' } 48 | 49 | env: 50 | OS: ${{ matrix.platform.os }} 51 | DIST: ${{ matrix.platform.dist }} 52 | 53 | steps: 54 | - name: Clone the module 55 | uses: actions/checkout@v3 56 | # `actions/checkout` performs shallow clone of repo. To provide 57 | # proper version of the package to `packpack` we need to have 58 | # complete repository, otherwise it will be `0.0.1`. 59 | with: 60 | fetch-depth: 0 61 | 62 | - name: Clone the packpack tool 63 | uses: actions/checkout@v3 64 | with: 65 | repository: packpack/packpack 66 | path: packpack 67 | 68 | - name: Fetch tags 69 | # Found that Github checkout Actions pulls all the tags, but 70 | # right it deannotates the testing tag, check: 71 | # https://github.com/actions/checkout/issues/290 72 | # But we use 'git describe ..' calls w/o '--tags' flag and it 73 | # prevents us from getting the needed tag for packages version 74 | # setup. To avoid of it, let's fetch it manually, to be sure 75 | # that all tags will exist always. 76 | run: git fetch --tags -f 77 | 78 | - name: Create packages 79 | run: ./packpack/packpack 80 | 81 | - name: Deploy packages 82 | # We need this step to run only on push with tag. 83 | if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }} 84 | env: 85 | RWS_URL_PART: https://rws.tarantool.org/tarantool-modules 86 | RWS_AUTH: ${{ secrets.RWS_AUTH }} 87 | PRODUCT_NAME: tarantool-http 88 | working-directory: build 89 | run: | 90 | CURL_CMD="curl -LfsS \ 91 | -X PUT ${RWS_URL_PART}/${OS}/${DIST} \ 92 | -u ${RWS_AUTH} \ 93 | -F product=${PRODUCT_NAME}" 94 | 95 | shopt -s nullglob 96 | for f in *.deb *.rpm *.dsc *.tar.xz *.tar.gz; do 97 | CURL_CMD+=" -F $(basename ${f})=@${f}" 98 | done 99 | 100 | echo ${CURL_CMD} 101 | 102 | ${CURL_CMD} 103 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ssl_verify_client_incorrect_value = { 109 | opts = { 110 | ssl_verify_client = "unknown", 111 | }, 112 | expected_err_msg = '"unknown" option not exists. Available options: "on", "off", "optional"' 113 | }, 114 | ssl_socket_not_supported = { 115 | check_ssl = true, 116 | opts = { 117 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 118 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 119 | }, 120 | expected_err_msg = 'ssl socket is not supported', 121 | } 122 | } 123 | 124 | for name, case in pairs(test_cases) do 125 | g['test_ssl_option_' .. name] = function() 126 | helpers.skip_if_ssl_not_enabled() 127 | if case.check_ssl == true then 128 | helpers.skip_if_ssl_enabled() 129 | end 130 | t.assert_error_msg_contains(case.expected_err_msg, function() 131 | http_server.new('host', 8080, case.opts) 132 | end) 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /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 | local is_tarantool1 = luatest_utils.version_ge( 16 | luatest_utils.get_tarantool_version(), 17 | luatest_utils.version(1, 0, 0) 18 | ) 19 | 20 | helpers.CONNECTION_REFUSED_ERR_MSG = "Failure when receiving data from the peer: Connection refused" 21 | if is_tarantool1 then 22 | helpers.CONNECTION_REFUSED_ERR_MSG = "Failure when receiving data from the peer" 23 | end 24 | 25 | helpers.get_testdir_path = function() 26 | local path = os.getenv('LUA_SOURCE_DIR') or './' 27 | return fio.pathjoin(path, 'test') 28 | end 29 | 30 | helpers.cfgserv = function(opts) 31 | local path = helpers.get_testdir_path() 32 | 33 | local opts = opts or {} 34 | local opts = http_server.internal.extend({ 35 | app_dir = path, 36 | log_requests = false, 37 | log_errors = false 38 | }, opts) 39 | 40 | local httpd = http_server.new(helpers.base_host, helpers.base_port, opts) 41 | :route({path = '/abc/:cde/:def', name = 'test'}, function() end) 42 | :route({path = '/abc'}, function() end) 43 | :route({path = '/ctxaction'}, 'module.controller#action') 44 | :route({path = '/absentaction'}, 'module.controller#absent') 45 | :route({path = '/absent'}, 'module.absent#action') 46 | :route({path = '/abc/:cde'}, function() end) 47 | :route({path = '/abc_:cde_def'}, function() end) 48 | :route({path = '/abc-:cde-def'}, function() end) 49 | :route({path = '/aba*def'}, function() end) 50 | :route({path = '/abb*def/cde', name = 'star'}, function() end) 51 | :route({path = '/banners/:token'}) 52 | :helper('helper_title', function(self, a) return 'Hello, ' .. a end) 53 | :route({path = '/helper', file = 'helper.html.el'}) 54 | :route({path = '/test', file = 'test.html.el' }, 55 | function(cx) return cx:render({ title = 'title: 123' }) end) 56 | 57 | return httpd 58 | end 59 | 60 | local log_queue = {} 61 | 62 | helpers.clear_log_queue = function() 63 | log_queue = {} 64 | end 65 | 66 | helpers.custom_logger = { 67 | debug = function() end, 68 | verbose = function() 69 | table.insert(log_queue, { 70 | log_lvl = 'verbose', 71 | }) 72 | end, 73 | info = function(...) 74 | table.insert(log_queue, { 75 | log_lvl = 'info', 76 | msg = string.format(...) 77 | }) 78 | end, 79 | warn = function(...) 80 | table.insert(log_queue, { 81 | log_lvl = 'warn', 82 | msg = string.format(...) 83 | }) 84 | end, 85 | error = function(...) 86 | table.insert(log_queue, { 87 | log_lvl = 'error', 88 | msg = string.format(...) 89 | }) 90 | end 91 | } 92 | 93 | helpers.find_msg_in_log_queue = function(msg, strict) 94 | for _, log in ipairs(log_queue) do 95 | if not strict then 96 | if log.msg:match(msg) then 97 | return log 98 | end 99 | else 100 | if log.msg == msg then 101 | return log 102 | end 103 | end 104 | end 105 | end 106 | 107 | helpers.teardown = function(httpd) 108 | local host = httpd.host 109 | local port = httpd.port 110 | httpd:stop() 111 | helpers.retrying({ 112 | timeout = 1, 113 | }, function() 114 | local s, _ = socket.tcp_connect(host, port) 115 | if s ~= nil then 116 | s:close() 117 | end 118 | assert(s == nil, 'http server is stopped') 119 | end) 120 | end 121 | 122 | helpers.is_tarantool3 = function() 123 | local tarantool_version = luatest_utils.get_tarantool_version() 124 | return luatest_utils.version_ge(tarantool_version, luatest_utils.version(3, 0, 0)) 125 | end 126 | 127 | helpers.skip_if_not_tarantool3 = function() 128 | luatest.skip_if(not helpers.is_tarantool3(), 'Only Tarantool 3 is supported') 129 | end 130 | 131 | helpers.update_lua_env_variables = function(server) 132 | local ROOT = fio.dirname(fio.dirname(fio.abspath(package.search('http.server')))) 133 | 134 | server.env.LUA_PATH = (server.env.LUA_PATH or '') .. 135 | ROOT .. '/?.lua;' .. ROOT .. '/?/?.lua;' .. 136 | ROOT .. '/.rocks/share/tarantool/?.lua;' .. 137 | ROOT .. '/.rocks/share/tarantool/?/init.lua;' 138 | server.env.LUA_CPATH = (server.env.LUA_CPATH or '') .. 139 | ROOT .. '/.rocks/lib/tarantool/?.so;' .. 140 | ROOT .. '/.rocks/lib/tarantool/?/?.so;' 141 | end 142 | 143 | helpers.tcp_connection_exists = function(host, port) 144 | local tcp = socket.tcp() 145 | tcp:settimeout(0.3) 146 | local ok, _ = tcp:connect(host, port) 147 | tcp:close() 148 | return ok 149 | end 150 | 151 | local ffi = require('ffi') 152 | local has_tls_method = pcall(function() 153 | return ffi.C.TLS_server_method() ~= nil 154 | end) 155 | 156 | helpers.skip_if_ssl_not_enabled = function() 157 | luatest.skip_if(not has_tls_method, 'tarantool does not support ssl') 158 | end 159 | 160 | helpers.skip_if_ssl_enabled = function() 161 | luatest.skip_if(has_tls_method, 'tarantool supports ssl') 162 | end 163 | 164 | return helpers 165 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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.9.0] - 2025-11-12 16 | 17 | The release introduces a new `ssl_verify_client` option and changes default 18 | behavior with provided `ca_file` param. Also a few bugs were fixed. 19 | 20 | ### Added 21 | 22 | - `ssl_verify_client` option (#207). 23 | 24 | ### Fixed 25 | 26 | - Do not recreate server if it's address and port were not changed (#219). 27 | - Server doesn't change after updating parameters on config reload (#216). 28 | - **Breaking change**: Mutual TLS with `ca_file` option enabled by default (#217). 29 | 30 | ## [1.8.0] - 2025-07-07 31 | 32 | The release introduces a new log level configuration option in `httpd` role 33 | and several bug fixes. 34 | 35 | ### Added 36 | 37 | - `log_requests` option into `roles.httpd`. 38 | 39 | ### Fixed 40 | 41 | - Removing the server if it is not in use (#212). 42 | - Changing server address if it was edited (#209). 43 | 44 | ## [1.7.0] - 2024-11-15 45 | 46 | The release introduces a TLS support. 47 | 48 | ### Added 49 | 50 | - SSL support (#35). 51 | - SSL support into roles (#199). 52 | 53 | ## [1.6.0] - 2024-09-13 54 | 55 | The release introduces a role for Tarantool 3. 56 | 57 | ### Fixed 58 | 59 | - Fixed request crash with empty body and unexpected header 60 | Content-Type (#189). 61 | 62 | ### Added 63 | 64 | - `roles.httpd` role to configure one or more HTTP servers (#196). 65 | - `httpd:delete(name)` method to delete named routes (#197). 66 | 67 | ## [1.5.0] - 2023-03-29 68 | 69 | ### Added 70 | 71 | - Add versioning support. 72 | 73 | ### Fixed 74 | 75 | - Allow dot in path segment. 76 | 77 | ## [1.4.0] - 2022-12-30 78 | 79 | ### Added 80 | 81 | - Add path_raw field. This field contains request path without encoding. 82 | 83 | ## [1.3.0] - 2022-07-27 84 | 85 | ### Changed 86 | 87 | - Allow to use a non-standard socket (for example, `sslsocket` with TLS 88 | support). 89 | - When processing a GET request, the plus sign in the parameter name and 90 | value is now replaced with a space. In order to explicitly pass a "+" 91 | sign it must be represented as "%2B". 92 | 93 | ### Added 94 | 95 | - Add option to control keepalive connection state (#137). 96 | - Add option to control idle connections (#137). 97 | 98 | ## [1.2.0] - 2021-11-10 99 | 100 | ### Changed 101 | 102 | - Disable option display_errors by default. 103 | 104 | ## [1.1.1] - 2021-10-28 105 | 106 | ### Changed 107 | 108 | - Revert all changes related to http v2 (#134). 109 | - Rewrite TAP tests with luatest. 110 | - Create a separate target for running tests in CMake. 111 | - Replace io with fio module. 112 | 113 | ### Added 114 | 115 | - Replace Travis CI with Github Actions. 116 | - Add workflow that publish rockspec. 117 | - Add editorconfig to configure indentation. 118 | - Add luacheck integration. 119 | - Add option to get cookie without escaping. 120 | - Add option to set cookie without escaping and change escaping algorithm. 121 | 122 | ### Fixed 123 | 124 | - Fix FindTarantool.cmake module. 125 | - Fix SEGV_MAPERR when box httpd html escaping. 126 | 127 | ## [2.1.0] - 2020-01-30 128 | 129 | ### Added 130 | 131 | - Return ability to set loggers for a specific route. 132 | - Return ability to server and route to use custom loggers. 133 | 134 | ### Fixed 135 | 136 | - Fix routes overlapping by any pattern in route's path. 137 | - Fix req:redirect_to method. 138 | 139 | ## [2.0.1] - 2019-10-09 140 | 141 | ### Fixed 142 | 143 | - Fix installation paths to not contain extra directories. 144 | 145 | ## [2.0.0] - 2019-10-04 146 | 147 | ### Added 148 | 149 | - Major rewrite since version 1.x. 150 | - Ability to be used with internal http server and an nginx upstream module 151 | (without modifying the backend code). 152 | - Standardized request object (similar to WSGI). 153 | - A new router with route priorities inspired by Mojolicious. 154 | - Middleware support (for e.g. for centrally handling authorization). 155 | 156 | ## [1.1.0] - 2019-05-30 157 | ### Added 158 | - Travis builds for tags. 159 | 160 | ## [1.0.6] - 2019-05-19 161 | ### Added 162 | - Custom logger per route support. 163 | 164 | ### Fixed 165 | - Fixed buffer reading when timeout or disconnect occurs. 166 | - Fixed cookies formatting: stop url-encoding cookie path and quoting cookie expire date. 167 | 168 | ### Changed 169 | - Readme updates: fix setcookie() description, how to use unix socket. 170 | 171 | ## [1.0.5] - 2018-09-03 172 | ### Changed 173 | - Protocol upgrade: detaching mechanism is re-worked. 174 | 175 | ## [1.0.4] - 2018-08-31 176 | ### Added 177 | - Detach callback support for protocol upgrade implementations. 178 | 179 | ## [1.0.3] - 2018-06-29 180 | ### Fixed 181 | - Fixed eof detection. 182 | 183 | ## [1.0.2] - 2018-02-01 184 | ### Fixed 185 | - Fixed request parsing with headers longer than 4096 bytes. 186 | 187 | ## [1.0.1] - 2018-01-22 188 | ### Added 189 | - Added RPM and DEB specs. 190 | - Enabled builds for Tarantool 1.7. 191 | 192 | ### Fixed 193 | - Fixed building on Mac OS X. 194 | - Fixed server handler and before_routes hooks. 195 | - Fixed "data" response body rendering option. 196 | - Fixed crash in uri_escape and uri_unescape when multiple arguments with same name. 197 | - Fixed no distinction between PUT and DELETE methods (#25). 198 | - Fixed compatibility with Tarantool 1.7. 199 | - Fixed empty Content-Type header handling. 200 | - Fixed curl delay: add support for expect=100-Continue. 201 | 202 | ## [1.0] - 2016-11-29 203 | ### Added 204 | - Added requets peer host and port. 205 | - Show tarantool version in HTTP header. 206 | - Support for new Tarantool uri parser. 207 | - Chunked encoding support in responses. 208 | - Chunked encoding support in client. 209 | 210 | ### Changed 211 | - Fedora 23 build is disabled. 212 | - HTTP new sockets API. 213 | - Refactor handler API. 214 | - Cloud build is enabled. 215 | - Update the list of supported OS for tarantool/build. 216 | 217 | ### Fixed 218 | - Fixed build without -std=c99. 219 | - Fixed socket:write() problem: use :write instead :send, use new sockets in http_client. 220 | - Fixed directory traversal attack. 221 | - Fixed routes with dots don't work as expected (#17). 222 | - Fixed truncated rendered template (#18). 223 | 224 | ## [0.0.1] - 2014-05-05 225 | -------------------------------------------------------------------------------- /roles/httpd.lua: -------------------------------------------------------------------------------- 1 | local checks = require('checks') 2 | local urilib = require('uri') 3 | local log = require('log') 4 | local http_server = require('http.server') 5 | 6 | local M = { 7 | DEFAULT_SERVER_NAME = 'default', 8 | } 9 | local servers = {} 10 | 11 | local function parse_listen(listen) 12 | if listen == nil then 13 | return nil, nil, "must exist" 14 | end 15 | if type(listen) ~= "string" and type(listen) ~= "number" then 16 | return nil, nil, "must be a string or a number, got " .. type(listen) 17 | end 18 | 19 | local host 20 | local port 21 | if type(listen) == "string" then 22 | local uri, err = urilib.parse(listen) 23 | if err ~= nil then 24 | return nil, nil, "failed to parse URI: " .. err 25 | end 26 | 27 | if uri.scheme ~= nil then 28 | if uri.scheme == "unix" then 29 | uri.unix = uri.path 30 | else 31 | return nil, nil, "URI scheme is not supported" 32 | end 33 | end 34 | 35 | if uri.login ~= nil or uri.password then 36 | return nil, nil, "URI login and password are not supported" 37 | end 38 | 39 | if uri.query ~= nil then 40 | return nil, nil, "URI query component is not supported" 41 | end 42 | 43 | if uri.unix ~= nil then 44 | host = "unix/" 45 | port = uri.unix 46 | else 47 | if uri.service == nil then 48 | return nil, nil, "URI must contain a port" 49 | end 50 | 51 | port = tonumber(uri.service) 52 | if port == nil then 53 | return nil, nil, "URI port must be a number" 54 | end 55 | if uri.host ~= nil then 56 | host = uri.host 57 | elseif uri.ipv4 ~= nil then 58 | host = uri.ipv4 59 | elseif uri.ipv6 ~= nil then 60 | host = uri.ipv6 61 | else 62 | host = "0.0.0.0" 63 | end 64 | end 65 | elseif type(listen) == "number" then 66 | host = "0.0.0.0" 67 | port = listen 68 | end 69 | 70 | if type(port) == "number" and (port < 1 or port > 65535) then 71 | return nil, nil, "port must be in the range [1, 65535]" 72 | end 73 | return host, port, nil 74 | end 75 | 76 | -- parse_params returns table with set options from config to pass 77 | -- it into new() function. 78 | local function parse_params(node) 79 | local log_requests = node.log_requests 80 | if type(log_requests) == "string" then 81 | local level = log_requests:lower() 82 | if level == "error" then 83 | log_requests = log.error 84 | elseif level == "warn" then 85 | log_requests = log.warn 86 | elseif level == "info" then 87 | log_requests = log.info 88 | elseif level == "verbose" then 89 | log_requests = log.verbose 90 | elseif level == "debug" then 91 | log_requests = log.debug 92 | else 93 | error("invalid log_requests: " .. log_requests) 94 | end 95 | elseif log_requests ~= nil then 96 | error("log_requests option should be a string") 97 | end 98 | 99 | return { 100 | log_requests = log_requests, 101 | ssl_cert_file = node.ssl_cert_file, 102 | ssl_key_file = node.ssl_key_file, 103 | ssl_password = node.ssl_password, 104 | ssl_password_file = node.ssl_password_file, 105 | ssl_ca_file = node.ssl_ca_file, 106 | ssl_ciphers = node.ssl_ciphers, 107 | ssl_verify_client = node.ssl_verify_client, 108 | } 109 | end 110 | 111 | local function server_has_changed(name, node_params, host, port) 112 | if servers[name].httpd.host ~= host or servers[name].httpd.port ~= port then 113 | return true 114 | end 115 | for k in pairs(node_params) do 116 | if k ~= 'listen' and servers[name].httpd.options[k] ~= node_params[k] then 117 | return true 118 | end 119 | end 120 | return false 121 | end 122 | 123 | local function apply_http(name, node) 124 | local host, port, err = parse_listen(node.listen) 125 | if err ~= nil then 126 | error("failed to parse URI: " .. err) 127 | end 128 | 129 | local params = parse_params(node) 130 | 131 | if servers[name] == nil then 132 | local httpd = http_server.new(host, port, params) 133 | 134 | httpd:start() 135 | servers[name] = { 136 | httpd = httpd, 137 | routes = {}, 138 | } 139 | elseif server_has_changed(name, params, host, port) then 140 | servers[name].httpd:stop() 141 | servers[name].httpd = http_server.new(host, port, params) 142 | servers[name].httpd:start() 143 | end 144 | end 145 | 146 | M.validate = function(conf) 147 | if conf ~= nil and type(conf) ~= "table" then 148 | error("configuration must be a table, got " .. type(conf)) 149 | end 150 | conf = conf or {} 151 | 152 | for name, node in pairs(conf) do 153 | if type(name) ~= 'string' then 154 | error("name of the server must be a string") 155 | end 156 | 157 | local host, port, err = parse_listen(node.listen) 158 | if err ~= nil then 159 | error("failed to parse http 'listen' param: " .. err) 160 | end 161 | 162 | local ok, err = pcall(http_server.new, host, port, parse_params(node)) 163 | if not ok then 164 | error("failed to parse params in " .. name .. " server: " .. tostring(err)) 165 | end 166 | end 167 | end 168 | 169 | M.apply = function(conf) 170 | -- This should be called on the role's lifecycle, but it's better to give 171 | -- a meaningful error if something goes wrong. 172 | M.validate(conf) 173 | 174 | local actual_servers = {} 175 | 176 | for name, node in pairs(conf or {}) do 177 | actual_servers[name] = true 178 | 179 | apply_http(name, node) 180 | end 181 | 182 | -- Stop server if it is not used anymore. 183 | for name, server in pairs(servers) do 184 | if actual_servers[name] == nil then 185 | server.httpd:stop() 186 | servers[name] = nil 187 | end 188 | end 189 | end 190 | 191 | M.stop = function() 192 | for _, server in pairs(servers) do 193 | server.httpd:stop() 194 | end 195 | servers = {} 196 | end 197 | 198 | M.get_server = function(name) 199 | checks('?string') 200 | 201 | name = name or M.DEFAULT_SERVER_NAME 202 | return (servers[name] or {}).httpd 203 | end 204 | 205 | return M 206 | -------------------------------------------------------------------------------- /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_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 | g.before_all(function() 13 | helpers.skip_if_ssl_not_enabled() 14 | end) 15 | 16 | local server_test_cases = { 17 | test_key_password_missing = { 18 | ssl_opts = { 19 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key'), 20 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 21 | }, 22 | expected_err_msg = 'Private key is invalid or password mismatch', 23 | }, 24 | test_incorrect_key_password = { 25 | ssl_opts = { 26 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key'), 27 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 28 | ssl_password = 'incorrect_password', 29 | }, 30 | expected_err_msg = 'Private key is invalid or password mismatch', 31 | }, 32 | test_invalid_ciphers_server = { 33 | ssl_opts = { 34 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 35 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 36 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 37 | ssl_ciphers = "INVALID", 38 | }, 39 | expected_err_msg = "Ciphers are invalid", 40 | }, 41 | test_invalid_ca = { 42 | ssl_opts = { 43 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 44 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 45 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.key'), 46 | }, 47 | expected_err_msg = "CA file is invalid", 48 | }, 49 | } 50 | 51 | for name, tc in pairs(server_test_cases) do 52 | g.before_test(name, function() 53 | g.httpd = helpers.cfgserv(tc.ssl_opts) 54 | end) 55 | 56 | g.after_test(name, function() 57 | if g.httpd.is_run then 58 | helpers.teardown(g.httpd) 59 | end 60 | end) 61 | 62 | g[name] = function() 63 | t.assert_error_msg_contains(tc.expected_err_msg, function() 64 | g.httpd:start() 65 | end) 66 | end 67 | end 68 | 69 | local client_test_cases = { 70 | test_basic = { 71 | ssl_opts = { 72 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 73 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 74 | }, 75 | }, 76 | test_encrypted_key_ok = { 77 | ssl_opts = { 78 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key'), 79 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 80 | ssl_password = '1q2w3e', 81 | }, 82 | }, 83 | test_encrypted_key_password_file = { 84 | ssl_opts = { 85 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key'), 86 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 87 | ssl_password_file = fio.pathjoin(ssl_data_dir, 'passwd'), 88 | }, 89 | }, 90 | test_encrypted_key_many_passwords_file = { 91 | ssl_opts = { 92 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key'), 93 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 94 | ssl_password_file = fio.pathjoin(ssl_data_dir, 'passwords'), 95 | }, 96 | }, 97 | test_key_crt_ca_server_key_crt_client = { 98 | ssl_opts = { 99 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 100 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 101 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 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 | test_verify_client_optional_with_certs_valid = { 153 | ssl_opts = { 154 | ssl_verify_client = 'optional', 155 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 156 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 157 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 158 | }, 159 | request_opts = { 160 | ssl_cert = fio.pathjoin(ssl_data_dir, 'client.crt'), 161 | ssl_key = fio.pathjoin(ssl_data_dir, 'client.key'), 162 | }, 163 | }, 164 | test_verify_client_optional_with_certs_invalid = { 165 | ssl_opts = { 166 | ssl_verify_client = 'optional', 167 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 168 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 169 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 170 | }, 171 | request_opts = { 172 | ssl_cert = fio.pathjoin(ssl_data_dir, 'bad_client.crt'), 173 | ssl_key = fio.pathjoin(ssl_data_dir, 'bad_client.key'), 174 | }, 175 | expected_err_msg = helpers.CONNECTION_REFUSED_ERR_MSG, 176 | }, 177 | test_verify_client_optional_withouts_certs = { 178 | ssl_opts = { 179 | ssl_verify_client = 'optional', 180 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 181 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 182 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 183 | }, 184 | }, 185 | test_verify_client_on_valid = { 186 | ssl_opts = { 187 | ssl_verify_client = 'on', 188 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 189 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 190 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 191 | }, 192 | request_opts = { 193 | ssl_cert = fio.pathjoin(ssl_data_dir, 'client.crt'), 194 | ssl_key = fio.pathjoin(ssl_data_dir, 'client.key'), 195 | }, 196 | }, 197 | test_verify_client_on_invalid = { 198 | ssl_opts = { 199 | ssl_verify_client = 'on', 200 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 201 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 202 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 203 | }, 204 | request_opts = { 205 | ssl_cert = fio.pathjoin(ssl_data_dir, 'bad_client.crt'), 206 | ssl_key = fio.pathjoin(ssl_data_dir, 'bad_client.key'), 207 | }, 208 | expected_err_msg = helpers.CONNECTION_REFUSED_ERR_MSG, 209 | }, 210 | test_verify_client_on_certs_missing = { 211 | ssl_opts = { 212 | ssl_verify_client = 'on', 213 | ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), 214 | ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), 215 | ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 216 | }, 217 | expected_err_msg = helpers.CONNECTION_REFUSED_ERR_MSG, 218 | }, 219 | } 220 | 221 | for name, tc in pairs(client_test_cases) do 222 | g.before_test(name, function() 223 | g.httpd = helpers.cfgserv(tc.ssl_opts) 224 | g.httpd:start() 225 | end) 226 | 227 | g.after_test(name, function() 228 | helpers.teardown(g.httpd) 229 | end) 230 | 231 | g[name] = function() 232 | local req_opts = http_server.internal.extend({ 233 | -- We need to provide ca_file by default because curl uses the 234 | -- system native CA store for verification. 235 | -- See: https://curl.se/docs/sslcerts.html 236 | ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 237 | verbose = true, 238 | }, tc.request_opts or {}) 239 | 240 | if tc.expected_err_msg ~= nil then 241 | t.assert_error_msg_contains(tc.expected_err_msg, function() 242 | http_client:get(helpers.tls_uri .. '/test', req_opts) 243 | end) 244 | else 245 | local r = http_client:get(helpers.tls_uri .. '/test', req_opts) 246 | t.assert_equals(r.status, 200, 'response not 200') 247 | end 248 | end 249 | end 250 | -------------------------------------------------------------------------------- /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 | ["log_requests_invalid_string"] = { 213 | cfg = { 214 | server = { 215 | listen = "localhost:123", 216 | log_requests = "yes", 217 | }, 218 | }, 219 | err = "invalid log_requests", 220 | }, 221 | ["log_requests_invalid_type"] = { 222 | cfg = { 223 | server = { 224 | listen = "localhost:123", 225 | log_requests = 42, 226 | }, 227 | }, 228 | err = "log_requests option should be a string", 229 | }, 230 | ["ssl_verify_client_invalid_type"] = { 231 | cfg = { 232 | server = { 233 | listen = "localhost:123", 234 | ssl_verify_client = 1, 235 | } 236 | }, 237 | err = "ssl_verify_client option must be a string", 238 | }, 239 | ["ssl_verify_client_invalid_value"] = { 240 | cfg = { 241 | server = { 242 | listen = "localhost:123", 243 | ssl_verify_client = "unknown", 244 | } 245 | }, 246 | err = '"unknown" option not exists. Available options: "on", "off", "optional"', 247 | } 248 | } 249 | 250 | for name, case in pairs(validation_cases) do 251 | local test_name = ('test_validate_http_%s%s'):format( 252 | (case.err ~= nil) and 'fails_on_' or 'success_for_', 253 | name 254 | ) 255 | 256 | g[test_name] = function() 257 | if name:find('ssl_') ~= nil and case.err == nil then 258 | helpers.skip_if_ssl_not_enabled() 259 | end 260 | local ok, res = pcall(httpd_role.validate, case.cfg) 261 | 262 | if case.err ~= nil then 263 | t.assert_not(ok) 264 | t.assert_str_contains(res, case.err) 265 | else 266 | t.assert(ok) 267 | t.assert_is(res, nil) 268 | end 269 | end 270 | end 271 | 272 | g['test_get_default_without_apply'] = function() 273 | local result = httpd_role.get_server() 274 | t.assert_is(result, nil) 275 | end 276 | 277 | g['test_get_default_no_default'] = function() 278 | local cfg = { 279 | not_a_default = { 280 | listen = 13000, 281 | }, 282 | } 283 | 284 | httpd_role.apply(cfg) 285 | 286 | local result = httpd_role.get_server() 287 | t.assert_is(result, nil) 288 | end 289 | 290 | g['test_get_default'] = function() 291 | local cfg = { 292 | [httpd_role.DEFAULT_SERVER_NAME] = { 293 | listen = 13001, 294 | }, 295 | } 296 | 297 | httpd_role.apply(cfg) 298 | 299 | local result = httpd_role.get_server() 300 | t.assert_not_equals(result, nil) 301 | t.assert_is(result.port, 13001) 302 | end 303 | 304 | g['test_get_server_bad_type'] = function() 305 | local ok, res = pcall(httpd_role.get_server, {}) 306 | 307 | t.assert_not(ok) 308 | t.assert_str_contains(res, '?string expected, got table') 309 | end 310 | 311 | g.test_stop_unused_servers = function() 312 | local cfg = { 313 | [httpd_role.DEFAULT_SERVER_NAME] = { 314 | listen = 13001, 315 | }, 316 | additional = { 317 | listen = 13002, 318 | }, 319 | } 320 | 321 | httpd_role.apply(cfg) 322 | for name, body in pairs(cfg) do 323 | local res = httpd_role.get_server(name) 324 | t.assert(res) 325 | t.assert_equals(res.port, body.listen) 326 | end 327 | 328 | cfg.additional = nil 329 | httpd_role.apply(cfg) 330 | t.assert(httpd_role.get_server()) 331 | t.assert_equals(httpd_role.get_server('additional')) 332 | end 333 | 334 | g.test_edit_server_address = function() 335 | local cfg = { 336 | [httpd_role.DEFAULT_SERVER_NAME] = { 337 | listen = 13001, 338 | }, 339 | } 340 | 341 | httpd_role.apply(cfg) 342 | local result = httpd_role.get_server() 343 | t.assert(result) 344 | t.assert_equals(result.port, cfg[httpd_role.DEFAULT_SERVER_NAME].listen) 345 | 346 | cfg[httpd_role.DEFAULT_SERVER_NAME].listen = 13002 347 | 348 | httpd_role.apply(cfg) 349 | result = httpd_role.get_server() 350 | t.assert(result) 351 | t.assert_equals(result.port, cfg[httpd_role.DEFAULT_SERVER_NAME].listen) 352 | end 353 | 354 | g.test_log_requests = function() 355 | local cfg = { 356 | server1 = { 357 | listen = 13002, 358 | log_requests = 'info', 359 | }, 360 | server2 = { 361 | listen = 13003, 362 | log_requests = 'verbose', 363 | }, 364 | } 365 | 366 | httpd_role.apply(cfg) 367 | 368 | local server1 = httpd_role.get_server('server1') 369 | t.assert(server1) 370 | t.assert_type(server1.options.log_requests, 'function') 371 | 372 | local server2 = httpd_role.get_server('server2') 373 | t.assert(server2) 374 | t.assert_type(server2.options.log_requests, 'function') 375 | end 376 | -------------------------------------------------------------------------------- /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 LOG_LEVELS = { 13 | INFO = 5, 14 | VERBOSE = 6, 15 | DEBUG = 7, 16 | } 17 | 18 | local g = t.group(nil, t.helpers.matrix({ 19 | use_tls = {true, false}, 20 | })) 21 | 22 | local ssl_data_dir = fio.abspath(fio.pathjoin(helpers.get_testdir_path(), "ssl_data")) 23 | 24 | local config = { 25 | credentials = { 26 | users = { 27 | guest = { 28 | roles = {'super'}, 29 | }, 30 | }, 31 | }, 32 | iproto = { 33 | listen = {{uri = 'unix/:./{{ instance_name }}.iproto'}}, 34 | }, 35 | groups = { 36 | ['group-001'] = { 37 | replicasets = { 38 | ['replicaset-001'] = { 39 | roles = { 40 | 'roles.httpd', 41 | 'test.mocks.mock_role', 42 | }, 43 | roles_cfg = { 44 | ['roles.httpd'] = { 45 | default = { 46 | listen = 13000, 47 | log_requests = 'info', 48 | }, 49 | additional = { 50 | listen = 13001, 51 | log_requests = 'verbose', 52 | }, 53 | additional_debug = { 54 | listen = 13002, 55 | log_requests = 'debug', 56 | }, 57 | }, 58 | ['test.mocks.mock_role'] = { 59 | { 60 | id = 1, 61 | }, 62 | { 63 | id = 2, 64 | name = 'additional', 65 | }, 66 | { 67 | id = 3, 68 | name = 'additional_debug', 69 | } 70 | }, 71 | }, 72 | instances = { 73 | ['instance-001'] = {}, 74 | }, 75 | }, 76 | }, 77 | }, 78 | }, 79 | } 80 | 81 | local tls_config = table.deepcopy(config) 82 | tls_config.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default 83 | .ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt') 84 | 85 | tls_config.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default 86 | .ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key') 87 | 88 | tls_config.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default 89 | .ssl_password_file = fio.pathjoin(ssl_data_dir, 'passwords') 90 | 91 | g.before_all(function() 92 | helpers.skip_if_ssl_not_enabled() 93 | end) 94 | 95 | g.before_each(function(cg) 96 | helpers.skip_if_not_tarantool3() 97 | 98 | local dir = treegen.prepare_directory({}, {}) 99 | 100 | local cfg = config 101 | if cg.params.use_tls then 102 | cfg = tls_config 103 | end 104 | 105 | local config_file = treegen.write_file(dir, 'config.yaml', 106 | yaml.encode(cfg)) 107 | local opts = {config_file = config_file, chdir = dir} 108 | cg.server = server:new(fun.chain(opts, {alias = 'instance-001'}):tomap()) 109 | helpers.update_lua_env_variables(cg.server) 110 | 111 | cg.server:start() 112 | end) 113 | 114 | g.after_each(function(cg) 115 | helpers.teardown(cg.server) 116 | end) 117 | 118 | g.test_httpd_role_usage = function(cg) 119 | if cg.params.use_tls then 120 | local resp = http_client:get('https://localhost:13000/ping', { 121 | ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt') 122 | }) 123 | t.assert_equals(resp.status, 200, 'response not 200') 124 | t.assert_equals(resp.body, 'pong') 125 | end 126 | 127 | -- We can use https only for one endpoind due to we haven't publish separate 128 | -- certificates for it. 129 | local resp = http_client:get('http://localhost:13001/ping') 130 | t.assert_equals(resp.status, 200, 'response not 200') 131 | t.assert_equals(resp.body, 'pong') 132 | 133 | t.assert_equals(cg.server:eval( 134 | 'return require("test.mocks.mock_role").get_server_port(1)' 135 | ), 13000) 136 | t.assert_equals(cg.server:eval( 137 | 'return require("test.mocks.mock_role").get_server_port(2)' 138 | ), 13001) 139 | end 140 | 141 | g.test_stop_server_after_remove = function(cg) 142 | local protocol = cg.params.use_tls and 'https' or 'http' 143 | 144 | t.assert(helpers.tcp_connection_exists('localhost', 13000)) 145 | local resp = http_client:get(protocol .. '://localhost:13000/ping', { 146 | ca_file = cg.params.use_tls and fio.pathjoin(ssl_data_dir, 'ca.crt'), 147 | }) 148 | t.assert_equals(resp.status, 200, 'response not 200') 149 | t.assert_equals(resp.body, 'pong') 150 | 151 | local cfg = table.deepcopy(config) 152 | cfg.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default = nil 153 | treegen.write_file(cg.server.chdir, 'config.yaml', yaml.encode(cfg)) 154 | local _, err = cg.server:eval("require('config'):reload()") 155 | t.assert_not(err) 156 | 157 | t.assert_not(helpers.tcp_connection_exists('localhost', 13000)) 158 | end 159 | 160 | g.test_change_server_addr_on_the_run = function(cg) 161 | t.skip_if(cg.params.use_tls, 'no certs for testing addr') 162 | 163 | local resp = http_client:get('http://0.0.0.0:13001/ping') 164 | t.assert_equals(resp.status, 200, 'response not 200') 165 | t.assert_equals(resp.body, 'pong') 166 | 167 | local cfg = table.deepcopy(config) 168 | cfg.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].additional.listen = 'localhost:13001' 169 | treegen.write_file(cg.server.chdir, 'config.yaml', yaml.encode(cfg)) 170 | local _, err = cg.server:eval("require('config'):reload()") 171 | t.assert_not(err) 172 | 173 | t.assert_not(helpers.tcp_connection_exists('0.0.0.0', 13001)) 174 | resp = http_client:get('http://localhost:13001/ping') 175 | t.assert_equals(resp.status, 200, 'response not 200') 176 | t.assert_equals(resp.body, 'pong') 177 | end 178 | 179 | g.test_keep_existing_server_routes_on_config_reload = function(cg) 180 | local protocol = cg.params.use_tls and 'https' or 'http' 181 | 182 | local resp = http_client:get(protocol .. '://localhost:13000/ping_once', { 183 | ca_file = cg.params.use_tls and fio.pathjoin(ssl_data_dir, 'ca.crt'), 184 | }) 185 | t.assert_equals(resp.status, 200, 'response not 200') 186 | t.assert_equals(resp.body, 'pong once') 187 | 188 | local cfg = table.deepcopy(config) 189 | cfg.credentials.users.testguest = { roles = {'super'} } 190 | treegen.write_file(cg.server.chdir, 'config.yaml', yaml.encode(cfg)) 191 | local _, err = cg.server:eval("require('config'):reload()") 192 | t.assert_not(err) 193 | 194 | t.assert(helpers.tcp_connection_exists('localhost', 13000)) 195 | resp = http_client:get(protocol .. '://localhost:13000/ping_once', { 196 | ca_file = cg.params.use_tls and fio.pathjoin(ssl_data_dir, 'ca.crt'), 197 | }) 198 | t.assert_equals(resp.status, 200, 'response not 200') 199 | t.assert_equals(resp.body, 'pong once') 200 | end 201 | 202 | for log_name, log_lvl in pairs(LOG_LEVELS) do 203 | g.before_test('test_log_requests_' .. string.lower(log_name), function(cg) 204 | local cfg = table.copy(config) 205 | cfg.log = {level = log_lvl} 206 | treegen.write_file(cg.server.chdir, 'config.yaml', yaml.encode(cfg)) 207 | local _, err = cg.server:eval("require('config'):reload()") 208 | t.assert_not(err) 209 | end) 210 | 211 | g['test_log_requests_' .. string.lower(log_name)] = function(cg) 212 | t.skip_if(cg.params.use_tls) 213 | 214 | local function make_request(address) 215 | local resp = http_client:get(string.format('http://%s/ping', address)) 216 | t.assert_equals(resp.status, 200, 'response not 200') 217 | end 218 | 219 | local function assert_should_log(expected) 220 | local grep_res = cg.server:grep_log('GET /ping', math.huge) 221 | if expected then 222 | t.assert(grep_res) 223 | else 224 | t.assert_not(grep_res) 225 | end 226 | end 227 | 228 | local log_level = tonumber(log_lvl) 229 | 230 | make_request('localhost:13002') 231 | assert_should_log(log_level >= LOG_LEVELS.DEBUG) 232 | 233 | make_request('localhost:13001') 234 | assert_should_log(log_level >= LOG_LEVELS.VERBOSE) 235 | 236 | make_request('localhost:13000') 237 | assert_should_log(log_level >= LOG_LEVELS.INFO) 238 | end 239 | end 240 | 241 | g.test_enable_tls_on_config_reload = function(cg) 242 | -- We should start with no tls firstly. 243 | t.skip_if(cg.params.use_tls) 244 | 245 | local resp = http_client:get('http://localhost:13000/ping') 246 | t.assert_equals(resp.status, 200, 'response not 200') 247 | t.assert_equals(resp.body, 'pong') 248 | 249 | treegen.write_file(cg.server.chdir, 'config.yaml', yaml.encode(tls_config)) 250 | local _, err = cg.server:eval("require('config'):reload()") 251 | t.assert_not(err) 252 | 253 | resp = http_client:get('https://localhost:13000/ping', { 254 | ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt') 255 | }) 256 | t.assert_equals(resp.status, 200, 'response not 200') 257 | t.assert_equals(resp.body, 'pong') 258 | 259 | local resp = http_client:get('http://localhost:13000/ping') 260 | t.assert_equals(resp.status, 444, 'response not 444') 261 | end 262 | 263 | g.test_ssl_verify_client = function(cg) 264 | t.skip_if(not cg.params.use_tls, 'tls config required') 265 | 266 | local cfg = table.copy(tls_config) 267 | 268 | cfg.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default 269 | .ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt') 270 | cfg.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default 271 | .ssl_verify_client = "on" 272 | treegen.write_file(cg.server.chdir, 'config.yaml', yaml.encode(cfg)) 273 | local _, err = cg.server:eval("require('config'):reload()") 274 | t.assert_not(err) 275 | 276 | t.assert_error_msg_contains(helpers.CONNECTION_REFUSED_ERR_MSG, function() 277 | http_client:get('https://localhost:13000/ping', { 278 | ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt') 279 | }) 280 | end) 281 | 282 | local resp = http_client:get('https://localhost:13000/ping', { 283 | ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 284 | ssl_cert = fio.pathjoin(ssl_data_dir, 'client.crt'), 285 | ssl_key = fio.pathjoin(ssl_data_dir, 'client.key'), 286 | }) 287 | t.assert_equals(resp.status, 200, 'response not 200') 288 | t.assert_equals(resp.body, 'pong') 289 | 290 | cfg.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default 291 | .ssl_verify_client = "optional" 292 | treegen.write_file(cg.server.chdir, 'config.yaml', yaml.encode(cfg)) 293 | _, err = cg.server:eval("require('config'):reload()") 294 | t.assert_not(err) 295 | 296 | t.assert_error_msg_contains(helpers.CONNECTION_REFUSED_ERR_MSG, function() 297 | http_client:get('https://localhost:13000/ping', { 298 | ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 299 | ssl_cert = fio.pathjoin(ssl_data_dir, 'bad_client.crt'), 300 | ssl_key = fio.pathjoin(ssl_data_dir, 'bad_client.key'), 301 | }) 302 | end) 303 | 304 | resp = http_client:get('https://localhost:13000/ping', { 305 | ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), 306 | ssl_cert = fio.pathjoin(ssl_data_dir, 'client.crt'), 307 | ssl_key = fio.pathjoin(ssl_data_dir, 'client.key'), 308 | }) 309 | t.assert_equals(resp.status, 200, 'response not 200') 310 | t.assert_equals(resp.body, 'pong') 311 | end 312 | 313 | g.after_test('test_ssl_verify_client', function(cg) 314 | treegen.write_file(cg.server.chdir, 'config.yaml', yaml.encode(tls_config)) 315 | local _, err = cg.server:eval("require('config'):reload()") 316 | t.assert_not(err) 317 | end) 318 | -------------------------------------------------------------------------------- /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(ffi.cdef, 'typedef struct SSL_METHOD {} SSL_METHOD;') 13 | pcall(ffi.cdef, 'typedef struct SSL_CTX {} SSL_CTX;') 14 | pcall(ffi.cdef, 'typedef struct SSL {} SSL;') 15 | 16 | pcall(ffi.cdef, [[ 17 | const SSL_METHOD *TLS_server_method(void); 18 | const SSL_METHOD *TLS_client_method(void); 19 | 20 | SSL_CTX *SSL_CTX_new(const SSL_METHOD *method); 21 | void SSL_CTX_free(SSL_CTX *); 22 | 23 | int SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type); 24 | int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type); 25 | void SSL_CTX_set_default_passwd_cb_userdata(SSL_CTX *ctx, void *u); 26 | typedef int (*pem_passwd_cb)(char *buf, int size, int rwflag, void *userdata); 27 | 28 | void SSL_CTX_set_default_passwd_cb(SSL_CTX *ctx, pem_passwd_cb cb); 29 | 30 | int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, 31 | const char *CApath); 32 | int SSL_CTX_set_cipher_list(SSL_CTX *ctx, const char *str); 33 | void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, 34 | int (*verify_callback)(int, void *)); 35 | 36 | SSL *SSL_new(SSL_CTX *ctx); 37 | void SSL_free(SSL *ssl); 38 | 39 | int SSL_set_fd(SSL *s, int fd); 40 | 41 | void SSL_set_connect_state(SSL *s); 42 | void SSL_set_accept_state(SSL *s); 43 | 44 | int SSL_write(SSL *ssl, const void *buf, int num); 45 | int SSL_read(SSL *ssl, void *buf, int num); 46 | 47 | int SSL_pending(const SSL *ssl); 48 | 49 | void ERR_clear_error(void); 50 | char *ERR_error_string(unsigned long e, char *buf); 51 | unsigned long ERR_peek_last_error(void); 52 | 53 | int SSL_get_error(const SSL *s, int ret_code); 54 | 55 | void *memmem(const void *haystack, size_t haystacklen, 56 | const void *needle, size_t needlelen); 57 | ]]) 58 | 59 | local SET_VERIFY_FLAGS = { 60 | SSL_VERIFY_NONE = 0x00, 61 | SSL_VERIFY_PEER = 0x01, 62 | SSL_VERIFY_FAIL_IF_NO_PEER = 0x02, 63 | } 64 | 65 | local VERIFY_CLIENT_OPTS = { 66 | off = SET_VERIFY_FLAGS.SSL_VERIFY_NONE, 67 | optional = SET_VERIFY_FLAGS.SSL_VERIFY_PEER, 68 | on = bit.bor(SET_VERIFY_FLAGS.SSL_VERIFY_PEER, SET_VERIFY_FLAGS.SSL_VERIFY_FAIL_IF_NO_PEER), 69 | } 70 | 71 | local function slice_wait(timeout, starttime) 72 | if timeout == nil then 73 | return nil 74 | end 75 | 76 | return timeout - (clock.time() - starttime) 77 | end 78 | 79 | local X509_FILETYPE_PEM = 1 80 | 81 | local function tls_server_method() 82 | return ffi.C.TLS_server_method() 83 | end 84 | 85 | local function ctx(method) 86 | ffi.C.ERR_clear_error() 87 | 88 | return ffi.gc(ffi.C.SSL_CTX_new(method), ffi.C.SSL_CTX_free) 89 | end 90 | 91 | local function ctx_use_private_key_file(ctx, pem_file, password, password_file) 92 | ffi.C.SSL_CTX_set_default_passwd_cb(ctx, box.NULL); 93 | 94 | if password ~= nil then 95 | log.info('set private key password') 96 | 97 | ffi.C.SSL_CTX_set_default_passwd_cb_userdata(ctx, 98 | ffi.cast('void*', ffi.cast('char*', password))) 99 | local rc = ffi.C.SSL_CTX_use_PrivateKey_file(ctx, pem_file, X509_FILETYPE_PEM) 100 | ffi.C.SSL_CTX_set_default_passwd_cb_userdata(ctx, box.NULL); 101 | 102 | if rc == 1 then 103 | return true 104 | end 105 | ffi.C.ERR_clear_error() 106 | end 107 | 108 | if password_file ~= nil then 109 | local fh = fio.open(password_file, {'O_RDONLY'}) 110 | if fh == nil then 111 | return false 112 | end 113 | 114 | local is_loaded = false 115 | local password_from_file = "" 116 | local char 117 | while char ~= '' do 118 | while true do 119 | char = fh:read(1) 120 | if char == '\n' or char == '' then 121 | break 122 | end 123 | password_from_file = password_from_file .. char 124 | end 125 | 126 | ffi.C.SSL_CTX_set_default_passwd_cb_userdata(ctx, 127 | ffi.cast('void*', ffi.cast('char*', password_from_file))) 128 | local rc = ffi.C.SSL_CTX_use_PrivateKey_file(ctx, pem_file, X509_FILETYPE_PEM) 129 | ffi.C.SSL_CTX_set_default_passwd_cb_userdata(ctx, box.NULL); 130 | 131 | if rc == 1 then 132 | is_loaded = true 133 | break 134 | end 135 | 136 | ffi.C.ERR_clear_error() 137 | password_from_file = '' 138 | end 139 | 140 | fh:close() 141 | 142 | return is_loaded 143 | end 144 | 145 | local rc = ffi.C.SSL_CTX_use_PrivateKey_file(ctx, pem_file, X509_FILETYPE_PEM) 146 | if rc ~= 1 then 147 | ffi.C.ERR_clear_error() 148 | return false 149 | end 150 | 151 | return true 152 | end 153 | 154 | local function ctx_use_certificate_file(ctx, pem_file) 155 | if ffi.C.SSL_CTX_use_certificate_file(ctx, pem_file, X509_FILETYPE_PEM) ~= 1 then 156 | ffi.C.ERR_clear_error() 157 | return false 158 | end 159 | return true 160 | end 161 | 162 | local function ctx_load_verify_locations(ctx, ca_file) 163 | if ffi.C.SSL_CTX_load_verify_locations(ctx, ca_file, box.NULL) ~= 1 then 164 | ffi.C.ERR_clear_error() 165 | return false 166 | end 167 | return true 168 | end 169 | 170 | local function ctx_set_cipher_list(ctx, str) 171 | if ffi.C.SSL_CTX_set_cipher_list(ctx, str) ~= 1 then 172 | return false 173 | end 174 | return true 175 | end 176 | 177 | local function ctx_set_verify(ctx, mode) 178 | mode = mode or 'off' 179 | ffi.C.SSL_CTX_set_verify(ctx, VERIFY_CLIENT_OPTS[mode], box.NULL) 180 | end 181 | 182 | local default_ctx = ctx(ffi.C.TLS_server_method()) 183 | 184 | local SSL_ERROR_WANT_READ = 2 185 | local SSL_ERROR_WANT_WRITE = 3 186 | local SSL_ERROR_SYSCALL = 5 -- Look at error stack/return value/errno. 187 | local SSL_ERROR_ZERO_RETURN = 6 188 | 189 | local sslsocket = { 190 | } 191 | sslsocket.__index = sslsocket 192 | 193 | local WAIT_FOR_READ =1 194 | local WAIT_FOR_WRITE =2 195 | 196 | function sslsocket.write(self, data, timeout) 197 | local start = clock.time() 198 | 199 | local size = #data 200 | local s = ffi.cast('const char *', data) 201 | 202 | local mode = WAIT_FOR_WRITE 203 | 204 | while true do 205 | local rc = nil 206 | if mode == WAIT_FOR_READ then 207 | rc = self.sock:readable(slice_wait(timeout, start)) 208 | elseif mode == WAIT_FOR_WRITE then 209 | rc = self.sock:writable(slice_wait(timeout, start)) 210 | else 211 | assert(false) 212 | end 213 | 214 | if not rc then 215 | self.sock._errno = errno.ETIMEDOUT 216 | return nil, 'Timeout exceeded' 217 | end 218 | 219 | ffi.C.ERR_clear_error() 220 | local num = ffi.C.SSL_write(self.ssl, s, size); 221 | if num <= 0 then 222 | local ssl_error = ffi.C.SSL_get_error(self.ssl, num); 223 | if ssl_error == SSL_ERROR_WANT_WRITE then 224 | mode = WAIT_FOR_WRITE 225 | elseif ssl_error == SSL_ERROR_WANT_READ then 226 | mode = WAIT_FOR_READ 227 | elseif ssl_error == SSL_ERROR_SYSCALL then 228 | return nil, self.sock:error() 229 | elseif ssl_error == SSL_ERROR_ZERO_RETURN then 230 | return 0 231 | else 232 | local error_string = ffi.string(ffi.C.ERR_error_string(ssl_error, nil)) 233 | log.info(error_string) 234 | return nil, error_string 235 | end 236 | else 237 | return num 238 | end 239 | end 240 | end 241 | 242 | function sslsocket.close(self) 243 | return self.sock:close() 244 | end 245 | 246 | function sslsocket.error(self) 247 | local error_string = 248 | ffi.string(ffi.C.ERR_error_string(ffi.C.ERR_peek_last_error(), nil)) 249 | 250 | return self.sock:error() or error_string 251 | end 252 | 253 | function sslsocket.errno(self) 254 | return self.sock:errno() or ffi.C.ERR_peek_last_error() 255 | end 256 | 257 | function sslsocket.fd(self) 258 | return self.sock:fd() 259 | end 260 | 261 | function sslsocket.nonblock(self, nb) 262 | return self.sock:nonblock(nb) 263 | end 264 | 265 | local function sysread(self, charptr, size, timeout) 266 | local start = clock.time() 267 | 268 | local mode = rawget(self, 'first_state') or WAIT_FOR_READ 269 | rawset(self, 'first_state', nil) 270 | 271 | while true do 272 | local rc = nil 273 | if mode == WAIT_FOR_READ then 274 | if ffi.C.SSL_pending(self.ssl) > 0 then 275 | rc = true 276 | else 277 | rc = self.sock:readable(slice_wait(timeout, start)) 278 | end 279 | elseif mode == WAIT_FOR_WRITE then 280 | rc = self.sock:writable(slice_wait(timeout, start)) 281 | else 282 | assert(false) 283 | end 284 | 285 | if not rc then 286 | self.sock._errno = errno.ETIMEDOUT 287 | return nil, 'Timeout exceeded' 288 | end 289 | 290 | ffi.C.ERR_clear_error() 291 | local num = ffi.C.SSL_read(self.ssl, charptr, size); 292 | if num <= 0 then 293 | local ssl_error = ffi.C.SSL_get_error(self.ssl, num); 294 | if ssl_error == SSL_ERROR_WANT_WRITE then 295 | mode = WAIT_FOR_WRITE 296 | elseif ssl_error == SSL_ERROR_WANT_READ then 297 | mode = WAIT_FOR_READ 298 | elseif ssl_error == SSL_ERROR_SYSCALL then 299 | return nil, self.sock:error() 300 | elseif ssl_error == SSL_ERROR_ZERO_RETURN then 301 | return 0 302 | else 303 | local error_string = ffi.string(ffi.C.ERR_error_string(ssl_error, nil)) 304 | log.info(error_string) 305 | return nil, error_string 306 | end 307 | else 308 | return num 309 | end 310 | end 311 | end 312 | 313 | local function read(self, limit, timeout, check, ...) 314 | assert(limit >= 0) 315 | 316 | local start = clock.time() 317 | 318 | limit = math.min(limit, LIMIT_INFINITY) 319 | local rbuf = self.rbuf 320 | if rbuf == nil then 321 | rbuf = buffer.ibuf() 322 | rawset(self, 'rbuf', rbuf) 323 | end 324 | 325 | local len = check(self, limit, ...) 326 | if len ~= nil then 327 | local data = ffi.string(rbuf.rpos, len) 328 | rbuf.rpos = rbuf.rpos + len 329 | return data 330 | end 331 | 332 | while true do 333 | assert(rbuf:size() < limit) 334 | local to_read = math.min(limit - rbuf:size(), buffer.READAHEAD) 335 | local data = rbuf:reserve(to_read) 336 | assert(rbuf:unused() >= to_read) 337 | 338 | local res, err = sysread(self, data, rbuf:unused(), slice_wait(timeout, start)) 339 | if res == 0 then -- EOF. 340 | local len = rbuf:size() 341 | local data = ffi.string(rbuf.rpos, len) 342 | rbuf.rpos = rbuf.rpos + len 343 | return data 344 | elseif res ~= nil then 345 | rbuf.wpos = rbuf.wpos + res 346 | local len = check(self, limit, ...) 347 | if len ~= nil then 348 | local data = ffi.string(rbuf.rpos, len) 349 | rbuf.rpos = rbuf.rpos + len 350 | return data 351 | end 352 | else 353 | return nil, err 354 | end 355 | end 356 | 357 | -- Not reached. 358 | end 359 | 360 | local function check_limit(self, limit) 361 | if self.rbuf:size() >= limit then 362 | return limit 363 | end 364 | return nil 365 | end 366 | 367 | local function check_delimiter(self, limit, eols) 368 | if limit == 0 then 369 | return 0 370 | end 371 | local rbuf = self.rbuf 372 | if rbuf:size() == 0 then 373 | return nil 374 | end 375 | 376 | local shortest 377 | for _, eol in ipairs(eols) do 378 | local data = ffi.C.memmem(rbuf.rpos, rbuf:size(), eol, #eol) 379 | if data ~= nil then 380 | local len = ffi.cast('char *', data) - rbuf.rpos + #eol 381 | if shortest == nil or shortest > len then 382 | shortest = len 383 | end 384 | end 385 | end 386 | if shortest ~= nil and shortest <= limit then 387 | return shortest 388 | elseif limit <= rbuf:size() then 389 | return limit 390 | end 391 | return nil 392 | end 393 | 394 | function sslsocket.read(self, opts, timeout) 395 | timeout = timeout or TIMEOUT_INFINITY 396 | if type(opts) == 'number' then 397 | return read(self, opts, timeout, check_limit) 398 | elseif type(opts) == 'string' then 399 | return read(self, LIMIT_INFINITY, timeout, check_delimiter, { opts }) 400 | elseif type(opts) == 'table' then 401 | local chunk = opts.chunk or opts.size or LIMIT_INFINITY 402 | local delimiter = opts.delimiter or opts.line 403 | if delimiter == nil then 404 | return read(self, chunk, timeout, check_limit) 405 | elseif type(delimiter) == 'string' then 406 | return read(self, chunk, timeout, check_delimiter, { delimiter }) 407 | elseif type(delimiter) == 'table' then 408 | return read(self, chunk, timeout, check_delimiter, delimiter) 409 | end 410 | end 411 | error('Usage: s:read(delimiter|chunk|{delimiter = x, chunk = x}, timeout)') 412 | end 413 | 414 | function sslsocket.readable(self, timeout) 415 | return self.sock:readable(timeout) 416 | end 417 | 418 | local function wrap_accepted_socket(sock, sslctx) 419 | sslctx = sslctx or default_ctx 420 | 421 | ffi.C.ERR_clear_error() 422 | local ssl = ffi.gc(ffi.C.SSL_new(sslctx), 423 | ffi.C.SSL_free) 424 | if ssl == nil then 425 | sock:close() 426 | return nil, 'SSL_new failed' 427 | end 428 | 429 | sock:nonblock(true) 430 | 431 | ffi.C.ERR_clear_error() 432 | local rc = ffi.C.SSL_set_fd(ssl, sock:fd()) 433 | if rc == 0 then 434 | sock:close() 435 | return nil, 'SSL_set_fd failed' 436 | end 437 | 438 | ffi.C.ERR_clear_error() 439 | ffi.C.SSL_set_accept_state(ssl); 440 | 441 | local self = setmetatable({}, sslsocket) 442 | rawset(self, 'sock', sock) 443 | rawset(self, 'ctx', sslctx) 444 | rawset(self, 'ssl', ssl) 445 | return self 446 | end 447 | 448 | local function tcp_server(host, port, handler, timeout, sslctx) 449 | sslctx = sslctx or default_ctx 450 | 451 | local handler_function = handler.handler 452 | 453 | local wrapper = function(sock, from) 454 | local self, err = wrap_accepted_socket(sock, sslctx) 455 | if not self then 456 | log.info('sslsocket.tcp_server error: %s ', err) 457 | else 458 | handler_function(self, from) 459 | end 460 | end 461 | 462 | handler.handler = wrapper 463 | 464 | return socket.tcp_server(host, port, handler, timeout) 465 | end 466 | 467 | return { 468 | tls_server_method = tls_server_method, 469 | 470 | ctx = ctx, 471 | ctx_use_private_key_file = ctx_use_private_key_file, 472 | ctx_use_certificate_file = ctx_use_certificate_file, 473 | ctx_load_verify_locations = ctx_load_verify_locations, 474 | ctx_set_cipher_list = ctx_set_cipher_list, 475 | ctx_set_verify = ctx_set_verify, 476 | 477 | tcp_server = tcp_server, 478 | 479 | wrap_accepted_socket = wrap_accepted_socket, 480 | } 481 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------