├── .busted ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .luacheckrc ├── .luacov ├── CONTRIBUTING.md ├── LICENSE.md ├── NEWS ├── README.md ├── doc ├── Makefile ├── README.md ├── interfaces.md ├── interfaces │ ├── connection.md │ └── stream.md ├── introduction.md ├── links.md ├── metadata.yaml ├── modules.md ├── modules │ ├── http.bit.md │ ├── http.client.md │ ├── http.compat.prosody.md │ ├── http.compat.socket.md │ ├── http.cookie.md │ ├── http.h1_connection.md │ ├── http.h1_reason_phrases.md │ ├── http.h1_stream.md │ ├── http.h2_connection.md │ ├── http.h2_error.md │ ├── http.h2_stream.md │ ├── http.headers.md │ ├── http.hpack.md │ ├── http.hsts.md │ ├── http.proxies.md │ ├── http.request.md │ ├── http.server.md │ ├── http.socks.md │ ├── http.tls.md │ ├── http.util.md │ ├── http.version.md │ ├── http.websocket.md │ └── http.zlib.md ├── site.css └── template.html ├── examples ├── h2_streaming.lua ├── serve_dir.lua ├── server_hello.lua ├── server_sent_events.lua ├── simple_request.lua └── websocket_client.lua ├── http-scm-0.rockspec ├── http ├── bit.lua ├── bit.tld ├── client.lua ├── compat │ ├── prosody.lua │ └── socket.lua ├── connection_common.lua ├── connection_common.tld ├── cookie.lua ├── cookie.tld ├── h1_connection.lua ├── h1_reason_phrases.lua ├── h1_reason_phrases.tld ├── h1_stream.lua ├── h2_connection.lua ├── h2_error.lua ├── h2_error.tld ├── h2_stream.lua ├── headers.lua ├── headers.tld ├── hpack.lua ├── hsts.lua ├── hsts.tld ├── proxies.lua ├── proxies.tld ├── request.lua ├── request.tld ├── server.lua ├── socks.lua ├── stream_common.lua ├── stream_common.tld ├── tls.lua ├── tls.tld ├── util.lua ├── util.tld ├── version.lua ├── version.tld ├── websocket.lua ├── zlib.lua └── zlib.tld └── spec ├── client_spec.lua ├── compat_prosody_spec.lua ├── compat_socket_spec.lua ├── cookie_spec.lua ├── h1_connection_spec.lua ├── h1_stream_spec.lua ├── h2_connection_spec.lua ├── h2_error_spec.lua ├── h2_stream_spec.lua ├── headers_spec.lua ├── helper.lua ├── hpack_spec.lua ├── hsts_spec.lua ├── path_spec.lua ├── proxies_spec.lua ├── request_spec.lua ├── require-all.lua ├── server_spec.lua ├── socks_spec.lua ├── stream_common_spec.lua ├── tls_spec.lua ├── util_spec.lua ├── websocket_spec.lua └── zlib_spec.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | lpath = "./?.lua"; 4 | ["auto-insulate"] = false; 5 | helper = "spec/helper.lua"; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: [ $default-branch ] 7 | 8 | jobs: 9 | luacheck: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | path: lua-http 15 | - uses: leafo/gh-actions-lua@v8.0.0 16 | - uses: leafo/gh-actions-luarocks@v4.0.0 17 | - name: install-tooling 18 | run: luarocks install luacheck 19 | - name: luacheck 20 | run: | 21 | cd lua-http 22 | luacheck . 23 | 24 | test: 25 | runs-on: ubuntu-latest 26 | strategy: 27 | matrix: 28 | luaVersion: 29 | - "5.1" 30 | - "5.2" 31 | - "5.3" 32 | - "5.4" 33 | - luajit-2.0.5 34 | - luajit-2.1.0-beta3 35 | luaCompileFlags: [""] 36 | zlib: ["", "lzlib", "lua-zlib"] 37 | remove_compat53: [false] 38 | 39 | exclude: 40 | # lzlib doesn't support Lua 5.4+ 41 | - luaVersion: "5.4" 42 | zlib: "lzlib" 43 | include: 44 | - luaVersion: "5.3" 45 | luaCompileFlags: LUA_CFLAGS="-DLUA_INT_TYPE=LUA_INT_INT" 46 | - luaVersion: "5.3" 47 | remove_compat53: true 48 | 49 | steps: 50 | - uses: actions/checkout@v2 51 | with: 52 | path: lua-http 53 | - uses: leafo/gh-actions-lua@v8.0.0 54 | with: 55 | luaVersion: ${{ matrix.luaVersion }} 56 | - uses: leafo/gh-actions-luarocks@v4.0.0 57 | - name: install-tooling 58 | run: | 59 | luarocks install luacov-coveralls 60 | luarocks install busted 61 | - name: install-dependencies 62 | run: | 63 | cd lua-http 64 | luarocks install --only-deps http-scm-0.rockspec 65 | 66 | - name: install-lzlib 67 | if: matrix.zlib == 'lzlib' 68 | run: luarocks install lzlib 69 | - name: install-lua-zlib 70 | if: matrix.zlib == 'lua-zlib' 71 | run: luarocks install lua-zlib 72 | 73 | - name: remove-compat53 74 | if: matrix.remove_compat53 75 | run: luarocks remove compat53 76 | 77 | - name: test 78 | run: | 79 | cd lua-http 80 | busted -c -o utfTerminal 81 | 82 | - name: coveralls 83 | continue-on-error: true 84 | env: 85 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 86 | run: | 87 | cd lua-http 88 | luacov-coveralls -v 89 | 90 | typedlua: 91 | runs-on: ubuntu-latest 92 | steps: 93 | - uses: actions/checkout@v2 94 | with: 95 | path: lua-http 96 | - uses: leafo/gh-actions-lua@v8.0.0 97 | with: 98 | luaVersion: "5.3" # tlc doesn't work with 5.4+ 99 | - uses: leafo/gh-actions-luarocks@v4.0.0 100 | - name: install-tooling 101 | run: luarocks install https://raw.githubusercontent.com/andremm/typedlua/master/typedlua-scm-1.rockspec 102 | - name: install-dependencies 103 | run: | 104 | cd lua-http 105 | luarocks install --only-deps http-scm-0.rockspec 106 | - name: typedlua 107 | run: | 108 | cd lua-http 109 | tlc -o /dev/null spec/require-all.lua 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /luacov.report.out 2 | /luacov.stats.out 3 | /*.rock 4 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "min" 2 | files["spec"] = { 3 | std = "+busted"; 4 | new_globals = { 5 | "TEST_TIMEOUT"; 6 | "assert_loop"; 7 | }; 8 | } 9 | max_line_length = false 10 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | return { 2 | statsfile = "luacov.stats.out"; 3 | reportfile = "luacov.report.out"; 4 | deletestats = true; 5 | include = { 6 | "/http/[^/]+$"; 7 | "/http/compat/[^/]+$"; 8 | }; 9 | exclude = { 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Hello and thank-you for considering contributing to lua-http! 2 | 3 | If you haven't already, see the [getting started](https://github.com/daurnimator/lua-http#getting-started) section of the main readme. 4 | 5 | # Contributing 6 | 7 | To submit your code for inclusion, please [send a "pull request" using github](https://github.com/daurnimator/lua-http/pulls). 8 | For a speedy approval, please: 9 | 10 | - Follow the [coding style](#coding-style) 11 | - Run [`luacheck`](https://github.com/mpeterv/luacheck) to lint your code 12 | - Include [tests](#tests) 13 | - Bug fixes should add a test exhibiting the issue 14 | - Enhancements must add tests for the new feature 15 | - [Sign off](#dco) your code 16 | 17 | 18 | If you are requested by a project maintainer to fix an issue with your pull request, please edit your existing commits (using e.g. `git commit --amend` or [`git fixup`](https://github.com/hashbang/dotfiles/blob/master/git/.local/bin/git-fixup)) rather than pushing new commits on top of the old ones. 19 | 20 | All commits *should* have the project in an operational state. 21 | 22 | 23 | # Coding Style 24 | 25 | When editing an existing file, please follow the coding style used in that file. 26 | If not clear from context or if you're starting a new file: 27 | 28 | - Indent with tabs 29 | - Alignment should not be done; when unavoidable, align with spaces 30 | - Remove any trailing whitespace (unless whitespace is significant as it can be in e.g. markdown) 31 | - Things (e.g. table fields) should be ordered by: 32 | 1. Required vs optional 33 | 2. Importance 34 | 3. Lexographically (alphabetically) 35 | 36 | 37 | ## Lua conventions 38 | 39 | - Add a `__name` field to metatables 40 | - Use a separate table than the metatable itself for `__index` 41 | - Single-line table definitions should use commas (`,`) for delimiting elements 42 | - Multi-line table definitions should use semicolons (`;`) for delimiting elements 43 | 44 | 45 | ## Markdown conventions 46 | 47 | - Files should have two blank lines at the end of a section 48 | - Repository information files (e.g. README.md/CONTRIBUTING.md) should use github compatible markdown features 49 | - Files used to generate documentation can use any `pandoc` features they want 50 | 51 | 52 | # Tests 53 | 54 | The project has a test suite using the [`busted`](https://github.com/Olivine-Labs/busted) framework. 55 | Coverage is measured using [`luacov`](https://github.com/keplerproject/luacov). 56 | 57 | Tests can be found in the `spec/` directory at the root of the repository. Each source file should have its own file full of tests. 58 | 59 | Tests should avoid running any external processes. Use `cqueues` to start up various test servers and clients in-process. 60 | 61 | A successful test should close any file handles and sockets to avoid resource exhaustion. 62 | 63 | 64 | # Legal 65 | 66 | All code in the repository is covered by `LICENSE.md`. 67 | 68 | ## DCO 69 | 70 | A git `Signed-off-by` statement in a commit message in this repository refers to the [Developer Certificate of Origin](https://developercertificate.org/) (DCO). 71 | By signing off your commit you are making a legal statement that the work is contributed under the license of this project. 72 | You can add the statement to your commit by passing `-s` to `git commit` 73 | 74 | 75 | # Security 76 | 77 | If you find a security vulnerabilities in the project and do not wish to file it publically on the [issue tracker](https://github.com/daurnimator/lua-http/issues) then you may email [lua-http-security@daurnimator.com](mailto:lua-http-security@daurnimator.com). You may encrypt your mail using PGP to the key with fingerprint [954A3772D62EF90E4B31FBC6C91A9911192C187A](https://daurnimator.com/post/109075829529/gpg-key). 78 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2021 Daurnimator 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 0.4 - 2021-02-06 2 | 3 | - Support multiple elliptic curves under OpenSSL 1.1.1+ (#150) 4 | - Improve support for Lua 5.4 (not longer require bit library to be installed) (#180) 5 | - Ignore delayed RST_STREAM frames in HTTP 2 (#145) 6 | 7 | 8 | 0.3 - 2019-02-13 9 | 10 | - Fix incorrect Sec-WebSocket-Protocol negotiation 11 | - Fix incorrect timeout handling in `websocket:receive()` 12 | - Add workaround to allow being required in openresty (#98) 13 | - Add http.tls.old_cipher_list (#112) 14 | - Add http.cookie module (#117) 15 | - Improvements to http.hsts module (#119) 16 | - Add `options` argument form to `stream:write_body_from_file()` (#125) 17 | 18 | 19 | 0.2 - 2017-05-28 20 | 21 | - Remove broken http.server `.client_timeout` option (replaced with `.connection_setup_timeout`) 22 | - Fix http1 pipelining locks 23 | - Miscellaneous http2 fixes 24 | - HTTP 2 streams no longer have to be used in order of creation 25 | - No longer raise decode errors in hpack module 26 | - Fix `hpack:lookup_index()` to treat static entries without values as empty string 27 | - Fix HTTP 1 client in locales with non-"." decimal separator 28 | - Add h1_stream.max_header_lines property to prevent infinite list of headers 29 | - New '.bind' option for requests and http.client module 30 | 31 | 32 | 0.1 - 2016-12-17 33 | 34 | - Support for HTTP versions 1, 1.1 and 2 35 | - Provides both client and server APIs 36 | - Friendly request API with sensible defaults for security 37 | - All operations are fully non-blocking and can be managed with cqueues 38 | - Support for WebSockets (client and server), including ping/pong, binary data transfer and TLS encryption. 39 | - Transport Layer Security (TLS) - lua-http supports HTTPS and WSS via luaossl. 40 | - luasocket compatibility API if you're looking to use lua-http with older projects. 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP library for Lua. 2 | 3 | ## Features 4 | 5 | - Optionally asynchronous (including DNS lookups and TLS) 6 | - Supports HTTP(S) version 1.0, 1.1 and 2 7 | - Functionality for both client and server 8 | - Cookie Management 9 | - Websockets 10 | - Compatible with Lua 5.1, 5.2, 5.3, 5.4 and [LuaJIT](http://luajit.org/) 11 | 12 | 13 | ## Documentation 14 | 15 | Can be found at [https://daurnimator.github.io/lua-http/](https://daurnimator.github.io/lua-http/) 16 | 17 | 18 | ## Status 19 | 20 | [![Build Status](https://github.com/daurnimator/lua-http/workflows/ci/badge.svg)](https://github.com/daurnimator/lua-http/actions?query=workflow%3Aci) 21 | [![Coverage Status](https://coveralls.io/repos/daurnimator/lua-http/badge.svg?branch=master&service=github)](https://coveralls.io/github/daurnimator/lua-http?branch=master) 22 | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/108/badge)](https://bestpractices.coreinfrastructure.org/projects/108) 23 | 24 | 25 | # Installation 26 | 27 | It's recommended to install lua-http by using [luarocks](https://luarocks.org/). 28 | This will automatically install run-time lua dependencies for you. 29 | 30 | $ luarocks install http 31 | 32 | ## Dependencies 33 | 34 | - [cqueues](http://25thandclement.com/~william/projects/cqueues.html) >= 20161214 (Note: cqueues currently doesn't support Microsoft Windows operating systems) 35 | - [luaossl](http://25thandclement.com/~william/projects/luaossl.html) >= 20161208 36 | - [basexx](https://github.com/aiq/basexx/) >= 0.2.0 37 | - [lpeg](http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html) 38 | - [lpeg_patterns](https://github.com/daurnimator/lpeg_patterns) >= 0.5 39 | - [binaryheap](https://github.com/Tieske/binaryheap.lua) >= 0.3 40 | - [fifo](https://github.com/daurnimator/fifo.lua) 41 | 42 | To use gzip compression you need **one** of: 43 | 44 | - [lzlib](https://github.com/LuaDist/lzlib) or [lua-zlib](https://github.com/brimworks/lua-zlib) 45 | 46 | To check cookies against a public suffix list: 47 | 48 | - [lua-psl](https://github.com/daurnimator/lua-psl) 49 | 50 | If using lua < 5.3 you will need 51 | 52 | - [compat-5.3](https://github.com/keplerproject/lua-compat-5.3) >= 0.3 53 | 54 | If using lua 5.1 you will need 55 | 56 | - [luabitop](http://bitop.luajit.org/) (comes [with LuaJIT](http://luajit.org/extensions.html)) or a [backported bit32](https://luarocks.org/modules/siffiejoe/bit32) 57 | 58 | ### For running tests 59 | 60 | - [luacheck](https://github.com/mpeterv/luacheck) 61 | - [busted](http://olivinelabs.com/busted/) 62 | - [luacov](https://keplerproject.github.io/luacov/) 63 | 64 | 65 | # Development 66 | 67 | ## Getting started 68 | 69 | - Clone the repo: 70 | ``` 71 | $ git clone https://github.com/daurnimator/lua-http.git 72 | $ cd lua-http 73 | ``` 74 | 75 | - Install dependencies 76 | ``` 77 | $ luarocks install --only-deps http-scm-0.rockspec 78 | ``` 79 | 80 | - Lint the code (check for common programming errors) 81 | ``` 82 | $ luacheck . 83 | ``` 84 | 85 | - Run tests and view coverage report ([install tools first](#for-running-tests)) 86 | ``` 87 | $ busted -c 88 | $ luacov && less luacov.report.out 89 | ``` 90 | 91 | - Install your local copy: 92 | ``` 93 | $ luarocks make http-scm-0.rockspec 94 | ``` 95 | 96 | 97 | ## Generating documentation 98 | 99 | Documentation is written in markdown and intended to be consumed by [pandoc](http://pandoc.org/). See the `doc/` directory for more information. 100 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | INTERFACES = \ 2 | connection.md \ 3 | stream.md 4 | 5 | MODULES = \ 6 | http.bit.md \ 7 | http.client.md \ 8 | http.cookie.md \ 9 | http.h1_connection.md \ 10 | http.h1_reason_phrases.md \ 11 | http.h1_stream.md \ 12 | http.h2_connection.md \ 13 | http.h2_error.md \ 14 | http.h2_stream.md \ 15 | http.headers.md \ 16 | http.hpack.md \ 17 | http.hsts.md \ 18 | http.proxies.md \ 19 | http.request.md \ 20 | http.server.md \ 21 | http.socks.md \ 22 | http.tls.md \ 23 | http.util.md \ 24 | http.version.md \ 25 | http.websocket.md \ 26 | http.zlib.md \ 27 | http.compat.prosody.md \ 28 | http.compat.socket.md 29 | 30 | FILES = \ 31 | introduction.md \ 32 | interfaces.md \ 33 | $(addprefix interfaces/,$(INTERFACES)) \ 34 | modules.md \ 35 | $(addprefix modules/,$(MODULES)) \ 36 | links.md 37 | 38 | all: lua-http.html lua-http.pdf lua-http.3 39 | 40 | lua-http.html: template.html site.css metadata.yaml $(FILES) 41 | pandoc -o $@ -t html5 -s --toc --template=template.html --section-divs --self-contained -c site.css metadata.yaml $(FILES) 42 | 43 | lua-http.pdf: metadata.yaml $(FILES) 44 | pandoc -o $@ -t latex -s --toc --toc-depth=2 -V documentclass=article -V classoption=oneside -V links-as-notes -V geometry=a4paper,includeheadfoot,margin=2.54cm metadata.yaml $(FILES) 45 | 46 | lua-http.3: metadata.yaml $(FILES) 47 | pandoc -o $@ -t man -s metadata.yaml $(FILES) 48 | 49 | man: lua-http.3 50 | man -l $^ 51 | 52 | clean: 53 | rm -f lua-http.html lua-http.pdf lua-http.3 54 | 55 | .PHONY: all man install clean 56 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | Documentation in this directory is intended to be converted to other formats using [pandoc](http://pandoc.org/). 2 | 3 | An online HTML version can be found at [https://daurnimator.github.io/lua-http/](https://daurnimator.github.io/lua-http/) 4 | 5 | The *Makefile* in this directory should be used to compile the documentation. 6 | -------------------------------------------------------------------------------- /doc/interfaces.md: -------------------------------------------------------------------------------- 1 | # Interfaces 2 | 3 | lua-http has separate modules for HTTP 1 vs HTTP 2 protocols, yet the different versions share many common concepts. lua-http provides a common interface for operations that make sense for both protocol versions (as well as any future developments). 4 | 5 | The following sections outline the interfaces exposed by the lua-http library. 6 | -------------------------------------------------------------------------------- /doc/interfaces/connection.md: -------------------------------------------------------------------------------- 1 | ## connection 2 | 3 | A connection encapsulates a socket and provides protocol specific operations. A connection may have [*streams*](#stream) which encapsulate the requests/responses happening over a conenction. Alternatively, you can ignore streams entirely and use low level protocol specific operations to read and write to the socket. 4 | 5 | All *connection* types expose the following fields: 6 | 7 | ### `connection.type` {#connection.type} 8 | 9 | The mode of use for the connection object. Valid values are: 10 | 11 | - `"client"`: Acts as a client; this connection type is used by entities who want to make requests 12 | - `"server"`: Acts as a server; this conenction type is used by entities who want to respond to requests 13 | 14 | 15 | ### `connection.version` {#connection.version} 16 | 17 | The HTTP version number of the connection as a number. 18 | 19 | 20 | ### `connection:pollfd()` {#connection:pollfd} 21 | 22 | 23 | ### `connection:events()` {#connection:events} 24 | 25 | 26 | ### `connection:timeout()` {#connection:timeout} 27 | 28 | 29 | ### `connection:connect(timeout)` {#connection:connect} 30 | 31 | Completes the connection to the remote server using the address specified, HTTP version and any options specified in the `connection.new` constructor. The `connect` function will yield until the connection attempt finishes (success or failure) or until `timeout` is exceeded. Connecting may include DNS lookups, TLS negotiation and HTTP2 settings exchange. Returns `true` on success. On error, returns `nil`, an error message and an error number. 32 | 33 | 34 | ### `connection:checktls()` {#connection:checktls} 35 | 36 | Checks the socket for a valid Transport Layer Security connection. Returns the luaossl ssl object if the connection is secured. Returns `nil` and an error message if there is no active TLS session. Please see the [luaossl website](http://25thandclement.com/~william/projects/luaossl.html) for more information about the ssl object. 37 | 38 | 39 | ### `connection:localname()` {#connection:localname} 40 | 41 | Returns the connection information for the local socket. Returns address family, IP address and port for an external socket. For Unix domain sockets, the function returns `AF_UNIX` and the path. If the connection object is not connected, returns `AF_UNSPEC` (0). On error, returns `nil`, an error message and an error number. 42 | 43 | 44 | ### `connection:peername()` {#connection:peername} 45 | 46 | Returns the connection information for the socket *peer* (as in, the next hop). Returns address family, IP address and port for an external socket. For unix sockets, the function returns `AF_UNIX` and the path. If the connection object is not connected, returns `AF_UNSPEC` (0). On error, returns `nil`, an error message and an error number. 47 | 48 | *Note: If the client is using a proxy, the values returned `:peername()` point to the proxy, not the remote server.* 49 | 50 | 51 | ### `connection:flush(timeout)` {#connection:flush} 52 | 53 | Flushes buffered outgoing data on the socket to the operating system. Returns `true` on success. On error, returns `nil`, an error message and an error number. 54 | 55 | 56 | ### `connection:shutdown()` {#connection:shutdown} 57 | 58 | Performs an orderly shutdown of the connection by closing all streams and calls `:shutdown()` on the socket. The connection cannot be re-opened. 59 | 60 | 61 | ### `connection:close()` {#connection:close} 62 | 63 | Closes a connection and releases operating systems resources. Note that `:close()` performs a [`connection:shutdown()`](#connection:shutdown) prior to releasing resources. 64 | 65 | 66 | ### `connection:new_stream()` {#connection:new_stream} 67 | 68 | Creates and returns a new [*stream*](#stream) on the connection. 69 | 70 | 71 | ### `connection:get_next_incoming_stream(timeout)` {#connection:get_next_incoming_stream} 72 | 73 | Returns the next peer initiated [*stream*](#stream) on the connection. This function can be used to yield and "listen" for incoming HTTP streams. 74 | 75 | 76 | ### `connection:onidle(new_handler)` {#http.connection:onidle} 77 | 78 | Provide a callback to get called when the connection becomes idle i.e. when there is no request in progress and no pipelined streams waiting. When called it will receive the `connection` as the first argument. Returns the previous handler. 79 | -------------------------------------------------------------------------------- /doc/interfaces/stream.md: -------------------------------------------------------------------------------- 1 | ## stream 2 | 3 | An HTTP *stream* is an abstraction of a request/response within a HTTP connection. Within a stream there may be a number of "header" blocks as well as data known as the "body". 4 | 5 | All stream types expose the following fields and functions: 6 | 7 | ### `stream.connection` {#stream.connection} 8 | 9 | The underlying [*connection*](#connection) object. 10 | 11 | 12 | ### `stream:checktls()` {#stream:checktls} 13 | 14 | Convenience wrapper equivalent to [`stream.connection:checktls()`](#connection:checktls) 15 | 16 | 17 | ### `stream:localname()` {#stream:localname} 18 | 19 | Convenience wrapper equivalent to [`stream.connection:localname()`](#connection:localname) 20 | 21 | 22 | ### `stream:peername()` {#stream:peername} 23 | 24 | Convenience wrapper equivalent to [`stream.connection:peername()`](#connection:peername) 25 | 26 | 27 | ### `stream:get_headers(timeout)` {#stream:get_headers} 28 | 29 | Retrieves the next complete headers object (i.e. a block of headers or trailers) from the stream. 30 | 31 | 32 | ### `stream:write_headers(headers, end_stream, timeout)` {#stream:write_headers} 33 | 34 | Write the given [*headers*](#http.headers) object to the stream. The function takes a flag indicating if this is the last chunk in the stream, if `true` the stream will be closed. If `timeout` is specified, the stream will wait for the send to complete until `timeout` is exceeded. 35 | 36 | 37 | ### `stream:write_continue(timeout)` {#stream:write_continue} 38 | 39 | Sends a 100-continue header block. 40 | 41 | 42 | ### `stream:get_next_chunk(timeout)` {#stream:get_next_chunk} 43 | 44 | Returns the next chunk of the http body from the socket, potentially yielding for up to `timeout` seconds. On error, returns `nil`, an error message and an error number. 45 | 46 | 47 | ### `stream:each_chunk()` {#stream:each_chunk} 48 | 49 | Iterator over [`stream:get_next_chunk()`](#stream:get_next_chunk) 50 | 51 | 52 | ### `stream:get_body_as_string(timeout)` {#stream:get_body_as_string} 53 | 54 | Reads the entire body from the stream and return it as a string. On error, returns `nil`, an error message and an error number. 55 | 56 | 57 | ### `stream:get_body_chars(n, timeout)` {#stream:get_body_chars} 58 | 59 | Reads `n` characters (bytes) of body from the stream and return them as a string. If the stream ends before `n` characters are read then returns the partial result. On error, returns `nil`, an error message and an error number. 60 | 61 | 62 | ### `stream:get_body_until(pattern, plain, include_pattern, timeout)` {#stream:get_body_until} 63 | 64 | Reads in body data from the stream until the [lua pattern](http://www.lua.org/manual/5.4/manual.html#6.4.1) `pattern` is found and returns the data as a string. `plain` is a boolean that indicates that pattern matching facilities should be turned off so that function does a plain "find substring" operation, with no characters in pattern being considered magic. `include_patterns` specifies if the pattern itself should be included in the returned string. On error, returns `nil`, an error message and an error number. 65 | 66 | 67 | ### `stream:save_body_to_file(file, timeout)` {#stream:save_body_to_file} 68 | 69 | Reads the body from the stream and saves it to the [lua file handle](http://www.lua.org/manual/5.4/manual.html#6.8) `file`. On error, returns `nil`, an error message and an error number. 70 | 71 | 72 | ### `stream:get_body_as_file(timeout)` {#stream:get_body_as_file} 73 | 74 | Reads the body from the stream into a temporary file and returns a [lua file handle](http://www.lua.org/manual/5.4/manual.html#6.8). On error, returns `nil`, an error message and an error number. 75 | 76 | 77 | ### `stream:unget(str)` {#stream:unget} 78 | 79 | Places `str` back on the incoming data buffer, allowing it to be returned again on a subsequent command ("un-gets" the data). Returns `true` on success. On error, returns `nil`, an error message and an error number. 80 | 81 | 82 | ### `stream:write_chunk(chunk, end_stream, timeout)` {#stream:write_chunk} 83 | 84 | Writes the string `chunk` to the stream. If `end_stream` is true, the body will be finalized and the stream will be closed. `write_chunk` yields indefinitely, or until `timeout` is exceeded. On error, returns `nil`, an error message and an error number. 85 | 86 | 87 | ### `stream:write_body_from_string(str, timeout)` {#stream:write_body_from_string} 88 | 89 | Writes the string `str` to the stream and ends the stream. On error, returns `nil`, an error message and an error number. 90 | 91 | 92 | ### `stream:write_body_from_file(options|file, timeout)` {#stream:write_body_from_file} 93 | 94 | - `options` is a table containing: 95 | - `.file` (file) 96 | - `.count` (positive integer): number of bytes of `file` to write 97 | defaults to infinity (the whole file will be written) 98 | 99 | Writes the contents of file `file` to the stream and ends the stream. `file` will not be automatically seeked, so ensure it is at the correct offset before calling. On error, returns `nil`, an error message and an error number. 100 | 101 | 102 | ### `stream:shutdown()` {#stream:shutdown} 103 | 104 | Closes the stream. The resources are released and the stream can no longer be used. 105 | -------------------------------------------------------------------------------- /doc/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | lua-http is an performant, capable HTTP and WebSocket library for Lua 5.1, 5.2, 5.3, 5.4 and LuaJIT. Some of the features of the library include: 4 | 5 | - Support for HTTP versions 1, 1.1 and 2 as specified by [RFC 7230](https://tools.ietf.org/html/rfc7230) and [RFC 7540](https://tools.ietf.org/html/rfc7540) 6 | - Provides both client and server APIs 7 | - Fully asynchronous API that does not block the current thread when executing operations that typically block 8 | - Support for WebSockets as specified by [RFC 6455](https://tools.ietf.org/html/rfc6455) including ping/pong, binary data transfer and TLS encryption 9 | - Transport Layer Security (TLS) - lua-http supports HTTPS and WSS via [luaossl](https://github.com/wahern/luaossl). 10 | - Easy integration into other event-loop or scripts 11 | 12 | ### Why lua-http? 13 | 14 | The lua-http library was written to fill a gap in the Lua ecosystem by providing an HTTP and WebSocket library with the following traits: 15 | 16 | - Asynchronous and performant 17 | - Can be used without forcing the developer to follow a specific pattern. Conversely, the library can be adapted to many common patterns. 18 | - Can be used at a very high level without need to understand the transportation of HTTP data (other than connection addresses). 19 | - Provides a rich low level API, if desired, for creating powerful HTTP based tools at the protocol level. 20 | 21 | As a result of these design goals, the library is simple and unobtrusive and can accommodate tens of thousands of connections on commodity hardware. 22 | 23 | lua-http is a flexible HTTP and WebSocket library that allows developers to concentrate on line-of-business features when building Internet enabled applications. If you are looking for a way to streamline development of an internet enabled application, enable HTTP networking in your game, create a new Internet Of Things (IoT) system, or write a performant custom web server for a specific use case, lua-http has the tools you need. 24 | 25 | 26 | ### Portability 27 | 28 | lua-http is pure Lua code with dependencies on the following external libraries: 29 | 30 | - [cqueues](http://25thandclement.com/~william/projects/cqueues.html) - Posix API library for Lua 31 | - [luaossl](http://25thandclement.com/~william/projects/luaossl.html) - Lua bindings for TLS/SSL 32 | - [lua-zlib](https://github.com/brimworks/lua-zlib) - Optional Lua bindings for zlib 33 | 34 | lua-http can run on any operating system supported by cqueues and openssl, which at the time of writing is GNU/Linux, FreeBSD, NetBSD, OpenBSD, OSX and Solaris. 35 | 36 | 37 | ## Common Use Cases 38 | 39 | The following are two simple demonstrations of how the lua-http library can be used: 40 | 41 | ### Retrieving a Document 42 | 43 | The highest level interface for clients is [*http.request*](#http.request). By constructing a [*request*](#http.request) object from a URI using [`new_from_uri`](#http.request.new_from_uri) and immediately evaluating it, you can easily fetch an HTTP resource. 44 | 45 | ```lua 46 | local http_request = require "http.request" 47 | local headers, stream = assert(http_request.new_from_uri("http://example.com"):go()) 48 | local body = assert(stream:get_body_as_string()) 49 | if headers:get ":status" ~= "200" then 50 | error(body) 51 | end 52 | print(body) 53 | ``` 54 | 55 | 56 | ### WebSocket Communications {#http.websocket-example} 57 | 58 | To request information from a WebSocket server, use the `websocket` module to create a new WebSocket client. 59 | 60 | ```lua 61 | local websocket = require "http.websocket" 62 | local ws = websocket.new_from_uri("wss://echo.websocket.org") 63 | assert(ws:connect()) 64 | assert(ws:send("koo-eee!")) 65 | local data = assert(ws:receive()) 66 | assert(data == "koo-eee!") 67 | assert(ws:close()) 68 | ``` 69 | 70 | 71 | ## Asynchronous Operation 72 | 73 | lua-http has been written to perform asynchronously so that it can be used in your application, server or game without blocking your main loop. Asynchronous operations are achieved by utilizing cqueues, a Lua/C library that incorporates Lua yielding and kernel level APIs to reduce CPU usage. All lua-http operations including DNS lookup, TLS negotiation and read/write operations will not block the main application thread when run from inside a cqueue or cqueue enabled "container". While sometimes it is necessary to block a routine (yield) and wait for external data, any blocking API calls take an optional timeout to ensure good behaviour of networked applications and avoid unresponsive or "dead" routines. 74 | 75 | Asynchronous operations are one of the most powerful features of lua-http and require no effort on the developers part. For instance, an HTTP server can be instantiated within any Lua main loop and run alongside application code without adversely affecting the main application process. If other cqueue enabled components are integrated within a cqueue loop, the application is entirely event driven through kernel level polling APIs. 76 | 77 | cqueues can be used in conjunction with lua-http to integrate other features into your lua application and create powerful, performant, web enabled applications. Some of the examples in this guide will use cqueues for simple demonstrations. For more resources about cqueues, please see: 78 | 79 | - [The cqueues website](http://25thandclement.com/~william/projects/cqueues.html) for more information about the cqueues library. 80 | - cqueues examples can be found with the cqueues source code available through [git or archives](http://www.25thandclement.com/~william/projects/cqueues.html#download) or accessed online [here](https://github.com/wahern/cqueues/tree/master/examples). 81 | - For more information on integrating cqueues with other event loop libraries please see [integration with other event loops](https://github.com/wahern/cqueues/wiki/Integrations-with-other-main-loops). 82 | - For other libraries that use cqueues such as asynchronous APIs for Redis and PostgreSQL, please see [the cqueues wiki entry here](https://github.com/wahern/cqueues/wiki/Libraries-that-use-cqueues). 83 | 84 | 85 | ## Conventions 86 | 87 | The following is a list of API conventions and general reference: 88 | 89 | ### HTTP 90 | 91 | - HTTP 1 request and status line fields are passed around inside of _[headers](#http.headers)_ objects under keys `":authority"`, `":method"`, `":path"`, `":scheme"` and `":status"` as defined in HTTP 2. As such, they are all kept in string form (important to remember for the `:status` field). 92 | - Header fields should always be used with lower case keys. 93 | 94 | 95 | ### Errors 96 | 97 | - Invalid function parameters will throw a lua error (if validated). 98 | - Errors are returned as `nil`, error, errno unless noted otherwise. 99 | - Some HTTP 2 operations return/throw special [http 2 error objects](#http.h2_error). 100 | 101 | 102 | ### Timeouts 103 | 104 | All operations that may block the current thread take a `timeout` argument. 105 | This argument is always the number of seconds to allow before returning `nil, err_msg, ETIMEDOUT` where `err_msg` is a localised error message such as `"connection timed out"`. 106 | 107 | 108 | ## Terminology 109 | 110 | Much lua-http terminology is borrowed from HTTP 2. 111 | 112 | _[Connection](#connection)_ - An abstraction over an underlying TCP/IP socket. lua-http currently has two connection types: one for HTTP 1, one for HTTP 2. 113 | 114 | _[Stream](#stream)_ - A request/response on a connection object. lua-http has two stream types: one for [*HTTP 1 streams*](#http.h1_stream), and one for [*HTTP 2 streams*](#http.h2_stream). The common interfaces is described in [*stream*](#stream). 115 | -------------------------------------------------------------------------------- /doc/links.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | - [Github](https://github.com/daurnimator/lua-http) 4 | - [Issue tracker](https://github.com/daurnimator/lua-http/issues) 5 | -------------------------------------------------------------------------------- /doc/metadata.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | title: lua-http 4 | subtitle: HTTP library for Lua 5 | author: Daurnimator 6 | ... 7 | -------------------------------------------------------------------------------- /doc/modules.md: -------------------------------------------------------------------------------- 1 | # Modules 2 | -------------------------------------------------------------------------------- /doc/modules/http.bit.md: -------------------------------------------------------------------------------- 1 | ## http.bit 2 | 3 | An abstraction layer over the various lua bit libraries. 4 | 5 | Results are only consistent between underlying implementations when parameters and results are in the range of `0` to `0x7fffffff`. 6 | 7 | ### `band(a, b)` {#http.bit.band} 8 | 9 | Bitwise And operation. 10 | 11 | 12 | ### `bor(a, b)` {#http.bit.bor} 13 | 14 | Bitwise Or operation. 15 | 16 | 17 | ### `bxor(a, b)` {#http.bit.bxor} 18 | 19 | Bitwise XOr operation. 20 | 21 | 22 | ### Example {#http.bit-example} 23 | 24 | ```lua 25 | local bit = require "http.bit" 26 | print(bit.band(1, 3)) --> 1 27 | ``` 28 | -------------------------------------------------------------------------------- /doc/modules/http.client.md: -------------------------------------------------------------------------------- 1 | ## http.client 2 | 3 | Deals with obtaining a connection to an HTTP server. 4 | 5 | ### `negotiate(socket, options, timeout)` {#http.client.negotiate} 6 | 7 | Negotiates the HTTP settings with the remote server. If TLS has been specified, this function instantiates the encryption tunnel. Parameters are as follows: 8 | 9 | - `socket` is a cqueues socket object 10 | - `options` is a table containing: 11 | - `.tls` (boolean, optional): Should TLS be used? 12 | defaults to `false` 13 | - `.ctx` (userdata, optional): the `SSL_CTX*` to use if `.tls` is `true`. 14 | If `.ctx` is `nil` then a default context will be used. 15 | - `.sendname` (string|boolean, optional): the [TLS SNI](https://en.wikipedia.org/wiki/Server_Name_Indication) host to send. 16 | defaults to `true` 17 | - `true` indicates to copy the `.host` field as long as it is **not** an IP 18 | - `false` disables SNI 19 | - `.version` (`nil`|1.0|1.1|2): HTTP version to use. 20 | - `nil`: attempts HTTP 2 and falls back to HTTP 1.1 21 | - `1.0` 22 | - `1.1` 23 | - `2` 24 | - `.h2_settings` (table, optional): HTTP 2 settings to use. See [*http.h2_connection*](#http.h2_connection) for details 25 | 26 | 27 | ### `connect(options, timeout)` {#http.client.connect} 28 | 29 | This function returns a new connection to an HTTP server. Once a connection has been opened, a stream can be created to start a request/response exchange. Please see [`h1_stream.new_stream`](#h1_stream.new_stream) and [`h2_stream.new_stream`](#h2_stream.new_stream) for more information about creating streams. 30 | 31 | - `options` is a table containing the options to [`http.client.negotiate`](#http.client.negotiate), plus the following: 32 | - `family` (integer, optional): socket family to use. 33 | defaults to `AF_INET` 34 | - `host` (string): host to connect to. 35 | may be either a hostname or an IP address 36 | - `port` (string|integer): port to connect to in numeric form 37 | e.g. `"80"` or `80` 38 | - `path` (string): path to connect to (UNIX sockets) 39 | - `v6only` (boolean, optional): if the `IPV6_V6ONLY` flag should be set on the underlying socket. 40 | - `bind` (string, optional): the local outgoing address and optionally port to bind in the form of `"address[:port]"`, IPv6 addresses may be specified via square bracket notation. e.g. `"127.0.0.1"`, `"127.0.0.1:50000"`, `"[::1]:30000"`. 41 | - `timeout` (optional) is the maximum amount of time (in seconds) to allow for connection to be established. 42 | This includes time for DNS lookup, connection, TLS negotiation (if TLS enabled) and in the case of HTTP 2: settings exchange. 43 | 44 | #### Example {#http.client.connect-example} 45 | 46 | Connect to a local HTTP server running on port 8000 47 | 48 | ```lua 49 | local http_client = require "http.client" 50 | local myconnection = http_client.connect { 51 | host = "localhost"; 52 | port = 8000; 53 | tls = false; 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /doc/modules/http.compat.prosody.md: -------------------------------------------------------------------------------- 1 | ## http.compat.prosody 2 | 3 | Provides usage similar to [prosody's net.http](https://prosody.im/doc/developers/net/http) 4 | 5 | ### `request(url, ex, callback)` {#http.compat.prosody.request} 6 | 7 | A few key differences to the prosody `net.http.request`: 8 | 9 | - must be called from within a running cqueue 10 | - The callback may be called from a different thread in the cqueue 11 | - The returned object will be a [*http.request*](#http.request) object 12 | - This object is passed to the callback on errors and as the fourth argument on success 13 | - The default user-agent will be from lua-http (rather than `"Prosody XMPP Server"`) 14 | - lua-http features (such as HTTP2) will be used where possible 15 | 16 | 17 | ### Example {#http.compat.prosody-example} 18 | 19 | ```lua 20 | local prosody_http = require "http.compat.prosody" 21 | local cqueues = require "cqueues" 22 | local cq = cqueues.new() 23 | cq:wrap(function() 24 | prosody_http.request("http://httpbin.org/ip", {}, function(b, c, r) 25 | print(c) --> 200 26 | print(b) --> {"origin": "123.123.123.123"} 27 | end) 28 | end) 29 | assert(cq:loop()) 30 | ``` 31 | -------------------------------------------------------------------------------- /doc/modules/http.compat.socket.md: -------------------------------------------------------------------------------- 1 | ## http.compat.socket 2 | 3 | Provides compatibility with [luasocket's http.request module](http://w3.impa.br/~diego/software/luasocket/http.html). 4 | 5 | Differences: 6 | 7 | - Will automatically be non-blocking when run inside a cqueues managed coroutine 8 | - lua-http features (such as HTTP2) will be used where possible 9 | 10 | 11 | ### Example {#http.compat.socket-example} 12 | 13 | Using the 'simple' interface as part of a normal script: 14 | 15 | ```lua 16 | local socket_http = require "http.compat.socket" 17 | local body, code = assert(socket_http.request("http://lua.org")) 18 | print(code, #body) --> 200, 2514 19 | ``` 20 | -------------------------------------------------------------------------------- /doc/modules/http.h1_reason_phrases.md: -------------------------------------------------------------------------------- 1 | ## http.h1_reason_phrases 2 | 3 | A table mapping from status codes (as strings) to reason phrases for HTTP 1. Any unknown status codes return `"Unassigned"` 4 | 5 | ### Example {#http.h1_reason_phrases-example} 6 | 7 | ```lua 8 | local reason_phrases = require "http.h1_reason_phrases" 9 | print(reason_phrases["200"]) --> "OK" 10 | print(reason_phrases["342"]) --> "Unassigned" 11 | ``` 12 | -------------------------------------------------------------------------------- /doc/modules/http.h1_stream.md: -------------------------------------------------------------------------------- 1 | ## http.h1_stream 2 | 3 | The *h1_stream* module adheres to the [*stream*](#stream) interface and provides HTTP 1.x specific operations. 4 | 5 | The gzip transfer encoding is supported transparently. 6 | 7 | ### `h1_stream.connection` {#http.h1_stream.connection} 8 | 9 | See [`stream.connection`](#stream.connection) 10 | 11 | 12 | ### `h1_stream.max_header_lines` {#http.h1_stream.max_header_lines} 13 | 14 | The maximum number of header lines to read. Default is `100`. 15 | 16 | 17 | ### `h1_stream:checktls()` {#http.h1_stream:checktls} 18 | 19 | See [`stream:checktls()`](#stream:checktls) 20 | 21 | 22 | ### `h1_stream:localname()` {#http.h1_stream:localname} 23 | 24 | See [`stream:localname()`](#stream:localname) 25 | 26 | 27 | ### `h1_stream:peername()` {#http.h1_stream:peername} 28 | 29 | See [`stream:peername()`](#stream:peername) 30 | 31 | 32 | ### `h1_stream:get_headers(timeout)` {#http.h1_stream:get_headers} 33 | 34 | See [`stream:get_headers(timeout)`](#stream:get_headers) 35 | 36 | 37 | ### `h1_stream:write_headers(headers, end_stream, timeout)` {#http.h1_stream:write_headers} 38 | 39 | See [`stream:write_headers(headers, end_stream, timeout)`](#stream:write_headers) 40 | 41 | 42 | ### `h1_stream:write_continue(timeout)` {#http.h1_stream:write_continue} 43 | 44 | See [`stream:write_continue(timeout)`](#stream:write_continue) 45 | 46 | 47 | ### `h1_stream:get_next_chunk(timeout)` {#http.h1_stream:get_next_chunk} 48 | 49 | See [`stream:get_next_chunk(timeout)`](#stream:get_next_chunk) 50 | 51 | 52 | ### `h1_stream:each_chunk()` {#http.h1_stream:each_chunk} 53 | 54 | See [`stream:each_chunk()`](#stream:each_chunk) 55 | 56 | 57 | ### `h1_stream:get_body_as_string(timeout)` {#http.h1_stream:get_body_as_string} 58 | 59 | See [`stream:get_body_as_string(timeout)`](#stream:get_body_as_string) 60 | 61 | 62 | ### `h1_stream:get_body_chars(n, timeout)` {#http.h1_stream:get_body_chars} 63 | 64 | See [`stream:get_body_chars(n, timeout)`](#stream:get_body_chars) 65 | 66 | 67 | ### `h1_stream:get_body_until(pattern, plain, include_pattern, timeout)` {#http.h1_stream:get_body_until} 68 | 69 | See [`stream:get_body_until(pattern, plain, include_pattern, timeout)`](#stream:get_body_until) 70 | 71 | 72 | ### `h1_stream:save_body_to_file(file, timeout)` {#http.h1_stream:save_body_to_file} 73 | 74 | See [`stream:save_body_to_file(file, timeout)`](#stream:save_body_to_file) 75 | 76 | 77 | ### `h1_stream:get_body_as_file(timeout)` {#http.h1_stream:get_body_as_file} 78 | 79 | See [`stream:get_body_as_file(timeout)`](#stream:get_body_as_file) 80 | 81 | 82 | ### `h1_stream:unget(str)` {#http.h1_stream:unget} 83 | 84 | See [`stream:unget(str)`](#stream:unget) 85 | 86 | 87 | ### `h1_stream:write_chunk(chunk, end_stream, timeout)` {#http.h1_stream:write_chunk} 88 | 89 | See [`stream:write_chunk(chunk, end_stream, timeout)`](#stream:write_chunk) 90 | 91 | 92 | ### `h1_stream:write_body_from_string(str, timeout)` {#http.h1_stream:write_body_from_string} 93 | 94 | See [`stream:write_body_from_string(str, timeout)`](#stream:write_body_from_string) 95 | 96 | 97 | ### `h1_stream:write_body_from_file(options|file, timeout)` {#http.h1_stream:write_body_from_file} 98 | 99 | See [`stream:write_body_from_file(options|file, timeout)`](#stream:write_body_from_file) 100 | 101 | 102 | ### `h1_stream:shutdown()` {#http.h1_stream:shutdown} 103 | 104 | See [`stream:shutdown()`](#stream:shutdown) 105 | 106 | 107 | ### `h1_stream:set_state(new)` {#http.h1_stream:set_state} 108 | 109 | Sets the state of the stream to `new`. `new` must be one of the following valid states: 110 | 111 | - `"open"`: have sent or received headers; haven't sent body yet 112 | - `"half closed (local)"`: have sent whole body 113 | - `"half closed (remote)"`: have received whole body 114 | - `"closed"`: complete 115 | 116 | Not all state transitions are allowed. 117 | 118 | 119 | ### `h1_stream:read_headers(timeout)` {#http.h1_stream:read_headers} 120 | 121 | Reads and returns a [header block](#http.headers) from the underlying connection. Does *not* take into account buffered header blocks. On error, returns `nil`, an error message and an error number. 122 | 123 | This function should rarely be used, you're probably looking for [`:get_headers()`](#http.h1_stream:get_headers). 124 | 125 | 126 | ### `h1_stream:read_next_chunk(timeout)` {#http.h1_stream:read_next_chunk} 127 | 128 | Reads and returns the next chunk as a string from the underlying connection. Does *not* take into account buffered chunks. On error, returns `nil`, an error message and an error number. 129 | 130 | This function should rarely be used, you're probably looking for [`:get_next_chunk()`](#http.h1_stream:get_next_chunk). 131 | -------------------------------------------------------------------------------- /doc/modules/http.h2_connection.md: -------------------------------------------------------------------------------- 1 | ## http.h2_connection 2 | 3 | The *h2_connection* module adheres to the [*connection*](#connection) interface and provides HTTP 2 specific operations. An HTTP 2 connection can have multiple streams actively transmitting data at once, 4 | hence an *http.h2_connection* acts much like a scheduler. 5 | 6 | ### `new(socket, conn_type, settings)` {#http.h2_connection.new} 7 | 8 | Constructor for a new connection. Takes a cqueues socket object, a [connection type string](#connection.type) and an optional table of HTTP 2 settings. Returns the newly initialized connection object in a non-connected state. 9 | 10 | 11 | ### `h2_connection.version` {#http.h2_connection.version} 12 | 13 | Contains the HTTP connection version. Currently this will always be `2`. 14 | 15 | See [`connection.version`](#connection.version) 16 | 17 | 18 | ### `h2_connection:pollfd()` {#http.h2_connection:pollfd} 19 | 20 | See [`connection:pollfd()`](#connection:pollfd) 21 | 22 | 23 | ### `h2_connection:events()` {#http.h2_connection:events} 24 | 25 | See [`connection:events()`](#connection:events) 26 | 27 | 28 | ### `h2_connection:timeout()` {#http.h2_connection:timeout} 29 | 30 | See [`connection:timeout()`](#connection:timeout) 31 | 32 | 33 | ### `h2_connection:empty()` {#http.h2_connection:empty} 34 | 35 | 36 | ### `h2_connection:step(timeout)` {#http.h2_connection:step} 37 | 38 | 39 | ### `h2_connection:loop(timeout)` {#http.h2_connection:loop} 40 | 41 | 42 | ### `h2_connection:connect(timeout)` {#http.h2_connection:connect} 43 | 44 | See [`connection:connect(timeout)`](#connection:connect) 45 | 46 | 47 | ### `h2_connection:checktls()` {#http.h2_connection:checktls} 48 | 49 | See [`connection:checktls()`](#connection:checktls) 50 | 51 | 52 | ### `h2_connection:localname()` {#http.h2_connection:localname} 53 | 54 | See [`connection:localname()`](#connection:localname) 55 | 56 | 57 | ### `h2_connection:peername()` {#http.h2_connection:peername} 58 | 59 | See [`connection:peername()`](#connection:peername) 60 | 61 | 62 | ### `h2_connection:flush(timeout)` {#http.h2_connection:flush} 63 | 64 | See [`connection:flush(timeout)`](#connection:flush) 65 | 66 | 67 | ### `h2_connection:shutdown()` {#http.h2_connection:shutdown} 68 | 69 | See [`connection:shutdown()`](#connection:shutdown) 70 | 71 | 72 | ### `h2_connection:close()` {#http.h2_connection:close} 73 | 74 | See [`connection:close()`](#connection:close) 75 | 76 | 77 | ### `h2_connection:new_stream(id)` {#http.h2_connection:new_stream} 78 | 79 | Create and return a new [*h2_stream*](#http.h2_stream). 80 | `id` (optional) is the stream id to assign the new stream, if not specified for client initiated streams this will be the next free odd numbered stream, for server initiated streams this will be the next free even numbered stream. 81 | 82 | See [`connection:new_stream()`](#connection:new_stream) for more information. 83 | 84 | 85 | ### `h2_connection:get_next_incoming_stream(timeout)` {#http.h2_connection:get_next_incoming_stream} 86 | 87 | See [`connection:get_next_incoming_stream(timeout)`](#connection:get_next_incoming_stream) 88 | 89 | 90 | ### `h2_connection:onidle(new_handler)` {#http.h2_connection:onidle} 91 | 92 | See [`connection:onidle(new_handler)`](#connection:onidle) 93 | 94 | 95 | ### `h2_connection:read_http2_frame(timeout)` {#http.h2_connection:read_http2_frame} 96 | 97 | 98 | ### `h2_connection:write_http2_frame(typ, flags, streamid, payload, timeout, flush)` {#http.h2_connection:write_http2_frame} 99 | 100 | 101 | ### `h2_connection:ping(timeout)` {#http.h2_connection:ping} 102 | 103 | 104 | ### `h2_connection:write_window_update(inc, timeout)` {#http.h2_connection:write_window_update} 105 | 106 | 107 | ### `h2_connection:write_goaway_frame(last_stream_id, err_code, debug_msg, timeout)` {#http.h2_connection:write_goaway_frame} 108 | 109 | 110 | ### `h2_connection:set_peer_settings(peer_settings)` {#http.h2_connection:set_peer_settings} 111 | 112 | 113 | ### `h2_connection:ack_settings()` {#http.h2_connection:ack_settings} 114 | 115 | 116 | ### `h2_connection:settings(tbl, timeout)` {#http.h2_connection:settings} 117 | -------------------------------------------------------------------------------- /doc/modules/http.h2_error.md: -------------------------------------------------------------------------------- 1 | ## http.h2_error 2 | 3 | A type of error object that encapsulates HTTP 2 error information. 4 | An `http.h2_error` object has fields: 5 | 6 | - `name`: The error name: a short identifier for this error 7 | - `code`: The error code 8 | - `description`: The description of the error code 9 | - `message`: An error message 10 | - `traceback`: A traceback taken at the point the error was thrown 11 | - `stream_error`: A boolean that indicates if this is a stream level or protocol level error 12 | 13 | ### `errors` {#http.h2_error.errors} 14 | 15 | A table containing errors [as defined by the HTTP 2 specification](https://http2.github.io/http2-spec/#iana-errors). 16 | It can be indexed by error name (e.g. `errors.PROTOCOL_ERROR`) or numeric code (e.g. `errors[0x1]`). 17 | 18 | 19 | ### `is(ob)` {#http.h2_error.is} 20 | 21 | Returns a boolean indicating if the object `ob` is an `http.h2_error` object 22 | 23 | 24 | ### `h2_error:new(ob)` {#http.h2_error:new} 25 | 26 | Creates a new error object from the passed table. 27 | The table should have the form of an error object i.e. with fields `name`, `code`, `message`, `traceback`, etc. 28 | 29 | Fields `name`, `code` and `description` are inherited from the parent `h2_error` object if not specified. 30 | 31 | `stream_error` defaults to `false`. 32 | 33 | 34 | ### `h2_error:new_traceback(message, stream_error, lvl)` {#http.h2_error:new_traceback} 35 | 36 | Creates a new error object, recording a traceback from the current thread. 37 | 38 | 39 | ### `h2_error:error(message, stream_error, lvl)` {#http.h2_error:error} 40 | 41 | Creates and throws a new error. 42 | 43 | 44 | ### `h2_error:assert(cond, ...)` {#http.h2_error:assert} 45 | 46 | If `cond` is truthy, returns `cond, ...` 47 | 48 | If `cond` is falsy (i.e. `false` or `nil`), throws an error with the first element of `...` as the `message`. 49 | -------------------------------------------------------------------------------- /doc/modules/http.h2_stream.md: -------------------------------------------------------------------------------- 1 | ## http.h2_stream 2 | 3 | An h2_stream represents an HTTP 2 stream. The module follows the [*stream*](#stream) interface as well as HTTP 2 specific functions. 4 | 5 | ### `h2_stream.connection` {#http.h2_stream.connection} 6 | 7 | See [`stream.connection`](#stream.connection) 8 | 9 | 10 | ### `h2_stream:checktls()` {#http.h2_stream:checktls} 11 | 12 | See [`stream:checktls()`](#stream:checktls) 13 | 14 | 15 | ### `h2_stream:localname()` {#http.h2_stream:localname} 16 | 17 | See [`stream:localname()`](#stream:localname) 18 | 19 | 20 | ### `h2_stream:peername()` {#http.h2_stream:peername} 21 | 22 | See [`stream:peername()`](#stream:peername) 23 | 24 | 25 | ### `h2_stream:get_headers(timeout)` {#http.h2_stream:get_headers} 26 | 27 | See [`stream:get_headers(timeout)`](#stream:get_headers) 28 | 29 | 30 | ### `h2_stream:write_headers(headers, end_stream, timeout)` {#http.h2_stream:write_headers} 31 | 32 | See [`stream:write_headers(headers, end_stream, timeout)`](#stream:write_headers) 33 | 34 | 35 | ### `h2_stream:write_continue(timeout)` {#http.h2_stream:write_continue} 36 | 37 | See [`stream:write_continue(timeout)`](#stream:write_continue) 38 | 39 | 40 | ### `h2_stream:get_next_chunk(timeout)` {#http.h2_stream:get_next_chunk} 41 | 42 | See [`stream:get_next_chunk(timeout)`](#stream:get_next_chunk) 43 | 44 | 45 | ### `h2_stream:each_chunk()` {#http.h2_stream:each_chunk} 46 | 47 | See [`stream:each_chunk()`](#stream:each_chunk) 48 | 49 | 50 | ### `h2_stream:get_body_as_string(timeout)` {#http.h2_stream:get_body_as_string} 51 | 52 | See [`stream:get_body_as_string(timeout)`](#stream:get_body_as_string) 53 | 54 | 55 | ### `h2_stream:get_body_chars(n, timeout)` {#http.h2_stream:get_body_chars} 56 | 57 | See [`stream:get_body_chars(n, timeout)`](#stream:get_body_chars) 58 | 59 | 60 | ### `h2_stream:get_body_until(pattern, plain, include_pattern, timeout)` {#http.h2_stream:get_body_until} 61 | 62 | See [`stream:get_body_until(pattern, plain, include_pattern, timeout)`](#stream:get_body_until) 63 | 64 | 65 | ### `h2_stream:save_body_to_file(file, timeout)` {#http.h2_stream:save_body_to_file} 66 | 67 | See [`stream:save_body_to_file(file, timeout)`](#stream:save_body_to_file) 68 | 69 | 70 | ### `h2_stream:get_body_as_file(timeout)` {#http.h2_stream:get_body_as_file} 71 | 72 | See [`stream:get_body_as_file(timeout)`](#stream:get_body_as_file) 73 | 74 | 75 | ### `h2_stream:unget(str)` {#http.h2_stream:unget} 76 | 77 | See [`stream:unget(str)`](#stream:unget) 78 | 79 | 80 | ### `h2_stream:write_chunk(chunk, end_stream, timeout)` {#http.h2_stream:write_chunk} 81 | 82 | See [`stream:write_chunk(chunk, end_stream, timeout)`](#stream:write_chunk) 83 | 84 | 85 | ### `h2_stream:write_body_from_string(str, timeout)` {#http.h2_stream:write_body_from_string} 86 | 87 | See [`stream:write_body_from_string(str, timeout)`](#stream:write_body_from_string) 88 | 89 | 90 | ### `h2_stream:write_body_from_file(options|file, timeout)` {#http.h2_stream:write_body_from_file} 91 | 92 | See [`stream:write_body_from_file(options|file, timeout)`](#stream:write_body_from_file) 93 | 94 | 95 | ### `h2_stream:shutdown()` {#http.h2_stream:shutdown} 96 | 97 | See [`stream:shutdown()`](#stream:shutdown) 98 | 99 | 100 | ### `h2_stream:pick_id(id)` {#http.h2_stream:pick_id} 101 | 102 | 103 | ### `h2_stream:set_state(new)` {#http.h2_stream:set_state} 104 | 105 | 106 | ### `h2_stream:reprioritise(child, exclusive)` {#http.h2_stream:reprioritise} 107 | 108 | 109 | ### `h2_stream:write_http2_frame(typ, flags, payload, timeout, flush)` {#http.h2_stream:write_http2_frame} 110 | 111 | Writes a frame with `h2_stream`'s stream id. 112 | 113 | See [`h2_connection:write_http2_frame(typ, flags, streamid, payload, timeout, flush)`](#http.h2_connection:write_http2_frame) 114 | 115 | 116 | ### `h2_stream:write_data_frame(payload, end_stream, padded, timeout, flush)` {#http.h2_stream:write_data_frame} 117 | 118 | 119 | ### `h2_stream:write_headers_frame(payload, end_stream, end_headers, padded, exclusive, stream_dep, weight, timeout, flush)` {#http.h2_stream:write_headers_frame} 120 | 121 | 122 | ### `h2_stream:write_priority_frame(exclusive, stream_dep, weight, timeout, flush)` {#http.h2_stream:write_priority_frame} 123 | 124 | 125 | ### `h2_stream:write_rst_stream_frame(err_code, timeout, flush)` {#http.h2_stream:write_rst_stream} 126 | 127 | 128 | ### `h2_stream:rst_stream(err, timeout)` {#http.h2_stream:rst_stream} 129 | 130 | 131 | ### `h2_stream:write_settings_frame(ACK, settings, timeout, flush)` {#http.h2_stream:write_settings_frame} 132 | 133 | 134 | ### `h2_stream:write_push_promise_frame(promised_stream_id, payload, end_headers, padded, timeout, flush)` {#http.h2_stream:write_push_promise_frame} 135 | 136 | 137 | ### `h2_stream:push_promise(headers, timeout)` {#http.h2_stream:push_promise} 138 | 139 | Pushes a new promise to the client. 140 | 141 | Returns the new stream as a [h2_stream](#http.h2_stream). 142 | 143 | 144 | ### `h2_stream:write_ping_frame(ACK, payload, timeout, flush)` {#http.h2_stream:write_ping_frame} 145 | 146 | 147 | ### `h2_stream:write_goaway_frame(last_streamid, err_code, debug_msg, timeout, flush)` {#http.h2_stream:write_goaway_frame} 148 | 149 | 150 | ### `h2_stream:write_window_update_frame(inc, timeout, flush)` {#http.h2_stream:write_window_update_frame} 151 | 152 | 153 | ### `h2_stream:write_window_update(inc, timeout)` {#http.h2_stream:write_window_update} 154 | 155 | 156 | ### `h2_stream:write_continuation_frame(payload, end_headers, timeout, flush)` {#http.h2_stream:write_continuation_frame} 157 | -------------------------------------------------------------------------------- /doc/modules/http.headers.md: -------------------------------------------------------------------------------- 1 | ## http.headers 2 | 3 | An ordered list of header fields. 4 | Each field has a *name*, a *value* and a *never_index* flag that indicates if the header field is potentially sensitive data. 5 | 6 | Each headers object has an index by field name to efficiently retrieve values by key. Keep in mind that there can be multiple values for a given field name. (e.g. an HTTP server may send two `Set-Cookie` headers). 7 | 8 | As noted in the [Conventions](#conventions) section, HTTP 1 request and status line fields are passed around inside of headers objects under keys `":authority"`, `":method"`, `":path"`, `":scheme"` and `":status"` as defined in HTTP 2. As such, they are all kept in string form (important to remember for the `":status"` field). 9 | 10 | ### `new()` {#http.headers.new} 11 | 12 | Creates and returns a new headers object. 13 | 14 | 15 | ### `headers:len()` {#http.headers:len} 16 | 17 | Returns the number of headers. 18 | 19 | Also available as `#headers` in Lua 5.2+. 20 | 21 | 22 | ### `headers:clone()` {#http.headers:clone} 23 | 24 | Creates and returns a clone of the headers object. 25 | 26 | 27 | ### `headers:append(name, value, never_index)` {#http.headers:append} 28 | 29 | Append a header. 30 | 31 | - `name` is the header field name. Lower case is the convention. It will not be validated at this time. 32 | - `value` is the header field value. It will not be validated at this time. 33 | - `never_index` is an optional boolean that indicates if the `value` should be considered secret. Defaults to true for header fields: authorization, proxy-authorization, cookie and set-cookie. 34 | 35 | 36 | ### `headers:each()` {#http.headers:each} 37 | 38 | An iterator over all headers that emits `name, value, never_index`. 39 | 40 | #### Example 41 | 42 | ```lua 43 | local http_headers = require "http.headers" 44 | local myheaders = http_headers.new() 45 | myheaders:append(":status", "200") 46 | myheaders:append("set-cookie", "foo=bar") 47 | myheaders:append("connection", "close") 48 | myheaders:append("set-cookie", "baz=qux") 49 | for name, value, never_index in myheaders:each() do 50 | print(name, value, never_index) 51 | end 52 | --[[ prints: 53 | ":status", "200", false 54 | "set-cookie", "foo=bar", true 55 | "connection", "close", false 56 | "set-cookie", "baz=qux", true 57 | ]] 58 | ``` 59 | 60 | 61 | ### `headers:has(name)` {#http.headers:has} 62 | 63 | Returns a boolean indicating if the headers object has a field with the given `name`. 64 | 65 | 66 | ### `headers:delete(name)` {#http.headers:delete} 67 | 68 | Removes all occurrences of a field name from the headers object. 69 | 70 | 71 | ### `headers:geti(i)` {#http.headers:geti} 72 | 73 | Return the `i`-th header as `name, value, never_index` 74 | 75 | 76 | ### `headers:get_as_sequence(name)` {#http.headers:get_as_sequence} 77 | 78 | Returns all headers with the given name in a table. The table will contain a field `.n` with the number of elements. 79 | 80 | #### Example 81 | 82 | ```lua 83 | local http_headers = require "http.headers" 84 | local myheaders = http_headers.new() 85 | myheaders:append(":status", "200") 86 | myheaders:append("set-cookie", "foo=bar") 87 | myheaders:append("connection", "close") 88 | myheaders:append("set-cookie", "baz=qux") 89 | local mysequence = myheaders:get_as_sequence("set-cookie") 90 | --[[ mysequence will be: 91 | {n = 2; "foo=bar"; "baz=qux"} 92 | ]] 93 | ``` 94 | 95 | 96 | ### `headers:get(name)` {#http.headers:get} 97 | 98 | Returns all headers with the given name as multiple return values. 99 | 100 | 101 | ### `headers:get_comma_separated(name)` {#http.headers:get_comma_separated} 102 | 103 | Returns all headers with the given name as items in a comma separated string. 104 | 105 | 106 | ### `headers:modifyi(i, value, never_index)` {#http.headers:modifyi} 107 | 108 | Change the `i`-th's header to a new `value` and `never_index`. 109 | 110 | 111 | ### `headers:upsert(name, value, never_index)` {#http.headers:upsert} 112 | 113 | If a header with the given `name` already exists, replace it. If not, [`append`](#http.headers:append) it to the list of headers. 114 | 115 | Cannot be used when a header `name` already has multiple values. 116 | 117 | 118 | ### `headers:sort()` {#http.headers:sort} 119 | 120 | Sort the list of headers by their field name, ordering those starting with `:` first. If `name`s are equal then sort by `value`, then by `never_index`. 121 | 122 | 123 | ### `headers:dump(file, prefix)` {#http.headers:dump} 124 | 125 | Print the headers list to the given file, one per line. 126 | If `file` is not given, then print to `stderr`. 127 | `prefix` is prefixed to each line. 128 | -------------------------------------------------------------------------------- /doc/modules/http.hpack.md: -------------------------------------------------------------------------------- 1 | ## http.hpack 2 | 3 | ### `new(SETTINGS_HEADER_TABLE_SIZE)` {#http.hpack.new} 4 | 5 | 6 | ### `hpack_context:append_data(val)` {#http.hpack:append_data} 7 | 8 | 9 | ### `hpack_context:render_data()` {#http.hpack:render_data} 10 | 11 | 12 | ### `hpack_context:clear_data()` {#http.hpack:clear_data} 13 | 14 | 15 | ### `hpack_context:evict_from_dynamic_table()` {#http.hpack:evict_from_dynamic_table} 16 | 17 | 18 | ### `hpack_context:dynamic_table_tostring()` {#http.hpack:dynamic_table_tostring} 19 | 20 | 21 | ### `hpack_context:set_max_dynamic_table_size(SETTINGS_HEADER_TABLE_SIZE)` {#http.hpack:set_max_dynamic_table_size} 22 | 23 | 24 | ### `hpack_context:encode_max_size(val)` {#http.hpack:encode_max_size} 25 | 26 | 27 | ### `hpack_context:resize_dynamic_table(new_size)` {#http.hpack:resize_dynamic_table} 28 | 29 | 30 | ### `hpack_context:add_to_dynamic_table(name, value, k)` {#http.hpack:add_to_dynamic_table} 31 | 32 | 33 | ### `hpack_context:dynamic_table_id_to_index(id)` {#http.hpack:dynamic_table_id_to_index} 34 | 35 | 36 | ### `hpack_context:lookup_pair_index(k)` {#http.hpack:lookup_pair_index} 37 | 38 | 39 | ### `hpack_context:lookup_name_index(name)` {#http.hpack:lookup_name_index} 40 | 41 | 42 | ### `hpack_context:lookup_index(index)` {#http.hpack:lookup_index} 43 | 44 | 45 | ### `hpack_context:add_header_indexed(name, value, huffman)` {#http.hpack:add_header_indexed} 46 | 47 | 48 | ### `hpack_context:add_header_never_indexed(name, value, huffman)` {#http.hpack:add_header_never_indexed} 49 | 50 | 51 | ### `hpack_context:encode_headers(headers)` {#http.hpack:encode_headers} 52 | 53 | 54 | ### `hpack_context:decode_headers(payload, header_list, pos)` {#http.hpack:decode_headers} 55 | -------------------------------------------------------------------------------- /doc/modules/http.hsts.md: -------------------------------------------------------------------------------- 1 | ## http.hsts 2 | 3 | Data structures useful for HSTS (HTTP Strict Transport Security) 4 | 5 | ### `new_store()` {#http.hsts.new_store} 6 | 7 | Creates and returns a new HSTS store. 8 | 9 | 10 | ### `hsts_store.max_items` {#http.hsts.max_items} 11 | 12 | The maximum number of items allowed in the store. 13 | Decreasing this value will only prevent new items from being added, it will not remove old items. 14 | 15 | Defaults to infinity (any number of items is allowed). 16 | 17 | 18 | ### `hsts_store:clone()` {#http.hsts:clone} 19 | 20 | Creates and returns a copy of a store. 21 | 22 | 23 | ### `hsts_store:store(host, directives)` {#http.hsts:store} 24 | 25 | Add new directives to the store about the given `host`. `directives` should be a table of directives, which *must* include the key `"max-age"`. 26 | 27 | Returns a boolean indicating if the item was accepted. 28 | 29 | 30 | ### `hsts_store:remove(host)` {#http.hsts:remove} 31 | 32 | Removes the entry for `host` from the store (if it exists). 33 | 34 | 35 | ### `hsts_store:check(host)` {#http.hsts:check} 36 | 37 | Returns a boolean indicating if the given `host` is a known HSTS host. 38 | 39 | 40 | ### `hsts_store:clean_due()` {#http.hsts:clean_due} 41 | 42 | Returns the number of seconds until the next item in the store expires. 43 | 44 | 45 | ### `hsts_store:clean()` {#http.hsts:clean} 46 | 47 | Removes expired entries from the store. 48 | -------------------------------------------------------------------------------- /doc/modules/http.proxies.md: -------------------------------------------------------------------------------- 1 | ## http.proxies 2 | 3 | ### `new()` {#http.proxies.new} 4 | 5 | Returns an empty 'proxies' object 6 | 7 | 8 | ### `proxies:update(getenv)` {#http.proxies:update} 9 | 10 | `getenv` defaults to [`os.getenv`](http://www.lua.org/manual/5.4/manual.html#pdf-os.getenv) 11 | 12 | Reads environmental variables that are used to control if requests go through a proxy. 13 | 14 | - `http_proxy` (or `CGI_HTTP_PROXY` if running in a program with `GATEWAY_INTERFACE` set): the proxy to use for normal HTTP connections 15 | - `https_proxy` or `HTTPS_PROXY`: the proxy to use for HTTPS connections 16 | - `all_proxy` or `ALL_PROXY`: the proxy to use for **all** connections, overridden by other options 17 | - `no_proxy` or `NO_PROXY`: a list of hosts to **not** use a proxy for 18 | 19 | Returns `proxies`. 20 | 21 | 22 | ### `proxies:choose(scheme, host)` {#http.proxies:choose} 23 | 24 | Returns the proxy to use for the given `scheme` and `host` as a URI. 25 | -------------------------------------------------------------------------------- /doc/modules/http.request.md: -------------------------------------------------------------------------------- 1 | ## http.request 2 | 3 | The http.request module encapsulates all the functionality required to retrieve an HTTP document from a server. 4 | 5 | ### `new_from_uri(uri)` {#http.request.new_from_uri} 6 | 7 | Creates a new `http.request` object from the given URI. 8 | 9 | 10 | ### `new_connect(uri, connect_authority)` {#http.request.new_connect} 11 | 12 | Creates a new `http.request` object from the given URI that will perform a *CONNECT* request. 13 | 14 | 15 | ### `request.host` {#http.request.host} 16 | 17 | The host this request should be sent to. 18 | 19 | 20 | ### `request.port` {#http.request.port} 21 | 22 | The port this request should be sent to. 23 | 24 | 25 | ### `request.bind` {#http.request.bind} 26 | 27 | The local outgoing address and optionally port to bind in the form of `"address[:port]"`. Default is to allow the kernel to choose an address+port. 28 | 29 | IPv6 addresses may be specified via square bracket notation. e.g. `"127.0.0.1"`, `"127.0.0.1:50000"`, `"[::1]:30000"`. 30 | 31 | This option is rarely needed. Supplying an address can be used to manually select the network interface to make the request from, while supplying a port is only really used to interoperate with firewalls or devices that demand use of a certain port. 32 | 33 | 34 | ### `request.tls` {#http.request.tls} 35 | 36 | A boolean indicating if TLS should be used. 37 | 38 | 39 | ### `request.ctx` {#http.request.ctx} 40 | 41 | An alternative `SSL_CTX*` to use. 42 | If not specified, uses the default TLS settings (see [*http.tls*](#http.tls) for information). 43 | 44 | 45 | ### `request.sendname` {#http.request.sendname} 46 | 47 | The TLS SNI host name used. 48 | 49 | 50 | ### `request.version` {#http.request.version} 51 | 52 | The HTTP version to use; leave as `nil` to auto-select. 53 | 54 | 55 | ### `request.proxy` {#http.request.proxy} 56 | 57 | Specifies the a proxy that the request will be made through. 58 | The value should be a URI or `false` to turn off proxying for the request. 59 | 60 | 61 | ### `request.headers` {#http.request.headers} 62 | 63 | A [*http.headers*](#http.headers) object of headers that will be sent in the request. 64 | 65 | 66 | ### `request.hsts` {#http.request.hsts} 67 | 68 | The [*http.hsts*](#http.hsts) store that will be used to enforce HTTP strict transport security. 69 | An attempt will be made to add strict transport headers from a response to the store. 70 | 71 | Defaults to a shared store. 72 | 73 | 74 | ### `request.proxies` {#http.request.proxies} 75 | 76 | The [*http.proxies*](#http.proxies) object used to select a proxy for the request. 77 | Only consulted if `request.proxy` is `nil`. 78 | 79 | 80 | ### `request.cookie_store` {#http.request.cookie_store} 81 | 82 | The [*http.cookie.store*](#http.cookie.store) that will be used to find cookies for the request. 83 | An attempt will be made to add cookies from a response to the store. 84 | 85 | Defaults to a shared store. 86 | 87 | 88 | ### `request.is_top_level` {#http.request.is_top_level} 89 | 90 | A boolean flag indicating if this request is a "top level" request (See [RFC 6265bis-02 Section 5.2](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-02#section-5.2)) 91 | 92 | Defaults to `true` 93 | 94 | 95 | ### `request.site_for_cookies` {#http.request.site_for_cookies} 96 | 97 | A string containing the host that should be considered as the "site for cookies" (See [RFC 6265bis-02 Section 5.2](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-02#section-5.2)), can be `nil` if unknown. 98 | 99 | Defaults to `nil`. 100 | 101 | 102 | ### `request.follow_redirects` {#http.request.follow_redirects} 103 | 104 | Boolean indicating if `:go()` should follow redirects. 105 | Defaults to `true`. 106 | 107 | 108 | ### `request.expect_100_timeout` {#http.request.expect_100_timeout} 109 | 110 | Number of seconds to wait for a 100 Continue response before proceeding to send a request body. 111 | Defaults to `1`. 112 | 113 | 114 | ### `request.max_redirects` {#http.request.max_redirects} 115 | 116 | Maximum number of redirects to follow before giving up. 117 | Defaults to `5`. 118 | Set to `math.huge` to not give up. 119 | 120 | 121 | ### `request.post301` {#http.request.post301} 122 | 123 | Respect RFC 2616 Section 10.3.2 and **don't** convert POST requests into body-less GET requests when following a 301 redirect. The non-RFC behaviour is ubiquitous in web browsers and assumed by servers. Modern HTTP endpoints send status code 308 to indicate that they don't want the method to be changed. 124 | Defaults to `false`. 125 | 126 | 127 | ### `request.post302` {#http.request.post302} 128 | 129 | Respect RFC 2616 Section 10.3.3 and **don't** convert POST requests into body-less GET requests when following a 302 redirect. The non-RFC behaviour is ubiquitous in web browsers and assumed by servers. Modern HTTP endpoints send status code 307 to indicate that they don't want the method to be changed. 130 | Defaults to `false`. 131 | 132 | 133 | ### `request:clone()` {#http.request:clone} 134 | 135 | Creates and returns a clone of the request. 136 | 137 | The clone has its own deep copies of the [`.headers`](#http.request.headers) and [`.h2_settings`](#http.request.h2_settings) fields. 138 | 139 | The [`.tls`](#http.request.tls) and [`.body`](#http.request.body) fields are shallow copied from the original request. 140 | 141 | 142 | ### `request:handle_redirect(headers)` {#http.request:handle_redirect} 143 | 144 | Process a redirect. 145 | 146 | `headers` should be response headers for a redirect. 147 | 148 | Returns a new `request` object that will fetch from new location. 149 | 150 | 151 | ### `request:to_uri(with_userinfo)` {#http.request:to_uri} 152 | 153 | Returns a URI for the request. 154 | 155 | If `with_userinfo` is `true` and the request has an `authorization` header (or `proxy-authorization` for a CONNECT request), the returned URI will contain a userinfo component. 156 | 157 | 158 | ### `request:set_body(body)` {#http.request:set_body} 159 | 160 | Allows setting a request body. `body` may be a string, function or lua file object. 161 | 162 | - If `body` is a string it will be sent as given. 163 | - If `body` is a function, it will be called repeatedly like an iterator. It should return chunks of the request body as a string or `nil` if done. 164 | - If `body` is a lua file object, it will be [`:seek`'d](http://www.lua.org/manual/5.4/manual.html#pdf-file:seek) to the start, then sent as a body. Any errors encountered during file operations **will be thrown**. 165 | 166 | 167 | ### `request:go(timeout)` {#http.request:timeout} 168 | 169 | Performs the request. 170 | 171 | The request object is **not** invalidated; and can be reused for a new request. 172 | On success, returns the response [*headers*](#http.headers) and a [*stream*](#stream). 173 | -------------------------------------------------------------------------------- /doc/modules/http.socks.md: -------------------------------------------------------------------------------- 1 | ## http.socks 2 | 3 | Implements a subset of the SOCKS proxy protocol. 4 | 5 | ### `connect(uri)` {#http.socks.connect} 6 | 7 | `uri` is a string with the address of the SOCKS server. A scheme of `"socks5"` will resolve hosts locally, a scheme of `"socks5h"` will resolve hosts on the SOCKS server. If the URI has a userinfo component it will be sent to the SOCKS server as a username and password. 8 | 9 | Returns a *http.socks* object. 10 | 11 | 12 | ### `fdopen(socket)` {#http.socks.fdopen} 13 | 14 | This function takes an existing cqueues.socket as a parameter and returns a *http.socks* object with `socket` as its base. 15 | 16 | 17 | ### `socks.needs_resolve` {#http.socks.needs_resolve} 18 | 19 | Specifies if the destination host should be resolved locally. 20 | 21 | 22 | ### `socks:clone()` {#http.socks:clone} 23 | 24 | Make a clone of a given socks object. 25 | 26 | 27 | ### `socks:add_username_password_auth(username, password)` {#http.socks:add_username_password_auth} 28 | 29 | Add username + password authorisation to the set of allowed authorisation methods with the given credentials. 30 | 31 | 32 | ### `socks:negotiate(host, port, timeout)` {#http.socks:negotiate} 33 | 34 | Complete the SOCKS connection. 35 | 36 | Negotiates a socks connection. `host` is a required string passed to the SOCKS server as the host address. The address will be resolved locally if [`.needs_resolve`](#http.socks.needs_resolve) is `true`. `port` is a required number to pass to the SOCKS server as the connection port. On error, returns `nil`, an error message and an error number. 37 | 38 | 39 | ### `socks:close()` {#http.socks:close} 40 | 41 | 42 | ### `socks:take_socket()` {#http.socks:take_socket} 43 | 44 | Take possession of the socket object managed by the http.socks object. Returns the socket (or `nil` if not available). 45 | -------------------------------------------------------------------------------- /doc/modules/http.tls.md: -------------------------------------------------------------------------------- 1 | ## http.tls 2 | 3 | ### `has_alpn` {#http.tls.has_alpn} 4 | 5 | Boolean indicating if ALPN is available in the current environment. 6 | 7 | It may be disabled if OpenSSL was compiled without ALPN support, or is an old version. 8 | 9 | 10 | ### `has_hostname_validation` {#http.tls.has_hostname_validation} 11 | 12 | Boolean indicating if [hostname validation](https://wiki.openssl.org/index.php/Hostname_validation) is available in the current environment. 13 | 14 | It may be disabled if OpenSSL is an old version. 15 | 16 | 17 | ### `modern_cipher_list` {#http.tls.modern_cipher_list} 18 | 19 | The [Mozilla "Modern" cipher list](https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility) as a colon separated list, ready to pass to OpenSSL 20 | 21 | 22 | ### `intermediate_cipher_list` {#http.tls.intermediate_cipher_list} 23 | 24 | The [Mozilla "Intermediate" cipher list](https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29) as a colon separated list, ready to pass to OpenSSL 25 | 26 | 27 | ### `old_cipher_list` {#http.tls.old_cipher_list} 28 | 29 | The [Mozilla "Old" cipher list](https://wiki.mozilla.org/Security/Server_Side_TLS#Old_backward_compatibility) as a colon separated list, ready to pass to OpenSSL 30 | 31 | 32 | ### `banned_ciphers` {#http.tls.banned_ciphers} 33 | 34 | A set (table with string keys and values of `true`) of the [ciphers banned in HTTP 2](https://http2.github.io/http2-spec/#BadCipherSuites) where the keys are OpenSSL cipher names. 35 | 36 | Ciphers not known by OpenSSL are missing from the set. 37 | 38 | 39 | ### `new_client_context()` {#http.tls.new_client_context} 40 | 41 | Create and return a new luaossl SSL context useful for HTTP client connections. 42 | 43 | 44 | ### `new_server_context()` {#http.tls.new_server_context} 45 | 46 | Create and return a new luaossl SSL context useful for HTTP server connections. 47 | -------------------------------------------------------------------------------- /doc/modules/http.util.md: -------------------------------------------------------------------------------- 1 | ## http.util 2 | 3 | ### `encodeURI(str)` {#http.util.encodeURI} 4 | 5 | 6 | ### `encodeURIComponent(str)` {#http.util.encodeURIComponent} 7 | 8 | 9 | ### `decodeURI(str)` {#http.util.decodeURI} 10 | 11 | 12 | ### `decodeURIComponent(str)` {#http.util.decodeURIComponent} 13 | 14 | 15 | ### `query_args(str)` {#http.util.query_args} 16 | 17 | Returns an iterator over the pairs in `str` 18 | 19 | #### Example 20 | 21 | ```lua 22 | local http_util = require "http.util" 23 | for name, value in http_util.query_args("foo=bar&baz=qux") do 24 | print(name, value) 25 | end 26 | --[[ prints: 27 | "foo", "bar" 28 | "baz", "qux" 29 | ]] 30 | ``` 31 | 32 | 33 | ### `dict_to_query(dict)` {#http.util.dict_to_query} 34 | 35 | Converts a dictionary (table with string keys) with string values to an encoded query string. 36 | 37 | #### Example 38 | 39 | ```lua 40 | local http_util = require "http.util" 41 | print(http_util.dict_to_query({foo = "bar"; baz = "qux"})) --> "baz=qux&foo=bar" 42 | ``` 43 | 44 | 45 | ### `resolve_relative_path(orig_path, relative_path)` {#http.util.resolve_relative_path} 46 | 47 | 48 | ### `is_safe_method(method)` {#http.util.is_safe_method} 49 | 50 | Returns a boolean indicating if the passed string `method` is a "safe" method. 51 | See [RFC 7231 section 4.2.1](https://tools.ietf.org/html/rfc7231#section-4.2.1) for more information. 52 | 53 | 54 | ### `is_ip(str)` {#http.util.is_ip} 55 | 56 | Returns a boolean indicating if the passed string `str` is a valid IP. 57 | 58 | 59 | ### `scheme_to_port` {#http.util.scheme_to_port} 60 | 61 | Map from schemes (as strings) to default ports (as integers). 62 | 63 | 64 | ### `split_authority(authority, scheme)` {#http.util.split_authority} 65 | 66 | Splits an `authority` into host and port components. 67 | If the authority has no port component, will attempt to use the default for the `scheme`. 68 | 69 | #### Example 70 | 71 | ```lua 72 | local http_util = require "http.util" 73 | print(http_util.split_authority("localhost:8000", "http")) --> "localhost", 8000 74 | print(http_util.split_authority("example.com", "https")) --> "localhost", 443 75 | ``` 76 | 77 | 78 | ### `to_authority(host, port, scheme)` {#http.util.to_authority} 79 | 80 | Joins the `host` and `port` to create a valid authority component. 81 | Omits the port if it is the default for the `scheme`. 82 | 83 | 84 | ### `imf_date(time)` {#http.util.imf_date} 85 | 86 | Returns the time in HTTP preferred date format (See [RFC 7231 section 7.1.1.1](https://tools.ietf.org/html/rfc7231#section-7.1.1.1)) 87 | 88 | `time` defaults to the current time 89 | 90 | 91 | ### `maybe_quote(str)` {#http.util.maybe_quote} 92 | 93 | - If `str` is a valid `token`, return it as-is. 94 | - If `str` would be valid as a `quoted-string`, return the quoted version 95 | - Otherwise, returns `nil` 96 | -------------------------------------------------------------------------------- /doc/modules/http.version.md: -------------------------------------------------------------------------------- 1 | ## http.version 2 | 3 | ### `name` {#http.version.name} 4 | 5 | `"lua-http"` 6 | 7 | 8 | ### `version` {#http.version.version} 9 | 10 | Current version of lua-http as a string. 11 | -------------------------------------------------------------------------------- /doc/modules/http.websocket.md: -------------------------------------------------------------------------------- 1 | ## http.websocket 2 | 3 | ### `new_from_uri(uri, protocols)` {#http.websocket.new_from_uri} 4 | 5 | Creates a new `http.websocket` object of type `"client"` from the given URI. 6 | 7 | - `protocols` (optional) should be a lua table containing a sequence of protocols to send to the server 8 | 9 | 10 | ### `new_from_stream(stream, headers)` {#http.websocket.new_from_stream} 11 | 12 | Attempts to create a new `http.websocket` object of type `"server"` from the given request headers and stream. 13 | 14 | - [`stream`](#http.h1_stream) should be a live HTTP 1 stream of the `"server"` type. 15 | - [`headers`](#http.headers) should be headers of a suspected websocket upgrade request from an HTTP 1 client. 16 | 17 | This function does **not** have side effects, and is hence okay to use tentatively. 18 | 19 | 20 | ### `websocket.close_timeout` {#http.websocket.close_timeout} 21 | 22 | Amount of time (in seconds) to wait between sending a close frame and actually closing the connection. 23 | Defaults to `3` seconds. 24 | 25 | 26 | ### `websocket:accept(options, timeout)` {#http.websocket:accept} 27 | 28 | Completes negotiation with a websocket client. 29 | 30 | - `options` is a table containing: 31 | - `headers` (optional) a [headers](#http.headers) object to use as a prototype for the response headers 32 | - `protocols` (optional) should be a lua table containing a sequence of protocols to allow from the client 33 | 34 | Usually called after a successful [`new_from_stream`](#http.websocket.new_from_stream) 35 | 36 | 37 | ### `websocket:connect(timeout)` {#http.websocket:connect} 38 | 39 | Connect to a websocket server. 40 | 41 | Usually called after a successful [`new_from_uri`](#http.websocket.new_from_uri) 42 | 43 | 44 | ### `websocket:receive(timeout)` {#http.websocket:receive} 45 | 46 | Reads and returns the next data frame plus its opcode. 47 | Any ping frames received while reading will be responded to. 48 | 49 | The opcode `0x1` will be returned as `"text"` and `0x2` will be returned as `"binary"`. 50 | 51 | 52 | ### `websocket:each()` {#http.websocket:each} 53 | 54 | Iterator over [`websocket:receive()`](#http.websocket:receive). 55 | 56 | 57 | ### `websocket:send_frame(frame, timeout)` {#http.websocket:send_frame} 58 | 59 | Low level function to send a raw frame. 60 | 61 | 62 | ### `websocket:send(data, opcode, timeout)` {#http.websocket:send} 63 | 64 | Send the given `data` as a data frame. 65 | 66 | - `data` should be a string 67 | - `opcode` can be a numeric opcode, `"text"` or `"binary"`. If `nil`, defaults to a text frame. 68 | Note this `opcode` is the websocket frame opcode, not an application specific opcode. The opcode should be one from the [IANA registry](https://www.iana.org/assignments/websocket/websocket.xhtml#opcode). 69 | 70 | 71 | ### `websocket:send_ping(data, timeout)` {#http.websocket:send_ping} 72 | 73 | Sends a ping frame. 74 | 75 | - `data` is optional 76 | 77 | 78 | ### `websocket:send_pong(data, timeout)` {#http.websocket:send_pong} 79 | 80 | Sends a pong frame. Works as a unidirectional keep-alive. 81 | 82 | - `data` is optional 83 | 84 | 85 | ### `websocket:close(code, reason, timeout)` {#http.websocket:close} 86 | 87 | Closes the websocket connection. 88 | 89 | - `code` defaults to `1000` 90 | - `reason` is an optional string 91 | -------------------------------------------------------------------------------- /doc/modules/http.zlib.md: -------------------------------------------------------------------------------- 1 | ## http.zlib 2 | 3 | An abstraction layer over the various lua zlib libraries. 4 | 5 | ### `engine` {#http.zlib.engine} 6 | 7 | Currently either [`"lua-zlib"`](https://github.com/brimworks/lua-zlib) or [`"lzlib"`](https://github.com/LuaDist/lzlib) 8 | 9 | 10 | ### `inflate()` {#http.zlib.inflate} 11 | 12 | Returns a closure that inflates (uncompresses) a zlib stream. 13 | 14 | The closure takes a string of compressed data and an end of stream flag (`boolean`) as parameters and returns the inflated output as a string. The function will throw an error if the input is not a valid zlib stream. 15 | 16 | 17 | ### `deflate()` {#http.zlib.deflate} 18 | 19 | Returns a closure that deflates (compresses) a zlib stream. 20 | 21 | The closure takes a string of uncompressed data and an end of stream flag (`boolean`) as parameters and returns the deflated output as a string. 22 | 23 | 24 | ### Example {#http.zlib-example} 25 | 26 | ```lua 27 | local zlib = require "http.zlib" 28 | local original = "the racecar raced around the racecar track" 29 | local deflater = zlib.deflate() 30 | local compressed = deflater(original, true) 31 | print(#original, #compressed) -- compressed should be smaller 32 | local inflater = zlib.inflate() 33 | local uncompressed = inflater(compressed, true) 34 | assert(original == uncompressed) 35 | ``` 36 | -------------------------------------------------------------------------------- /doc/site.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box 5 | } 6 | html, 7 | body { 8 | height: 100% 9 | } 10 | article, 11 | aside, 12 | figure, 13 | footer, 14 | header, 15 | hgroup, 16 | menu, 17 | nav, 18 | section { 19 | display: block 20 | } 21 | body { 22 | margin: 0 23 | } 24 | h1, 25 | h2, 26 | h3 { 27 | margin: 1rem 0 28 | } 29 | h4, 30 | h5, 31 | h6, 32 | ul, 33 | ol, 34 | dl, 35 | blockquote, 36 | address, 37 | p, 38 | figure { 39 | margin: 0 0 1rem 0 40 | } 41 | img { 42 | max-width: 100% 43 | } 44 | h1, 45 | h2, 46 | h3, 47 | h4, 48 | h5, 49 | h6 { 50 | font-weight: 700 51 | } 52 | h1 { 53 | font-size: 2.5rem; 54 | line-height: 3rem 55 | } 56 | h2 { 57 | font-size: 1.5rem; 58 | line-height: 2rem 59 | } 60 | h3 { 61 | font-size: 1.25rem; 62 | line-height: 1.5rem 63 | } 64 | h4, 65 | h5, 66 | h6 { 67 | font-size: 1rem; 68 | line-height: 1.25rem 69 | } 70 | hr { 71 | border: 0; 72 | border-bottom: 1px solid; 73 | margin-top: -1px; 74 | margin-bottom: 1rem 75 | } 76 | a:hover { 77 | color: inherit 78 | } 79 | small { 80 | font-size: .875rem 81 | } 82 | ul, 83 | ol { 84 | padding-left: 1rem 85 | } 86 | ul ul, 87 | ul ol, 88 | ol ol, 89 | ol ul { 90 | margin: 0 91 | } 92 | dt { 93 | font-weight: 700 94 | } 95 | dd { 96 | margin: 0 97 | } 98 | blockquote { 99 | border-left: 1px solid; 100 | padding-left: 1rem 101 | } 102 | address { 103 | font-style: normal 104 | } 105 | html { 106 | color: #333; 107 | font: 100%/1.5 Avenir, 'Helvetica Neue', Helvetica, Arial, sans-serif; 108 | -webkit-font-smoothing: antialiased; 109 | -webkit-text-size-adjust: 100%; 110 | -ms-text-size-adjust: 100%; 111 | background: #FFF; 112 | } 113 | a { 114 | color: #999; 115 | text-decoration: none; 116 | transition: color 0.3s; 117 | } 118 | a > h1, 119 | a > h2, 120 | a > h3 { 121 | color: #333; 122 | } 123 | 124 | body > * { 125 | padding: 0 1rem; 126 | } 127 | .subtitle { 128 | font-size: 1rem; 129 | line-height: 1.5rem 130 | } 131 | .author { 132 | display: none 133 | } 134 | @media screen and (min-width: 55rem) { 135 | .meta { 136 | position: fixed; 137 | width: 20rem; 138 | height: 100%; 139 | overflow: auto; 140 | background: #FFF; 141 | z-index: 1; 142 | } 143 | main { 144 | display: block; /* required for e.g. konqueror */ 145 | margin-left: 20rem; 146 | overflow: auto; 147 | } 148 | } 149 | @media print { 150 | section.level1 { 151 | page-break-inside: avoid 152 | } 153 | nav a::after { 154 | content: leader('.') target-counter(attr(href url), page, decimal) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /doc/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $for(author-meta)$ 8 | 9 | $endfor$ 10 | $if(date-meta)$ 11 | 12 | $endif$ 13 | $if(keywords)$ 14 | 15 | $endif$ 16 | $if(title-prefix)$$title-prefix$ – $endif$$pagetitle$ 17 | 18 | $if(quotes)$ 19 | 20 | $endif$ 21 | $if(highlighting-css)$ 22 | 25 | $endif$ 26 | $for(css)$ 27 | 28 | $endfor$ 29 | $if(math)$ 30 | $math$ 31 | $endif$ 32 | 35 | $for(header-includes)$ 36 | $header-includes$ 37 | $endfor$ 38 | 39 | 40 | $for(include-before)$ 41 | $include-before$ 42 | $endfor$ 43 |
44 | $if(title)$ 45 |
46 |

