├── .gitignore ├── .gitmodules ├── .travis.yml ├── CMakeLists.txt ├── LICENSE-MIT ├── README ├── appveyor.yml ├── bench.lua ├── lua-http-parser-2.7-1.rockspec ├── lua-http-parser.c └── test.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | *.obj 5 | *.elf 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Libraries 12 | *.lib 13 | *.a 14 | *.la 15 | *.lo 16 | 17 | # Shared objects (inc. Windows DLLs) 18 | *.dll 19 | *.so 20 | *.so.* 21 | *.dylib 22 | 23 | # Executables 24 | *.exe 25 | *.out 26 | *.app 27 | *.i*86 28 | *.x86_64 29 | *.hex 30 | 31 | # Debug files 32 | *.dSYM/ 33 | 34 | # Generated files 35 | *.manifest 36 | *.exp 37 | *.def 38 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "http-parser"] 2 | path = http-parser 3 | url = git://github.com/nodejs/http-parser.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | sudo: false 4 | 5 | matrix: 6 | include: 7 | - env: LUA="lua 5.1" 8 | os: osx 9 | - env: LUA="lua 5.1" 10 | os: linux 11 | - env: LUA="lua 5.2" 12 | os: linux 13 | - env: LUA="lua 5.3" 14 | os: linux 15 | - env: LUA="lua 5.4" 16 | os: linux 17 | - env: LUA="luajit 2.0" 18 | os: linux 19 | - env: LUA="luajit 2.1" 20 | os: linux 21 | 22 | cache: 23 | directories: 24 | - here 25 | - $HOME/.cache/pip 26 | 27 | before_install: 28 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export PATH=$PATH:~/Library/Python/2.7/bin/; fi 29 | - pip install --user hererocks 30 | - hererocks here -r^ --$LUA 31 | - source here/bin/activate 32 | 33 | install: 34 | - luarocks make lua-http-parser-2.7-1.rockspec 35 | 36 | before_script: 37 | - luarocks show luacov-coveralls > /dev/null 2>&1 || luarocks install luacov-coveralls 38 | - luarocks show luasocket > /dev/null 2>&1 || luarocks install luasocket 39 | 40 | script: 41 | - lua test.lua 42 | - lua bench.lua 43 | 44 | before_cache: 45 | - luarocks remove lua-http-parser 46 | - rm -f /home/travis/.cache/pip/log/debug.log 47 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8) 2 | 3 | project(lua-http-parser C) 4 | 5 | set(BUILD_SHARED_LIBS TRUE) 6 | 7 | set(INSTALL_CMOD share/lua/cmod CACHE PATH "Directory to install Lua binary modules (configure lua via LUA_CPATH)") 8 | 9 | ## Lua 5.1.x 10 | include(FindLua51) 11 | if(!${LUA51_FOUND}) 12 | message(FATAL_ERROR "The FindLua51 module could not find lua :-(") 13 | endif() 14 | 15 | 16 | set(WARN_CFLAGS "-Wall -Wextra -Wshadow -W -pedantic -Wno-overlength-strings") 17 | if(CMAKE_COMPILER_IS_GNUCC) 18 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe ${WARN_CFLAGS} -std=gnu99 -fgnu89-inline") 19 | set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3 -march=native -g") 20 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0 -g") 21 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_PROFILE} -O2 -g -DNDEBUG") 22 | set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_WITHDEBINFO} -O2 -g") 23 | endif(CMAKE_COMPILER_IS_GNUCC) 24 | 25 | ## setup git submodules 26 | 27 | if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/http-parser/http_parser.c") 28 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git/") 29 | execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 30 | COMMAND git submodule init) 31 | execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 32 | COMMAND git submodule update) 33 | else() 34 | execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 35 | COMMAND git clone "git://github.com/joyent/http-parser.git") 36 | endif() 37 | endif() 38 | 39 | include_directories(${CMAKE_CURRENT_SOURCE_DIR} 40 | ${CMAKE_CURRENT_SOURCE_DIR}/http-parser 41 | ${LUA_INCLUDE_DIR}) 42 | 43 | add_library(parser MODULE lua-http-parser.c http-parser/http_parser.c) 44 | target_link_libraries(parser ${LUA_LIBRARIES}) 45 | set_target_properties(parser PROPERTIES PREFIX "") 46 | set_target_properties(parser PROPERTIES COMPILE_FLAGS "${CFLAGS}") 47 | set_target_properties(parser PROPERTIES OUTPUT_NAME parser) 48 | 49 | install(TARGETS parser 50 | DESTINATION "${INSTALL_CMOD}/http") 51 | 52 | 53 | ## Setup test stuff 54 | include(CTest) 55 | add_test(test ${LUA} ${CMAKE_CURRENT_SOURCE_DIR}/test.lua ${CMAKE_CURRENT_SOURCE_DIR}/ ${CMAKE_CURRENT_BINARY_DIR}/) 56 | #add_test(benchmark ${LUA} ${CMAKE_CURRENT_SOURCE_DIR}/benchmark.lua ${CMAKE_CURRENT_SOURCE_DIR}/ ${CMAKE_CURRENT_BINARY_DIR}/) 57 | set_tests_properties(test 58 | PROPERTIES 59 | FAIL_REGULAR_EXPRESSION 60 | "not ok") 61 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2009,2010 Phoenix Sol 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ********************************************************************** 2 | * Author : Brian Maher 3 | * Library : lua-http-parser - Lua 5.1 interface to ry's http-parser 4 | * 5 | * The MIT License 6 | * 7 | * Copyright (c) 2009-1010 Brian Maher 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | ********************************************************************** 27 | 28 | To use this library, you need nodejs http-parser, get it here: 29 | git clone git://github.com/nodejs/http-parser.git 30 | 31 | To build this library, you need luarocks, get it here: 32 | http://www.luarocks.org/en/Download 33 | 34 | Then build it like this: 35 | 36 | luarocks make CFLAGS=-g 37 | 38 | Loading the library: 39 | 40 | If you built the library as a loadable package 41 | [local] lhp = require 'http.parser' 42 | 43 | If you compiled the package statically into your application, call 44 | the function "luaopen_http_parser(L)". It will create a table with the 45 | http parser functions and leave it on the top of the stack. 46 | 47 | To test the library run: 48 | 49 | ./test.lua 50 | 51 | ...assuming that the `lua` on your PATH properly uses the luarocks 52 | installed module. 53 | 54 | API: 55 | 56 | parser = lhp.request { 57 | on_url = function(url) ... end 58 | on_header = function(hkey, hval) ... end 59 | on_body = function(body) ... end 60 | 61 | on_message_begin = function() ... end 62 | on_message_complete = function() ... end 63 | on_headers_complete = function() ... end 64 | 65 | on_chunk_header = function(content_length) ... end 66 | on_chunk_complete = function() ... end 67 | } 68 | 69 | parser = lhp.response { -- same as request except `on_url`. Plus 70 | on_status = function(code, text) ... end 71 | } 72 | 73 | Create a new HTTP parser to handle either an HTTP request or 74 | HTTP response respectively. Pass in a table of callbacks that 75 | are ran when the parser encounters the various fields. All 76 | callbacks will be ran with the full value for that field with 77 | on_body being the notible exception. 78 | 79 | You can treat the on_body handler as an LTN12 "source" since 80 | it is always garanteed to send a terminating on_body(nil) 81 | event when the complete body has been read. Note that the 82 | on_body(nil) event will even be sent if there is an empty 83 | body. 84 | 85 | NOTE: Ry's http parser may call any of these callbacks with a 86 | partial value if the input_bytes are split on a "token" 87 | boundary. At this point we always assume that you will want 88 | everything except the body to be buffered, but if you have a 89 | valid use case for manually setting which callbacks are 90 | buffered, please send an email to the author. 91 | 92 | bytes_read = parser:execute(input_bytes) 93 | 94 | Feed the parser some partial input. Returns how many bytes 95 | where read. A short read may happen if a request is being 96 | "upgraded" or was an invalid format. See parser:is_upgrade() 97 | below to differentiate between these two events (if you want 98 | to support an upgraded protocol). 99 | 100 | NOTE: If the input_bytes causes more than about 2000 event 101 | callbacks to be executed a short read will occur and any 102 | attempt to continue the parse will fail. If this is a 103 | problem, restrict your input to < 8K input_bytes at a time, 104 | although in general this is probaly more of a symptom of a 105 | DDoS attack and therefore returning status 400 (Bad Request) 106 | is probably the best thing to do anyways. 107 | 108 | parser:should_keep_alive() 109 | 110 | Returns true if this TCP connection should be "kept alive" 111 | so that it can be re-used for a future request/response. 112 | If this returns false and you are the server, then respond 113 | with the "Connection: close" header. If you are the client, 114 | then simply close the connection. 115 | 116 | parser:is_upgrade() 117 | 118 | Returns true if the input request wants to "upgrade" to a 119 | different protocol. 120 | 121 | parser:version() 122 | 123 | Returns HTTP version as two numbers (major, minor). 124 | 125 | parser:method() 126 | 127 | Returns the name of the request method. This is only valid 128 | on HTTP requests. 129 | 130 | parser:error() 131 | 132 | Returns errno(number), error name(string), error description(string). 133 | 134 | parser:reset([callbacks]) 135 | 136 | Re-initialize HTTP parser clearing any previous error/state. 137 | 138 | parser:status_code() 139 | 140 | Returns the HTTP status code of a response. This is only valid 141 | on HTTP responses. 142 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.9.2.{build} 2 | 3 | os: 4 | - Windows Server 2012 R2 5 | 6 | shallow_clone: true 7 | 8 | environment: 9 | LR_EXTERNAL: c:\external 10 | 11 | matrix: 12 | - LUA: "lua 5.1" 13 | - LUA: "lua 5.2" 14 | - LUA: "lua 5.3" 15 | - LUA: "lua 5.4" 16 | 17 | platform: 18 | - x64 19 | - x86 20 | - mingw 21 | 22 | cache: 23 | - c:\hererocks -> appveyor.yml 24 | - c:\external -> appveyor.yml 25 | 26 | clone_script: 27 | - ps: >- 28 | if ( -not $env:appveyor_pull_request_number ) { 29 | git clone -q --branch=$env:appveyor_repo_branch https://github.com/$env:appveyor_repo_name.git $env:appveyor_build_folder 30 | git checkout -qf $env:appveyor_repo_commit 31 | git submodule update -q --init --recursive 32 | } else { 33 | git clone -q https://github.com/$env:appveyor_repo_name.git $env:appveyor_build_folder 34 | git fetch -q origin +refs/pull/$env:appveyor_pull_request_number/merge: 35 | git checkout -qf FETCH_HEAD 36 | git submodule update -q --init --recursive 37 | } 38 | 39 | install: 40 | - set PATH=C:\Python27\Scripts;%LR_EXTERNAL%;%PATH% 41 | - if /I "%platform%"=="x86" set HR_TARGET=vs_32 42 | - if /I "%platform%"=="x64" set HR_TARGET=vs_64 43 | - if /I "%platform%"=="mingw" set HR_TARGET=mingw 44 | - if /I "%platform%"=="mingw" set PATH=C:\MinGW\bin;%PATH% 45 | - if not exist "%LR_EXTERNAL%" ( 46 | mkdir "%LR_EXTERNAL%" && 47 | mkdir "%LR_EXTERNAL%\lib" && 48 | mkdir "%LR_EXTERNAL%\include" 49 | ) 50 | - if not exist c:\hererocks ( 51 | pip install hererocks && 52 | hererocks c:\hererocks --%LUA% --target %HR_TARGET% -rlatest 53 | ) 54 | - call c:\hererocks\bin\activate 55 | 56 | before_build: 57 | # external deps 58 | 59 | build_script: 60 | - luarocks make lua-http-parser-2.7-1.rockspec 61 | 62 | before_test: 63 | # test deps 64 | - luarocks show luacov-coveralls >nul 2>&1 || luarocks install luacov-coveralls 65 | - luarocks show luasocket >nul 2>&1 || luarocks install luasocket 66 | 67 | test_script: 68 | - lua test.lua 69 | - lua bench.lua 70 | -------------------------------------------------------------------------------- /bench.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | local socket = require"socket" 3 | local time = socket.gettime 4 | local clock = os.clock 5 | local quiet = false 6 | local disable_gc = true 7 | local type = type 8 | local tconcat = table.concat 9 | 10 | if arg[1] == '-gc' then 11 | disable_gc = false 12 | table.remove(arg,1) 13 | else 14 | print"GC is disabled so we can track memory usage better" 15 | print"" 16 | end 17 | 18 | local N=tonumber(arg[1]) or 10000 19 | 20 | local function printf(fmt, ...) 21 | local res 22 | if not quiet then 23 | fmt = fmt or '' 24 | res = print(string.format(fmt, ...)) 25 | io.stdout:flush() 26 | end 27 | return res 28 | end 29 | 30 | local function full_gc() 31 | -- make sure all free-able memory is freed 32 | collectgarbage"collect" 33 | collectgarbage"collect" 34 | collectgarbage"collect" 35 | end 36 | 37 | local function bench(name, N, func, ...) 38 | local start1,start2 39 | printf('run bench: %s', name) 40 | start1 = clock() 41 | start2 = time() 42 | func(N, ...) 43 | local diff1 = (clock() - start1) 44 | local diff2 = (time() - start2) 45 | printf("total time: %10.6f (%10.6f) seconds", diff1, diff2) 46 | return diff1, diff2 47 | end 48 | 49 | local lhp = require 'http.parser' 50 | 51 | local function parse_path_query_fragment(uri) 52 | local path, query, fragment, off 53 | -- parse path 54 | path, off = uri:match('([^?]*)()') 55 | -- parse query 56 | if uri:sub(off, off) == '?' then 57 | query, off = uri:match('([^#]*)()', off + 1) 58 | end 59 | -- parse fragment 60 | if uri:sub(off, off) == '#' then 61 | fragment = uri:sub(off + 1) 62 | off = #uri 63 | end 64 | return path or '/', query, fragment 65 | end 66 | 67 | local expects = {} 68 | local requests = {} 69 | 70 | -- NOTE: All requests must be version HTTP/1.1 since we re-use the same HTTP parser for all requests. 71 | requests.ab = { 72 | "GET /foo/t.html?qstring#frag HTTP/1.1\r\nHost: localhost:8000\r\nUser-Agent: ApacheBench/2.3\r\nContent-Length: 5\r\nAccept: */*\r\n\r\nbody\n", 73 | } 74 | 75 | expects.ab = { 76 | method = "GET", 77 | url = "/foo/t.html?qstring#frag", 78 | path = "/foo/t.html", 79 | query_string = "qstring", 80 | fragment = "frag", 81 | headers = { 82 | Host = "localhost:8000", 83 | ["User-Agent"] = "ApacheBench/2.3", 84 | Accept = "*/*", 85 | }, 86 | body = "body\n", 87 | } 88 | 89 | requests.no_buff_body = { 90 | "GET / HTTP/1.1\r\n", 91 | "Host: foo:80\r\n", 92 | "Content-Length: 12\r\n", 93 | "\r\n", 94 | "chunk1", "chunk2", 95 | } 96 | 97 | expects.no_buff_body = { 98 | method = "GET", 99 | body = "chunk1chunk2" 100 | } 101 | 102 | requests.httperf = { 103 | "GET / HTTP/1.1\r\nHost: localhost\r\nUser-Agent: httperf/0.9.0\r\n\r\n" 104 | } 105 | 106 | expects.httperf = { 107 | method = "GET", 108 | url = "/", 109 | path = "/", 110 | headers = { 111 | Host = "localhost", 112 | ["User-Agent"] = "httperf/0.9.0", 113 | }, 114 | } 115 | 116 | requests.firefox = { 117 | "GET / HTTP/1.1\r\nHost: two.local:8000\r\nUser-Agent: Mozilla/5.0 (X11; U;Linux i686; en-US; rv:1.9.0.15)Gecko/2009102815 Ubuntu/9.04 (jaunty)Firefox/3.0.15\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language:en-gb,en;q=0.5\r\nAccept-Encoding: gzip,deflate\r\nAccept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 300\r\nConnection:keep-alive\r\n\r\n" 118 | } 119 | 120 | expects.firefox = { 121 | method = "GET", 122 | url = "/", 123 | path = "/", 124 | headers = { 125 | ["User-Agent"] = "Mozilla/5.0 (X11; U;Linux i686; en-US; rv:1.9.0.15)Gecko/2009102815 Ubuntu/9.04 (jaunty)Firefox/3.0.15", 126 | Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 127 | ["Accept-Language"] = "en-gb,en;q=0.5", 128 | ["Accept-Encoding"] = "gzip,deflate", 129 | ["Accept-Charset"] = "ISO-8859-1,utf-8;q=0.7,*;q=0.7", 130 | ["Keep-Alive"] = "300", 131 | Connection = "keep-alive", 132 | } 133 | } 134 | 135 | local names_list = {} 136 | local data_list = {} 137 | for name, data in pairs(requests) do 138 | names_list[#names_list + 1] = name 139 | data_list[#data_list + 1] = data 140 | end 141 | 142 | local function init_parser(reqs) 143 | local cur = nil 144 | local parser 145 | 146 | local cb = {} 147 | function cb.on_message_begin() 148 | assert(cur == nil) 149 | cur = { headers = {} } 150 | end 151 | 152 | function cb.on_url(value) 153 | cur.url = value 154 | cur.path, cur.query_string, cur.fragment = parse_path_query_fragment(value) 155 | end 156 | 157 | function cb.on_body(value) 158 | if not cur.body then 159 | cur.body = value 160 | elseif nil ~= value then 161 | cur.body = cur.body .. value 162 | end 163 | end 164 | 165 | function cb.on_header(field, value) 166 | cur.headers[field] = value 167 | end 168 | 169 | function cb.on_headers_complete() 170 | cur.method = parser:method() 171 | end 172 | 173 | function cb.on_message_complete() 174 | assert(nil ~= cur) 175 | if reqs then table.insert(reqs, cur) end 176 | cur = nil 177 | end 178 | 179 | parser = lhp.request(cb) 180 | return parser 181 | end 182 | 183 | local function null_cb() 184 | end 185 | local null_cbs = { 186 | on_message_begin = null_cb, 187 | on_url = null_cb, 188 | on_header = null_cb, 189 | on_headers_complete = null_cb, 190 | on_body = null_cb, 191 | on_message_complete = null_cb, 192 | } 193 | local function init_null_parser() 194 | return lhp.request(null_cbs) 195 | end 196 | 197 | local function assert_deeply(got, expect, context) 198 | assert(type(expect) == "table", "Expected [" .. context .. "] to be a table") 199 | for k, v in pairs(expect) do 200 | local ctx = context .. "." .. k 201 | if type(expect[k]) == "table" then 202 | assert_deeply(got[k], expect[k], ctx) 203 | else 204 | assert(got[k] == expect[k], "Expected [" .. ctx .. "] to be '" .. tostring(expect[k]) .. "', but got '" .. tostring(got[k]) .. "'") 205 | end 206 | end 207 | end 208 | 209 | local function good_client(parser, data) 210 | for i=1,#data do 211 | local line = data[i] 212 | local bytes_read = parser:execute(line) 213 | if bytes_read ~= #line then 214 | error("only ["..tostring(bytes_read).."] bytes read, expected ["..tostring(#line).."]") 215 | end 216 | end 217 | end 218 | 219 | local function bad_client(parser, data) 220 | for i=1,#data do 221 | local line = data[i] 222 | local total = 0 223 | for i=1,#line do 224 | local bytes_read = parser:execute(line:sub(i,i)) 225 | if 1 ~= bytes_read then 226 | error("only ["..tostring(bytes_read).."] bytes read, expected ["..tostring(#line).."]") 227 | end 228 | total = total + 1 229 | end 230 | if total ~= #line then 231 | error("only ["..tostring(bytes_read).."] bytes read, expected ["..tostring(#line).."]") 232 | end 233 | end 234 | end 235 | 236 | local function apply_client(N, client, parser, requests) 237 | for i=1,N do 238 | for x=1,#requests do 239 | client(parser, requests[x]) 240 | parser:reset() 241 | end 242 | end 243 | end 244 | 245 | local function apply_client_memtest(name, client, N) 246 | local start_mem, end_mem 247 | 248 | local reqs = {} 249 | local parser = init_parser(reqs) 250 | full_gc() 251 | start_mem = (collectgarbage"count" * 1024) 252 | --print(name, 'start memory size: ', start_mem) 253 | if disable_gc then collectgarbage"stop" end 254 | apply_client(N, client, parser, data_list) 255 | end_mem = (collectgarbage"count" * 1024) 256 | --print(name, 'end memory size: ', end_mem) 257 | print(name, 'N=', N, 'total memory used: ', (end_mem - start_mem)) 258 | print() 259 | 260 | -- validate parsed request data. 261 | local idx = 0 262 | for name, data in pairs(requests) do 263 | idx = idx + 1 264 | local got = reqs[idx] 265 | local expect = expects[name] 266 | assert_deeply(got, expect, name) 267 | end 268 | 269 | reqs = nil 270 | parser = nil 271 | collectgarbage"restart" 272 | full_gc() 273 | end 274 | 275 | local function apply_client_speedtest(name, client, N) 276 | local start_mem, end_mem 277 | 278 | local parser = init_parser() 279 | full_gc() 280 | start_mem = (collectgarbage"count" * 1024) 281 | --print(name, 'start memory size: ', start_mem) 282 | if disable_gc then collectgarbage"stop" end 283 | local diff1, diff2 = bench(name, N, apply_client, client, parser, data_list) 284 | end_mem = (collectgarbage"count" * 1024) 285 | local total = N * #data_list 286 | printf("units/sec: %10.6f (%10.6f) units/sec", total/diff1, total/diff2) 287 | --print(name, 'end memory size: ', end_mem) 288 | print(name, 'N=', N, 'total memory used: ', (end_mem - start_mem)) 289 | print() 290 | 291 | parser = nil 292 | collectgarbage"restart" 293 | full_gc() 294 | end 295 | 296 | local function per_parser_overhead(N) 297 | local start_mem, end_mem 298 | local parsers = {} 299 | 300 | -- pre-grow table 301 | for i=1,N do 302 | parsers[i] = true -- add place-holder values. 303 | end 304 | full_gc() 305 | start_mem = (collectgarbage"count" * 1024) 306 | --print('overhead: start memory size: ', start_mem) 307 | for i=1,N do 308 | parsers[i] = init_null_parser() 309 | end 310 | full_gc() 311 | end_mem = (collectgarbage"count" * 1024) 312 | --print('overhead: end memory size: ', end_mem) 313 | print('overhead: total memory used: ', (end_mem - start_mem) / N, ' bytes per parser') 314 | 315 | parsers = nil 316 | full_gc() 317 | end 318 | 319 | local clients = { 320 | good = { cb = good_client, mem_N=1, speed_N=N*10}, 321 | bad = { cb = bad_client, mem_N=1, speed_N=N}, 322 | } 323 | 324 | print('memory test') 325 | for name,client in pairs(clients) do 326 | apply_client_memtest(name, client.cb, client.mem_N) 327 | end 328 | 329 | print('speed test') 330 | for name,client in pairs(clients) do 331 | apply_client_speedtest(name, client.cb, client.speed_N) 332 | end 333 | 334 | print('overhead test') 335 | per_parser_overhead(N) 336 | 337 | 338 | -------------------------------------------------------------------------------- /lua-http-parser-2.7-1.rockspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | package = 'lua-http-parser' 4 | version = '2.7-1' 5 | source = { 6 | url = 'gitrec+https://github.com/brimworks/lua-http-parser' 7 | } 8 | description = { 9 | summary = "A Lua binding to Ryan Dahl's http request/response parser.", 10 | detailed = '', 11 | homepage = 'http://github.com/brimworks/lua-http-parser', 12 | license = 'MIT', --as with Ryan's 13 | } 14 | dependencies = { 15 | 'lua >= 5.1, < 5.5', 16 | 'luarocks-fetch-gitrec', 17 | } 18 | build = { 19 | type = 'builtin', 20 | modules = { 21 | ['http.parser'] = { 22 | sources = { 23 | "http-parser/http_parser.c", 24 | "lua-http-parser.c" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lua-http-parser.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "http-parser/http_parser.h" 5 | 6 | #if LUA_VERSION_NUM >= 502 7 | #define lua_setfenv lua_setuservalue 8 | #define lua_getfenv lua_getuservalue 9 | #endif 10 | 11 | #define PARSER_MT "http.parser{parser}" 12 | 13 | #define check_parser(L, narg) \ 14 | ((lhttp_parser*)luaL_checkudata((L), (narg), PARSER_MT)) 15 | 16 | /* The Lua stack indices */ 17 | #define ST_FENV_IDX 3 18 | #define ST_BUFFER_IDX 4 19 | #define ST_LEN ST_BUFFER_IDX 20 | 21 | /* Callback identifiers are indices into the fenv table where the 22 | * callback is saved. If you add/remove/change anything about these, 23 | * be sure to update lhp_callback_names and FLAG_GET_BUF_CB_ID. 24 | */ 25 | #define CB_ON_MESSAGE_BEGIN 1 26 | #define CB_ON_URL 2 27 | #define CB_ON_STATUS 3 28 | #define CB_ON_HEADER 4 29 | #define CB_ON_HEADERS_COMPLETE 5 30 | #define CB_ON_BODY 6 31 | #define CB_ON_MESSAGE_COMPLETE 7 32 | #define CB_ON_CHUNK_HEADER 8 33 | #define CB_ON_CHUNK_COMPLETE 9 34 | #define CB_LEN (sizeof(lhp_callback_names)/sizeof(*lhp_callback_names)) 35 | 36 | static const char *lhp_callback_names[] = { 37 | /* The MUST be in the same order as the above callbacks */ 38 | "on_message_begin", 39 | "on_url", 40 | "on_status", 41 | "on_header", 42 | "on_headers_complete", 43 | "on_body", 44 | "on_message_complete", 45 | "on_chunk_header", 46 | "on_chunk_complete", 47 | }; 48 | 49 | /* Non-callback FENV indices. */ 50 | #define FENV_BUFFER_IDX CB_LEN + 1 51 | #define FENV_LEN FENV_BUFFER_IDX 52 | 53 | #define FLAGS_BUF_CB_ID_BITS 3 54 | #define FLAGS_BUF_CB_ID_MASK ((1<<(FLAGS_BUF_CB_ID_BITS))-1) 55 | /* Get the cb_id that has information stored in buff */ 56 | #define FLAG_GET_BUF_CB_ID(flags) ((flags) & FLAGS_BUF_CB_ID_MASK) 57 | 58 | #define FLAGS_CB_ID_FIRST_BIT (1<<(FLAGS_BUF_CB_ID_BITS)) 59 | #define CB_ID_TO_CB_BIT(cb_id) ((FLAGS_CB_ID_FIRST_BIT)<<(cb_id)) 60 | 61 | /* Test/set/remove a bit from the flags field of lhttp_parser. The 62 | * FLAG_*_CB() macros test/set/remove the bit that signifies that a 63 | * callback with that id has been registered in the FENV. 64 | * 65 | * The FLAG_*_BUF() macros test/set/remove the bit that signifies that 66 | * data is buffered for that callback. 67 | * 68 | * The FLAG_*_HFIELD() macros test/set/remove the bit that signifies 69 | * that the first element of the buffer is the header field key. 70 | */ 71 | #define FLAG_HAS_CB(flags, cb_id) ( (flags) & CB_ID_TO_CB_BIT(cb_id) ) 72 | #define FLAG_SET_CB(flags, cb_id) ( (flags) |= CB_ID_TO_CB_BIT(cb_id) ) 73 | #define FLAG_RM_CB(flags, cb_id) ( (flags) &= ~CB_ID_TO_CB_BIT(cb_id) ) 74 | 75 | #define FLAG_HAS_BUF(flags, cb_id) ( FLAG_GET_BUF_CB_ID(flags) == cb_id ) 76 | #define FLAG_SET_BUF(flags, cb_id) ( (flags) = (((flags) & ~FLAGS_BUF_CB_ID_MASK) \ 77 | | ((cb_id) & FLAGS_BUF_CB_ID_MASK)) ) 78 | #define FLAG_RM_BUF(flags) ( (flags) &= ~FLAGS_BUF_CB_ID_MASK ) 79 | 80 | #define FLAG_HAS_HFIELD(flags) ( ((flags) & FLAGS_CB_ID_FIRST_BIT) >> FLAGS_BUF_CB_ID_BITS ) 81 | #define FLAG_SET_HFIELD(flags) ( (flags) |= FLAGS_CB_ID_FIRST_BIT ) 82 | #define FLAG_RM_HFIELD(flags) ( (flags) &= ~FLAGS_CB_ID_FIRST_BIT ) 83 | 84 | static void lhp_pushint64(lua_State *L, int64_t v){ 85 | // compilers usially remove constant condition on compile time 86 | if(sizeof(lua_Integer) >= sizeof(int64_t)){ 87 | lua_pushinteger(L, (lua_Integer)v); 88 | return; 89 | } 90 | lua_pushnumber(L, (lua_Number)v); 91 | } 92 | 93 | typedef struct lhttp_parser { 94 | http_parser parser; /* embedded http_parser. */ 95 | int flags; /* See above flag test/set/remove macros. */ 96 | int buf_len; /* number of buffered chunks for current callback. */ 97 | } lhttp_parser; 98 | 99 | /* Concatinate and remove elements from the table at idx starting at 100 | * element begin and going to length len. The final concatinated string 101 | * is on the top of the stack. 102 | */ 103 | static int lhp_table_concat_and_clear(lua_State *L, int idx, int begin, int len) { 104 | luaL_Buffer buff; 105 | int real_len = len-begin+1; 106 | 107 | /* Empty table? */ 108 | if ( !real_len ) { 109 | lua_pushliteral(L, ""); 110 | return 0; 111 | } 112 | 113 | /* One element? */ 114 | if ( 1 == real_len ) { 115 | lua_rawgeti(L, idx, begin); 116 | /* remove values from buffer. */ 117 | lua_pushnil(L); 118 | lua_rawseti(L, idx, begin); 119 | return 0; 120 | } 121 | 122 | /* do a table concat. */ 123 | luaL_buffinit(L, &buff); 124 | for(; begin <= len; begin++) { 125 | lua_rawgeti(L, idx, begin); 126 | luaL_addvalue(&buff); 127 | /* remove values from buffer. */ 128 | lua_pushnil(L); 129 | lua_rawseti(L, idx, begin); 130 | } 131 | luaL_pushresult(&buff); 132 | return 0; 133 | } 134 | 135 | /* Clear all elements in the table at idx starting at 136 | * element begin and going to length len. 137 | */ 138 | static int lhp_table_clear(lua_State *L, int idx, int begin, int len) { 139 | /* nil all elements. */ 140 | for(; begin <= len; begin++) { 141 | /* remove element from table. */ 142 | lua_pushnil(L); 143 | lua_rawseti(L, idx, begin); 144 | } 145 | return 0; 146 | } 147 | 148 | /* "Flush" the buffer for the callback identified by cb_id. The 149 | * CB_ON_HEADER cb_id is flushed by inspecting FLAG_HAS_HFIELD(). 150 | * If that bit is not set, then the buffer is concatinated into 151 | * a single string element in the buffer and nothing is pushed 152 | * on the Lua stack. Otherwise the buffer table is cleared after 153 | * pushing the following onto the Lua stack: 154 | * 155 | * CB_ON_HEADER function, 156 | * first element of the buffer, 157 | * second - length element of the buffer concatinated 158 | * 159 | * If cb_id is not CB_ON_HEADER then the buffer table is cleared after 160 | * pushing the following onto the Lua stack: 161 | * 162 | * cb_id function, 163 | * first - length elements of the buffer concatinated 164 | * 165 | */ 166 | static int lhp_flush(lhttp_parser* lparser, int cb_id) { 167 | lua_State* L = (lua_State*)lparser->parser.data; 168 | int begin, len, result, top, save; 169 | 170 | assert(cb_id); 171 | assert(FLAG_HAS_BUF(lparser->flags, cb_id)); 172 | 173 | if ( ! lua_checkstack(L, 7) ) return -1; 174 | 175 | len = lparser->buf_len; 176 | begin = 1; 177 | top = lua_gettop(L); 178 | 179 | FLAG_RM_BUF(lparser->flags); 180 | if ( CB_ON_HEADER == cb_id ) { 181 | if ( FLAG_HAS_HFIELD(lparser->flags) ) { 182 | /* Push , [, ] */ 183 | lua_rawgeti(L, ST_FENV_IDX, cb_id); 184 | lua_rawgeti(L, ST_BUFFER_IDX, 1); 185 | lua_pushnil(L); 186 | lua_rawseti(L, ST_BUFFER_IDX, 1); 187 | 188 | begin = 2; 189 | save = 0; 190 | lparser->buf_len = 0; 191 | FLAG_RM_HFIELD(lparser->flags); 192 | } else { 193 | /* Save */ 194 | begin = 1; 195 | save = 1; 196 | lparser->buf_len = 1; 197 | } 198 | } else { 199 | /* Push [, ] */ 200 | lua_rawgeti(L, ST_FENV_IDX, cb_id); 201 | if (CB_ON_STATUS == cb_id){ 202 | lua_pushinteger(L, lparser->parser.status_code); 203 | } 204 | begin = 1; 205 | save = 0; 206 | lparser->buf_len = 0; 207 | } 208 | 209 | result = lhp_table_concat_and_clear(L, ST_BUFFER_IDX, begin, len); 210 | if ( 0 != result ) { 211 | lua_settop(L, top); 212 | return result; 213 | } 214 | 215 | if ( save ) lua_rawseti(L, ST_BUFFER_IDX, 1); 216 | 217 | return 0; 218 | } 219 | 220 | /* Puts the str of length len into the buffer table and 221 | * updates buf_len. It also sets the buf flag for cb_id. 222 | */ 223 | static int lhp_buffer(lhttp_parser* lparser, int cb_id, const char* str, size_t len, int hfield) { 224 | lua_State* L = (lua_State*)lparser->parser.data; 225 | 226 | assert(cb_id); 227 | assert(FLAG_HAS_CB(lparser->flags, cb_id)); 228 | 229 | /* insert event chunk into buffer. */ 230 | FLAG_SET_BUF(lparser->flags, cb_id); 231 | if ( hfield ) { 232 | FLAG_SET_HFIELD(lparser->flags); 233 | } 234 | 235 | lua_pushlstring(L, str, len); 236 | lua_rawseti(L, ST_BUFFER_IDX, ++(lparser->buf_len)); 237 | 238 | return 0; 239 | } 240 | 241 | /* Push the zero argument event for cb_id. Post condition: 242 | * Lua stack contains , nil 243 | */ 244 | static int lhp_push_nil_event(lhttp_parser* lparser, int cb_id) { 245 | lua_State* L = (lua_State*)lparser->parser.data; 246 | 247 | assert(FLAG_HAS_CB(lparser->flags, cb_id)); 248 | 249 | if ( ! lua_checkstack(L, 5) ) return -1; 250 | 251 | lua_rawgeti(L, ST_FENV_IDX, cb_id); 252 | if(CB_ON_CHUNK_HEADER == cb_id){ 253 | lhp_pushint64(L, lparser->parser.content_length); 254 | } 255 | else{ 256 | lua_pushnil(L); 257 | } 258 | 259 | return 0; 260 | } 261 | 262 | /* Flush the buffer as long as it is not the except_cb_id being buffered. 263 | */ 264 | static int lhp_flush_except(lhttp_parser* lparser, int except_cb_id, int hfield) { 265 | int flush = 0; 266 | int cb_id = FLAG_GET_BUF_CB_ID(lparser->flags); 267 | 268 | /* flush previous event and/or url */ 269 | if ( cb_id ) { 270 | if ( cb_id == CB_ON_HEADER ) { 271 | flush = hfield ^ FLAG_HAS_HFIELD(lparser->flags); 272 | } else if ( cb_id != except_cb_id ) { 273 | flush = 1; 274 | } 275 | } 276 | 277 | if ( flush ) { 278 | int result = lhp_flush(lparser, cb_id); 279 | if ( 0 != result ) return result; 280 | } 281 | return 0; 282 | } 283 | 284 | /* The event for cb_id where cb_id takes a string argument. 285 | */ 286 | static int lhp_http_data_cb(http_parser* parser, int cb_id, const char* str, size_t len, int hfield) { 287 | lhttp_parser* lparser = (lhttp_parser*)parser; 288 | 289 | int result = lhp_flush_except(lparser, cb_id, hfield); 290 | if ( 0 != result ) return result; 291 | 292 | if ( ! FLAG_HAS_CB(lparser->flags, cb_id) ) return 0; 293 | 294 | return lhp_buffer(lparser, cb_id, str, len, hfield); 295 | } 296 | 297 | static int lhp_http_cb(http_parser* parser, int cb_id) { 298 | lhttp_parser* lparser = (lhttp_parser*)parser; 299 | 300 | int result = lhp_flush_except(lparser, cb_id, 0); 301 | if ( 0 != result ) return result; 302 | 303 | if ( ! FLAG_HAS_CB(lparser->flags, cb_id) ) return 0; 304 | 305 | return lhp_push_nil_event(lparser, cb_id); 306 | } 307 | 308 | static int lhp_message_begin_cb(http_parser* parser) { 309 | return lhp_http_cb(parser, CB_ON_MESSAGE_BEGIN); 310 | } 311 | 312 | static int lhp_url_cb(http_parser* parser, const char* str, size_t len) { 313 | return lhp_http_data_cb(parser, CB_ON_URL, str, len, 0); 314 | } 315 | 316 | static int lhp_status_cb(http_parser* parser, const char* str, size_t len) { 317 | return lhp_http_data_cb(parser, CB_ON_STATUS, str, len, 0); 318 | } 319 | 320 | static int lhp_header_field_cb(http_parser* parser, const char* str, size_t len) { 321 | return lhp_http_data_cb(parser, CB_ON_HEADER, str, len, 0); 322 | } 323 | 324 | static int lhp_header_value_cb(http_parser* parser, const char* str, size_t len) { 325 | return lhp_http_data_cb(parser, CB_ON_HEADER, str, len, 1); 326 | } 327 | 328 | static int lhp_headers_complete_cb(http_parser* parser) { 329 | return lhp_http_cb(parser, CB_ON_HEADERS_COMPLETE); 330 | } 331 | 332 | static int lhp_body_cb(http_parser* parser, const char* str, size_t len) { 333 | /* on_headers_complete did any flushing, so just push the cb */ 334 | lhttp_parser* lparser = (lhttp_parser*)parser; 335 | lua_State* L = (lua_State*)lparser->parser.data; 336 | 337 | if ( ! FLAG_HAS_CB(lparser->flags, CB_ON_BODY) ) return 0; 338 | 339 | if ( ! lua_checkstack(L, 5) ) return -1; 340 | 341 | lua_rawgeti(L, ST_FENV_IDX, CB_ON_BODY); 342 | lua_pushlstring(L, str, len); 343 | 344 | return 0; 345 | } 346 | 347 | static int lhp_message_complete_cb(http_parser* parser) { 348 | /* Send on_body(nil) message to comply with LTN12 */ 349 | lhttp_parser* lparser = (lhttp_parser*)parser; 350 | if( FLAG_HAS_CB(lparser->flags, CB_ON_BODY) ) { 351 | int result = lhp_push_nil_event((lhttp_parser*)parser, CB_ON_BODY); 352 | if ( 0 != result ) return result; 353 | } 354 | 355 | return lhp_http_cb(parser, CB_ON_MESSAGE_COMPLETE); 356 | } 357 | 358 | static int lhp_chunk_header_cb(http_parser* parser) { 359 | return lhp_http_cb(parser, CB_ON_CHUNK_HEADER); 360 | } 361 | 362 | static int lhp_chunk_complete_cb(http_parser* parser) { 363 | return lhp_http_cb(parser, CB_ON_CHUNK_COMPLETE); 364 | } 365 | 366 | static int lhp_init(lua_State* L, enum http_parser_type type) { 367 | int cb_id; 368 | /* Stack: callbacks */ 369 | 370 | lhttp_parser* lparser; 371 | http_parser* parser; 372 | luaL_checktype(L, 1, LUA_TTABLE); 373 | lparser = (lhttp_parser*)lua_newuserdata(L, sizeof(lhttp_parser)); 374 | parser = &(lparser->parser); 375 | assert(NULL != parser); 376 | /* Stack: callbacks, userdata */ 377 | 378 | lparser->flags = 0; 379 | lparser->buf_len = 0; 380 | 381 | /* Get the metatable: */ 382 | luaL_getmetatable(L, PARSER_MT); 383 | assert(!lua_isnil(L, -1)/* PARSER_MT found? */); 384 | /* Stack: callbacks, userdata, metatable */ 385 | 386 | /* Copy functions to new fenv table */ 387 | lua_createtable(L, FENV_LEN, 0); 388 | /* Stack: callbacks, userdata, metatable, fenv */ 389 | for (cb_id = 1; cb_id <= CB_LEN; cb_id++ ) { 390 | lua_getfield(L, 1, lhp_callback_names[cb_id-1]); 391 | if ( lua_isfunction(L, -1) ) { 392 | lua_rawseti(L, -2, cb_id); /* fenv[cb_id] = callback */ 393 | FLAG_SET_CB(lparser->flags, cb_id); 394 | } else { 395 | lua_pop(L, 1); /* pop non-function value. */ 396 | } 397 | } 398 | /* Create buffer table and add it to the fenv table. */ 399 | lua_createtable(L, 1, 0); 400 | lua_rawseti(L, -2, FENV_BUFFER_IDX); 401 | /* Stack: callbacks, userdata, metatable, fenv */ 402 | lua_setfenv(L, -3); 403 | /* Stack: callbacks, userdata, metatable */ 404 | 405 | http_parser_init(parser, type); 406 | parser->data = NULL; 407 | 408 | lua_setmetatable(L, -2); 409 | 410 | return 1; 411 | } 412 | 413 | static int lhp_request(lua_State* L) { 414 | return lhp_init(L, HTTP_REQUEST); 415 | } 416 | 417 | static int lhp_response(lua_State* L) { 418 | return lhp_init(L, HTTP_RESPONSE); 419 | } 420 | 421 | static int lhp_execute(lua_State* L) { 422 | lhttp_parser* lparser = check_parser(L, 1); 423 | http_parser* parser = &(lparser->parser); 424 | size_t len; 425 | size_t result; 426 | const char* str = luaL_checklstring(L, 2, &len); 427 | 428 | static const http_parser_settings settings = { 429 | lhp_message_begin_cb, 430 | lhp_url_cb, 431 | lhp_status_cb, 432 | lhp_header_field_cb, 433 | lhp_header_value_cb, 434 | lhp_headers_complete_cb, 435 | lhp_body_cb, 436 | lhp_message_complete_cb, 437 | lhp_chunk_header_cb, 438 | lhp_chunk_complete_cb 439 | }; 440 | 441 | /* truncate stack to (userdata, string) */ 442 | lua_settop(L, 2); 443 | 444 | lua_getfenv(L, 1); 445 | assert(lua_istable(L, -1)); 446 | assert(lua_gettop(L) == ST_FENV_IDX); 447 | 448 | lua_rawgeti(L, ST_FENV_IDX, FENV_BUFFER_IDX); 449 | assert(lua_istable(L, -1)); 450 | assert(lua_gettop(L) == ST_BUFFER_IDX); 451 | 452 | assert(lua_gettop(L) == ST_LEN); 453 | lua_pushnil(L); 454 | 455 | /* Stack: (userdata, string, fenv, buffer, url, nil) */ 456 | parser->data = L; 457 | 458 | result = http_parser_execute(parser, &settings, str, len); 459 | 460 | parser->data = NULL; 461 | 462 | /* replace nil place-holder with 'result' code. */ 463 | lhp_pushint64(L, result); 464 | lua_replace(L, ST_LEN+1); 465 | /* Transform the stack into a table: */ 466 | len = lua_gettop(L) - ST_LEN; 467 | 468 | return len; 469 | } 470 | 471 | static int lhp_should_keep_alive(lua_State* L) { 472 | lhttp_parser* lparser = check_parser(L, 1); 473 | lua_pushboolean(L, http_should_keep_alive(&lparser->parser)); 474 | return 1; 475 | } 476 | 477 | static int lhp_is_upgrade(lua_State* L) { 478 | lhttp_parser* lparser = check_parser(L, 1); 479 | lua_pushboolean(L, lparser->parser.upgrade); 480 | return 1; 481 | } 482 | 483 | static int lhp__tostring(lua_State* L) { 484 | lhttp_parser* lparser = check_parser(L, 1); 485 | lua_pushfstring(L, PARSER_MT" %p", lparser); 486 | return 1; 487 | } 488 | 489 | static int lhp_method(lua_State* L) { 490 | lhttp_parser* lparser = check_parser(L, 1); 491 | switch(lparser->parser.method) { 492 | case HTTP_DELETE: lua_pushliteral(L, "DELETE"); break; 493 | case HTTP_GET: lua_pushliteral(L, "GET"); break; 494 | case HTTP_HEAD: lua_pushliteral(L, "HEAD"); break; 495 | case HTTP_POST: lua_pushliteral(L, "POST"); break; 496 | case HTTP_PUT: lua_pushliteral(L, "PUT"); break; 497 | case HTTP_CONNECT: lua_pushliteral(L, "CONNECT"); break; 498 | case HTTP_OPTIONS: lua_pushliteral(L, "OPTIONS"); break; 499 | case HTTP_TRACE: lua_pushliteral(L, "TRACE"); break; 500 | case HTTP_COPY: lua_pushliteral(L, "COPY"); break; 501 | case HTTP_LOCK: lua_pushliteral(L, "LOCK"); break; 502 | case HTTP_MKCOL: lua_pushliteral(L, "MKCOL"); break; 503 | case HTTP_MOVE: lua_pushliteral(L, "MOVE"); break; 504 | case HTTP_PROPFIND: lua_pushliteral(L, "PROPFIND"); break; 505 | case HTTP_PROPPATCH: lua_pushliteral(L, "PROPPATCH"); break; 506 | case HTTP_UNLOCK: lua_pushliteral(L, "UNLOCK"); break; 507 | default: 508 | lua_pushnumber(L, lparser->parser.method); 509 | } 510 | return 1; 511 | } 512 | 513 | static int lhp_version(lua_State* L) { 514 | lhttp_parser* lparser = check_parser(L, 1); 515 | lua_pushnumber(L, lparser->parser.http_major); 516 | lua_pushnumber(L, lparser->parser.http_minor); 517 | return 2; 518 | } 519 | 520 | static int lhp_status_code(lua_State* L) { 521 | lhttp_parser* lparser = check_parser(L, 1); 522 | lua_pushnumber(L, lparser->parser.status_code); 523 | return 1; 524 | } 525 | 526 | static int lhp_error(lua_State* L) { 527 | lhttp_parser* lparser = check_parser(L, 1); 528 | enum http_errno http_errno = lparser->parser.http_errno; 529 | lua_pushinteger(L, http_errno); 530 | lua_pushstring(L, http_errno_name(http_errno)); 531 | lua_pushstring(L, http_errno_description(http_errno)); 532 | return 3; 533 | } 534 | 535 | static int lhp_parse_url(lua_State* L){ 536 | 537 | #define SET_UF_FIELD(id, name) \ 538 | if(url.field_set & (1 << id)){ \ 539 | lua_pushlstring(L, u + url.field_data[id].off, url.field_data[id].len); \ 540 | lua_setfield(L, -2, name); \ 541 | } 542 | 543 | size_t len; const char *u = luaL_checklstring(L, 1, &len); 544 | int is_connect = lua_toboolean(L, 2); 545 | struct http_parser_url url; 546 | int result = http_parser_parse_url(u, len, is_connect, &url); 547 | if (result != 0) { 548 | lua_pushnil(L); 549 | lua_pushinteger(L, result); 550 | } 551 | 552 | lua_newtable(L); 553 | 554 | if(url.field_set & (1 << UF_PORT)){ 555 | lua_pushinteger(L, url.port); 556 | lua_setfield(L, -2, "port"); 557 | } 558 | 559 | SET_UF_FIELD(UF_SCHEMA, "schema" ); 560 | SET_UF_FIELD(UF_HOST, "host" ); 561 | SET_UF_FIELD(UF_PATH, "path" ); 562 | SET_UF_FIELD(UF_QUERY, "query" ); 563 | SET_UF_FIELD(UF_FRAGMENT, "fragment"); 564 | SET_UF_FIELD(UF_USERINFO, "userinfo"); 565 | 566 | return 1; 567 | #undef SET_UF_FIELD 568 | } 569 | 570 | static int lhp_reset(lua_State* L) { 571 | lhttp_parser* lparser = check_parser(L, 1); 572 | http_parser* parser = &(lparser->parser); 573 | 574 | /* truncate stack to (userdata) and calbacks */ 575 | lua_settop(L, 2); 576 | 577 | /* re-initialize http-parser. */ 578 | http_parser_init(parser, parser->type); 579 | 580 | /* truncate stack to (userdata) calbacks fenv */ 581 | lua_getfenv(L, 1); 582 | 583 | /* reset callbacks */ 584 | if(lua_istable(L, 2)){ 585 | int cb_id; 586 | for (cb_id = 1; cb_id <= CB_LEN; cb_id++ ) { 587 | lua_getfield(L, 2, lhp_callback_names[cb_id-1]); 588 | if ( lua_isfunction(L, -1) ) { 589 | FLAG_SET_CB(lparser->flags, cb_id); 590 | } else { 591 | FLAG_RM_CB(lparser->flags, cb_id); 592 | lua_pop(L, 1); /* pop non-function value. */ 593 | lua_pushnil(L); /* set callback as nil */ 594 | } 595 | lua_rawseti(L, -2, cb_id); /* fenv[cb_id] = callback */ 596 | } 597 | } 598 | 599 | /* clear buffer */ 600 | lua_rawgeti(L, 2, FENV_BUFFER_IDX); 601 | lhp_table_clear(L, 3, 1, lparser->buf_len); 602 | 603 | /* reset buffer length and flags. */ 604 | lparser->buf_len = 0; 605 | FLAG_RM_BUF(lparser->flags); 606 | FLAG_RM_HFIELD(lparser->flags); 607 | return 0; 608 | } 609 | 610 | static int lhp_is_function(lua_State* L) { 611 | lua_pushboolean(L, lua_isfunction(L, 1)); 612 | return 1; 613 | } 614 | 615 | /* The execute method has a "lua based stub" so that callbacks 616 | * can yield without having to apply the CoCo patch to Lua. */ 617 | static const char* lhp_execute_lua = 618 | "local c_execute, is_function = ...\n" 619 | "local function execute(result, cb, arg1, arg2, ...)\n" 620 | " if ( not cb ) then\n" 621 | " return result\n" 622 | " end\n" 623 | " if ( is_function(arg2) ) then\n" 624 | " cb(arg1)\n" 625 | " return execute(result, arg2, ...)" 626 | " end\n" 627 | " cb(arg1, arg2)\n" 628 | " return execute(result, ...)\n" 629 | "end\n" 630 | "return function(...)\n" 631 | " return execute(c_execute(...))\n" 632 | "end"; 633 | static void lhp_push_execute_fn(lua_State* L) { 634 | #ifndef NDEBUG 635 | int top = lua_gettop(L); 636 | #endif 637 | int err = luaL_loadstring(L, lhp_execute_lua); 638 | 639 | if ( err ) lua_error(L); 640 | 641 | lua_pushcfunction(L, lhp_execute); 642 | lua_pushcfunction(L, lhp_is_function); 643 | lua_call(L, 2, 1); 644 | 645 | /* Compiled lua function should be at the top of the stack now. */ 646 | assert(lua_gettop(L) == top + 1); 647 | assert(lua_isfunction(L, -1)); 648 | } 649 | 650 | LUALIB_API int luaopen_http_parser(lua_State* L) { 651 | /* parser metatable init */ 652 | luaL_newmetatable(L, PARSER_MT); 653 | 654 | lua_pushvalue(L, -1); 655 | lua_setfield(L, -2, "__index"); 656 | 657 | lua_pushcfunction(L, lhp_is_upgrade); 658 | lua_setfield(L, -2, "is_upgrade"); 659 | 660 | lua_pushcfunction(L, lhp__tostring); 661 | lua_setfield(L, -2, "__tostring"); 662 | 663 | lua_pushcfunction(L, lhp_method); 664 | lua_setfield(L, -2, "method"); 665 | 666 | lua_pushcfunction(L, lhp_version); 667 | lua_setfield(L, -2, "version"); 668 | 669 | lua_pushcfunction(L, lhp_status_code); 670 | lua_setfield(L, -2, "status_code"); 671 | 672 | lua_pushcfunction(L, lhp_error); 673 | lua_setfield(L, -2, "error"); 674 | 675 | lua_pushcfunction(L, lhp_should_keep_alive); 676 | lua_setfield(L, -2, "should_keep_alive"); 677 | 678 | lhp_push_execute_fn(L); 679 | lua_setfield(L, -2, "execute"); 680 | 681 | lua_pushcfunction(L, lhp_reset); 682 | lua_setfield(L, -2, "reset"); 683 | 684 | lua_pop(L, 1); 685 | 686 | /* export http.parser */ 687 | lua_newtable(L); /* Stack: table */ 688 | 689 | lua_pushcfunction(L, lhp_request); 690 | lua_setfield(L, -2, "request"); 691 | 692 | lua_pushcfunction(L, lhp_response); 693 | lua_setfield(L, -2, "response"); 694 | 695 | lua_pushcfunction(L, lhp_parse_url); 696 | lua_setfield(L, -2, "parse_url"); 697 | 698 | return 1; 699 | } 700 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | -- Make it easier to test 4 | local src_dir, build_dir = ... 5 | if ( src_dir ) then 6 | package.path = src_dir .. "?.lua;" .. package.path 7 | package.cpath = build_dir .. "?.so;" .. package.cpath 8 | end 9 | 10 | local lhp = require 'http.parser' 11 | 12 | local counter, failure = 1, 0 13 | 14 | function ok(assert_true, desc) 15 | if not assert_true then failure = failure + 1 end 16 | 17 | local msg = ( assert_true and "ok " or "not ok " ) .. counter 18 | if ( desc ) then 19 | msg = msg .. " - " .. desc 20 | end 21 | print(msg) 22 | counter = counter + 1 23 | end 24 | 25 | local function parse_path_query_fragment(uri) 26 | local url = lhp.parse_url(uri, true) 27 | return url.path, url.query, url.fragment 28 | end 29 | 30 | local pipeline = [[ 31 | GET / HTTP/1.1 32 | Host: localhost 33 | User-Agent: httperf/0.9.0 34 | Connection: keep-alive 35 | 36 | GET /header.jpg HTTP/1.1 37 | Host: localhost 38 | User-Agent: httperf/0.9.0 39 | Connection: keep-alive 40 | 41 | ]] 42 | pipeline = pipeline:gsub('\n', '\r\n') 43 | 44 | function pipeline_test() 45 | local cbs = {} 46 | local complete_count = 0 47 | local body = '' 48 | function cbs.on_body(chunk) 49 | if chunk then body = body .. chunk end 50 | end 51 | function cbs.on_message_complete() 52 | complete_count = complete_count + 1 53 | end 54 | 55 | local parser = lhp.request(cbs) 56 | ok(parser:execute(pipeline) == #pipeline) 57 | ok(parser:execute('') == 0) 58 | 59 | ok(parser:should_keep_alive() == true) 60 | ok(parser:method() == "GET") 61 | ok(complete_count == 2) 62 | ok(#body == 0) 63 | end 64 | 65 | function parse_url_test() 66 | 67 | local tast_cases = { 68 | {"http://hello.com:8080/some/path?with=1%23&args=value", false, 69 | { 70 | schema='http', 71 | host='hello.com', 72 | port=8080, 73 | path='/some/path', 74 | query='with=1%23&args=value', 75 | }, 76 | }, 77 | {"/foo/t.html?qstring#frag", true, 78 | { 79 | path='/foo/t.html', 80 | query='qstring', 81 | fragment='frag', 82 | }, 83 | }, 84 | } 85 | 86 | for _, tast_case in ipairs(tast_cases) do 87 | local url, is_connect, expect = tast_case[1], tast_case[2], tast_case[3] 88 | local result = lhp.parse_url(url, is_connect) 89 | is_deeply(result, expect, 'Url: ' .. url) 90 | end 91 | end 92 | 93 | function status_code_test() 94 | local response = { "HTTP/1.1 404 Not found", "", ""} 95 | local code, text 96 | local parser = lhp.response{ 97 | on_status = function(a, b) code, text = a, b end 98 | } 99 | parser:execute(table.concat(response, '\r\n')) 100 | ok(code == 404, 'Expected status code: 404, got ' .. tostring(code)) 101 | ok(text == 'Not found', 'Expected status text: `Not found`, got `' .. tostring(text) .. '`') 102 | end 103 | 104 | function chunk_header_test() 105 | local response = { 106 | "HTTP/1.1 200 OK"; 107 | "Transfer-Encoding: chunked"; 108 | ""; 109 | ""; 110 | } 111 | local content_length 112 | local parser = lhp.response{ 113 | on_chunk_header = function(a) content_length = a end 114 | } 115 | 116 | parser:execute(table.concat(response, '\r\n')) 117 | 118 | content_length = nil 119 | parser:execute("23\r\n") 120 | ok(content_length == 0x23, "first chunk Content-Length expected: 0x23, got " .. (content_length and string.format("0x%2X", content_length) or 'nil')) 121 | parser:execute("This is the data in the first chunk\r\n") 122 | 123 | content_length = nil 124 | parser:execute("1A\r\n") 125 | ok(content_length == 0x1A, "second chunk Content-Length expected: 0x1A, got " .. (content_length and string.format("0x%2X", content_length) or 'nil')) 126 | parser:execute("abcdefghigklmnopqrstuvwxyz\r\n") 127 | 128 | content_length = nil 129 | parser:execute("FFFFFFFFFF\r\n") 130 | ok(content_length == 1099511627775, "Content-Length over than 32 bits: 1099511627775, got " .. (content_length and string.format("%g", content_length) or 'nil')) 131 | end 132 | 133 | function reset_test() 134 | local url 135 | 136 | local parser = lhp.request{ 137 | on_url = function(u) url = u end; 138 | } 139 | 140 | parser:execute('GET /path1 HTTP/1.1') 141 | 142 | parser:reset() 143 | 144 | parser:execute('' 145 | .. 'POST /path2 HTTP/1.1\r\n' 146 | .. '\r\n' 147 | ) 148 | 149 | ok(parser:method() == 'POST', "reset should reinit parser") 150 | ok(url == '/path2', "reset should clear buffer and do not touch callbacks") 151 | end 152 | 153 | function reset_callback_test() 154 | local headers1, headers2, url = {}, {} 155 | 156 | local parser = lhp.request{ 157 | on_header = function(h, v) headers1[h] = v end; 158 | on_url = function(u) url = u end; 159 | } 160 | 161 | -- reset on_header and remove on_url 162 | parser:reset{ 163 | on_header = function(h, v) headers2[h] = v end; 164 | } 165 | 166 | parser:execute('' 167 | .. 'GET /path HTTP/1.1\r\n' 168 | .. 'A: b\r\n' 169 | .. '\r\n' 170 | ) 171 | 172 | ok(url == nil, "reset should remove callback") 173 | ok(headers1.A == nil and headers2.A == 'b', "reset should change callback") 174 | end 175 | 176 | -- NOTE: http-parser fails if the first response is HTTP 1.0: 177 | -- HTTP/1.0 100 Please continue mate. 178 | -- Which I think is a HTTP spec violation, but other HTTP clients, still work. 179 | -- http-parser will fail by seeing only one HTTP response and putting everything elses in 180 | -- the response body for the first 100 response, until socket close. 181 | local please_continue = [[ 182 | HTTP/1.1 100 Please continue mate. 183 | 184 | HTTP/1.1 200 OK 185 | Date: Wed, 02 Feb 2011 00:50:50 GMT 186 | Content-Length: 10 187 | Connection: close 188 | 189 | 0123456789]] 190 | please_continue = please_continue:gsub('\n', '\r\n') 191 | 192 | function please_continue_test() 193 | local cbs = {} 194 | local complete_count = 0 195 | local body = '' 196 | function cbs.on_body(chunk) 197 | if chunk then body = body .. chunk end 198 | end 199 | function cbs.on_message_complete() 200 | complete_count = complete_count + 1 201 | end 202 | 203 | local parser = lhp.response(cbs) 204 | parser:execute(please_continue) 205 | parser:execute('') 206 | 207 | ok(parser:should_keep_alive() == false) 208 | ok(parser:status_code() == 200) 209 | ok(complete_count == 2) 210 | ok(#body == 10) 211 | end 212 | 213 | local connection_close = [[ 214 | HTTP/1.1 200 OK 215 | Date: Wed, 02 Feb 2011 00:50:50 GMT 216 | Connection: close 217 | 218 | 0123456789]] 219 | connection_close = connection_close:gsub('\n', '\r\n') 220 | 221 | function connection_close_test() 222 | local cbs = {} 223 | local complete_count = 0 224 | local body = '' 225 | function cbs.on_body(chunk) 226 | if chunk then body = body .. chunk end 227 | end 228 | function cbs.on_message_complete() 229 | complete_count = complete_count + 1 230 | end 231 | 232 | local parser = lhp.response(cbs) 233 | parser:execute(connection_close) 234 | parser:execute('') 235 | 236 | ok(parser:should_keep_alive() == false) 237 | ok(parser:status_code() == 200) 238 | ok(complete_count == 1) 239 | ok(#body == 10) 240 | end 241 | 242 | function nil_body_test() 243 | local cbs = {} 244 | local body_count = 0 245 | local body = {} 246 | function cbs.on_body(chunk) 247 | body[#body+1] = chunk 248 | body_count = body_count + 1 249 | end 250 | 251 | local parser = lhp.request(cbs) 252 | parser:execute("GET / HTTP/1.1\r\n") 253 | parser:execute("Transfer-Encoding: chunked\r\n") 254 | parser:execute("\r\n") 255 | parser:execute("23\r\n") 256 | parser:execute("This is the data in the first chunk\r\n") 257 | parser:execute("1C\r\n") 258 | parser:execute("X and this is the second one\r\n") 259 | ok(body_count == 2) 260 | 261 | is_deeply(body, 262 | {"This is the data in the first chunk", 263 | "X and this is the second one"}) 264 | 265 | -- This should cause on_body(nil) to be sent 266 | parser:execute("0\r\n\r\n") 267 | 268 | ok(body_count == 3) 269 | ok(#body == 2) 270 | end 271 | 272 | function max_events_test(N) 273 | N = N or 3000 274 | 275 | -- The goal of this test is to generate the most possible events 276 | local input_tbl = { 277 | "GET / HTTP/1.1\r\n", 278 | } 279 | -- Generate enough events to trigger a "stack overflow" 280 | local header_cnt = N 281 | for i=1, header_cnt do 282 | input_tbl[#input_tbl+1] = "a:\r\n" 283 | end 284 | input_tbl[#input_tbl+1] = "\r\n" 285 | 286 | local cbs = {} 287 | local field_cnt = 0 288 | function cbs.on_header(field, value) 289 | field_cnt = field_cnt + 1 290 | end 291 | 292 | local parser = lhp.request(cbs) 293 | local input = table.concat(input_tbl) 294 | local result = parser:execute(input) 295 | 296 | N = N * 2 297 | if (#input == result) and ( N < 100000 ) then 298 | return max_events_test(N) 299 | end 300 | 301 | input = input:sub(result + 1) 302 | 303 | -- We should have generated a stack overflow event that should be 304 | -- handled gracefully... note that 305 | ok(#input < result, 306 | "Expect " .. header_cnt .. " field events, got " .. field_cnt) 307 | 308 | result = parser:execute(input) 309 | 310 | ok(0 == result, "Parser can not continue after stack overflow [" 311 | .. tostring(result) .. "]") 312 | end 313 | 314 | function regression_no_body_cb_test() 315 | -- The goal of this test is to generate the most possible events 316 | local input_tbl = { 317 | "GET / HTTP/1.1\r\n", 318 | "Header: value\r\n", 319 | "\r\n", 320 | } 321 | 322 | local parser = lhp.request{} 323 | 324 | local input = table.concat(input_tbl) 325 | 326 | local result = parser:execute(input) 327 | ok(result == #input, 'can work without on_body callback') 328 | end 329 | 330 | function basic_tests() 331 | local expects = {} 332 | local requests = {} 333 | 334 | -- NOTE: All requests must be version HTTP/1.1 since we re-use the same HTTP parser for all requests. 335 | requests.ab = { 336 | "GET /", "foo/t", ".html?", "qst", "ring", "#fr", "ag ", "HTTP/1.1\r\n", 337 | "Ho", 338 | "st: loca", 339 | "lhos", 340 | "t:8000\r\nUser-Agent: ApacheBench/2.3\r\n", 341 | "Con", "tent-L", "ength", ": 5\r\n", 342 | "Accept: */*\r\n\r", 343 | "\nbody\n", 344 | } 345 | 346 | expects.ab = { 347 | path = "/foo/t.html", 348 | query_string = "qstring", 349 | fragment = "frag", 350 | url = "/foo/t.html?qstring#frag", 351 | headers = { 352 | Host = "localhost:8000", 353 | ["User-Agent"] = "ApacheBench/2.3", 354 | Accept = "*/*", 355 | }, 356 | body = { "body\n" } 357 | } 358 | 359 | requests.no_buff_body = { 360 | "GET / HTTP/1.1\r\n", 361 | "Host: foo:80\r\n", 362 | "Content-Length: 12\r\n", 363 | "\r\n", 364 | "chunk1", "chunk2", 365 | } 366 | 367 | expects.no_buff_body = { 368 | body = { 369 | "chunk1", "chunk2" 370 | } 371 | } 372 | 373 | requests.httperf = { 374 | "GET / HTTP/1.1\r\n", 375 | "Host: localhost\r\n", 376 | "User-Agent: httperf/0.9.0\r\n\r\n" 377 | } 378 | 379 | expects.httperf = { 380 | } 381 | 382 | requests.firefox = { 383 | "GET / HTTP/1.1\r\n", 384 | "Host: two.local:8000\r\n", 385 | "User-Agent: Mozilla/5.0 (X11; U;", 386 | "Linux i686; en-US; rv:1.9.0.15)", 387 | "Gecko/2009102815 Ubuntu/9.04 (jaunty)", 388 | "Firefox/3.0.15\r\n", 389 | "Accept: text/html,application/xhtml+xml,application/xml;", 390 | "q=0.9,*/*;q=0.8\r\n", 391 | "Accept-Language:en-gb,en;q=0.5\r\n", 392 | "Accept-Encoding: gzip,deflate\r\n", 393 | "Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n", 394 | "Keep-Alive: 300\r\n", 395 | "Connection:keep-alive\r\n\r\n" 396 | } 397 | 398 | expects.firefox = { 399 | headers = { 400 | ["User-Agent"] = "Mozilla/5.0 (X11; U;Linux i686; en-US; rv:1.9.0.15)Gecko/2009102815 Ubuntu/9.04 (jaunty)Firefox/3.0.15", 401 | } 402 | } 403 | 404 | local parser, reqs = init_parser() 405 | 406 | for name, data in pairs(requests) do 407 | for _, line in ipairs(data) do 408 | local bytes_read = parser:execute(line) 409 | ok(bytes_read == #line, "only ["..tostring(bytes_read).."] bytes read, expected ["..tostring(#line).."] in ".. name) 410 | end 411 | 412 | local got = reqs[#reqs] 413 | local expect = expects[name] 414 | ok(parser:method() == "GET", "Method is GET") 415 | is_deeply(got, expect, "Check " .. name) 416 | end 417 | end 418 | 419 | function buffer_tests() 420 | local cb = {} 421 | local cb_cnt = 0 422 | local cb_val 423 | 424 | cb = {} 425 | cb_cnt = 0 426 | cb_val = nil 427 | function cb.on_url(value) 428 | cb_cnt = cb_cnt + 1 429 | cb_val = value 430 | end 431 | 432 | req = lhp.request(cb) 433 | 434 | req:execute("GET /path") 435 | req:execute("?qs HTTP/1.1\r\n") 436 | req:execute("Header-Field:") 437 | 438 | ok(cb_cnt == 1, "on_url flushed? " .. tostring(cb_cnt)) 439 | ok(cb_val == "/path?qs", "on_url buffered") 440 | end 441 | 442 | function init_parser() 443 | local reqs = {} 444 | local cur = nil 445 | local cb = {} 446 | 447 | function cb.on_message_begin() 448 | ok(cur == nil) 449 | cur = { headers = {} } 450 | end 451 | 452 | function cb.on_url(value) 453 | ok(cur.url == nil, "expected [url]=nil, but got ["..tostring(cur.url).. 454 | "] when setting field [" .. tostring(value) .. "]") 455 | cur.url = value; 456 | cur.path, cur.query_string, cur.fragment = parse_path_query_fragment(value) 457 | end 458 | 459 | function cb.on_body(value) 460 | if ( nil == cur.body ) then 461 | cur.body = {} 462 | end 463 | table.insert(cur.body, value) 464 | end 465 | 466 | function cb.on_header(field, value) 467 | ok(cur.headers[field] == nil) 468 | cur.headers[field] = value 469 | end 470 | 471 | function cb.on_message_complete() 472 | ok(nil ~= cur) 473 | table.insert(reqs, cur) 474 | cur = nil 475 | end 476 | 477 | return lhp.request(cb), reqs 478 | end 479 | 480 | function is_deeply(got, expect, msg, context) 481 | if ( type(expect) ~= "table" ) then 482 | print("# Expected [" .. context .. "] to be a table") 483 | ok(false, msg) 484 | return false 485 | end 486 | for k, v in pairs(expect) do 487 | local ctx 488 | if ( nil == context ) then 489 | ctx = k 490 | else 491 | ctx = context .. "." .. k 492 | end 493 | if type(expect[k]) == "table" then 494 | if ( not is_deeply(got[k], expect[k], msg, ctx) ) then 495 | return false 496 | end 497 | else 498 | if ( got[k] ~= expect[k] ) then 499 | print("# Expected [" .. ctx .. "] to be '" 500 | .. tostring(expect[k]) .. "', but got '" 501 | .. tostring(got[k]) 502 | .. "'") 503 | ok(false, msg) 504 | return false 505 | end 506 | end 507 | end 508 | if ( nil == context ) then 509 | ok(true, msg); 510 | end 511 | return true 512 | end 513 | 514 | buffer_tests() 515 | basic_tests() 516 | max_events_test() 517 | nil_body_test() 518 | pipeline_test() 519 | please_continue_test() 520 | connection_close_test() 521 | regression_no_body_cb_test() 522 | status_code_test() 523 | chunk_header_test() 524 | parse_url_test() 525 | reset_test() 526 | reset_callback_test() 527 | 528 | print("1.." .. counter-1) 529 | if failure > 0 then os.exit(-1) end 530 | --------------------------------------------------------------------------------