summaryrefslogtreecommitdiffstatshomepage
path: root/plugins/http-codec/init.lua
blob: 11a329457291eacd90a55a369331d9c58aeec6c3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
--[[

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.

--]]

local exports = {}

exports.name = "luvit/http-codec"
exports.version = "1.0.0-1"
exports.homepage = "https://github.com/luvit/luvit/blob/master/deps/http-codec.lua"
exports.description = "A simple pair of functions for converting between hex and raw strings."
exports.tags = {"codec", "http"}
exports.license = "Apache 2"
exports.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
  [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
}

exports.encoder = function ()

  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

exports.decoder = function ()

  -- 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)
    if not chunk then return end

    local _, length = find(chunk, "\r?\n\r?\n", 1)
    -- First make sure we have all the head before continuing
    if not length then
      if #chunk < 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")
    if offset then
      head.code = tonumber(head.code)
    else
      _, offset, head.method, head.path, version =
        find(chunk, "^(%u+) ([^ ]+) HTTP/(%d%.%d)\r?\n")
      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, sub(chunk, length + 1)

  end

  -- This is used for inserting a single empty string into the output string for known empty bodies
  function decodeEmpty(chunk)
    mode = decodeHead
    return "", chunk or ""
  end

  function decodeRaw(chunk)
    if not chunk then return "", "" end
    if #chunk == 0 then return end
    return chunk, ""
  end

  function decodeChunked(chunk)
    local len, term
    len, term = match(chunk, "^(%x+)(..)")
    if not len then return end
    assert(term == "\r\n")
    local length = tonumber(len, 16)
    if #chunk < length + 4 + #len then return end
    if length == 0 then
      mode = decodeHead
    end
    chunk = sub(chunk, #len + 3)
    assert(sub(chunk, length + 1, length + 2) == "\r\n")
    return sub(chunk, 1, length), sub(chunk, length + 3)
  end

  function decodeCounted(chunk)
    if bytesLeft == 0 then
      mode = decodeEmpty
      return mode(chunk)
    end
    local length = #chunk
    -- Make sure we have at least one byte to process
    if length == 0 then return end

    if length >= bytesLeft then
      mode = decodeEmpty
    end

    -- If the entire chunk fits, pass it all through
    if length <= bytesLeft then
      bytesLeft = bytesLeft - length
      return chunk, ""
    end

    return sub(chunk, 1, bytesLeft), sub(chunk, bytesLeft + 1)
  end

  -- Switch between states by changing which decoder mode points to
  mode = decodeHead
  return function (chunk)
    return mode(chunk)
  end

end

return exports