$title$

47 | $if(subtitle)$ 48 |

$subtitle$

49 | $endif$ 50 | $for(author)$ 51 |

$author$

52 | $endfor$ 53 | $if(date)$ 54 |

$date$

55 | $endif$ 56 |
57 | $endif$ 58 | $if(toc)$ 59 | 62 | $endif$ 63 |
64 |
65 | $body$ 66 |
67 | $for(include-after)$ 68 | $include-after$ 69 | $endfor$ 70 | 94 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /examples/h2_streaming.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --[[ 3 | Makes a request to an HTTP2 endpoint that has an infinite length response. 4 | 5 | Usage: lua examples/h2_streaming.lua 6 | ]] 7 | 8 | local request = require "http.request" 9 | 10 | -- This endpoint returns a never-ending stream of chunks containing the current time 11 | local req = request.new_from_uri("https://http2.golang.org/clockstream") 12 | local _, stream = assert(req:go()) 13 | for chunk in stream:each_chunk() do 14 | io.write(chunk) 15 | end 16 | -------------------------------------------------------------------------------- /examples/serve_dir.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --[=[ 3 | This example serves a file/directory browser 4 | It defaults to serving the current directory. 5 | 6 | Usage: lua examples/serve_dir.lua [ []] 7 | ]=] 8 | 9 | local port = arg[1] or 8000 10 | local dir = arg[2] or "." 11 | 12 | local new_headers = require "http.headers".new 13 | local http_server = require "http.server" 14 | local http_util = require "http.util" 15 | local http_version = require "http.version" 16 | local ce = require "cqueues.errno" 17 | local lfs = require "lfs" 18 | local lpeg = require "lpeg" 19 | local uri_patts = require "lpeg_patterns.uri" 20 | 21 | local mdb do 22 | -- If available, use libmagic https://github.com/mah0x211/lua-magic 23 | local ok, magic = pcall(require, "magic") 24 | if ok then 25 | mdb = magic.open(magic.MIME_TYPE+magic.PRESERVE_ATIME+magic.RAW+magic.ERROR) 26 | if mdb:load() ~= 0 then 27 | error(magic:error()) 28 | end 29 | end 30 | end 31 | 32 | local uri_reference = uri_patts.uri_reference * lpeg.P(-1) 33 | 34 | local default_server = string.format("%s/%s", http_version.name, http_version.version) 35 | 36 | local xml_escape do 37 | local escape_table = { 38 | ["'"] = "'"; 39 | ["\""] = """; 40 | ["<"] = "<"; 41 | [">"] = ">"; 42 | ["&"] = "&"; 43 | } 44 | function xml_escape(str) 45 | str = string.gsub(str, "['&<>\"]", escape_table) 46 | str = string.gsub(str, "[%c\r\n]", function(c) 47 | return string.format("&#x%x;", string.byte(c)) 48 | end) 49 | return str 50 | end 51 | end 52 | 53 | local human do -- Utility function to convert to a human readable number 54 | local suffixes = { 55 | [0] = ""; 56 | [1] = "K"; 57 | [2] = "M"; 58 | [3] = "G"; 59 | [4] = "T"; 60 | [5] = "P"; 61 | } 62 | local log = math.log 63 | if _VERSION:match("%d+%.?%d*") < "5.1" then 64 | log = require "compat53.module".math.log 65 | end 66 | function human(n) 67 | if n == 0 then return "0" end 68 | local order = math.floor(log(n, 2) / 10) 69 | if order > 5 then order = 5 end 70 | n = math.ceil(n / 2^(order*10)) 71 | return string.format("%d%s", n, suffixes[order]) 72 | end 73 | end 74 | 75 | local function reply(myserver, stream) -- luacheck: ignore 212 76 | -- Read in headers 77 | local req_headers = assert(stream:get_headers()) 78 | local req_method = req_headers:get ":method" 79 | 80 | -- Log request to stdout 81 | assert(io.stdout:write(string.format('[%s] "%s %s HTTP/%g" "%s" "%s"\n', 82 | os.date("%d/%b/%Y:%H:%M:%S %z"), 83 | req_method or "", 84 | req_headers:get(":path") or "", 85 | stream.connection.version, 86 | req_headers:get("referer") or "-", 87 | req_headers:get("user-agent") or "-" 88 | ))) 89 | 90 | -- Build response headers 91 | local res_headers = new_headers() 92 | res_headers:append(":status", nil) 93 | res_headers:append("server", default_server) 94 | res_headers:append("date", http_util.imf_date()) 95 | 96 | if req_method ~= "GET" and req_method ~= "HEAD" then 97 | res_headers:upsert(":status", "405") 98 | assert(stream:write_headers(res_headers, true)) 99 | return 100 | end 101 | 102 | local path = req_headers:get(":path") 103 | local uri_t = assert(uri_reference:match(path), "invalid path") 104 | path = http_util.resolve_relative_path("/", uri_t.path) 105 | local real_path = dir .. path 106 | local file_type = lfs.attributes(real_path, "mode") 107 | if file_type == "directory" then 108 | -- directory listing 109 | path = path:gsub("/+$", "") .. "/" 110 | res_headers:upsert(":status", "200") 111 | res_headers:append("content-type", "text/html; charset=utf-8") 112 | assert(stream:write_headers(res_headers, req_method == "HEAD")) 113 | if req_method ~= "HEAD" then 114 | assert(stream:write_chunk(string.format([[ 115 | 116 | 117 | 118 | Index of %s 119 | 146 | 147 | 148 |

