You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
302 lines
9.1 KiB
Lua
302 lines
9.1 KiB
Lua
--[[
|
|
|
|
Copyright 2014-2015 The Luvit Authors. All Rights Reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS-IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
--]]
|
|
|
|
--[[lit-meta
|
|
name = "luvit/http-codec"
|
|
version = "3.0.6"
|
|
homepage = "https://github.com/luvit/luvit/blob/master/deps/http-codec.lua"
|
|
description = "A simple pair of functions for converting between hex and raw strings."
|
|
tags = {"codec", "http"}
|
|
license = "Apache 2"
|
|
author = { name = "Tim Caswell" }
|
|
]]
|
|
|
|
local sub = string.sub
|
|
local gsub = string.gsub
|
|
local lower = string.lower
|
|
local find = string.find
|
|
local format = string.format
|
|
local concat = table.concat
|
|
local match = string.match
|
|
|
|
local STATUS_CODES = {
|
|
[100] = 'Continue',
|
|
[101] = 'Switching Protocols',
|
|
[102] = 'Processing', -- RFC 2518, obsoleted by RFC 4918
|
|
[200] = 'OK',
|
|
[201] = 'Created',
|
|
[202] = 'Accepted',
|
|
[203] = 'Non-Authoritative Information',
|
|
[204] = 'No Content',
|
|
[205] = 'Reset Content',
|
|
[206] = 'Partial Content',
|
|
[207] = 'Multi-Status', -- RFC 4918
|
|
[300] = 'Multiple Choices',
|
|
[301] = 'Moved Permanently',
|
|
[302] = 'Moved Temporarily',
|
|
[303] = 'See Other',
|
|
[304] = 'Not Modified',
|
|
[305] = 'Use Proxy',
|
|
[307] = 'Temporary Redirect',
|
|
[400] = 'Bad Request',
|
|
[401] = 'Unauthorized',
|
|
[402] = 'Payment Required',
|
|
[403] = 'Forbidden',
|
|
[404] = 'Not Found',
|
|
[405] = 'Method Not Allowed',
|
|
[406] = 'Not Acceptable',
|
|
[407] = 'Proxy Authentication Required',
|
|
[408] = 'Request Time-out',
|
|
[409] = 'Conflict',
|
|
[410] = 'Gone',
|
|
[411] = 'Length Required',
|
|
[412] = 'Precondition Failed',
|
|
[413] = 'Request Entity Too Large',
|
|
[414] = 'Request-URI Too Large',
|
|
[415] = 'Unsupported Media Type',
|
|
[416] = 'Requested Range Not Satisfiable',
|
|
[417] = 'Expectation Failed',
|
|
[418] = "I'm a teapot", -- RFC 2324
|
|
[422] = 'Unprocessable Entity', -- RFC 4918
|
|
[423] = 'Locked', -- RFC 4918
|
|
[424] = 'Failed Dependency', -- RFC 4918
|
|
[425] = 'Unordered Collection', -- RFC 4918
|
|
[426] = 'Upgrade Required', -- RFC 2817
|
|
[428] = 'Precondition Required', -- RFC 6585
|
|
[429] = 'Too Many Requests', -- RFC 6585
|
|
[431] = 'Request Header Fields Too Large', -- RFC 6585
|
|
[500] = 'Internal Server Error',
|
|
[501] = 'Not Implemented',
|
|
[502] = 'Bad Gateway',
|
|
[503] = 'Service Unavailable',
|
|
[504] = 'Gateway Time-out',
|
|
[505] = 'HTTP Version not supported',
|
|
[506] = 'Variant Also Negotiates', -- RFC 2295
|
|
[507] = 'Insufficient Storage', -- RFC 4918
|
|
[509] = 'Bandwidth Limit Exceeded',
|
|
[510] = 'Not Extended', -- RFC 2774
|
|
[511] = 'Network Authentication Required' -- RFC 6585
|
|
}
|
|
|
|
local function encoder()
|
|
|
|
local mode
|
|
local encodeHead, encodeRaw, encodeChunked
|
|
|
|
function encodeHead(item)
|
|
if not item or item == "" then
|
|
return item
|
|
elseif not (type(item) == "table") then
|
|
error("expected a table but got a " .. type(item) .. " when encoding data")
|
|
end
|
|
local head, chunkedEncoding
|
|
local version = item.version or 1.1
|
|
if item.method then
|
|
local path = item.path
|
|
assert(path and #path > 0, "expected non-empty path")
|
|
head = { item.method .. ' ' .. item.path .. ' HTTP/' .. version .. '\r\n' }
|
|
else
|
|
local reason = item.reason or STATUS_CODES[item.code]
|
|
head = { 'HTTP/' .. version .. ' ' .. item.code .. ' ' .. reason .. '\r\n' }
|
|
end
|
|
for i = 1, #item do
|
|
local key, value = unpack(item[i])
|
|
local lowerKey = lower(key)
|
|
if lowerKey == "transfer-encoding" then
|
|
chunkedEncoding = lower(value) == "chunked"
|
|
end
|
|
value = gsub(tostring(value), "[\r\n]+", " ")
|
|
head[#head + 1] = key .. ': ' .. tostring(value) .. '\r\n'
|
|
end
|
|
head[#head + 1] = '\r\n'
|
|
|
|
mode = chunkedEncoding and encodeChunked or encodeRaw
|
|
return concat(head)
|
|
end
|
|
|
|
function encodeRaw(item)
|
|
if type(item) ~= "string" then
|
|
mode = encodeHead
|
|
return encodeHead(item)
|
|
end
|
|
return item
|
|
end
|
|
|
|
function encodeChunked(item)
|
|
if type(item) ~= "string" then
|
|
mode = encodeHead
|
|
local extra = encodeHead(item)
|
|
if extra then
|
|
return "0\r\n\r\n" .. extra
|
|
else
|
|
return "0\r\n\r\n"
|
|
end
|
|
end
|
|
if #item == 0 then
|
|
mode = encodeHead
|
|
end
|
|
return format("%x", #item) .. "\r\n" .. item .. "\r\n"
|
|
end
|
|
|
|
mode = encodeHead
|
|
return function (item)
|
|
return mode(item)
|
|
end
|
|
end
|
|
|
|
local function decoder()
|
|
|
|
-- This decoder is somewhat stateful with 5 different parsing states.
|
|
local decodeHead, decodeEmpty, decodeRaw, decodeChunked, decodeCounted
|
|
local mode -- state variable that points to various decoders
|
|
local bytesLeft -- For counted decoder
|
|
|
|
-- This state is for decoding the status line and headers.
|
|
function decodeHead(chunk, index)
|
|
if not chunk or index > #chunk then return end
|
|
|
|
local _, last = find(chunk, "\r?\n\r?\n", index)
|
|
-- First make sure we have all the head before continuing
|
|
if not last then
|
|
if (#chunk - index) <= 8 * 1024 then return end
|
|
-- But protect against evil clients by refusing heads over 8K long.
|
|
error("entity too large")
|
|
end
|
|
|
|
-- Parse the status/request line
|
|
local head = {}
|
|
local _, offset
|
|
local version
|
|
_, offset, version, head.code, head.reason =
|
|
find(chunk, "^HTTP/(%d%.%d) (%d+) ([^\r\n]*)\r?\n", index)
|
|
if offset then
|
|
head.code = tonumber(head.code)
|
|
else
|
|
_, offset, head.method, head.path, version =
|
|
find(chunk, "^(%u+) ([^ ]+) HTTP/(%d%.%d)\r?\n", index)
|
|
if not offset then
|
|
error("expected HTTP data")
|
|
end
|
|
end
|
|
version = tonumber(version)
|
|
head.version = version
|
|
head.keepAlive = version > 1.0
|
|
|
|
-- We need to inspect some headers to know how to parse the body.
|
|
local contentLength
|
|
local chunkedEncoding
|
|
|
|
-- Parse the header lines
|
|
while true do
|
|
local key, value
|
|
_, offset, key, value = find(chunk, "^([^:\r\n]+): *([^\r\n]*)\r?\n", offset + 1)
|
|
if not offset then break end
|
|
local lowerKey = lower(key)
|
|
|
|
-- Inspect a few headers and remember the values
|
|
if lowerKey == "content-length" then
|
|
contentLength = tonumber(value)
|
|
elseif lowerKey == "transfer-encoding" then
|
|
chunkedEncoding = lower(value) == "chunked"
|
|
elseif lowerKey == "connection" then
|
|
head.keepAlive = lower(value) == "keep-alive"
|
|
end
|
|
head[#head + 1] = {key, value}
|
|
end
|
|
|
|
if head.keepAlive and (not (chunkedEncoding or (contentLength and contentLength > 0)))
|
|
or (head.method == "GET" or head.method == "HEAD") then
|
|
mode = decodeEmpty
|
|
elseif chunkedEncoding then
|
|
mode = decodeChunked
|
|
elseif contentLength then
|
|
bytesLeft = contentLength
|
|
mode = decodeCounted
|
|
elseif not head.keepAlive then
|
|
mode = decodeRaw
|
|
end
|
|
return head, last + 1
|
|
|
|
end
|
|
|
|
-- This is used for inserting a single empty string into the output string for known empty bodies
|
|
function decodeEmpty(chunk, index)
|
|
mode = decodeHead
|
|
return "", index
|
|
end
|
|
|
|
function decodeRaw(chunk, index)
|
|
if #chunk < index then return end
|
|
return sub(chunk, index)
|
|
end
|
|
|
|
function decodeChunked(chunk, index)
|
|
local len, term
|
|
len, term = match(chunk, "^(%x+)(..)", index)
|
|
if not len then return end
|
|
if term ~= "\r\n" then
|
|
-- Wait for full chunk-size\r\n header
|
|
if #chunk < 18 then return end
|
|
-- But protect against evil clients by refusing chunk-sizes longer than 16 hex digits.
|
|
error("chunk-size field too large")
|
|
end
|
|
index = index + #len + 2
|
|
local offset = index - 1
|
|
local length = tonumber(len, 16)
|
|
if #chunk < offset + length + 2 then return end
|
|
if length == 0 then
|
|
mode = decodeHead
|
|
end
|
|
assert(sub(chunk, index + length, index + length + 1) == "\r\n")
|
|
local piece = sub(chunk, index, index + length - 1)
|
|
return piece, index + length + 2
|
|
end
|
|
|
|
function decodeCounted(chunk, index)
|
|
if bytesLeft == 0 then
|
|
mode = decodeEmpty
|
|
return mode(chunk, index)
|
|
end
|
|
local offset = index - 1
|
|
local length = #chunk - offset
|
|
-- Make sure we have at least one byte to process
|
|
if length == 0 then return end
|
|
|
|
-- If there isn't enough data left, emit what we got so far
|
|
if length < bytesLeft then
|
|
bytesLeft = bytesLeft - length
|
|
return sub(chunk, index)
|
|
end
|
|
|
|
mode = decodeEmpty
|
|
return sub(chunk, index, offset + bytesLeft), index + bytesLeft
|
|
end
|
|
|
|
-- Switch between states by changing which decoder mode points to
|
|
mode = decodeHead
|
|
return function (chunk, index)
|
|
return mode(chunk, index)
|
|
end
|
|
|
|
end
|
|
|
|
return {
|
|
encoder = encoder,
|
|
decoder = decoder,
|
|
}
|