Index of %s

149 | 150 | 151 | 152 | 153 | 154 | ]], xml_escape(path), xml_escape(path)), false)) 155 | -- lfs doesn't provide a way to get an errno for attempting to open a directory 156 | -- See https://github.com/keplerproject/luafilesystem/issues/87 157 | for filename in lfs.dir(real_path) do 158 | if not (filename == ".." and path == "/") then -- Exclude parent directory entry listing from top level 159 | local stats = lfs.attributes(real_path .. "/" .. filename) 160 | if stats.mode == "directory" then 161 | filename = filename .. "/" 162 | end 163 | assert(stream:write_chunk(string.format("\t\t\t\n", 164 | xml_escape(stats.mode:gsub("%s", "-")), 165 | xml_escape(http_util.encodeURI(path .. filename)), 166 | xml_escape(filename), 167 | stats.size, 168 | xml_escape(human(stats.size)), 169 | xml_escape(os.date("!%Y-%m-%d %X", stats.modification)) 170 | ), false)) 171 | end 172 | end 173 | assert(stream:write_chunk([[ 174 | 175 |
File NameSizeModified
%s%s
176 | 177 | 178 | ]], true)) 179 | end 180 | elseif file_type == "file" then 181 | local fd, err, errno = io.open(real_path, "rb") 182 | local code 183 | if not fd then 184 | if errno == ce.ENOENT then 185 | code = "404" 186 | elseif errno == ce.EACCES then 187 | code = "403" 188 | else 189 | code = "503" 190 | end 191 | res_headers:upsert(":status", code) 192 | res_headers:append("content-type", "text/plain") 193 | assert(stream:write_headers(res_headers, req_method == "HEAD")) 194 | if req_method ~= "HEAD" then 195 | assert(stream:write_body_from_string("Fail!\n"..err.."\n")) 196 | end 197 | else 198 | res_headers:upsert(":status", "200") 199 | local mime_type = mdb and mdb:file(real_path) or "application/octet-stream" 200 | res_headers:append("content-type", mime_type) 201 | assert(stream:write_headers(res_headers, req_method == "HEAD")) 202 | if req_method ~= "HEAD" then 203 | assert(stream:write_body_from_file(fd)) 204 | end 205 | end 206 | elseif file_type == nil then 207 | res_headers:upsert(":status", "404") 208 | assert(stream:write_headers(res_headers, true)) 209 | else 210 | res_headers:upsert(":status", "403") 211 | assert(stream:write_headers(res_headers, true)) 212 | end 213 | end 214 | 215 | local myserver = assert(http_server.listen { 216 | host = "localhost"; 217 | port = port; 218 | max_concurrent = 100; 219 | onstream = reply; 220 | onerror = function(myserver, context, op, err, errno) -- luacheck: ignore 212 221 | local msg = op .. " on " .. tostring(context) .. " failed" 222 | if err then 223 | msg = msg .. ": " .. tostring(err) 224 | end 225 | assert(io.stderr:write(msg, "\n")) 226 | end; 227 | }) 228 | 229 | -- Manually call :listen() so that we are bound before calling :localname() 230 | assert(myserver:listen()) 231 | do 232 | local bound_port = select(3, myserver:localname()) 233 | assert(io.stderr:write(string.format("Now listening on port %d\n", bound_port))) 234 | end 235 | -- Start the main server loop 236 | assert(myserver:loop()) 237 | -------------------------------------------------------------------------------- /examples/server_hello.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --[[ 3 | A simple HTTP server 4 | 5 | If a request is not a HEAD method, then reply with "Hello world!" 6 | 7 | Usage: lua examples/server_hello.lua [] 8 | ]] 9 | 10 | local port = arg[1] or 0 -- 0 means pick one at random 11 | 12 | local http_server = require "http.server" 13 | local http_headers = require "http.headers" 14 | 15 | local function reply(myserver, stream) -- luacheck: ignore 212 16 | -- Read in headers 17 | local req_headers = assert(stream:get_headers()) 18 | local req_method = req_headers:get ":method" 19 | 20 | -- Log request to stdout 21 | assert(io.stdout:write(string.format('[%s] "%s %s HTTP/%g" "%s" "%s"\n', 22 | os.date("%d/%b/%Y:%H:%M:%S %z"), 23 | req_method or "", 24 | req_headers:get(":path") or "", 25 | stream.connection.version, 26 | req_headers:get("referer") or "-", 27 | req_headers:get("user-agent") or "-" 28 | ))) 29 | 30 | -- Build response headers 31 | local res_headers = http_headers.new() 32 | res_headers:append(":status", "200") 33 | res_headers:append("content-type", "text/plain") 34 | -- Send headers to client; end the stream immediately if this was a HEAD request 35 | assert(stream:write_headers(res_headers, req_method == "HEAD")) 36 | if req_method ~= "HEAD" then 37 | -- Send body, ending the stream 38 | assert(stream:write_chunk("Hello world!\n", true)) 39 | end 40 | end 41 | 42 | local myserver = assert(http_server.listen { 43 | host = "localhost"; 44 | port = port; 45 | onstream = reply; 46 | onerror = function(myserver, context, op, err, errno) -- luacheck: ignore 212 47 | local msg = op .. " on " .. tostring(context) .. " failed" 48 | if err then 49 | msg = msg .. ": " .. tostring(err) 50 | end 51 | assert(io.stderr:write(msg, "\n")) 52 | end; 53 | }) 54 | 55 | -- Manually call :listen() so that we are bound before calling :localname() 56 | assert(myserver:listen()) 57 | do 58 | local bound_port = select(3, myserver:localname()) 59 | assert(io.stderr:write(string.format("Now listening on port %d\n", bound_port))) 60 | end 61 | -- Start the main server loop 62 | assert(myserver:loop()) 63 | -------------------------------------------------------------------------------- /examples/server_sent_events.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --[[ 3 | A server that responds with an infinite server-side-events format. 4 | https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format 5 | 6 | Usage: lua examples/server_sent_events.lua [] 7 | ]] 8 | 9 | local port = arg[1] or 0 -- 0 means pick one at random 10 | 11 | local cqueues = require "cqueues" 12 | local http_server = require "http.server" 13 | local http_headers = require "http.headers" 14 | 15 | local myserver = assert(http_server.listen { 16 | host = "localhost"; 17 | port = port; 18 | onstream = function(myserver, stream) -- luacheck: ignore 212 19 | -- Read in headers 20 | local req_headers = assert(stream:get_headers()) 21 | local req_method = req_headers:get ":method" 22 | 23 | -- Build response headers 24 | local res_headers = http_headers.new() 25 | if req_method ~= "GET" and req_method ~= "HEAD" then 26 | res_headers:upsert(":status", "405") 27 | assert(stream:write_headers(res_headers, true)) 28 | return 29 | end 30 | if req_headers:get ":path" == "/" then 31 | res_headers:append(":status", "200") 32 | res_headers:append("content-type", "text/html") 33 | -- Send headers to client; end the stream immediately if this was a HEAD request 34 | assert(stream:write_headers(res_headers, req_method == "HEAD")) 35 | if req_method ~= "HEAD" then 36 | assert(stream:write_chunk([[ 37 | 38 | 39 | 40 | EventSource demo 41 | 42 | 43 |

This page uses server-sent_events to show the live server time:

44 |
45 | 52 | 53 | 54 | ]], true)) 55 | end 56 | elseif req_headers:get ":path" == "/event-stream" then 57 | res_headers:append(":status", "200") 58 | res_headers:append("content-type", "text/event-stream") 59 | -- Send headers to client; end the stream immediately if this was a HEAD request 60 | assert(stream:write_headers(res_headers, req_method == "HEAD")) 61 | if req_method ~= "HEAD" then 62 | -- Start a loop that sends the current time to the client each second 63 | while true do 64 | local msg = string.format("data: The time is now %s.\n\n", os.date()) 65 | assert(stream:write_chunk(msg, false)) 66 | cqueues.sleep(1) -- yield the current thread for a second. 67 | end 68 | end 69 | else 70 | res_headers:append(":status", "404") 71 | assert(stream:write_headers(res_headers, true)) 72 | end 73 | end; 74 | onerror = function(myserver, context, op, err, errno) -- luacheck: ignore 212 75 | local msg = op .. " on " .. tostring(context) .. " failed" 76 | if err then 77 | msg = msg .. ": " .. tostring(err) 78 | end 79 | assert(io.stderr:write(msg, "\n")) 80 | end; 81 | }) 82 | 83 | -- Manually call :listen() so that we are bound before calling :localname() 84 | assert(myserver:listen()) 85 | do 86 | local bound_port = select(3, myserver:localname()) 87 | assert(io.stderr:write(string.format("Now listening on port %d\nOpen http://localhost:%d/ in your browser\n", bound_port, bound_port))) 88 | end 89 | -- Start the main server loop 90 | assert(myserver:loop()) 91 | -------------------------------------------------------------------------------- /examples/simple_request.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --[[ 3 | Verbosely fetches an HTTP resource 4 | If a body is given, use a POST request 5 | 6 | Usage: lua examples/simple_request.lua [] 7 | ]] 8 | 9 | local uri = assert(arg[1], "URI needed") 10 | local req_body = arg[2] 11 | local req_timeout = 10 12 | 13 | local request = require "http.request" 14 | 15 | local req = request.new_from_uri(uri) 16 | if req_body then 17 | req.headers:upsert(":method", "POST") 18 | req:set_body(req_body) 19 | end 20 | 21 | print("# REQUEST") 22 | print("## HEADERS") 23 | for k, v in req.headers:each() do 24 | print(k, v) 25 | end 26 | print() 27 | if req.body then 28 | print("## BODY") 29 | print(req.body) 30 | print() 31 | end 32 | 33 | print("# RESPONSE") 34 | local headers, stream = req:go(req_timeout) 35 | if headers == nil then 36 | io.stderr:write(tostring(stream), "\n") 37 | os.exit(1) 38 | end 39 | print("## HEADERS") 40 | for k, v in headers:each() do 41 | print(k, v) 42 | end 43 | print() 44 | print("## BODY") 45 | local body, err = stream:get_body_as_string() 46 | if not body and err then 47 | io.stderr:write(tostring(err), "\n") 48 | os.exit(1) 49 | end 50 | print(body) 51 | print() 52 | -------------------------------------------------------------------------------- /examples/websocket_client.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --[[ 3 | Example of websocket client usage 4 | 5 | - Connects to the gdax market data feed. 6 | Documentation of feed: https://docs.gdax.com/#websocket-feed 7 | - Sends a subscribe message 8 | - Prints off 5 messages 9 | - Close the socket and clean up. 10 | ]] 11 | 12 | local websocket = require "http.websocket" 13 | 14 | local ws = websocket.new_from_uri("wss://ws-feed.gdax.com") 15 | assert(ws:connect()) 16 | assert(ws:send([[{"type": "subscribe", "product_id": "BTC-USD"}]])) 17 | for _=1, 5 do 18 | local data = assert(ws:receive()) 19 | print(data) 20 | end 21 | assert(ws:close()) 22 | -------------------------------------------------------------------------------- /http-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "http" 2 | version = "scm-0" 3 | 4 | description = { 5 | summary = "HTTP library for Lua"; 6 | homepage = "https://github.com/daurnimator/lua-http"; 7 | license = "MIT"; 8 | } 9 | 10 | source = { 11 | url = "git+https://github.com/daurnimator/lua-http.git"; 12 | } 13 | 14 | dependencies = { 15 | "lua >= 5.1"; 16 | "compat53 >= 0.3"; -- Only if lua < 5.3 17 | "bit32"; -- Only if lua == 5.1 18 | "cqueues >= 20161214"; 19 | "luaossl >= 20161208"; 20 | "basexx >= 0.2.0"; 21 | "lpeg"; 22 | "lpeg_patterns >= 0.5"; 23 | "binaryheap >= 0.3"; 24 | "fifo"; 25 | -- "psl"; -- Optional 26 | } 27 | 28 | build = { 29 | type = "builtin"; 30 | modules = { 31 | ["http.bit"] = "http/bit.lua"; 32 | ["http.client"] = "http/client.lua"; 33 | ["http.connection_common"] = "http/connection_common.lua"; 34 | ["http.cookie"] = "http/cookie.lua"; 35 | ["http.h1_connection"] = "http/h1_connection.lua"; 36 | ["http.h1_reason_phrases"] = "http/h1_reason_phrases.lua"; 37 | ["http.h1_stream"] = "http/h1_stream.lua"; 38 | ["http.h2_connection"] = "http/h2_connection.lua"; 39 | ["http.h2_error"] = "http/h2_error.lua"; 40 | ["http.h2_stream"] = "http/h2_stream.lua"; 41 | ["http.headers"] = "http/headers.lua"; 42 | ["http.hpack"] = "http/hpack.lua"; 43 | ["http.hsts"] = "http/hsts.lua"; 44 | ["http.proxies"] = "http/proxies.lua"; 45 | ["http.request"] = "http/request.lua"; 46 | ["http.server"] = "http/server.lua"; 47 | ["http.socks"] = "http/socks.lua"; 48 | ["http.stream_common"] = "http/stream_common.lua"; 49 | ["http.tls"] = "http/tls.lua"; 50 | ["http.util"] = "http/util.lua"; 51 | ["http.version"] = "http/version.lua"; 52 | ["http.websocket"] = "http/websocket.lua"; 53 | ["http.zlib"] = "http/zlib.lua"; 54 | ["http.compat.prosody"] = "http/compat/prosody.lua"; 55 | ["http.compat.socket"] = "http/compat/socket.lua"; 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /http/bit.lua: -------------------------------------------------------------------------------- 1 | --[[ This module smooths over all the various lua bit libraries 2 | 3 | The bit operations are only done 4 | - on bytes (8 bits), 5 | - with quantities <= LONG_MAX (0x7fffffff) 6 | - band with 0x80000000 that is subsequently compared with 0 7 | This means we can ignore the differences between bit libraries. 8 | ]] 9 | 10 | -- Lua 5.1 didn't have `load` or bitwise operators, just let it fall through. 11 | if _VERSION ~= "Lua 5.1" then 12 | -- Lua 5.3+ has built-in bit operators, wrap them in a function. 13 | -- Use debug.getinfo to get correct file+line numbers for loaded snippet 14 | local info = debug.getinfo(1, "Sl") 15 | local has_bitwise, bitwise = pcall(load(("\n"):rep(info.currentline+1)..[[return { 16 | band = function(a, b) return a & b end; 17 | bor = function(a, b) return a | b end; 18 | bxor = function(a, b) return a ~ b end; 19 | }]], info.source)) 20 | if has_bitwise then 21 | return bitwise 22 | end 23 | end 24 | 25 | -- The "bit" library that comes with luajit 26 | -- also available for lua 5.1 as "luabitop": http://bitop.luajit.org/ 27 | local has_bit, bit = pcall(require, "bit") 28 | if has_bit then 29 | return { 30 | band = bit.band; 31 | bor = bit.bor; 32 | bxor = bit.bxor; 33 | } 34 | end 35 | 36 | -- The "bit32" library shipped with lua 5.2 37 | local has_bit32, bit32 = pcall(require, "bit32") 38 | if has_bit32 then 39 | return { 40 | band = bit32.band; 41 | bor = bit32.bor; 42 | bxor = bit32.bxor; 43 | } 44 | end 45 | 46 | error("Please install a bit library") 47 | -------------------------------------------------------------------------------- /http/bit.tld: -------------------------------------------------------------------------------- 1 | band: (integer, integer) -> (integer) 2 | bor: (integer, integer) -> (integer) 3 | bxor: (integer, integer) -> (integer) 4 | -------------------------------------------------------------------------------- /http/client.lua: -------------------------------------------------------------------------------- 1 | local ca = require "cqueues.auxlib" 2 | local cs = require "cqueues.socket" 3 | local http_tls = require "http.tls" 4 | local http_util = require "http.util" 5 | local connection_common = require "http.connection_common" 6 | local onerror = connection_common.onerror 7 | local new_h1_connection = require "http.h1_connection".new 8 | local new_h2_connection = require "http.h2_connection".new 9 | local openssl_ssl = require "openssl.ssl" 10 | local openssl_ctx = require "openssl.ssl.context" 11 | local openssl_verify_param = require "openssl.x509.verify_param" 12 | 13 | -- Create a shared 'default' TLS context 14 | local default_ctx = http_tls.new_client_context() 15 | 16 | local function negotiate(s, options, timeout) 17 | s:onerror(onerror) 18 | local tls = options.tls 19 | local version = options.version 20 | if tls then 21 | local ctx = options.ctx or default_ctx 22 | local ssl = openssl_ssl.new(ctx) 23 | local host = options.host 24 | local host_is_ip = host and http_util.is_ip(host) 25 | local sendname = options.sendname 26 | if sendname == nil and not host_is_ip and host then 27 | sendname = host 28 | end 29 | if sendname then -- false indicates no sendname wanted 30 | ssl:setHostName(sendname) 31 | end 32 | if http_tls.has_alpn then 33 | if version == nil then 34 | ssl:setAlpnProtos({"h2", "http/1.1"}) 35 | elseif version == 1.1 then 36 | ssl:setAlpnProtos({"http/1.1"}) 37 | elseif version == 2 then 38 | ssl:setAlpnProtos({"h2"}) 39 | end 40 | end 41 | if version == 2 then 42 | ssl:setOptions(openssl_ctx.OP_NO_TLSv1 + openssl_ctx.OP_NO_TLSv1_1) 43 | end 44 | if host and http_tls.has_hostname_validation then 45 | local params = openssl_verify_param.new() 46 | if host_is_ip then 47 | params:setIP(host) 48 | else 49 | params:setHost(host) 50 | end 51 | -- Allow user defined params to override 52 | local old = ssl:getParam() 53 | old:inherit(params) 54 | ssl:setParam(old) 55 | end 56 | local ok, err, errno = s:starttls(ssl, timeout) 57 | if not ok then 58 | return nil, err, errno 59 | end 60 | end 61 | if version == nil then 62 | local ssl = s:checktls() 63 | if ssl then 64 | if http_tls.has_alpn and ssl:getAlpnSelected() == "h2" then 65 | version = 2 66 | else 67 | version = 1.1 68 | end 69 | else 70 | -- TODO: attempt upgrading http1 to http2 71 | version = 1.1 72 | end 73 | end 74 | if version < 2 then 75 | return new_h1_connection(s, "client", version) 76 | elseif version == 2 then 77 | return new_h2_connection(s, "client", options.h2_settings) 78 | else 79 | error("Unknown HTTP version: " .. tostring(version)) 80 | end 81 | end 82 | 83 | local function connect(options, timeout) 84 | local bind = options.bind 85 | if bind ~= nil then 86 | assert(type(bind) == "string") 87 | local bind_address, bind_port = bind:match("^(.-):(%d+)$") 88 | if bind_address then 89 | bind_port = tonumber(bind_port, 10) 90 | else 91 | bind_address = bind 92 | end 93 | local ipv6 = bind_address:match("^%[([:%x]+)%]$") 94 | if ipv6 then 95 | bind_address = ipv6 96 | end 97 | bind = { 98 | address = bind_address; 99 | port = bind_port; 100 | } 101 | end 102 | local s, err, errno = ca.fileresult(cs.connect { 103 | family = options.family; 104 | host = options.host; 105 | port = options.port; 106 | path = options.path; 107 | bind = bind; 108 | sendname = false; 109 | v6only = options.v6only; 110 | nodelay = true; 111 | }) 112 | if s == nil then 113 | return nil, err, errno 114 | end 115 | return negotiate(s, options, timeout) 116 | end 117 | 118 | return { 119 | negotiate = negotiate; 120 | connect = connect; 121 | } 122 | -------------------------------------------------------------------------------- /http/compat/prosody.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Compatibility module for prosody's net.http 3 | Documentation: https://prosody.im/doc/developers/net/http 4 | 5 | This has a few key differences: 6 | - `compat.prosody.request` must be called from within a running cqueue 7 | - The callback will be called from a different thread in the cqueue 8 | - The returned "request" object will be a lua-http request object 9 | - Same request object is passed to the callback on errors and as the 4th argument on success 10 | - The user-agent will be from lua-http 11 | - lua-http features (such as HTTP2) will be used where possible 12 | ]] 13 | 14 | local new_from_uri = require "http.request".new_from_uri 15 | local cqueues = require "cqueues" 16 | 17 | local function do_request(self, callback) 18 | local headers, stream = self:go() 19 | if headers == nil then 20 | -- `stream` is error message 21 | callback(stream, 0, self) 22 | return 23 | end 24 | local response_body, err = stream:get_body_as_string() 25 | stream:shutdown() 26 | if response_body == nil then 27 | callback(err, 0, self) 28 | return 29 | end 30 | -- code might not be convertible to a number in http2, so need `or` case 31 | local code = headers:get(":status") 32 | code = tonumber(code, 10) or code 33 | -- convert headers to table with comma separated values 34 | local headers_as_kv = {} 35 | for key, value in headers:each() do 36 | if key ~= ":status" then 37 | local old = headers_as_kv[key] 38 | if old then 39 | headers_as_kv[key] = old .. "," .. value 40 | else 41 | headers_as_kv[key] = value 42 | end 43 | end 44 | end 45 | local response = { 46 | code = code; 47 | httpversion = stream.peer_version; 48 | headers = headers_as_kv; 49 | body = response_body; 50 | } 51 | callback(response_body, code, response, self) 52 | end 53 | 54 | local function new_prosody(url, ex, callback) 55 | local cq = assert(cqueues.running(), "must be running inside a cqueue") 56 | local ok, req = pcall(new_from_uri, url) 57 | if not ok then 58 | callback(nil, 0, req) 59 | return nil, "invalid-url" 60 | end 61 | req.follow_redirects = false -- prosody doesn't follow redirects 62 | if ex then 63 | if ex.body then 64 | req.headers:upsert(":method", "POST") 65 | req:set_body(ex.body) 66 | req.headers:append("content-type", "application/x-www-form-urlencoded") 67 | end 68 | if ex.method then 69 | req.headers:upsert(":method", ex.method) 70 | end 71 | if ex.headers then 72 | for k, v in pairs(ex.headers) do 73 | req.headers:upsert(k:lower(), v) 74 | end 75 | end 76 | if ex.sslctx then 77 | req.ctx = ex.sslctx 78 | end 79 | end 80 | cq:wrap(do_request, req, callback) 81 | return req 82 | end 83 | 84 | return { 85 | request = new_prosody; 86 | } 87 | -------------------------------------------------------------------------------- /http/compat/socket.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Compatibility layer with luasocket's socket.http module 3 | Documentation: http://w3.impa.br/~diego/software/luasocket/http.html 4 | 5 | This module a few key differences: 6 | - The `.create` member is not supported 7 | - The user-agent will be from lua-http 8 | - lua-http features (such as HTTPS and HTTP2) will be used where possible 9 | - trailers are currently discarded 10 | - error messages are different 11 | ]] 12 | 13 | local monotime = require "cqueues".monotime 14 | local ce = require "cqueues.errno" 15 | local request = require "http.request" 16 | local version = require "http.version" 17 | local reason_phrases = require "http.h1_reason_phrases" 18 | 19 | local M = { 20 | PROXY = nil; -- default proxy used for connections 21 | TIMEOUT = 60; -- timeout for all I/O operations 22 | -- default user agent reported to server. 23 | USERAGENT = string.format("%s/%s (luasocket compatibility layer)", 24 | version.name, version.version); 25 | } 26 | 27 | local function ltn12_pump_step(src, snk) 28 | local chunk, src_err = src() 29 | local ret, snk_err = snk(chunk, src_err) 30 | if chunk and ret then return 1 31 | else return nil, src_err or snk_err end 32 | end 33 | 34 | local function get_body_as_string(stream, deadline) 35 | local body, err, errno = stream:get_body_as_string(deadline and deadline-monotime()) 36 | if not body then 37 | if err == nil then 38 | return nil 39 | elseif errno == ce.ETIMEDOUT then 40 | return nil, "timeout" 41 | else 42 | return nil, err 43 | end 44 | end 45 | return body 46 | end 47 | 48 | local function returns_1() 49 | return 1 50 | end 51 | 52 | function M.request(reqt, b) 53 | local deadline = M.TIMEOUT and monotime()+M.TIMEOUT 54 | local req, proxy, user_headers, get_body 55 | if type(reqt) == "string" then 56 | req = request.new_from_uri(reqt) 57 | proxy = M.PROXY 58 | if b ~= nil then 59 | assert(type(b) == "string", "body must be nil or string") 60 | req.headers:upsert(":method", "POST") 61 | req:set_body(b) 62 | req.headers:upsert("content-type", "application/x-www-form-urlencoded") 63 | end 64 | get_body = get_body_as_string 65 | else 66 | assert(reqt.create == nil, "'create' option not supported") 67 | req = request.new_from_uri(reqt.url) 68 | proxy = reqt.proxy or M.PROXY 69 | if reqt.host ~= nil then 70 | req.host = reqt.host 71 | end 72 | if reqt.port ~= nil then 73 | req.port = reqt.port 74 | end 75 | if reqt.method ~= nil then 76 | assert(type(reqt.method) == "string", "'method' option must be nil or string") 77 | req.headers:upsert(":method", reqt.method) 78 | end 79 | if reqt.redirect == false then 80 | req.follow_redirects = false 81 | else 82 | req.max_redirects = 5 - (reqt.nredirects or 0) 83 | end 84 | user_headers = reqt.headers 85 | local step = reqt.step or ltn12_pump_step 86 | local src = reqt.source 87 | if src ~= nil then 88 | local co = coroutine.create(function() 89 | while true do 90 | assert(step(src, coroutine.yield)) 91 | end 92 | end) 93 | req:set_body(function() 94 | -- Pass true through to coroutine to indicate success of last write 95 | local ok, chunk, err = coroutine.resume(co, true) 96 | if not ok then 97 | error(chunk) 98 | elseif err then 99 | error(err) 100 | else 101 | return chunk 102 | end 103 | end) 104 | end 105 | local sink = reqt.sink 106 | -- luasocket returns `1` when using a request table 107 | if sink ~= nil then 108 | get_body = function(stream, deadline) -- luacheck: ignore 431 109 | local function res_body_source() 110 | local chunk, err, errno = stream:get_next_chunk(deadline and deadline-monotime()) 111 | if not chunk then 112 | if err == nil then 113 | return nil 114 | elseif errno == ce.EPIPE then 115 | return nil, "closed" 116 | elseif errno == ce.ETIMEDOUT then 117 | return nil, "timeout" 118 | else 119 | return nil, err 120 | end 121 | end 122 | return chunk 123 | end 124 | -- This loop is the same as ltn12.pump.all 125 | while true do 126 | local ok, err = step(res_body_source, sink) 127 | if not ok then 128 | if err then 129 | return nil, err 130 | else 131 | return 1 132 | end 133 | end 134 | end 135 | end 136 | else 137 | get_body = returns_1 138 | end 139 | end 140 | req.headers:upsert("user-agent", M.USERAGENT) 141 | req.proxy = proxy or false 142 | if user_headers then 143 | for name, field in pairs(user_headers) do 144 | name = name:lower() 145 | field = "" .. field .. "" -- force coercion in same style as luasocket 146 | if name == "host" then 147 | req.headers:upsert(":authority", field) 148 | else 149 | req.headers:append(name, field) 150 | end 151 | end 152 | end 153 | local res_headers, stream, errno = req:go(deadline and deadline-monotime()) 154 | if not res_headers then 155 | if errno == ce.EPIPE or stream == nil then 156 | return nil, "closed" 157 | elseif errno == ce.ETIMEDOUT then 158 | return nil, "timeout" 159 | else 160 | return nil, stream 161 | end 162 | end 163 | local code = res_headers:get(":status") 164 | local status = reason_phrases[code] 165 | -- In luasocket, status codes are returned as numbers 166 | code = tonumber(code, 10) or code 167 | local headers = {} 168 | for name in res_headers:each() do 169 | if name ~= ":status" and headers[name] == nil then 170 | headers[name] = res_headers:get_comma_separated(name) 171 | end 172 | end 173 | local body, err = get_body(stream, deadline) 174 | stream:shutdown() 175 | if not body then 176 | return nil, err 177 | end 178 | return body, code, headers, status 179 | end 180 | 181 | return M 182 | -------------------------------------------------------------------------------- /http/connection_common.lua: -------------------------------------------------------------------------------- 1 | local cqueues = require "cqueues" 2 | local ca = require "cqueues.auxlib" 3 | local ce = require "cqueues.errno" 4 | 5 | local connection_methods = {} 6 | 7 | local function onerror(socket, op, why, lvl) -- luacheck: ignore 212 8 | local err = string.format("%s: %s", op, ce.strerror(why)) 9 | if op == "starttls" then 10 | local ssl = socket:checktls() 11 | if ssl and ssl.getVerifyResult then 12 | local code, msg = ssl:getVerifyResult() 13 | if code ~= 0 then 14 | err = err .. ":" .. msg 15 | end 16 | end 17 | end 18 | if why == ce.ETIMEDOUT then 19 | if op == "fill" or op == "read" then 20 | socket:clearerr("r") 21 | elseif op == "flush" then 22 | socket:clearerr("w") 23 | end 24 | end 25 | return err, why 26 | end 27 | 28 | function connection_methods:pollfd() 29 | if self.socket == nil then 30 | return nil 31 | end 32 | return self.socket:pollfd() 33 | end 34 | 35 | function connection_methods:events() 36 | if self.socket == nil then 37 | return nil 38 | end 39 | return self.socket:events() 40 | end 41 | 42 | function connection_methods:timeout() 43 | if self.socket == nil then 44 | return nil 45 | end 46 | return self.socket:timeout() 47 | end 48 | 49 | function connection_methods:onidle_() -- luacheck: ignore 212 50 | end 51 | 52 | function connection_methods:onidle(...) 53 | local old_handler = self.onidle_ 54 | if select("#", ...) > 0 then 55 | self.onidle_ = ... 56 | end 57 | return old_handler 58 | end 59 | 60 | function connection_methods:connect(timeout) 61 | if self.socket == nil then 62 | return nil 63 | end 64 | local ok, err, errno = self.socket:connect(timeout) 65 | if not ok then 66 | return nil, err, errno 67 | end 68 | return true 69 | end 70 | 71 | function connection_methods:checktls() 72 | if self.socket == nil then 73 | return nil 74 | end 75 | return self.socket:checktls() 76 | end 77 | 78 | function connection_methods:localname() 79 | if self.socket == nil then 80 | return nil 81 | end 82 | return ca.fileresult(self.socket:localname()) 83 | end 84 | 85 | function connection_methods:peername() 86 | if self.socket == nil then 87 | return nil 88 | end 89 | return ca.fileresult(self.socket:peername()) 90 | end 91 | 92 | -- Primarily used for testing 93 | function connection_methods:flush(timeout) 94 | return self.socket:flush("n", timeout) 95 | end 96 | 97 | function connection_methods:close() 98 | self:shutdown() 99 | if self.socket then 100 | cqueues.poll() 101 | cqueues.poll() 102 | self.socket:close() 103 | end 104 | return true 105 | end 106 | 107 | return { 108 | onerror = onerror; 109 | methods = connection_methods; 110 | } 111 | -------------------------------------------------------------------------------- /http/connection_common.tld: -------------------------------------------------------------------------------- 1 | interface connection 2 | -- implements cqueues polling interface 3 | const pollfd: (self) -> (nil)|(integer) -- TODO: cqueues condition 4 | const events: (self) -> (nil)|(string|integer) 5 | const timeout: (self) -> (nil)|(number) 6 | 7 | const checktls: (self) -> (nil)|(any) -- TODO: luaossl SSL object 8 | const localname: (self) -> (integer, string, integer?)|(nil)|(nil, string, number) 9 | const peername: (self) -> (integer, string, integer?)|(nil)|(nil, string, number) 10 | const onidle: (self, (connection)->()) -> ((connection)->()) 11 | const connect: (self) -> (true)|(nil)|(nil, string, number) 12 | const flush: (self, number) -> (true)|(nil, string, number) 13 | const close: (self) -> (true) 14 | 15 | -- Not in connection_common.lua 16 | const version: integer 17 | -- XXX: needs circular require https://github.com/andremm/typedlua/issues/120 18 | -- const new_stream: (self) -> (stream)|(nil) -- Note: in http2 this takes optional id argument 19 | -- const get_next_incoming_stream: (self, number?) -> (stream)|(nil)|(nil, string, number) 20 | const shutdown: (self) -> (true) 21 | end 22 | -------------------------------------------------------------------------------- /http/cookie.tld: -------------------------------------------------------------------------------- 1 | require "http.headers" 2 | 3 | bake: (string, string, number?, string?, string?, true?, true?, string?) -> (string) 4 | 5 | parse_cookie: (string) -> ({string:string}) 6 | parse_cookies: (headers) -> ({{string:string}}) 7 | parse_setcookie: (string) -> (string, string, {string:string}) 8 | 9 | interface cookie_store 10 | psl: any|false -- TODO: use psl type 11 | time: () -> (number) 12 | max_cookie_length: number 13 | max_cookies: number 14 | max_cookies_per_domain: number 15 | 16 | const store: (self, string, string, boolean, boolean, string?, string, string, {string:string}) -> (boolean) 17 | const store_from_request: (self, headers, headers, string, string?) -> (boolean) 18 | const get: (self, string, string, string) -> (string) 19 | const remove: (self, string, string?, string?) -> () 20 | const lookup: (self, string, string, boolean?, boolean?, boolean?, string?, boolean?, integer?) -> () 21 | const lookup_for_request: (self, headers, string, string?, boolean?, integer?) -> () 22 | const clean_due: (self) -> (number) 23 | const clean: (self) -> (boolean) 24 | const load_from_file: (self, file) -> (true) | (nil, string, integer) 25 | const save_to_file: (self, file) -> (true) | (nil, string, integer) 26 | end 27 | 28 | new_store: () -> (cookie_store) 29 | -------------------------------------------------------------------------------- /http/h1_reason_phrases.lua: -------------------------------------------------------------------------------- 1 | -- This list should be kept in sync with IANA. 2 | -- http://www.iana.org/assignments/http-status-codes 3 | 4 | local reason_phrases = setmetatable({ 5 | ["100"] = "Continue"; 6 | ["101"] = "Switching Protocols"; 7 | ["102"] = "Processing"; 8 | ["103"] = "Early Hints"; 9 | 10 | ["200"] = "OK"; 11 | ["201"] = "Created"; 12 | ["202"] = "Accepted"; 13 | ["203"] = "Non-Authoritative Information"; 14 | ["204"] = "No Content"; 15 | ["205"] = "Reset Content"; 16 | ["206"] = "Partial Content"; 17 | ["207"] = "Multi-Status"; 18 | ["208"] = "Already Reported"; 19 | 20 | ["226"] = "IM Used"; 21 | 22 | ["300"] = "Multiple Choices"; 23 | ["301"] = "Moved Permanently"; 24 | ["302"] = "Found"; 25 | ["303"] = "See Other"; 26 | ["304"] = "Not Modified"; 27 | ["305"] = "Use Proxy"; 28 | 29 | ["307"] = "Temporary Redirect"; 30 | ["308"] = "Permanent Redirect"; 31 | 32 | ["400"] = "Bad Request"; 33 | ["401"] = "Unauthorized"; 34 | ["402"] = "Payment Required"; 35 | ["403"] = "Forbidden"; 36 | ["404"] = "Not Found"; 37 | ["405"] = "Method Not Allowed"; 38 | ["406"] = "Not Acceptable"; 39 | ["407"] = "Proxy Authentication Required"; 40 | ["408"] = "Request Timeout"; 41 | ["409"] = "Conflict"; 42 | ["410"] = "Gone"; 43 | ["411"] = "Length Required"; 44 | ["412"] = "Precondition Failed"; 45 | ["413"] = "Request Entity Too Large"; 46 | ["414"] = "Request-URI Too Long"; 47 | ["415"] = "Unsupported Media Type"; 48 | ["416"] = "Requested Range Not Satisfiable"; 49 | ["417"] = "Expectation Failed"; 50 | ["418"] = "I'm a teapot"; -- not in IANA registry 51 | 52 | ["421"] = "Misdirected Request"; 53 | ["422"] = "Unprocessable Entity"; 54 | ["423"] = "Locked"; 55 | ["424"] = "Failed Dependency"; 56 | 57 | ["426"] = "Upgrade Required"; 58 | 59 | ["428"] = "Precondition Required"; 60 | ["429"] = "Too Many Requests"; 61 | 62 | ["431"] = "Request Header Fields Too Large"; 63 | 64 | ["451"] = "Unavailable For Legal Reasons"; 65 | 66 | ["500"] = "Internal Server Error"; 67 | ["501"] = "Not Implemented"; 68 | ["502"] = "Bad Gateway"; 69 | ["503"] = "Service Unavailable"; 70 | ["504"] = "Gateway Timeout"; 71 | ["505"] = "HTTP Version Not Supported"; 72 | ["506"] = "Variant Also Negotiates"; 73 | ["507"] = "Insufficient Storage"; 74 | ["508"] = "Loop Detected"; 75 | 76 | ["510"] = "Not Extended"; 77 | ["511"] = "Network Authentication Required"; 78 | }, {__index = function() return "Unassigned" end}) 79 | 80 | return reason_phrases 81 | -------------------------------------------------------------------------------- /http/h1_reason_phrases.tld: -------------------------------------------------------------------------------- 1 | reason_phrases: {string:string} 2 | -------------------------------------------------------------------------------- /http/h2_error.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This module implements an error object that can encapsulate the data required of an HTTP2 error. 3 | This data is: 4 | - the error 'name' 5 | - the error 'code' 6 | - the error 'description' 7 | - an optional error message 8 | 9 | Additionally, there is a field for a traceback. 10 | ]] 11 | 12 | local errors = {} 13 | 14 | local http_error_methods = {} 15 | local http_error_mt = { 16 | __name = "http.h2_error"; 17 | __index = http_error_methods; 18 | } 19 | 20 | function http_error_mt:__tostring() 21 | local s = string.format("%s(0x%x): %s", self.name, self.code, self.description) 22 | if self.message then 23 | s = s .. ": " .. self.message 24 | end 25 | if self.traceback then 26 | s = s .. "\n" .. self.traceback 27 | end 28 | return s 29 | end 30 | 31 | function http_error_methods:new(ob) 32 | return setmetatable({ 33 | name = ob.name or self.name; 34 | code = ob.code or self.code; 35 | description = ob.description or self.description; 36 | message = ob.message; 37 | traceback = ob.traceback; 38 | stream_error = ob.stream_error or false; 39 | }, http_error_mt) 40 | end 41 | 42 | function http_error_methods:new_traceback(message, stream_error, lvl) 43 | if lvl == nil then 44 | lvl = 2 45 | elseif lvl ~= 0 then 46 | lvl = lvl + 1 47 | end 48 | local e = { 49 | message = message; 50 | stream_error = stream_error; 51 | } 52 | if lvl ~= 0 then 53 | -- COMPAT: should be passing `nil` message (not the empty string) 54 | -- see https://github.com/keplerproject/lua-compat-5.3/issues/16 55 | e.traceback = debug.traceback("", lvl) 56 | end 57 | 58 | return self:new(e) 59 | end 60 | 61 | function http_error_methods:error(...) 62 | error(self:new_traceback(...), 0) 63 | end 64 | http_error_mt.__call = http_error_methods.error 65 | 66 | function http_error_methods:assert(cond, ...) 67 | if cond then 68 | return cond, ... 69 | else 70 | local message = ... 71 | self:error(message, 2) 72 | -- don't tail call, as error levels aren't well defined 73 | end 74 | end 75 | 76 | local function is(ob) 77 | return getmetatable(ob) == http_error_mt 78 | end 79 | 80 | local function add_error(name, code, description) 81 | local e = setmetatable({ 82 | name = name; 83 | code = code; 84 | description = description; 85 | }, http_error_mt) 86 | errors[name] = e 87 | errors[code] = e 88 | end 89 | 90 | -- Taken from https://http2.github.io/http2-spec/#iana-errors 91 | add_error("NO_ERROR", 0x0, "Graceful shutdown") 92 | add_error("PROTOCOL_ERROR", 0x1, "Protocol error detected") 93 | add_error("INTERNAL_ERROR", 0x2, "Implementation fault") 94 | add_error("FLOW_CONTROL_ERROR", 0x3, "Flow control limits exceeded") 95 | add_error("SETTINGS_TIMEOUT", 0x4, "Settings not acknowledged") 96 | add_error("STREAM_CLOSED", 0x5, "Frame received for closed stream") 97 | add_error("FRAME_SIZE_ERROR", 0x6, "Frame size incorrect") 98 | add_error("REFUSED_STREAM", 0x7, "Stream not processed") 99 | add_error("CANCEL", 0x8, "Stream cancelled") 100 | add_error("COMPRESSION_ERROR", 0x9, "Compression state not updated") 101 | add_error("CONNECT_ERROR", 0xa, "TCP connection error for CONNECT method") 102 | add_error("ENHANCE_YOUR_CALM", 0xb, "Processing capacity exceeded") 103 | add_error("INADEQUATE_SECURITY", 0xc, "Negotiated TLS parameters not acceptable") 104 | add_error("HTTP_1_1_REQUIRED", 0xd, "Use HTTP/1.1 for the request") 105 | 106 | return { 107 | errors = errors; 108 | is = is; 109 | } 110 | -------------------------------------------------------------------------------- /http/h2_error.tld: -------------------------------------------------------------------------------- 1 | interface h2_error 2 | const new: (self, { 3 | "name": string?, 4 | "code": integer?, 5 | "description": string?, 6 | "message": string?, 7 | "traceback": string?, 8 | "stream_error": boolean? 9 | }) -> (h2_error) 10 | const new_traceback: (self, string, boolean, integer?) -> (h2_error) 11 | const error: (self, string, boolean, integer?) -> (void) 12 | end 13 | 14 | errors: {any:h2_error} 15 | is: (any) -> (boolean) 16 | -------------------------------------------------------------------------------- /http/headers.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | HTTP Header data structure/type 3 | 4 | Design criteria: 5 | - the same header field is allowed more than once 6 | - must be able to fetch separate occurences (important for some headers e.g. Set-Cookie) 7 | - optionally available as comma separated list 8 | - http2 adds flag to headers that they should never be indexed 9 | - header order should be recoverable 10 | 11 | I chose to implement headers as an array of entries. 12 | An index of field name => array indices is kept. 13 | ]] 14 | 15 | local unpack = table.unpack or unpack -- luacheck: ignore 113 143 16 | 17 | local entry_methods = {} 18 | local entry_mt = { 19 | __name = "http.headers.entry"; 20 | __index = entry_methods; 21 | } 22 | 23 | local never_index_defaults = { 24 | authorization = true; 25 | ["proxy-authorization"] = true; 26 | cookie = true; 27 | ["set-cookie"] = true; 28 | } 29 | 30 | local function new_entry(name, value, never_index) 31 | if never_index == nil then 32 | never_index = never_index_defaults[name] or false 33 | end 34 | return setmetatable({ 35 | name = name; 36 | value = value; 37 | never_index = never_index; 38 | }, entry_mt) 39 | end 40 | 41 | function entry_methods:modify(value, never_index) 42 | self.value = value 43 | if never_index == nil then 44 | never_index = never_index_defaults[self.name] or false 45 | end 46 | self.never_index = never_index 47 | end 48 | 49 | function entry_methods:unpack() 50 | return self.name, self.value, self.never_index 51 | end 52 | 53 | function entry_methods:clone() 54 | return new_entry(self.name, self.value, self.never_index) 55 | end 56 | 57 | 58 | local headers_methods = {} 59 | local headers_mt = { 60 | __name = "http.headers"; 61 | __index = headers_methods; 62 | } 63 | 64 | local function new_headers() 65 | return setmetatable({ 66 | _n = 0; 67 | _data = {}; 68 | _index = {}; 69 | }, headers_mt) 70 | end 71 | 72 | function headers_methods:len() 73 | return self._n 74 | end 75 | headers_mt.__len = headers_methods.len 76 | 77 | function headers_mt:__tostring() 78 | return string.format("http.headers{%d headers}", self._n) 79 | end 80 | 81 | local function add_to_index(index, name, i) 82 | local dex = index[name] 83 | if dex == nil then 84 | dex = {n=1, i} 85 | index[name] = dex 86 | else 87 | local n = dex.n + 1 88 | dex[n] = i 89 | dex.n = n 90 | end 91 | end 92 | 93 | local function rebuild_index(self) 94 | local index = {} 95 | for i=1, self._n do 96 | local entry = self._data[i] 97 | add_to_index(index, entry.name, i) 98 | end 99 | self._index = index 100 | end 101 | 102 | function headers_methods:clone() 103 | local index, new_data = {}, {} 104 | for i=1, self._n do 105 | local entry = self._data[i] 106 | new_data[i] = entry:clone() 107 | add_to_index(index, entry.name, i) 108 | end 109 | return setmetatable({ 110 | _n = self._n; 111 | _data = new_data; 112 | _index = index; 113 | }, headers_mt) 114 | end 115 | 116 | function headers_methods:append(name, ...) 117 | local n = self._n + 1 118 | self._data[n] = new_entry(name, ...) 119 | add_to_index(self._index, name, n) 120 | self._n = n 121 | end 122 | 123 | function headers_methods:each() 124 | local i = 0 125 | return function(self) -- luacheck: ignore 432 126 | if i >= self._n then return end 127 | i = i + 1 128 | local entry = self._data[i] 129 | return entry:unpack() 130 | end, self 131 | end 132 | headers_mt.__pairs = headers_methods.each 133 | 134 | function headers_methods:has(name) 135 | local dex = self._index[name] 136 | return dex ~= nil 137 | end 138 | 139 | function headers_methods:delete(name) 140 | local dex = self._index[name] 141 | if dex then 142 | local n = dex.n 143 | for i=n, 1, -1 do 144 | table.remove(self._data, dex[i]) 145 | end 146 | self._n = self._n - n 147 | rebuild_index(self) 148 | return true 149 | else 150 | return false 151 | end 152 | end 153 | 154 | function headers_methods:geti(i) 155 | local e = self._data[i] 156 | if e == nil then return nil end 157 | return e:unpack() 158 | end 159 | 160 | function headers_methods:get_as_sequence(name) 161 | local dex = self._index[name] 162 | if dex == nil then return { n = 0; } end 163 | local r = { n = dex.n; } 164 | for i=1, r.n do 165 | r[i] = self._data[dex[i]].value 166 | end 167 | return r 168 | end 169 | 170 | function headers_methods:get(name) 171 | local r = self:get_as_sequence(name) 172 | return unpack(r, 1, r.n) 173 | end 174 | 175 | function headers_methods:get_comma_separated(name) 176 | local r = self:get_as_sequence(name) 177 | if r.n == 0 then 178 | return nil 179 | else 180 | return table.concat(r, ",", 1, r.n) 181 | end 182 | end 183 | 184 | function headers_methods:modifyi(i, ...) 185 | local e = self._data[i] 186 | if e == nil then error("invalid index") end 187 | e:modify(...) 188 | end 189 | 190 | function headers_methods:upsert(name, ...) 191 | local dex = self._index[name] 192 | if dex == nil then 193 | self:append(name, ...) 194 | else 195 | assert(dex[2] == nil, "Cannot upsert multi-valued field") 196 | self:modifyi(dex[1], ...) 197 | end 198 | end 199 | 200 | local function default_cmp(a, b) 201 | if a.name ~= b.name then 202 | -- Things with a colon *must* be before others 203 | local a_is_colon = a.name:sub(1,1) == ":" 204 | local b_is_colon = b.name:sub(1,1) == ":" 205 | if a_is_colon and not b_is_colon then 206 | return true 207 | elseif not a_is_colon and b_is_colon then 208 | return false 209 | else 210 | return a.name < b.name 211 | end 212 | end 213 | if a.value ~= b.value then 214 | return a.value < b.value 215 | end 216 | return a.never_index 217 | end 218 | 219 | function headers_methods:sort() 220 | table.sort(self._data, default_cmp) 221 | rebuild_index(self) 222 | end 223 | 224 | function headers_methods:dump(file, prefix) 225 | file = file or io.stderr 226 | prefix = prefix or "" 227 | for name, value in self:each() do 228 | assert(file:write(string.format("%s%s: %s\n", prefix, name, value))) 229 | end 230 | assert(file:flush()) 231 | end 232 | 233 | return { 234 | new = new_headers; 235 | methods = headers_methods; 236 | mt = headers_mt; 237 | } 238 | -------------------------------------------------------------------------------- /http/headers.tld: -------------------------------------------------------------------------------- 1 | interface headers 2 | const clone: (self) -> (headers) 3 | const append: (self, string, string, boolean?) -> () 4 | const each: (self) -> ((self) -> (string, string, boolean)) 5 | const has: (self, string) -> (boolean) 6 | const delete: (self, string) -> (boolean) 7 | const geti: (self, integer) -> (string, string, boolean) 8 | const get_as_sequence: (self, string) -> ({"n": integer, integer:string}) 9 | const get: (self, string) -> (string*) 10 | const get_comma_separated: (self, string) -> (string|nil) 11 | const modifyi: (self, integer, string, boolean?) -> () 12 | const upsert: (self, string, string, boolean?) -> () 13 | const sort: (self) -> () 14 | const dump: (self, file?, string?) -> () 15 | end 16 | 17 | new : () -> (headers) 18 | -------------------------------------------------------------------------------- /http/hsts.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Data structures useful for HSTS (HTTP Strict Transport Security) 3 | HSTS is described in RFC 6797 4 | ]] 5 | 6 | local binaryheap = require "binaryheap" 7 | local http_util = require "http.util" 8 | 9 | local store_methods = { 10 | time = function() return os.time() end; 11 | max_items = (1e999); 12 | } 13 | 14 | local store_mt = { 15 | __name = "http.hsts.store"; 16 | __index = store_methods; 17 | } 18 | 19 | local store_item_methods = {} 20 | local store_item_mt = { 21 | __name = "http.hsts.store_item"; 22 | __index = store_item_methods; 23 | } 24 | 25 | local function new_store() 26 | return setmetatable({ 27 | domains = {}; 28 | expiry_heap = binaryheap.minUnique(); 29 | n_items = 0; 30 | }, store_mt) 31 | end 32 | 33 | function store_methods:clone() 34 | local r = new_store() 35 | r.time = rawget(self, "time") 36 | r.n_items = rawget(self, "n_items") 37 | r.expiry_heap = binaryheap.minUnique() 38 | for host, item in pairs(self.domains) do 39 | r.domains[host] = item 40 | r.expiry_heap:insert(item.expires, item) 41 | end 42 | return r 43 | end 44 | 45 | function store_methods:store(host, directives) 46 | local now = self.time() 47 | local max_age = directives["max-age"] 48 | if max_age == nil then 49 | return nil, "max-age directive is required" 50 | elseif type(max_age) ~= "string" or max_age:match("[^0-9]") then 51 | return nil, "max-age directive does not match grammar" 52 | else 53 | max_age = tonumber(max_age, 10) 54 | end 55 | 56 | -- Clean now so that we can assume there are no expired items in store 57 | self:clean() 58 | 59 | if max_age == 0 then 60 | return self:remove(host) 61 | else 62 | if http_util.is_ip(host) then 63 | return false 64 | end 65 | -- add to store 66 | local old_item = self.domains[host] 67 | if old_item then 68 | self.expiry_heap:remove(old_item) 69 | else 70 | local n_items = self.n_items 71 | if n_items >= self.max_items then 72 | return false 73 | end 74 | self.n_items = n_items + 1 75 | end 76 | local expires = now + max_age 77 | local item = setmetatable({ 78 | host = host; 79 | includeSubdomains = directives.includeSubdomains; 80 | expires = expires; 81 | }, store_item_mt) 82 | self.domains[host] = item 83 | self.expiry_heap:insert(expires, item) 84 | end 85 | return true 86 | end 87 | 88 | function store_methods:remove(host) 89 | local item = self.domains[host] 90 | if item then 91 | self.expiry_heap:remove(item) 92 | self.domains[host] = nil 93 | self.n_items = self.n_items - 1 94 | end 95 | return true 96 | end 97 | 98 | function store_methods:check(host) 99 | if http_util.is_ip(host) then 100 | return false 101 | end 102 | 103 | -- Clean now so that we can assume there are no expired items in store 104 | self:clean() 105 | 106 | local h = host 107 | repeat 108 | local item = self.domains[h] 109 | if item then 110 | if host == h or item.includeSubdomains then 111 | return true 112 | end 113 | end 114 | local n 115 | h, n = h:gsub("^[^%.]+%.", "", 1) 116 | until n == 0 117 | return false 118 | end 119 | 120 | function store_methods:clean_due() 121 | local next_expiring = self.expiry_heap:peek() 122 | if not next_expiring then 123 | return (1e999) 124 | end 125 | return next_expiring.expires 126 | end 127 | 128 | function store_methods:clean() 129 | local now = self.time() 130 | while self:clean_due() < now do 131 | local item = self.expiry_heap:pop() 132 | self.domains[item.host] = nil 133 | self.n_items = self.n_items - 1 134 | end 135 | return true 136 | end 137 | 138 | return { 139 | new_store = new_store; 140 | store_mt = store_mt; 141 | store_methods = store_methods; 142 | } 143 | -------------------------------------------------------------------------------- /http/hsts.tld: -------------------------------------------------------------------------------- 1 | interface hsts_store 2 | time: () -> (number) 3 | max_items: number 4 | 5 | clone: (self) -> (hsts_store) 6 | store: (self, string, {string:string}) -> (boolean) 7 | remove: (self, string) -> (boolean) 8 | check: (self, hsts_store) -> (boolean) 9 | const clean_due: (self) -> (number) 10 | const clean: (self) -> (boolean) 11 | end 12 | 13 | new_store: () -> (hsts_store) 14 | -------------------------------------------------------------------------------- /http/proxies.lua: -------------------------------------------------------------------------------- 1 | -- Proxy from e.g. environmental variables. 2 | 3 | local proxies_methods = {} 4 | local proxies_mt = { 5 | __name = "http.proxies"; 6 | __index = proxies_methods; 7 | } 8 | 9 | local function new() 10 | return setmetatable({ 11 | http_proxy = nil; 12 | https_proxy = nil; 13 | all_proxy = nil; 14 | no_proxy = nil; 15 | }, proxies_mt) 16 | end 17 | 18 | function proxies_methods:update(getenv) 19 | if getenv == nil then 20 | getenv = os.getenv 21 | end 22 | -- prefers lower case over upper case; except for http_proxy where no upper case 23 | if getenv "GATEWAY_INTERFACE" then -- Mitigate httpoxy. see https://httpoxy.org/ 24 | self.http_proxy = getenv "CGI_HTTP_PROXY" 25 | else 26 | self.http_proxy = getenv "http_proxy" 27 | end 28 | self.https_proxy = getenv "https_proxy" or getenv "HTTPS_PROXY"; 29 | self.all_proxy = getenv "all_proxy" or getenv "ALL_PROXY"; 30 | self.no_proxy = getenv "no_proxy" or getenv "NO_PROXY"; 31 | return self 32 | end 33 | 34 | -- Finds the correct proxy for a given scheme/host 35 | function proxies_methods:choose(scheme, host) 36 | if self.no_proxy == "*" then 37 | return nil 38 | elseif self.no_proxy then 39 | -- cache no_proxy_set by overwriting self.no_proxy 40 | if type(self.no_proxy) == "string" then 41 | local no_proxy_set = {} 42 | -- wget allows domains in no_proxy list to be prefixed by "." 43 | -- e.g. no_proxy=.mit.edu 44 | for host_suffix in self.no_proxy:gmatch("%.?([^,]+)") do 45 | no_proxy_set[host_suffix] = true 46 | end 47 | self.no_proxy = no_proxy_set 48 | end 49 | -- From curl docs: 50 | -- matched as either a domain which contains the hostname, or the 51 | -- hostname itself. For example local.com would match local.com, 52 | -- local.com:80, and www.local.com, but not www.notlocal.com. 53 | for pos in host:gmatch("%f[^%z%.]()") do 54 | local host_suffix = host:sub(pos, -1) 55 | if self.no_proxy[host_suffix] then 56 | return nil 57 | end 58 | end 59 | end 60 | if scheme == "http" then 61 | if self.http_proxy then 62 | return self.http_proxy 63 | end 64 | elseif scheme == "https" then 65 | if self.https_proxy then 66 | return self.https_proxy 67 | end 68 | end 69 | return self.all_proxy 70 | end 71 | 72 | return { 73 | new = new; 74 | methods = proxies_methods; 75 | mt = proxies_mt; 76 | } 77 | -------------------------------------------------------------------------------- /http/proxies.tld: -------------------------------------------------------------------------------- 1 | interface proxies 2 | const update: (self, (string)->(string?))->(self) 3 | const choose: (self, string, string)->(string?) 4 | end 5 | 6 | new: proxies 7 | -------------------------------------------------------------------------------- /http/request.tld: -------------------------------------------------------------------------------- 1 | require "http.cookie" 2 | require "http.hsts" 3 | require "http.proxies" 4 | require "http.stream_common" 5 | 6 | interface request 7 | hsts: hsts_store|false 8 | proxies: proxies|false 9 | cookie_store: cookie_store|false 10 | is_top_level: boolean 11 | site_for_cookies: string? 12 | expect_100_timeout: integer 13 | follow_redirects: boolean 14 | max_redirects: integer 15 | post301: boolean 16 | post302: boolean 17 | headers: headers 18 | const clone: (self) -> (request) 19 | const to_uri: (self, boolean?) -> (string) 20 | const handle_redirect: (self, headers) -> (request)|(nil, string, integer) 21 | const set_body: (self, string|file|()->(string?)) -> () 22 | const go: (self, number) -> (headers, stream)|(nil, string, integer) 23 | end 24 | 25 | new_from_uri: (string, headers?) -> (request) 26 | new_connect: (string, string) -> (request) 27 | -------------------------------------------------------------------------------- /http/stream_common.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This module provides common functions for HTTP streams 3 | no matter the underlying protocol version. 4 | 5 | This is usually an internal module, and should be used by adding the 6 | `methods` exposed to your own HTTP stream objects. 7 | ]] 8 | 9 | local cqueues = require "cqueues" 10 | local monotime = cqueues.monotime 11 | local new_headers = require "http.headers".new 12 | 13 | local CHUNK_SIZE = 2^20 -- write in 1MB chunks 14 | 15 | local stream_methods = {} 16 | 17 | function stream_methods:checktls() 18 | return self.connection:checktls() 19 | end 20 | 21 | function stream_methods:localname() 22 | return self.connection:localname() 23 | end 24 | 25 | function stream_methods:peername() 26 | return self.connection:peername() 27 | end 28 | 29 | -- 100-Continue response 30 | local continue_headers = new_headers() 31 | continue_headers:append(":status", "100") 32 | function stream_methods:write_continue(timeout) 33 | return self:write_headers(continue_headers, false, timeout) 34 | end 35 | 36 | -- need helper to discard 'last' argument 37 | -- (which would otherwise end up going in 'timeout') 38 | local function each_chunk_helper(self) 39 | return self:get_next_chunk() 40 | end 41 | function stream_methods:each_chunk() 42 | return each_chunk_helper, self 43 | end 44 | 45 | function stream_methods:get_body_as_string(timeout) 46 | local deadline = timeout and (monotime()+timeout) 47 | local body, i = {}, 0 48 | while true do 49 | local chunk, err, errno = self:get_next_chunk(timeout) 50 | if chunk == nil then 51 | if err == nil then 52 | break 53 | else 54 | return nil, err, errno 55 | end 56 | end 57 | i = i + 1 58 | body[i] = chunk 59 | timeout = deadline and (deadline-monotime()) 60 | end 61 | return table.concat(body, "", 1, i) 62 | end 63 | 64 | function stream_methods:get_body_chars(n, timeout) 65 | local deadline = timeout and (monotime()+timeout) 66 | local body, i, len = {}, 0, 0 67 | while len < n do 68 | local chunk, err, errno = self:get_next_chunk(timeout) 69 | if chunk == nil then 70 | if err == nil then 71 | break 72 | else 73 | return nil, err, errno 74 | end 75 | end 76 | i = i + 1 77 | body[i] = chunk 78 | len = len + #chunk 79 | timeout = deadline and (deadline-monotime()) 80 | end 81 | if i == 0 then 82 | return nil 83 | end 84 | local r = table.concat(body, "", 1, i) 85 | if n < len then 86 | self:unget(r:sub(n+1, -1)) 87 | r = r:sub(1, n) 88 | end 89 | return r 90 | end 91 | 92 | function stream_methods:get_body_until(pattern, plain, include_pattern, timeout) 93 | local deadline = timeout and (monotime()+timeout) 94 | local body 95 | while true do 96 | local chunk, err, errno = self:get_next_chunk(timeout) 97 | if chunk == nil then 98 | if err == nil then 99 | return body, err 100 | else 101 | return nil, err, errno 102 | end 103 | end 104 | if body then 105 | body = body .. chunk 106 | else 107 | body = chunk 108 | end 109 | local s, e = body:find(pattern, 1, plain) 110 | if s then 111 | if e < #body then 112 | self:unget(body:sub(e+1, -1)) 113 | end 114 | if include_pattern then 115 | return body:sub(1, e) 116 | else 117 | return body:sub(1, s-1) 118 | end 119 | end 120 | timeout = deadline and (deadline-monotime()) 121 | end 122 | end 123 | 124 | function stream_methods:save_body_to_file(file, timeout) 125 | local deadline = timeout and (monotime()+timeout) 126 | while true do 127 | local chunk, err, errno = self:get_next_chunk(timeout) 128 | if chunk == nil then 129 | if err == nil then 130 | break 131 | else 132 | return nil, err, errno 133 | end 134 | end 135 | assert(file:write(chunk)) 136 | timeout = deadline and (deadline-monotime()) 137 | end 138 | return true 139 | end 140 | 141 | function stream_methods:get_body_as_file(timeout) 142 | local file = assert(io.tmpfile()) 143 | local ok, err, errno = self:save_body_to_file(file, timeout) 144 | if not ok then 145 | return nil, err, errno 146 | end 147 | assert(file:seek("set")) 148 | return file 149 | end 150 | 151 | function stream_methods:write_body_from_string(str, timeout) 152 | return self:write_chunk(str, true, timeout) 153 | end 154 | 155 | function stream_methods:write_body_from_file(options, timeout) 156 | local deadline = timeout and (monotime()+timeout) 157 | local file, count 158 | if io.type(options) then -- lua-http <= 0.2 took a file handle 159 | file = options 160 | else 161 | file = options.file 162 | count = options.count 163 | end 164 | if count == nil then 165 | count = math.huge 166 | elseif type(count) ~= "number" or count < 0 or count % 1 ~= 0 then 167 | error("invalid .count parameter (expected positive integer)") 168 | end 169 | while count > 0 do 170 | local chunk, err = file:read(math.min(CHUNK_SIZE, count)) 171 | if chunk == nil then 172 | if err then 173 | error(err) 174 | elseif count ~= math.huge and count > 0 then 175 | error("unexpected EOF") 176 | end 177 | break 178 | end 179 | local ok, err2, errno2 = self:write_chunk(chunk, false, deadline and (deadline-monotime())) 180 | if not ok then 181 | return nil, err2, errno2 182 | end 183 | count = count - #chunk 184 | end 185 | return self:write_chunk("", true, deadline and (deadline-monotime())) 186 | end 187 | 188 | return { 189 | methods = stream_methods; 190 | } 191 | -------------------------------------------------------------------------------- /http/stream_common.tld: -------------------------------------------------------------------------------- 1 | require "http.connection_common" 2 | 3 | interface stream 4 | const checktls: (self) -> (nil)|(any) 5 | const localname: (self) -> (integer, string, integer?)|(nil)|(nil, string, number) 6 | const peername: (self) -> (integer, string, integer?)|(nil)|(nil, string, number) 7 | const write_continue: (self, number?) -> (true)|(nil, string, number) 8 | const each_chunk: (self) -> ((stream)->(string)|(nil)|(nil, string, number), self) 9 | const get_body_as_string: (self, number?) -> (string)|(nil, string, number) 10 | const get_body_chars: (self, integer, number?) -> (string)|(nil, string, number) 11 | const get_body_until: (self, string, boolean, boolean, number?) -> (string)|(nil, string, number) 12 | const save_body_to_file: (self, file, number?) -> (true)|(nil, string, number) 13 | const get_body_as_file: (self, number?) -> (file)|(nil, string, number) 14 | const write_body_from_string: (self, string, number?) -> (true)|(nil, string, number) 15 | const write_body_from_file: (self, {"file":file, "count": integer?}|file, number?) -> (true)|(nil, string, number) 16 | 17 | -- Not in stream_common.lua 18 | const connection: connection 19 | const get_headers: (self, number?) -> (headers)|(nil)|(nil, string, number) 20 | const get_next_chunk: (self, number?) -> (string)|(nil)|(nil, string, number) 21 | const write_headers: (self, headers, boolean, number?) -> (true)|(nil, string, number) 22 | const write_chunk: (self, string, boolean, number?) -> (true)|(nil, string, number) 23 | const unget: (self, string) -> (true) 24 | const shutdown: (self) -> (true) 25 | end 26 | -------------------------------------------------------------------------------- /http/tls.tld: -------------------------------------------------------------------------------- 1 | has_alpn: boolean 2 | has_hostname_validation: boolean 3 | modern_cipher_list: string 4 | intermediate_cipher_list: string 5 | old_cipher_list: string 6 | banned_ciphers: {string: true} 7 | -- TODO: luaossl SSL context type 8 | new_client_context: any 9 | new_server_context: any 10 | -------------------------------------------------------------------------------- /http/util.tld: -------------------------------------------------------------------------------- 1 | encodeURI: (string) -> (string) 2 | encodeURIComponent: (string) -> (string) 3 | decodeURI: (string) -> (string) 4 | decodeURIComponent: (string) -> (string) 5 | query_args: (string) -> ((any) -> (string, string), any, any) 6 | dict_to_query: ({string:string}) -> (string) 7 | resolve_relative_path: (orig_path, relative_path) -> (string) 8 | is_safe_method: (method) -> (boolean) 9 | is_ip: (string) -> (boolean) 10 | scheme_to_port: {string:integer} 11 | split_authority: (string, string) -> (string, integer)|(nil, string) 12 | to_authority: (string, integer, string|nil) -> (string) 13 | imf_date: (time) -> (string) 14 | maybe_quote: (string) -> (string) 15 | yieldable_pcall: ((any*) -> (any*), any*) -> (boolean, any*) 16 | -------------------------------------------------------------------------------- /http/version.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This file contains the lua-http release. 3 | It should be updated as part of the release process 4 | ]] 5 | 6 | return { 7 | name = "lua-http"; 8 | version = "scm"; 9 | } 10 | -------------------------------------------------------------------------------- /http/version.tld: -------------------------------------------------------------------------------- 1 | name: string 2 | version: string 3 | -------------------------------------------------------------------------------- /http/zlib.lua: -------------------------------------------------------------------------------- 1 | -- Two different lua libraries claim the require string "zlib": 2 | -- lua-zlib and lzlib. 3 | -- They have very different APIs, but both provide the raw functionality we need. 4 | -- This module serves to normalise them to a single API 5 | 6 | local zlib = require "zlib" 7 | 8 | local _M = {} 9 | 10 | if zlib._VERSION:match "^lua%-zlib" then 11 | _M.engine = "lua-zlib" 12 | 13 | function _M.inflate() 14 | local stream = zlib.inflate() 15 | local end_of_gzip = false 16 | return function(chunk, end_stream) 17 | -- at end of file, end_of_gzip should have been set on the previous iteration 18 | assert(not end_of_gzip, "stream closed") 19 | chunk, end_of_gzip = stream(chunk) 20 | if end_stream then 21 | assert(end_of_gzip, "invalid stream") 22 | end 23 | return chunk 24 | end 25 | end 26 | 27 | function _M.deflate() 28 | local stream = zlib.deflate() 29 | return function(chunk, end_stream) 30 | local deflated = stream(chunk, end_stream and "finish" or "sync") 31 | return deflated 32 | end 33 | end 34 | elseif zlib._VERSION:match "^lzlib" then 35 | _M.engine = "lzlib" 36 | 37 | function _M.inflate() 38 | -- the function may get called multiple times 39 | local tmp 40 | local stream = zlib.inflate(function() 41 | local chunk = tmp 42 | tmp = nil 43 | return chunk 44 | end) 45 | return function(chunk, end_stream) 46 | -- lzlib doesn't report end of string 47 | tmp = chunk 48 | local data = assert(stream:read("*a")) 49 | if end_stream then 50 | stream:close() 51 | end 52 | return data 53 | end 54 | end 55 | 56 | function _M.deflate() 57 | local buf, n = {}, 0 58 | local stream = zlib.deflate(function(chunk) 59 | n = n + 1 60 | buf[n] = chunk 61 | end) 62 | return function(chunk, end_stream) 63 | stream:write(chunk) 64 | stream:flush() 65 | if end_stream then 66 | -- close performs a "finish" flush 67 | stream:close() 68 | end 69 | if n == 0 then 70 | return "" 71 | else 72 | local s = table.concat(buf, "", 1, n) 73 | buf, n = {}, 0 74 | return s 75 | end 76 | end 77 | end 78 | else 79 | error("unknown zlib library") 80 | end 81 | 82 | return _M 83 | -------------------------------------------------------------------------------- /http/zlib.tld: -------------------------------------------------------------------------------- 1 | inflate: () -> ((string, boolean) -> (string)) 2 | deflate: () -> ((string, boolean) -> (string)) 3 | -------------------------------------------------------------------------------- /spec/client_spec.lua: -------------------------------------------------------------------------------- 1 | describe("http.client module", function() 2 | local client = require "http.client" 3 | local http_connection_common = require "http.connection_common" 4 | local http_h1_connection = require "http.h1_connection" 5 | local http_h2_connection = require "http.h2_connection" 6 | local http_headers = require "http.headers" 7 | local http_tls = require "http.tls" 8 | local cqueues = require "cqueues" 9 | local ca = require "cqueues.auxlib" 10 | local cs = require "cqueues.socket" 11 | local openssl_pkey = require "openssl.pkey" 12 | local openssl_ctx = require "openssl.ssl.context" 13 | local openssl_x509 = require "openssl.x509" 14 | it("invalid network parameters return nil, err, errno", function() 15 | -- Invalid network parameters will return nil, err, errno 16 | local ok, err, errno = client.connect{host="127.0.0.1", port="invalid"} 17 | assert.same(nil, ok) 18 | assert.same("string", type(err)) 19 | assert.same("number", type(errno)) 20 | end) 21 | local function test_pair(client_options, server_func) 22 | local s, c = ca.assert(cs.pair()) 23 | local cq = cqueues.new(); 24 | cq:wrap(function() 25 | local conn = assert(client.negotiate(c, client_options)) 26 | local stream = conn:new_stream() 27 | local req_headers = http_headers.new() 28 | req_headers:append(":authority", "myauthority") 29 | req_headers:append(":method", "GET") 30 | req_headers:append(":path", "/") 31 | req_headers:append(":scheme", client_options.tls and "https" or "http") 32 | assert(stream:write_headers(req_headers, true)) 33 | local res_headers = assert(stream:get_headers()) 34 | assert.same("200", res_headers:get(":status")) 35 | end) 36 | cq:wrap(function() 37 | s = server_func(s) 38 | if not s then return end 39 | if client_options.tls then 40 | local ssl = s:checktls() 41 | assert.same(client_options.sendname, ssl:getHostName()) 42 | end 43 | local stream = assert(s:get_next_incoming_stream()) 44 | assert(stream:get_headers()) 45 | local res_headers = http_headers.new() 46 | res_headers:append(":status", "200") 47 | assert(stream:write_headers(res_headers, true)) 48 | end) 49 | assert_loop(cq, TEST_TIMEOUT) 50 | assert.truthy(cq:empty()) 51 | c:close() 52 | s:close() 53 | end 54 | local function new_server_ctx() 55 | local key = openssl_pkey.new({type="RSA", bits=2048}) 56 | local crt = openssl_x509.new() 57 | crt:setPublicKey(key) 58 | crt:sign(key) 59 | local ctx = http_tls.new_server_context() 60 | assert(ctx:setPrivateKey(key)) 61 | assert(ctx:setCertificate(crt)) 62 | return ctx 63 | end 64 | it("works with an http/1.1 server", function() 65 | test_pair({}, function(s) 66 | return http_h1_connection.new(s, "server", 1.1) 67 | end) 68 | end) 69 | it("works with an http/2 server", function() 70 | test_pair({ 71 | version = 2; 72 | }, function(s) 73 | return http_h2_connection.new(s, "server", {}) 74 | end) 75 | end) 76 | it("fails with unknown http version", function() 77 | assert.has.error(function() 78 | test_pair({ 79 | version = 5; 80 | }, function() end) 81 | end) 82 | end) 83 | it("works with an https/1.1 server", function() 84 | local client_ctx = http_tls.new_client_context() 85 | client_ctx:setVerify(openssl_ctx.VERIFY_NONE) 86 | test_pair({ 87 | tls = true; 88 | ctx = client_ctx; 89 | sendname = "mysendname"; 90 | }, function(s) 91 | assert(s:starttls(new_server_ctx())) 92 | return http_h1_connection.new(s, "server", 1.1) 93 | end) 94 | end) 95 | -- pending as older openssl (used by e.g. travis-ci) doesn't have any non-disallowed ciphers 96 | pending("works with an https/2 server", function() 97 | local client_ctx = http_tls.new_client_context() 98 | client_ctx:setVerify(openssl_ctx.VERIFY_NONE) 99 | test_pair({ 100 | tls = true; 101 | ctx = client_ctx; 102 | sendname = "mysendname"; 103 | version = 2; 104 | }, function(s) 105 | assert(s:starttls(new_server_ctx())) 106 | return http_h2_connection.new(s, "server", {}) 107 | end) 108 | end) 109 | it("reports errors from :starttls", function() 110 | -- default settings should fail as it should't allow self-signed 111 | local s, c = ca.assert(cs.pair()) 112 | local cq = cqueues.new(); 113 | cq:wrap(function() 114 | local ok, err = client.negotiate(c, { 115 | tls = true; 116 | }) 117 | assert.falsy(ok) 118 | assert.truthy(err:match("starttls: ")) 119 | end) 120 | cq:wrap(function() 121 | s:onerror(http_connection_common.onerror) 122 | local ok, err = s:starttls() 123 | assert.falsy(ok) 124 | assert.truthy(err:match("starttls: ")) 125 | end) 126 | assert_loop(cq, TEST_TIMEOUT) 127 | assert.truthy(cq:empty()) 128 | c:close() 129 | s:close() 130 | end) 131 | end) 132 | -------------------------------------------------------------------------------- /spec/compat_prosody_spec.lua: -------------------------------------------------------------------------------- 1 | describe("http.compat.prosody module", function() 2 | local cqueues = require "cqueues" 3 | local request = require "http.compat.prosody".request 4 | local new_headers = require "http.headers".new 5 | local server = require "http.server" 6 | it("invalid uris fail", function() 7 | local s = spy.new(function() end) 8 | assert(cqueues.new():wrap(function() 9 | assert.same({nil, "invalid-url"}, {request("this is not a url", {}, s)}) 10 | end):loop()) 11 | assert.spy(s).was.called() 12 | end) 13 | it("can construct a request from a uri", function() 14 | -- Only step; not loop. use `error` as callback as it should never be called 15 | assert(cqueues.new():wrap(function() 16 | assert(request("http://example.com", {}, error)) 17 | end):step()) 18 | assert(cqueues.new():wrap(function() 19 | local r = assert(request("http://example.com/something", { 20 | method = "PUT"; 21 | body = '{}'; 22 | headers = { 23 | ["content-type"] = "application/json"; 24 | } 25 | }, error)) 26 | assert.same("PUT", r.headers:get(":method")) 27 | assert.same("application/json", r.headers:get("content-type")) 28 | assert.same("2", r.headers:get("content-length")) 29 | assert.same("{}", r.body) 30 | end):step()) 31 | end) 32 | it("can perform a GET request", function() 33 | local cq = cqueues.new() 34 | local s = server.listen { 35 | host = "localhost"; 36 | port = 0; 37 | onstream = function(s, stream) 38 | local h = assert(stream:get_headers()) 39 | assert.same("http", h:get ":scheme") 40 | assert.same("GET", h:get ":method") 41 | assert.same("/", h:get ":path") 42 | local headers = new_headers() 43 | headers:append(":status", "200") 44 | headers:append("connection", "close") 45 | assert(stream:write_headers(headers, false)) 46 | assert(stream:write_chunk("success!", true)) 47 | stream:shutdown() 48 | stream.connection:shutdown() 49 | s:close() 50 | end; 51 | } 52 | assert(s:listen()) 53 | local _, host, port = s:localname() 54 | cq:wrap(function() 55 | assert_loop(s) 56 | end) 57 | cq:wrap(function() 58 | request(string.format("http://%s:%d", host, port), {}, function(b, c) 59 | assert.same(200, c) 60 | assert.same("success!", b) 61 | end) 62 | end) 63 | assert_loop(cq, TEST_TIMEOUT) 64 | assert.truthy(cq:empty()) 65 | end) 66 | it("can perform a POST request", function() 67 | local cq = cqueues.new() 68 | local s = server.listen { 69 | host = "localhost"; 70 | port = 0; 71 | onstream = function(s, stream) 72 | local h = assert(stream:get_headers()) 73 | assert.same("http", h:get ":scheme") 74 | assert.same("POST", h:get ":method") 75 | assert.same("/path", h:get ":path") 76 | assert.same("text/plain", h:get "content-type") 77 | local b = assert(stream:get_body_as_string()) 78 | assert.same("this is a body", b) 79 | local headers = new_headers() 80 | headers:append(":status", "201") 81 | headers:append("connection", "close") 82 | -- send duplicate headers 83 | headers:append("someheader", "foo") 84 | headers:append("someheader", "bar") 85 | assert(stream:write_headers(headers, false)) 86 | assert(stream:write_chunk("success!", true)) 87 | stream:shutdown() 88 | stream.connection:shutdown() 89 | s:close() 90 | end; 91 | } 92 | assert(s:listen()) 93 | local _, host, port = s:localname() 94 | cq:wrap(function() 95 | assert_loop(s) 96 | end) 97 | cq:wrap(function() 98 | request(string.format("http://%s:%d/path", host, port), { 99 | headers = { 100 | ["content-type"] = "text/plain"; 101 | }; 102 | body = "this is a body"; 103 | }, function(b, c, r) 104 | assert.same(201, c) 105 | assert.same("success!", b) 106 | assert.same("foo,bar", r.headers.someheader) 107 | end) 108 | end) 109 | assert_loop(cq, TEST_TIMEOUT) 110 | assert.truthy(cq:empty()) 111 | end) 112 | end) 113 | -------------------------------------------------------------------------------- /spec/compat_socket_spec.lua: -------------------------------------------------------------------------------- 1 | describe("http.compat.socket module", function() 2 | local http = require "http.compat.socket" 3 | local new_headers = require "http.headers".new 4 | local server = require "http.server" 5 | local util = require "http.util" 6 | local cqueues = require "cqueues" 7 | it("fails safely on an invalid host", function() 8 | -- in the luasocket example they use 'wrong.host', but 'host' is now a valid TLD. 9 | -- use 'wrong.invalid' instead for this test. 10 | local r, e = http.request("http://wrong.invalid/") 11 | assert.same(nil, r) 12 | -- in luasocket, the error is documented as "host not found", but we allow something else 13 | assert.same("string", type(e)) 14 | end) 15 | it("works against builtin server with GET request", function() 16 | local cq = cqueues.new() 17 | local authority 18 | local s = server.listen { 19 | host = "localhost"; 20 | port = 0; 21 | onstream = function(s, stream) 22 | local request_headers = assert(stream:get_headers()) 23 | assert.same("http", request_headers:get ":scheme") 24 | assert.same("GET", request_headers:get ":method") 25 | assert.same("/foo", request_headers:get ":path") 26 | assert.same(authority, request_headers:get ":authority") 27 | local headers = new_headers() 28 | headers:append(":status", "200") 29 | headers:append("connection", "close") 30 | assert(stream:write_headers(headers, false)) 31 | assert(stream:write_chunk("hello world", true)) 32 | s:close() 33 | end; 34 | } 35 | assert(s:listen()) 36 | local _, host, port = s:localname() 37 | authority = util.to_authority(host, port, "http") 38 | cq:wrap(function() 39 | assert_loop(s) 40 | end) 41 | cq:wrap(function() 42 | local r, e = http.request("http://"..authority.."/foo") 43 | assert.same("hello world", r) 44 | assert.same(200, e) 45 | end) 46 | assert_loop(cq, TEST_TIMEOUT) 47 | assert.truthy(cq:empty()) 48 | end) 49 | it("works against builtin server with POST request", function() 50 | local cq = cqueues.new() 51 | local authority 52 | local s = server.listen { 53 | host = "localhost"; 54 | port = 0; 55 | onstream = function(s, stream) 56 | local request_headers = assert(stream:get_headers()) 57 | assert.same("http", request_headers:get ":scheme") 58 | assert.same("POST", request_headers:get ":method") 59 | assert.same("/foo", request_headers:get ":path") 60 | assert.same(authority, request_headers:get ":authority") 61 | local body = assert(stream:get_body_as_string()) 62 | assert.same("a body", body) 63 | local headers = new_headers() 64 | headers:append(":status", "201") 65 | headers:append("connection", "close") 66 | assert(stream:write_headers(headers, false)) 67 | assert(stream:write_chunk("hello world", true)) 68 | s:close() 69 | end; 70 | } 71 | assert(s:listen()) 72 | local _, host, port = s:localname() 73 | authority = util.to_authority(host, port, "http") 74 | cq:wrap(function() 75 | assert_loop(s) 76 | end) 77 | cq:wrap(function() 78 | local r, e = http.request("http://"..authority.."/foo", "a body") 79 | assert.same("hello world", r) 80 | assert.same(201, e) 81 | end) 82 | assert_loop(cq, TEST_TIMEOUT) 83 | assert.truthy(cq:empty()) 84 | end) 85 | it("works against builtin server with complex request", function() 86 | local cq = cqueues.new() 87 | local s = server.listen { 88 | host = "localhost"; 89 | port = 0; 90 | onstream = function(s, stream) 91 | local a, b = stream:get_headers() 92 | local request_headers = assert(a,b) 93 | assert.same("http", request_headers:get ":scheme") 94 | assert.same("PUT", request_headers:get ":method") 95 | assert.same("/path?query", request_headers:get ":path") 96 | assert.same("otherhost.com:8080", request_headers:get ":authority") 97 | assert.same("fun", request_headers:get "myheader") 98 | assert.same("normalised", request_headers:get "camelcase") 99 | assert(stream:write_continue()) 100 | local body = assert(stream:get_body_as_string()) 101 | assert.same("a body", body) 102 | local headers = new_headers() 103 | headers:append(":status", "404") 104 | headers:append("connection", "close") 105 | assert(stream:write_headers(headers, false)) 106 | assert(stream:write_chunk("hello world", true)) 107 | s:close() 108 | end; 109 | } 110 | assert(s:listen()) 111 | cq:wrap(function() 112 | assert_loop(s) 113 | end) 114 | cq:wrap(function() 115 | local _, host, port = s:localname() 116 | local r, e = assert(http.request { 117 | url = "http://example.com/path?query"; 118 | host = host; 119 | port = port; 120 | method = "PUT"; 121 | headers = { 122 | host = "otherhost.com:8080"; 123 | myheader = "fun"; 124 | CamelCase = "normalised"; 125 | }; 126 | source = coroutine.wrap(function() 127 | coroutine.yield("a body") 128 | end); 129 | sink = coroutine.wrap(function(b) 130 | assert.same("hello world", b) 131 | assert.same(nil, coroutine.yield(true)) 132 | end); 133 | }) 134 | assert.same(1, r) 135 | assert.same(404, e) 136 | end) 137 | assert_loop(cq, TEST_TIMEOUT) 138 | assert.truthy(cq:empty()) 139 | end) 140 | it("returns nil, 'timeout' on timeout", function() 141 | local cq = cqueues.new() 142 | local authority 143 | local s = server.listen { 144 | host = "localhost"; 145 | port = 0; 146 | onstream = function(s, stream) 147 | assert(stream:get_headers()) 148 | cqueues.sleep(0.2) 149 | stream:shutdown() 150 | s:close() 151 | end; 152 | } 153 | assert(s:listen()) 154 | local _, host, port = s:localname() 155 | authority = util.to_authority(host, port, "http") 156 | cq:wrap(function() 157 | assert_loop(s) 158 | end) 159 | cq:wrap(function() 160 | local old_TIMEOUT = http.TIMEOUT 161 | http.TIMEOUT = 0.01 162 | local r, e = http.request("http://"..authority.."/") 163 | http.TIMEOUT = old_TIMEOUT 164 | assert.same(nil, r) 165 | assert.same("timeout", e) 166 | end) 167 | assert_loop(cq, TEST_TIMEOUT) 168 | assert.truthy(cq:empty()) 169 | end) 170 | it("handles timeouts in complex form", function() 171 | local cq = cqueues.new() 172 | local s = server.listen { 173 | host = "localhost"; 174 | port = 0; 175 | onstream = function(s, stream) 176 | local a, b = stream:get_headers() 177 | local request_headers = assert(a,b) 178 | assert.same("http", request_headers:get ":scheme") 179 | assert.same("GET", request_headers:get ":method") 180 | assert.same("/path?query", request_headers:get ":path") 181 | assert.same("example.com", request_headers:get ":authority") 182 | cqueues.sleep(0.2) 183 | s:close() 184 | end; 185 | } 186 | assert(s:listen()) 187 | cq:wrap(function() 188 | assert_loop(s) 189 | end) 190 | cq:wrap(function() 191 | local _, host, port = s:localname() 192 | local old_TIMEOUT = http.TIMEOUT 193 | http.TIMEOUT = 0.01 194 | local r, e = http.request { 195 | url = "http://example.com/path?query"; 196 | host = host; 197 | port = port; 198 | } 199 | http.TIMEOUT = old_TIMEOUT 200 | assert.same(nil, r) 201 | assert.same("timeout", e) 202 | end) 203 | assert_loop(cq, TEST_TIMEOUT) 204 | assert.truthy(cq:empty()) 205 | end) 206 | it("coerces numeric header values to strings", function() 207 | local cq = cqueues.new() 208 | local s = server.listen { 209 | host = "localhost"; 210 | port = 0; 211 | onstream = function(s, stream) 212 | local request_headers = assert(stream:get_headers()) 213 | assert.truthy(request_headers:has("myheader")) 214 | local headers = new_headers() 215 | headers:append(":status", "200") 216 | headers:append("connection", "close") 217 | assert(stream:write_headers(headers, true)) 218 | s:close() 219 | end; 220 | } 221 | assert(s:listen()) 222 | cq:wrap(function() 223 | assert_loop(s) 224 | end) 225 | cq:wrap(function() 226 | local _, host, port = s:localname() 227 | local r, e = assert(http.request { 228 | url = "http://anything/"; 229 | host = host; 230 | port = port; 231 | headers = { 232 | myheader = 2; 233 | }; 234 | }) 235 | assert.same(1, r) 236 | assert.same(200, e) 237 | end) 238 | assert_loop(cq, TEST_TIMEOUT) 239 | assert.truthy(cq:empty()) 240 | end) 241 | end) 242 | -------------------------------------------------------------------------------- /spec/h2_error_spec.lua: -------------------------------------------------------------------------------- 1 | describe("", function() 2 | local h2_error = require "http.h2_error" 3 | it("has the registered errors", function() 4 | for i=0, 0xd do 5 | -- indexed by code 6 | assert.same(i, h2_error.errors[i].code) 7 | -- and indexed by name 8 | assert.same(h2_error.errors[i], h2_error.errors[h2_error.errors[i].name]) 9 | end 10 | end) 11 | it("has a nice tostring", function() 12 | local e = h2_error.errors[0]:new{ 13 | message = "oops"; 14 | traceback = "some traceback"; 15 | } 16 | assert.same("NO_ERROR(0x0): Graceful shutdown: oops\nsome traceback", tostring(e)) 17 | end) 18 | it("`is` function works", function() 19 | assert.truthy(h2_error.is(h2_error.errors[0])) 20 | assert.falsy(h2_error.is({})) 21 | assert.falsy(h2_error.is("string")) 22 | assert.falsy(h2_error.is(1)) 23 | assert.falsy(h2_error.is(coroutine.create(function()end))) 24 | assert.falsy(h2_error.is(io.stdin)) 25 | end) 26 | it("throws errors when called", function() 27 | assert.has.errors(function() h2_error.errors[0]("oops", false, 0) end, { 28 | name = "NO_ERROR"; 29 | code = 0; 30 | description = "Graceful shutdown"; 31 | message = "oops"; 32 | stream_error = false; 33 | }) 34 | end) 35 | it("adds a traceback field", function() 36 | local ok, err = pcall(h2_error.errors[0]) 37 | assert.falsy(ok) 38 | assert.truthy(err.traceback) 39 | end) 40 | it(":assert works", function() 41 | assert.falsy(pcall(h2_error.errors[0].assert, h2_error.errors[0], false)) 42 | assert.truthy(pcall(h2_error.errors[0].assert, h2_error.errors[0], true)) 43 | end) 44 | it(":assert adds a traceback field", function() 45 | local ok, err = pcall(h2_error.errors[0].assert, h2_error.errors[0], false) 46 | assert.falsy(ok) 47 | assert.truthy(err.traceback) 48 | end) 49 | end) 50 | -------------------------------------------------------------------------------- /spec/headers_spec.lua: -------------------------------------------------------------------------------- 1 | describe("http.headers module", function() 2 | local headers = require "http.headers" 3 | it("__tostring works", function() 4 | local h = headers.new() 5 | assert.same("http.headers{", tostring(h):match("^.-%{")) 6 | end) 7 | it("multiple values can be added for same key", function() 8 | local h = headers.new() 9 | h:append("a", "a", false) 10 | h:append("a", "b", false) 11 | h:append("foo", "bar", true) 12 | h:append("a", "c", false) 13 | h:append("a", "a", true) 14 | local iter, state = h:each() 15 | assert.same({"a", "a", false}, {iter(state)}) 16 | assert.same({"a", "b", false}, {iter(state)}) 17 | assert.same({"foo", "bar", true}, {iter(state)}) 18 | assert.same({"a", "c", false}, {iter(state)}) 19 | assert.same({"a", "a", true}, {iter(state)}) 20 | end) 21 | it("entries are kept in order", function() 22 | local h = headers.new() 23 | h:append("a", "a", false) 24 | h:append("b", "b", true) 25 | h:append("c", "c", false) 26 | h:append("d", "d", true) 27 | h:append("d", "d", true) -- twice 28 | h:append("e", "e", false) 29 | local iter, state = h:each() 30 | assert.same({"a", "a", false}, {iter(state)}) 31 | assert.same({"b", "b", true}, {iter(state)}) 32 | assert.same({"c", "c", false}, {iter(state)}) 33 | assert.same({"d", "d", true}, {iter(state)}) 34 | assert.same({"d", "d", true}, {iter(state)}) 35 | assert.same({"e", "e", false}, {iter(state)}) 36 | end) 37 | it(":clone works", function() 38 | local h = headers.new() 39 | h:append("a", "a", false) 40 | h:append("b", "b", true) 41 | h:append("c", "c", false) 42 | local j = h:clone() 43 | assert.same(h, j) 44 | end) 45 | it(":has works", function() 46 | local h = headers.new() 47 | assert.same(h:has("a"), false) 48 | h:append("a", "a") 49 | assert.same(h:has("a"), true) 50 | assert.same(h:has("b"), false) 51 | end) 52 | it(":delete works", function() 53 | local h = headers.new() 54 | assert.falsy(h:delete("a")) 55 | h:append("a", "a") 56 | assert.truthy(h:has("a")) 57 | assert.truthy(h:delete("a")) 58 | assert.falsy(h:has("a")) 59 | assert.falsy(h:delete("a")) 60 | end) 61 | it(":get_comma_separated works", function() 62 | local h = headers.new() 63 | assert.same(nil, h:get_comma_separated("a")) 64 | h:append("a", "a") 65 | h:append("a", "b") 66 | h:append("a", "c") 67 | assert.same("a,b,c", h:get_comma_separated("a")) 68 | end) 69 | it(":modifyi works", function() 70 | local h = headers.new() 71 | h:append("key", "val") 72 | assert.same("val", h:get("key")) 73 | h:modifyi(1, "val") 74 | assert.same("val", h:get("key")) 75 | h:modifyi(1, "val2") 76 | assert.same("val2", h:get("key")) 77 | assert.has.errors(function() h:modifyi(2, "anything") end) 78 | end) 79 | it(":upsert works", function() 80 | local h = headers.new() 81 | h:append("a", "a", false) 82 | h:append("b", "b", true) 83 | h:append("c", "c", false) 84 | assert.same(3, h:len()) 85 | h:upsert("b", "foo", false) 86 | assert.same(3, h:len()) 87 | assert.same("foo", h:get("b")) 88 | h:upsert("d", "d", false) 89 | assert.same(4, h:len()) 90 | local iter, state = h:each() 91 | assert.same({"a", "a", false}, {iter(state)}) 92 | assert.same({"b", "foo", false}, {iter(state)}) 93 | assert.same({"c", "c", false}, {iter(state)}) 94 | assert.same({"d", "d", false}, {iter(state)}) 95 | end) 96 | it(":upsert fails on multi-valued field", function() 97 | local h = headers.new() 98 | h:append("a", "a") 99 | h:append("a", "b") 100 | assert.has.errors(function() h:upsert("a", "something else") end) 101 | end) 102 | it("never_index defaults to sensible boolean", function() 103 | local h = headers.new() 104 | h:append("content-type", "application/json") 105 | h:append("authorization", "supersecret") 106 | assert.same({"content-type", "application/json", false}, {h:geti(1)}) 107 | assert.same({"authorization", "supersecret", true}, {h:geti(2)}) 108 | h:upsert("authorization", "different secret") 109 | assert.same({"authorization", "different secret", true}, {h:geti(2)}) 110 | end) 111 | it(":sort works", function() 112 | -- should sort first by field name (':' first), then value, then never_index 113 | local h = headers.new() 114 | h:append("z", "1") 115 | h:append("b", "3") 116 | h:append("z", "2") 117 | h:append(":special", "!") 118 | h:append("a", "5") 119 | h:append("z", "6", true) 120 | for _=1, 2 do -- do twice to ensure consistency 121 | h:sort() 122 | assert.same({":special", "!", false}, {h:geti(1)}) 123 | assert.same({"a", "5", false}, {h:geti(2)}) 124 | assert.same({"b", "3", false}, {h:geti(3)}) 125 | assert.same({"z", "1", false}, {h:geti(4)}) 126 | assert.same({"z", "2", false}, {h:geti(5)}) 127 | assert.same({"z", "6", true }, {h:geti(6)}) 128 | end 129 | end) 130 | end) 131 | -------------------------------------------------------------------------------- /spec/helper.lua: -------------------------------------------------------------------------------- 1 | TEST_TIMEOUT = 10 2 | 3 | function assert_loop(cq, timeout) 4 | local ok, err, _, thd = cq:loop(timeout) 5 | if not ok then 6 | if thd then 7 | err = debug.traceback(thd, err) 8 | end 9 | error(err, 2) 10 | end 11 | end 12 | 13 | -- Solves https://github.com/keplerproject/luacov/issues/38 14 | local cqueues = require "cqueues" 15 | local has_luacov, luacov_runner = pcall(require, "luacov.runner") 16 | if has_luacov then 17 | local wrap; wrap = cqueues.interpose("wrap", function(self, func, ...) 18 | func = luacov_runner.with_luacov(func) 19 | return wrap(self, func, ...) 20 | end) 21 | end 22 | 23 | -- Allow tests to pick up configured locale 24 | local locale = os.getenv("LOCALE") 25 | if locale then 26 | os.setlocale(locale) 27 | if locale ~= os.setlocale(locale) then 28 | print("Locale " .. locale .. " is not available.") 29 | os.exit(1) -- busted doesn't fail if helper script throws errors: https://github.com/Olivine-Labs/busted/issues/549 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/hsts_spec.lua: -------------------------------------------------------------------------------- 1 | describe("hsts module", function() 2 | local http_hsts = require "http.hsts" 3 | it("doesn't store ip addresses", function() 4 | local s = http_hsts.new_store() 5 | assert.falsy(s:store("127.0.0.1", { 6 | ["max-age"] = "100"; 7 | })) 8 | assert.falsy(s:check("127.0.0.1")) 9 | end) 10 | it("can be cloned", function() 11 | local s = http_hsts.new_store() 12 | do 13 | local clone = s:clone() 14 | local old_heap = s.expiry_heap 15 | s.expiry_heap = nil 16 | clone.expiry_heap = nil 17 | assert.same(s, clone) 18 | s.expiry_heap = old_heap 19 | end 20 | assert.truthy(s:store("foo.example.com", { 21 | ["max-age"] = "100"; 22 | })) 23 | do 24 | local clone = s:clone() 25 | local old_heap = s.expiry_heap 26 | s.expiry_heap = nil 27 | clone.expiry_heap = nil 28 | assert.same(s, clone) 29 | s.expiry_heap = old_heap 30 | end 31 | local clone = s:clone() 32 | assert.truthy(s:check("foo.example.com")) 33 | assert.truthy(clone:check("foo.example.com")) 34 | end) 35 | it("rejects :store() when max-age directive is missing", function() 36 | local s = http_hsts.new_store() 37 | assert.falsy(s:store("foo.example.com", {})) 38 | assert.falsy(s:check("foo.example.com")) 39 | end) 40 | it("rejects :store() when max-age directive is invalid", function() 41 | local s = http_hsts.new_store() 42 | assert.falsy(s:store("foo.example.com", { 43 | ["max-age"] = "-1"; 44 | })) 45 | assert.falsy(s:check("foo.example.com")) 46 | end) 47 | it("erases on max-age == 0", function() 48 | local s = http_hsts.new_store() 49 | assert.truthy(s:store("foo.example.com", { 50 | ["max-age"] = "100"; 51 | })) 52 | assert.truthy(s:check("foo.example.com")) 53 | assert.truthy(s:store("foo.example.com", { 54 | ["max-age"] = "0"; 55 | })) 56 | assert.falsy(s:check("foo.example.com")) 57 | end) 58 | it("respects includeSubdomains", function() 59 | local s = http_hsts.new_store() 60 | assert(s:store("foo.example.com", { 61 | ["max-age"] = "100"; 62 | includeSubdomains = true; 63 | })) 64 | assert.truthy(s:check("foo.example.com")) 65 | assert.truthy(s:check("qaz.bar.foo.example.com")) 66 | assert.falsy(s:check("example.com")) 67 | assert.falsy(s:check("other.com")) 68 | end) 69 | it("removes expired entries on :clean()", function() 70 | local s = http_hsts.new_store() 71 | assert(s:store("foo.example.com", { 72 | ["max-age"] = "100"; 73 | })) 74 | assert(s:store("other.com", { 75 | ["max-age"] = "200"; 76 | })) 77 | assert(s:store("keep.me", { 78 | ["max-age"] = "100000"; 79 | })) 80 | -- Set clock forward 81 | local now = s.time() 82 | s.time = function() return now+1000 end 83 | assert.truthy(s:clean()) 84 | assert.falsy(s:check("qaz.bar.foo.example.com")) 85 | assert.falsy(s:check("foo.example.com")) 86 | assert.falsy(s:check("example.com")) 87 | assert.truthy(s:check("keep.me")) 88 | end) 89 | it("cleans out expired entries automatically", function() 90 | local s = http_hsts.new_store() 91 | assert(s:store("foo.example.com", { 92 | ["max-age"] = "100"; 93 | })) 94 | assert(s:store("other.com", { 95 | ["max-age"] = "200"; 96 | })) 97 | assert(s:store("keep.me", { 98 | ["max-age"] = "100000"; 99 | })) 100 | -- Set clock forward 101 | local now = s.time() 102 | s.time = function() return now+1000 end 103 | assert.falsy(s:check("qaz.bar.foo.example.com")) 104 | -- Set clock back to current; everything should have been cleaned out already. 105 | s.time = function() return now end 106 | assert.falsy(s:check("foo.example.com")) 107 | assert.falsy(s:check("example.com")) 108 | assert.truthy(s:check("keep.me")) 109 | end) 110 | it("enforces .max_items", function() 111 | local s = http_hsts.new_store() 112 | s.max_items = 0 113 | assert.falsy(s:store("example.com", { 114 | ["max-age"] = "100"; 115 | })) 116 | s.max_items = 1 117 | assert.truthy(s:store("example.com", { 118 | ["max-age"] = "100"; 119 | })) 120 | assert.falsy(s:store("other.com", { 121 | ["max-age"] = "100"; 122 | })) 123 | s:remove("example.com", "/", "foo") 124 | assert.truthy(s:store("other.com", { 125 | ["max-age"] = "100"; 126 | })) 127 | end) 128 | end) 129 | -------------------------------------------------------------------------------- /spec/path_spec.lua: -------------------------------------------------------------------------------- 1 | describe("Relative path resolution", function() 2 | local resolve_relative_path = require "http.util".resolve_relative_path 3 | it("should resolve .. correctly", function() 4 | assert.same("/foo", resolve_relative_path("/", "foo")) 5 | assert.same("/foo", resolve_relative_path("/", "./foo")) 6 | assert.same("/foo", resolve_relative_path("/", "../foo")) 7 | assert.same("/foo", resolve_relative_path("/", "../foo/../foo")) 8 | assert.same("/foo", resolve_relative_path("/", "foo/bar/..")) 9 | assert.same("/foo/", resolve_relative_path("/", "foo/bar/../")) 10 | assert.same("/foo/", resolve_relative_path("/", "foo/bar/../")) 11 | assert.same("/", resolve_relative_path("/", "../..")) 12 | assert.same("/", resolve_relative_path("/", "../../")) 13 | assert.same("/bar", resolve_relative_path("/foo/", "../bar")) 14 | assert.same("bar", resolve_relative_path("foo/", "../bar")) 15 | assert.same("bar/", resolve_relative_path("foo/", "../bar/")) 16 | end) 17 | it("should ignore .", function() 18 | assert.same("/", resolve_relative_path("/", ".")) 19 | assert.same("/", resolve_relative_path("/", "./././.")) 20 | assert.same("/", resolve_relative_path("/", "././././")) 21 | assert.same("/foo/bar/", resolve_relative_path("/foo/", "bar/././././")) 22 | end) 23 | it("should keep leading and trailing /", function() 24 | assert.same("/foo/", resolve_relative_path("/foo/", "./")) 25 | assert.same("foo/", resolve_relative_path("foo/", "./")) 26 | assert.same("/foo", resolve_relative_path("/foo/", ".")) 27 | assert.same("foo", resolve_relative_path("foo/", ".")) 28 | end) 29 | it("an absolute path as 2nd arg should be resolved", function() 30 | assert.same("/foo", resolve_relative_path("ignored", "/foo")) 31 | assert.same("/foo", resolve_relative_path("ignored", "/foo/./.")) 32 | assert.same("/foo", resolve_relative_path("ignored", "/foo/bar/..")) 33 | assert.same("/foo", resolve_relative_path("ignored", "/foo/bar/qux/./../././..")) 34 | assert.same("/foo/", resolve_relative_path("ignored", "/foo/././")) 35 | end) 36 | it("cannot go above root level", function() 37 | assert.same("/bar", resolve_relative_path("/", "../bar")) 38 | assert.same("/bar", resolve_relative_path("/foo", "../../../../bar")) 39 | assert.same("/bar", resolve_relative_path("/foo", "./../../../../bar")) 40 | assert.same("/", resolve_relative_path("/foo", "./../../../../")) 41 | assert.same("/", resolve_relative_path("/", "..")) 42 | assert.same("", resolve_relative_path("", "..")) 43 | assert.same("", resolve_relative_path("", "./..")) 44 | assert.same("bar", resolve_relative_path("", "../bar")) 45 | end) 46 | end) 47 | -------------------------------------------------------------------------------- /spec/proxies_spec.lua: -------------------------------------------------------------------------------- 1 | describe("http.proxies module", function() 2 | local http_proxies = require "http.proxies" 3 | 4 | it("works", function() 5 | local proxies = http_proxies.new():update(function(k) return ({ 6 | http_proxy = "http://http.proxy"; 7 | https_proxy = "http://https.proxy"; 8 | all_proxy = "http://all.proxy"; 9 | no_proxy = nil; 10 | })[k] end) 11 | assert.same({ 12 | http_proxy = "http://http.proxy"; 13 | https_proxy = "http://https.proxy"; 14 | all_proxy = "http://all.proxy"; 15 | no_proxy = nil; 16 | }, proxies) 17 | assert.same("http://http.proxy", proxies:choose("http", "myhost")) 18 | assert.same("http://https.proxy", proxies:choose("https", "myhost")) 19 | assert.same("http://all.proxy", proxies:choose("other", "myhost")) 20 | end) 21 | it("isn't vulnerable to httpoxy", function() 22 | assert.same({}, http_proxies.new():update(function(k) return ({ 23 | GATEWAY_INTERFACE = "CGI/1.1"; 24 | http_proxy = "vulnerable to httpoxy"; 25 | })[k] end)) 26 | end) 27 | it("works with no_proxy set to *", function() 28 | local proxies = http_proxies.new():update(function(k) return ({ 29 | http_proxy = "http://http.proxy"; 30 | https_proxy = "http://https.proxy"; 31 | all_proxy = "http://all.proxy"; 32 | no_proxy = "*"; 33 | })[k] end) 34 | -- Should return nil due to no_proxy being * 35 | assert.same(nil, proxies:choose("http", "myhost")) 36 | assert.same(nil, proxies:choose("https", "myhost")) 37 | assert.same(nil, proxies:choose("other", "myhost")) 38 | end) 39 | it("works with a no_proxy set", function() 40 | local proxies = http_proxies.new():update(function(k) return ({ 41 | http_proxy = "http://http.proxy"; 42 | no_proxy = "foo,bar.com,.extra.dot.com"; 43 | })[k] end) 44 | assert.same("http://http.proxy", proxies:choose("http", "myhost")) 45 | assert.is.table(proxies.no_proxy) 46 | assert.same(nil, proxies:choose("http", "foo")) 47 | assert.same(nil, proxies:choose("http", "bar.com")) 48 | assert.same(nil, proxies:choose("http", "subdomain.bar.com")) 49 | assert.same(nil, proxies:choose("http", "sub.sub.subdomain.bar.com")) 50 | assert.same(nil, proxies:choose("http", "someting.foo")) 51 | assert.same("http://http.proxy", proxies:choose("http", "else.com")) 52 | assert.same(nil, proxies:choose("http", "more.extra.dot.com")) 53 | assert.same(nil, proxies:choose("http", "extra.dot.com")) 54 | assert.same("http://http.proxy", proxies:choose("http", "dot.com")) 55 | end) 56 | end) 57 | -------------------------------------------------------------------------------- /spec/require-all.lua: -------------------------------------------------------------------------------- 1 | -- This file is used for linting .tld files with typedlua 2 | 3 | require "http.bit" 4 | require "http.client" 5 | require "http.connection_common" 6 | require "http.cookie" 7 | require "http.h1_connection" 8 | require "http.h1_reason_phrases" 9 | require "http.h1_stream" 10 | require "http.h2_connection" 11 | require "http.h2_error" 12 | require "http.h2_stream" 13 | require "http.headers" 14 | require "http.hpack" 15 | require "http.hsts" 16 | require "http.proxies" 17 | require "http.request" 18 | require "http.server" 19 | require "http.socks" 20 | require "http.stream_common" 21 | require "http.tls" 22 | require "http.util" 23 | require "http.version" 24 | require "http.websocket" 25 | require "http.zlib" 26 | require "http.compat.prosody" 27 | require "http.compat.socket" 28 | -------------------------------------------------------------------------------- /spec/socks_spec.lua: -------------------------------------------------------------------------------- 1 | local TEST_TIMEOUT = 2 2 | describe("http.socks module", function() 3 | local http_socks = require "http.socks" 4 | local cqueues = require "cqueues" 5 | local ca = require "cqueues.auxlib" 6 | local ce = require "cqueues.errno" 7 | local cs = require "cqueues.socket" 8 | it("works with connect constructor", function() 9 | assert(http_socks.connect("socks5://127.0.0.1")) 10 | assert(http_socks.connect("socks5h://username:password@127.0.0.1")) 11 | end) 12 | it("fails on unknown protocols", function() 13 | assert.has.errors(function() 14 | http_socks.connect("socks3://host") 15 | end) 16 | end) 17 | it("fails when userinfo is missing password", function() 18 | assert.has.errors(function() 19 | http_socks.connect("socks5h://user@host") 20 | end) 21 | end) 22 | it("has a working :clone", function() 23 | local socks = http_socks.connect("socks5://127.0.0.1") 24 | assert.same(socks, socks:clone()) 25 | end) 26 | it("has a working :clone when userinfo present", function() 27 | local socks = http_socks.connect("socks5://user:pass@127.0.0.1") 28 | assert.same(socks, socks:clone()) 29 | end) 30 | it("can negotiate a IPv4 connection with no auth", function() 31 | local s, c = ca.assert(cs.pair()) 32 | local cq = cqueues.new() 33 | cq:wrap(function() 34 | assert(http_socks.fdopen(c):negotiate("127.0.0.1", 123)) 35 | end) 36 | cq:wrap(function() 37 | assert.same("\5", s:read(1)) 38 | local n = assert(s:read(1)):byte() 39 | local available_auth = assert(s:read(n)) 40 | assert.same("\0", available_auth) 41 | assert(s:xwrite("\5\0", "n")) 42 | assert.same("\5\1\0\1\127\0\0\1\0\123", s:read(10)) 43 | assert(s:xwrite("\5\0\0\1\127\0\0\1\12\34", "n")) 44 | end) 45 | assert_loop(cq, TEST_TIMEOUT) 46 | assert.truthy(cq:empty()) 47 | s:close() 48 | c:close() 49 | end) 50 | it("can negotiate a IPv6 connection with username+password auth", function() 51 | local s, c = ca.assert(cs.pair()) 52 | local cq = cqueues.new() 53 | cq:wrap(function() 54 | c = http_socks.fdopen(c) 55 | assert(c:add_username_password_auth("open", "sesame")) 56 | assert(c:negotiate("::1", 123)) 57 | c:close() 58 | end) 59 | cq:wrap(function() 60 | assert.same("\5", s:read(1)) 61 | local n = assert(s:read(1)):byte() 62 | local available_auth = assert(s:read(n)) 63 | assert.same("\0\2", available_auth) 64 | assert(s:xwrite("\5\2", "n")) 65 | assert.same("\1\4open\6sesame", s:read(13)) 66 | assert(s:xwrite("\1\0", "n")) 67 | assert.same("\5\1\0\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\0\123", s:read(22)) 68 | assert(s:xwrite("\5\0\0\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\12\34", "n")) 69 | s:close() 70 | end) 71 | assert_loop(cq, TEST_TIMEOUT) 72 | assert.truthy(cq:empty()) 73 | end) 74 | it("can negotiate a connection where peername is a domain", function() 75 | local s, c = ca.assert(cs.pair()) 76 | local cq = cqueues.new() 77 | cq:wrap(function() 78 | c = http_socks.fdopen(c) 79 | assert(c:negotiate("127.0.0.1", 123)) 80 | assert.same(cs.AF_UNSPEC, c.dst_family) 81 | assert.same("test", c.dst_host) 82 | assert.same(1234, c.dst_port) 83 | c:close() 84 | end) 85 | cq:wrap(function() 86 | assert.same("\5", s:read(1)) 87 | local n = assert(s:read(1)):byte() 88 | local available_auth = assert(s:read(n)) 89 | assert.same("\0", available_auth) 90 | assert(s:xwrite("\5\0", "n")) 91 | assert.same("\5\1\0\1\127\0\0\1\0\123", s:read(10)) 92 | assert(s:xwrite("\5\0\0\3\4test\4\210", "n")) 93 | s:close() 94 | end) 95 | assert_loop(cq, TEST_TIMEOUT) 96 | assert.truthy(cq:empty()) 97 | end) 98 | it("fails incorrect username+password with EACCES", function() 99 | local s, c = ca.assert(cs.pair()) 100 | local cq = cqueues.new() 101 | cq:wrap(function() 102 | c = http_socks.fdopen(c) 103 | assert(c:add_username_password_auth("open", "sesame")) 104 | assert.same(ce.EACCES, select(3, c:negotiate("unused", 123))) 105 | c:close() 106 | end) 107 | cq:wrap(function() 108 | assert.same("\5", s:read(1)) 109 | local n = assert(s:read(1)):byte() 110 | local available_auth = assert(s:read(n)) 111 | assert.same("\0\2", available_auth) 112 | assert(s:xwrite("\5\2", "n")) 113 | assert.same("\1\4open\6sesame", s:read(13)) 114 | assert(s:xwrite("\1\1", "n")) 115 | s:close() 116 | end) 117 | assert_loop(cq, TEST_TIMEOUT) 118 | assert.truthy(cq:empty()) 119 | end) 120 | it("fails with correct error messages", function() 121 | for i, correct_errno in ipairs({ 122 | false; 123 | ce.EACCES; 124 | ce.ENETUNREACH; 125 | ce.EHOSTUNREACH; 126 | ce.ECONNREFUSED; 127 | ce.ETIMEDOUT; 128 | ce.EOPNOTSUPP; 129 | ce.EAFNOSUPPORT; 130 | }) do 131 | local c, s = ca.assert(cs.pair()) 132 | local cq = cqueues.new() 133 | cq:wrap(function() 134 | c = http_socks.fdopen(c) 135 | local ok, _, errno = c:negotiate("127.0.0.1", 123) 136 | assert.falsy(ok) 137 | if correct_errno then 138 | assert.same(correct_errno, errno) 139 | end 140 | c:close() 141 | end) 142 | cq:wrap(function() 143 | assert.same("\5", s:read(1)) 144 | local n = assert(s:read(1)):byte() 145 | local available_auth = assert(s:read(n)) 146 | assert.same("\0", available_auth) 147 | assert(s:xwrite("\5\0", "n")) 148 | assert.same("\5\1\0\1\127\0\0\1\0\123", s:read(10)) 149 | assert(s:xwrite("\5" .. string.char(i), "n")) 150 | s:close() 151 | end) 152 | assert_loop(cq, TEST_TIMEOUT) 153 | assert.truthy(cq:empty()) 154 | end 155 | end) 156 | it("fails with EAFNOSUPPORT on unknown address type", function() 157 | local s, c = ca.assert(cs.pair()) 158 | local cq = cqueues.new() 159 | cq:wrap(function() 160 | c = http_socks.fdopen(c) 161 | local ok, _, errno = c:negotiate("127.0.0.1", 123) 162 | assert.falsy(ok) 163 | assert.same(ce.EAFNOSUPPORT, errno) 164 | c:close() 165 | end) 166 | cq:wrap(function() 167 | assert.same("\5", s:read(1)) 168 | local n = assert(s:read(1)):byte() 169 | local available_auth = assert(s:read(n)) 170 | assert.same("\0", available_auth) 171 | assert(s:xwrite("\5\0", "n")) 172 | assert.same("\5\1\0\1\127\0\0\1\0\123", s:read(10)) 173 | assert(s:xwrite("\5\0\0\5", "n")) 174 | s:close() 175 | end) 176 | assert_loop(cq, TEST_TIMEOUT) 177 | assert.truthy(cq:empty()) 178 | end) 179 | it("has a working :take_socket", function() 180 | local s, c = ca.assert(cs.pair()) 181 | local socks = http_socks.fdopen(c) 182 | assert.same(c, socks:take_socket()) 183 | assert.same(nil, socks:take_socket()) 184 | s:close() 185 | c:close() 186 | end) 187 | end) 188 | -------------------------------------------------------------------------------- /spec/stream_common_spec.lua: -------------------------------------------------------------------------------- 1 | describe("http.stream_common", function() 2 | local h1_connection = require "http.h1_connection" 3 | local new_headers = require "http.headers".new 4 | local cqueues = require "cqueues" 5 | local ca = require "cqueues.auxlib" 6 | local cs = require "cqueues.socket" 7 | local function new_pair(version) 8 | local s, c = ca.assert(cs.pair()) 9 | s = h1_connection.new(s, "server", version) 10 | c = h1_connection.new(c, "client", version) 11 | return s, c 12 | end 13 | local function new_request_headers() 14 | local headers = new_headers() 15 | headers:append(":method", "GET") 16 | headers:append(":scheme", "http") 17 | headers:append(":authority", "myauthority") 18 | headers:append(":path", "/") 19 | return headers 20 | end 21 | it("Can read a number of characters", function() 22 | local server, client = new_pair(1.1) 23 | local cq = cqueues.new() 24 | cq:wrap(function() 25 | local stream = client:new_stream() 26 | assert(stream:write_headers(new_request_headers(), false)) 27 | assert(stream:write_chunk("foo", false)) 28 | assert(stream:write_chunk("\nb", false)) 29 | assert(stream:write_chunk("ar\n", true)) 30 | end) 31 | cq:wrap(function() 32 | local stream = server:get_next_incoming_stream() 33 | -- same size as next chunk 34 | assert.same("foo", stream:get_body_chars(3)) 35 | -- less than chunk 36 | assert.same("\n", stream:get_body_chars(1)) 37 | -- crossing chunks 38 | assert.same("bar", stream:get_body_chars(3)) 39 | -- more than available 40 | assert.same("\n", stream:get_body_chars(8)) 41 | -- when none available 42 | assert.same(nil, stream:get_body_chars(8)) 43 | end) 44 | assert_loop(cq, TEST_TIMEOUT) 45 | assert.truthy(cq:empty()) 46 | client:close() 47 | server:close() 48 | end) 49 | it("Can read a line", function() 50 | local server, client = new_pair(1.1) 51 | local cq = cqueues.new() 52 | cq:wrap(function() 53 | local stream = client:new_stream() 54 | assert(stream:write_headers(new_request_headers(), false)) 55 | assert(stream:write_chunk("foo", false)) 56 | assert(stream:write_chunk("\nb", false)) 57 | assert(stream:write_chunk("ar\n", true)) 58 | end) 59 | cq:wrap(function() 60 | local stream = server:get_next_incoming_stream() 61 | assert.same("foo", stream:get_body_until("\n", true, false)) 62 | assert.same("bar", stream:get_body_until("\n", true, false)) 63 | assert.same(nil, stream:get_body_until("\n", true, false)) 64 | end) 65 | assert_loop(cq, TEST_TIMEOUT) 66 | assert.truthy(cq:empty()) 67 | client:close() 68 | server:close() 69 | end) 70 | it("can read into a temporary file", function() 71 | local server, client = new_pair(1.1) 72 | local cq = cqueues.new() 73 | cq:wrap(function() 74 | local stream = client:new_stream() 75 | assert(stream:write_headers(new_request_headers(), false)) 76 | assert(stream:write_chunk("hello world!", true)) 77 | end) 78 | cq:wrap(function() 79 | local stream = assert(server:get_next_incoming_stream()) 80 | local file = assert(stream:get_body_as_file()) 81 | assert.same("hello world!", file:read"*a") 82 | end) 83 | assert_loop(cq, TEST_TIMEOUT) 84 | assert.truthy(cq:empty()) 85 | client:close() 86 | server:close() 87 | end) 88 | describe("write_body_from_file", function() 89 | it("works with a temporary file", function() 90 | local server, client = new_pair(1.1) 91 | local cq = cqueues.new() 92 | cq:wrap(function() 93 | local file = io.tmpfile() 94 | assert(file:write("hello world!")) 95 | assert(file:seek("set")) 96 | local stream = client:new_stream() 97 | assert(stream:write_headers(new_request_headers(), false)) 98 | assert(stream:write_body_from_file(file)) 99 | end) 100 | cq:wrap(function() 101 | local stream = assert(server:get_next_incoming_stream()) 102 | assert.same("hello world!", assert(stream:get_body_as_string())) 103 | end) 104 | assert_loop(cq, TEST_TIMEOUT) 105 | assert.truthy(cq:empty()) 106 | client:close() 107 | server:close() 108 | end) 109 | it("works using the options form", function() 110 | local server, client = new_pair(1.1) 111 | local cq = cqueues.new() 112 | cq:wrap(function() 113 | local file = io.tmpfile() 114 | assert(file:write("hello world!")) 115 | assert(file:seek("set")) 116 | local stream = client:new_stream() 117 | assert(stream:write_headers(new_request_headers(), false)) 118 | assert(stream:write_body_from_file({ 119 | file = file; 120 | })) 121 | end) 122 | cq:wrap(function() 123 | local stream = assert(server:get_next_incoming_stream()) 124 | assert.same("hello world!", assert(stream:get_body_as_string())) 125 | end) 126 | assert_loop(cq, TEST_TIMEOUT) 127 | assert.truthy(cq:empty()) 128 | client:close() 129 | server:close() 130 | end) 131 | it("validates .count option", function() 132 | local server, client = new_pair(1.1) 133 | local cq = cqueues.new() 134 | cq:wrap(function() 135 | local stream = client:new_stream() 136 | assert(stream:write_headers(new_request_headers(), false)) 137 | assert.has_error(function() 138 | stream:write_body_from_file({ 139 | file = io.tmpfile(); 140 | count = "invalid count field"; 141 | }) 142 | end) 143 | end) 144 | cq:wrap(function() 145 | assert(server:get_next_incoming_stream()) 146 | end) 147 | assert_loop(cq, TEST_TIMEOUT) 148 | assert.truthy(cq:empty()) 149 | client:close() 150 | server:close() 151 | end) 152 | it("limits number of bytes when using .count option", function() 153 | local server, client = new_pair(1.1) 154 | local cq = cqueues.new() 155 | cq:wrap(function() 156 | local file = io.tmpfile() 157 | assert(file:write("hello world!")) 158 | assert(file:seek("set")) 159 | local stream = client:new_stream() 160 | assert(stream:write_headers(new_request_headers(), false)) 161 | assert(stream:write_body_from_file({ 162 | file = file; 163 | count = 5; 164 | })) 165 | end) 166 | cq:wrap(function() 167 | local stream = assert(server:get_next_incoming_stream()) 168 | assert.same("hello", assert(stream:get_body_as_string())) 169 | end) 170 | assert_loop(cq, TEST_TIMEOUT) 171 | assert.truthy(cq:empty()) 172 | client:close() 173 | server:close() 174 | end) 175 | it("reports an error on early EOF", function() 176 | local server, client = new_pair(1.1) 177 | local cq = cqueues.new() 178 | cq:wrap(function() 179 | local file = io.tmpfile() 180 | assert(file:write("hello world!")) 181 | assert(file:seek("set")) 182 | local stream = client:new_stream() 183 | assert(stream:write_headers(new_request_headers(), false)) 184 | assert.has_error(function() 185 | assert(stream:write_body_from_file({ 186 | file = file; 187 | count = 50; -- longer than the file 188 | })) 189 | end) 190 | end) 191 | cq:wrap(function() 192 | assert(server:get_next_incoming_stream()) 193 | end) 194 | assert_loop(cq, TEST_TIMEOUT) 195 | assert.truthy(cq:empty()) 196 | client:close() 197 | server:close() 198 | end) 199 | end) 200 | end) 201 | -------------------------------------------------------------------------------- /spec/tls_spec.lua: -------------------------------------------------------------------------------- 1 | describe("http.tls module", function() 2 | local tls = require "http.tls" 3 | local cqueues = require "cqueues" 4 | local ca = require "cqueues.auxlib" 5 | local cs = require "cqueues.socket" 6 | local openssl_ctx = require "openssl.ssl.context" 7 | local openssl_pkey = require "openssl.pkey" 8 | local openssl_x509 = require "openssl.x509" 9 | it("banned ciphers list denies a negotiated banned cipher", function() 10 | local banned_cipher_list do 11 | local t = {} 12 | for cipher in pairs(tls.banned_ciphers) do 13 | table.insert(t, cipher) 14 | end 15 | banned_cipher_list = table.concat(t, ":") 16 | end 17 | local s, c = ca.assert(cs.pair()) 18 | local cq = cqueues.new() 19 | cq:wrap(function() 20 | local ctx = openssl_ctx.new("TLS", false) 21 | assert(c:starttls(ctx)) 22 | local ssl = assert(s:checktls()) 23 | local cipher = ssl:getCipherInfo() 24 | assert(tls.banned_ciphers[cipher.name]) 25 | end) 26 | cq:wrap(function() 27 | local ctx = openssl_ctx.new("TLS", true) 28 | ctx:setOptions(openssl_ctx.OP_NO_TLSv1_3) 29 | ctx:setCipherList(banned_cipher_list) 30 | ctx:setEphemeralKey(openssl_pkey.new{ type = "EC", curve = "prime256v1" }) 31 | local crt = openssl_x509.new() 32 | local key = openssl_pkey.new({type="RSA", bits=2048}) 33 | crt:setPublicKey(key) 34 | crt:sign(key) 35 | assert(ctx:setPrivateKey(key)) 36 | assert(ctx:setCertificate(crt)) 37 | assert(s:starttls(ctx)) 38 | local ssl = assert(s:checktls()) 39 | local cipher = ssl:getCipherInfo() 40 | assert(tls.banned_ciphers[cipher.name]) 41 | end) 42 | assert_loop(cq, TEST_TIMEOUT) 43 | assert.truthy(cq:empty()) 44 | s:close() 45 | c:close() 46 | end) 47 | it("can create a new client context", function() 48 | tls.new_client_context() 49 | end) 50 | it("can create a new server context", function() 51 | tls.new_server_context() 52 | end) 53 | end) 54 | -------------------------------------------------------------------------------- /spec/util_spec.lua: -------------------------------------------------------------------------------- 1 | describe("http.util module", function() 2 | local unpack = table.unpack or unpack -- luacheck: ignore 113 143 3 | local util = require "http.util" 4 | it("decodeURI works", function() 5 | assert.same("Encoded string", util.decodeURI("Encoded%20string")) 6 | end) 7 | it("decodeURI doesn't decode blacklisted characters", function() 8 | assert.same("%24", util.decodeURI("%24")) 9 | local s = util.encodeURIComponent("#$&+,/:;=?@") 10 | assert.same(s, util.decodeURI(s)) 11 | end) 12 | it("decodeURIComponent round-trips with encodeURIComponent", function() 13 | local allchars do 14 | local t = {} 15 | for i=0, 255 do 16 | t[i] = i 17 | end 18 | allchars = string.char(unpack(t, 0, 255)) 19 | end 20 | assert.same(allchars, util.decodeURIComponent(util.encodeURIComponent(allchars))) 21 | end) 22 | it("query_args works", function() 23 | do 24 | local iter, state, first = util.query_args("foo=bar") 25 | assert.same({"foo", "bar"}, {iter(state, first)}) 26 | assert.same(nil, iter(state, first)) 27 | end 28 | do 29 | local iter, state, first = util.query_args("foo=bar&baz=qux&foo=somethingelse") 30 | assert.same({"foo", "bar"}, {iter(state, first)}) 31 | assert.same({"baz", "qux"}, {iter(state, first)}) 32 | assert.same({"foo", "somethingelse"}, {iter(state, first)}) 33 | assert.same(nil, iter(state, first)) 34 | end 35 | do 36 | local iter, state, first = util.query_args("%3D=%26") 37 | assert.same({"=", "&"}, {iter(state, first)}) 38 | assert.same(nil, iter(state, first)) 39 | end 40 | do 41 | local iter, state, first = util.query_args("foo=bar&noequals") 42 | assert.same({"foo", "bar"}, {iter(state, first)}) 43 | assert.same({"noequals", nil}, {iter(state, first)}) 44 | assert.same(nil, iter(state, first)) 45 | end 46 | end) 47 | it("dict_to_query works", function() 48 | assert.same("foo=bar", util.dict_to_query{foo = "bar"}) 49 | assert.same("foo=%CE%BB", util.dict_to_query{foo = "λ"}) 50 | do 51 | local t = {foo = "bar"; baz = "qux"} 52 | local r = {} 53 | for k, v in util.query_args(util.dict_to_query(t)) do 54 | r[k] = v 55 | end 56 | assert.same(t, r) 57 | end 58 | end) 59 | it("is_safe_method works", function() 60 | assert.same(true, util.is_safe_method "GET") 61 | assert.same(true, util.is_safe_method "HEAD") 62 | assert.same(true, util.is_safe_method "OPTIONS") 63 | assert.same(true, util.is_safe_method "TRACE") 64 | assert.same(false, util.is_safe_method "POST") 65 | assert.same(false, util.is_safe_method "PUT") 66 | end) 67 | it("is_ip works", function() 68 | assert.same(true, util.is_ip "127.0.0.1") 69 | assert.same(true, util.is_ip "192.168.1.1") 70 | assert.same(true, util.is_ip "::") 71 | assert.same(true, util.is_ip "::1") 72 | assert.same(true, util.is_ip "2001:0db8:85a3:0042:1000:8a2e:0370:7334") 73 | assert.same(true, util.is_ip "::FFFF:204.152.189.116") 74 | assert.same(false, util.is_ip "not an ip") 75 | assert.same(false, util.is_ip "0x80") 76 | assert.same(false, util.is_ip "::FFFF:0.0.0") 77 | end) 78 | it("split_authority works", function() 79 | assert.same({"example.com", 80}, {util.split_authority("example.com", "http")}) 80 | assert.same({"example.com", 8000}, {util.split_authority("example.com:8000", "http")}) 81 | assert.falsy(util.split_authority("example.com", "madeupscheme")) 82 | -- IPv6 83 | assert.same({"::1", 443}, {util.split_authority("[::1]", "https")}) 84 | assert.same({"::1", 8000}, {util.split_authority("[::1]:8000", "https")}) 85 | end) 86 | it("to_authority works", function() 87 | assert.same("example.com", util.to_authority("example.com", 80, "http")) 88 | assert.same("example.com:8000", util.to_authority("example.com", 8000, "http")) 89 | -- IPv6 90 | assert.same("[::1]", util.to_authority("::1", 443, "https")) 91 | assert.same("[::1]:8000", util.to_authority("::1", 8000, "https")) 92 | end) 93 | it("generates correct looking Date header format", function() 94 | assert.same("Fri, 13 Feb 2009 23:31:30 GMT", util.imf_date(1234567890)) 95 | end) 96 | describe("maybe_quote", function() 97 | it("makes acceptable tokens or quoted-string", function() 98 | assert.same([[foo]], util.maybe_quote([[foo]])) 99 | assert.same([["with \" quote"]], util.maybe_quote([[with " quote]])) 100 | end) 101 | it("escapes all bytes correctly", function() 102 | local http_patts = require "lpeg_patterns.http" 103 | local s do -- Make a string containing every byte allowed in a quoted string 104 | local t = {"\t"} -- tab 105 | for i=32, 126 do 106 | t[#t+1] = string.char(i) 107 | end 108 | for i=128, 255 do 109 | t[#t+1] = string.char(i) 110 | end 111 | s = table.concat(t) 112 | end 113 | assert.same(s, http_patts.quoted_string:match(util.maybe_quote(s))) 114 | end) 115 | it("returns nil on invalid input", function() 116 | local function check(s) 117 | assert.same(nil, util.maybe_quote(s)) 118 | end 119 | for i=0, 8 do 120 | check(string.char(i)) 121 | end 122 | -- skip tab 123 | for i=10, 31 do 124 | check(string.char(i)) 125 | end 126 | check("\127") 127 | end) 128 | end) 129 | describe("yieldable_pcall", function() 130 | it("returns multiple return values", function() 131 | assert.same({true, 1, 2, 3, 4, nil, nil, nil, nil, nil, nil, "foo"}, 132 | {util.yieldable_pcall(function() return 1, 2, 3, 4, nil, nil, nil, nil, nil, nil, "foo" end)}) 133 | end) 134 | it("protects from errors", function() 135 | assert.falsy(util.yieldable_pcall(error)) 136 | end) 137 | it("returns error objects", function() 138 | local err = {"myerror"} 139 | local ok, err2 = util.yieldable_pcall(error, err) 140 | assert.falsy(ok) 141 | assert.equal(err, err2) 142 | end) 143 | it("works on all levels", function() 144 | local f = coroutine.wrap(function() 145 | return util.yieldable_pcall(coroutine.yield, true) 146 | end) 147 | assert.truthy(f()) -- 'true' that was yielded 148 | assert.truthy(f()) -- 'true' from the pcall 149 | assert.has.errors(f) -- cannot resume dead coroutine 150 | end) 151 | it("works with __call objects", function() 152 | local done = false 153 | local o = setmetatable({}, { 154 | __call=function() 155 | done = true 156 | end; 157 | }) 158 | util.yieldable_pcall(o) 159 | assert.truthy(done) 160 | end) 161 | end) 162 | end) 163 | -------------------------------------------------------------------------------- /spec/zlib_spec.lua: -------------------------------------------------------------------------------- 1 | local ok, http_zlib = pcall(require, "http.zlib"); 2 | (ok and describe or pending)("zlib compat layer", function() 3 | it("round trips", function() 4 | local function test(str) 5 | local compressor = http_zlib.deflate() 6 | local decompressor = http_zlib.inflate() 7 | local z = compressor(str, true) 8 | assert.same(str, decompressor(z, true)) 9 | end 10 | test "foo" 11 | test "hi" 12 | test(("az"):rep(100000)) 13 | end) 14 | it("streaming round trips", function() 15 | local function test(...) 16 | local compressor = http_zlib.deflate() 17 | local decompressor = http_zlib.inflate() 18 | local t = {...} 19 | local out = {} 20 | for i=1, #t do 21 | local z = compressor(t[i], false) 22 | out[i] = decompressor(z, false) or "" 23 | end 24 | out[#t+1] = decompressor(compressor("", true), true) 25 | assert.same(table.concat(t), table.concat(out)) 26 | end 27 | 28 | test( 29 | "short string", 30 | ("foo"):rep(100000), 31 | "middle", 32 | ("bar"):rep(100000), 33 | "end" 34 | ) 35 | end) 36 | it("decompressor errors on invalid input", function() 37 | local decompressor = http_zlib.inflate() 38 | assert.has.errors(function() decompressor("asdfghjk", false) end) 39 | end) 40 | it("decompresses over multiple sections", function() 41 | -- for whatever reason for certain input streams, zlib will not consume it in one go 42 | local decompressor = http_zlib.inflate() 43 | decompressor("\31\139\8\0\0\0\0\0\0\3\237\93\235\142\35\199\117\254\61" 44 | .. "\122\138\50\39\22\103\34\178\73\206\117\119\110\182\44\217\177" 45 | .. "\16\43\82\188\107\27\182\32\44\154\205\34\217\59\205\110\170\47" 46 | .. "\195\161\101\1\190\4\200\15\7\206\143\188\72\18\196\129\99\195" 47 | .. "\242\43\204\190\66\158\36\223\57\167\170\187\154\108\114\102" 48 | .. "\163\93\95\96\105\177\34\217\93\85\231\212\185\87\157\170\179\23" 49 | , false) 50 | end); 51 | -- lzlib doesn't report a missing end of string in inflate 52 | (http_zlib.engine == "lzlib" and pending or it)("decompressor fails on incorrect end_stream flag", function() 53 | local compressor = http_zlib.deflate() 54 | local decompressor = http_zlib.inflate() 55 | local z = compressor(("foo"):rep(100000), false) 56 | assert(#z > 0) 57 | assert.has.errors(function() decompressor(z, true) end) 58 | end) 59 | end) 60 | --------------------------------------------------------------------------------