* luci/libs/http: fix a few corner cases which can lead to bugs in mime decoding...
[project/luci.git] / libs / http / luasrc / http / protocol.lua
1 --[[
2
3 HTTP protocol implementation for LuCI
4 (c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net>
5
6 Licensed under the Apache License, Version 2.0 (the "License");
7 you may not use this file except in compliance with the License.
8 You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 $Id$
13
14 ]]--
15
16 module("luci.http.protocol", package.seeall)
17
18 local ltn12 = require("luci.ltn12")
19
20 HTTP_MAX_CONTENT = 1024*8 -- 8 kB maximum content size
21
22 -- Decode an urlencoded string.
23 -- Returns the decoded value.
24 function urldecode( str, no_plus )
25
26 local function __chrdec( hex )
27 return string.char( tonumber( hex, 16 ) )
28 end
29
30 if type(str) == "string" then
31 if not no_plus then
32 str = str:gsub( "+", " " )
33 end
34
35 str = str:gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
36 end
37
38 return str
39 end
40
41
42 -- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url.
43 -- Returns a table value with urldecoded values.
44 function urldecode_params( url, tbl )
45
46 local params = tbl or { }
47
48 if url:find("?") then
49 url = url:gsub( "^.+%?([^?]+)", "%1" )
50 end
51
52 for pair in url:gmatch( "[^&;]+" ) do
53
54 -- find key and value
55 local key = urldecode( pair:match("^([^=]+)") )
56 local val = urldecode( pair:match("^[^=]+=(.+)$") )
57
58 -- store
59 if type(key) == "string" and key:len() > 0 then
60 if type(val) ~= "string" then val = "" end
61
62 if not params[key] then
63 params[key] = val
64 elseif type(params[key]) ~= "table" then
65 params[key] = { params[key], val }
66 else
67 table.insert( params[key], val )
68 end
69 end
70 end
71
72 return params
73 end
74
75
76 -- Encode given string in urlencoded format.
77 -- Returns the encoded string.
78 function urlencode( str )
79
80 local function __chrenc( chr )
81 return string.format(
82 "%%%02x", string.byte( chr )
83 )
84 end
85
86 if type(str) == "string" then
87 str = str:gsub(
88 "([^a-zA-Z0-9$_%-%.%+!*'(),])",
89 __chrenc
90 )
91 end
92
93 return str
94 end
95
96
97 -- Encode given table to urlencoded string.
98 -- Returns the encoded string.
99 function urlencode_params( tbl )
100 local enc = ""
101
102 for k, v in pairs(tbl) do
103 enc = enc .. ( enc and "&" or "" ) ..
104 urlencode(k) .. "=" ..
105 urlencode(v)
106 end
107
108 return enc
109 end
110
111
112 -- Parameter helper
113 local function __initval( tbl, key )
114 if tbl[key] == nil then
115 tbl[key] = ""
116 elseif type(tbl[key]) == "string" then
117 tbl[key] = { tbl[key], "" }
118 else
119 table.insert( tbl[key], "" )
120 end
121 end
122
123 local function __appendval( tbl, key, chunk )
124 if type(tbl[key]) == "table" then
125 tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk
126 else
127 tbl[key] = tbl[key] .. chunk
128 end
129 end
130
131 local function __finishval( tbl, key, handler )
132 if handler then
133 if type(tbl[key]) == "table" then
134 tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] )
135 else
136 tbl[key] = handler( tbl[key] )
137 end
138 end
139 end
140
141
142 -- Table of our process states
143 local process_states = { }
144
145 -- Extract "magic", the first line of a http message.
146 -- Extracts the message type ("get", "post" or "response"), the requested uri
147 -- or the status code if the line descripes a http response.
148 process_states['magic'] = function( msg, chunk, err )
149
150 if chunk ~= nil then
151 -- ignore empty lines before request
152 if #chunk == 0 then
153 return true, nil
154 end
155
156 -- Is it a request?
157 local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$")
158
159 -- Yup, it is
160 if method then
161
162 msg.type = "request"
163 msg.request_method = method:lower()
164 msg.request_uri = uri
165 msg.http_version = tonumber( http_ver )
166 msg.headers = { }
167
168 -- We're done, next state is header parsing
169 return true, function( chunk )
170 return process_states['headers']( msg, chunk )
171 end
172
173 -- Is it a response?
174 else
175
176 local http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$")
177
178 -- Is a response
179 if code then
180
181 msg.type = "response"
182 msg.status_code = code
183 msg.status_message = message
184 msg.http_version = tonumber( http_ver )
185 msg.headers = { }
186
187 -- We're done, next state is header parsing
188 return true, function( chunk )
189 return process_states['headers']( msg, chunk )
190 end
191 end
192 end
193 end
194
195 -- Can't handle it
196 return nil, "Invalid HTTP message magic"
197 end
198
199
200 -- Extract headers from given string.
201 process_states['headers'] = function( msg, chunk )
202
203 if chunk ~= nil then
204
205 -- Look for a valid header format
206 local hdr, val = chunk:match( "^([A-Z][A-Za-z0-9%-_]+): +(.+)$" )
207
208 if type(hdr) == "string" and hdr:len() > 0 and
209 type(val) == "string" and val:len() > 0
210 then
211 msg.headers[hdr] = val
212
213 -- Valid header line, proceed
214 return true, nil
215
216 elseif #chunk == 0 then
217 -- Empty line, we won't accept data anymore
218 return false, nil
219 else
220 -- Junk data
221 return nil, "Invalid HTTP header received"
222 end
223 else
224 return nil, "Unexpected EOF"
225 end
226 end
227
228
229 -- Creates a header source from a given socket
230 function header_source( sock )
231 return ltn12.source.simplify( function()
232
233 local chunk, err, part = sock:receive("*l")
234
235 -- Line too long
236 if chunk == nil then
237 if err ~= "timeout" then
238 return nil, part
239 and "Line exceeds maximum allowed length"
240 or "Unexpected EOF"
241 else
242 return nil, err
243 end
244
245 -- Line ok
246 elseif chunk ~= nil then
247
248 -- Strip trailing CR
249 chunk = chunk:gsub("\r$","")
250
251 return chunk, nil
252 end
253 end )
254 end
255
256
257 -- Decode MIME encoded data.
258 function mimedecode_message_body( src, msg, filecb )
259
260 if msg and msg.env.CONTENT_TYPE then
261 msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
262 end
263
264 if not msg.mime_boundary then
265 return nil, "Invalid Content-Type found"
266 end
267
268
269 local tlen = 0
270 local inhdr = false
271 local field = nil
272 local store = nil
273 local lchunk = nil
274
275 local function parse_headers( chunk, field )
276
277 local stat
278 repeat
279 chunk, stat = chunk:gsub(
280 "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
281 function(k,v)
282 field.headers[k] = v
283 return ""
284 end
285 )
286 until stat == 0
287
288 chunk, stat = chunk:gsub("^\r\n","")
289
290 -- End of headers
291 if stat > 0 then
292 if field.headers["Content-Disposition"] then
293 if field.headers["Content-Disposition"]:match("^form%-data; ") then
294 field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
295 field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$')
296 end
297 end
298
299 if not field.headers["Content-Type"] then
300 field.headers["Content-Type"] = "text/plain"
301 end
302
303 if field.name and field.file and filecb then
304 __initval( msg.params, field.name )
305 __appendval( msg.params, field.name, field.file )
306
307 store = filecb
308 elseif field.name then
309 __initval( msg.params, field.name )
310
311 store = function( hdr, buf, eof )
312 __appendval( msg.params, field.name, buf )
313 end
314 else
315 store = nil
316 end
317
318 return chunk, true
319 end
320
321 return chunk, false
322 end
323
324 local function snk( chunk )
325
326 tlen = tlen + ( chunk and #chunk or 0 )
327
328 if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
329 return nil, "Message body size exceeds Content-Length"
330 end
331
332 if chunk and not lchunk then
333 lchunk = "\r\n" .. chunk
334
335 elseif lchunk then
336 local data = lchunk .. ( chunk or "" )
337 local spos, epos, found
338
339 repeat
340 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
341
342 if not spos then
343 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
344 end
345
346
347 if spos then
348 local predata = data:sub( 1, spos - 1 )
349
350 if inhdr then
351 predata, eof = parse_headers( predata, field )
352
353 if not eof then
354 return nil, "Invalid MIME section header"
355 elseif not field.name then
356 return nil, "Invalid Content-Disposition header"
357 end
358 end
359
360 if store then
361 store( field.headers, predata, true )
362 end
363
364
365 field = { headers = { } }
366 found = found or true
367
368 data, eof = parse_headers( data:sub( epos + 1, #data ), field )
369 inhdr = not eof
370 end
371 until not spos
372
373 if found then
374 if #data > 78 then
375 lchunk = data:sub( #data - 78 + 1, #data )
376 data = data:sub( 1, #data - 78 )
377
378 if store then
379 store( field.headers, data, false )
380 else
381 return nil, "Invalid MIME section header"
382 end
383 else
384 lchunk, data = data, nil
385 end
386 else
387 if inhdr then
388 lchunk, eof = parse_headers( data, field )
389 inhdr = not eof
390 else
391 store( field.headers, lchunk, false )
392 lchunk, chunk = chunk, nil
393 end
394 end
395 end
396
397 return true
398 end
399
400 return luci.ltn12.pump.all( src, snk )
401 end
402
403
404 -- Decode urlencoded data.
405 function urldecode_message_body( src, msg )
406
407 local tlen = 0
408 local lchunk = nil
409
410 local function snk( chunk )
411
412 tlen = tlen + ( chunk and #chunk or 0 )
413
414 if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
415 return nil, "Message body size exceeds Content-Length"
416 elseif tlen > HTTP_MAX_CONTENT then
417 return nil, "Message body size exceeds maximum allowed length"
418 end
419
420 if not lchunk and chunk then
421 lchunk = chunk
422
423 elseif lchunk then
424 local data = lchunk .. ( chunk or "&" )
425 local spos, epos
426
427 repeat
428 spos, epos = data:find("^.-[;&]")
429
430 if spos then
431 local pair = data:sub( spos, epos - 1 )
432 local key = pair:match("^(.-)=")
433 local val = pair:match("=(.*)$")
434
435 if key and #key > 0 then
436 __initval( msg.params, key )
437 __appendval( msg.params, key, val )
438 __finishval( msg.params, key, urldecode )
439 end
440
441 data = data:sub( epos + 1, #data )
442 end
443 until not spos
444
445 lchunk = data
446 end
447
448 return true
449 end
450
451 return luci.ltn12.pump.all( src, snk )
452 end
453
454
455 -- Parse a http message header
456 function parse_message_header( source )
457
458 local ok = true
459 local msg = { }
460
461 local sink = ltn12.sink.simplify(
462 function( chunk )
463 return process_states['magic']( msg, chunk )
464 end
465 )
466
467 -- Pump input data...
468 while ok do
469
470 -- get data
471 ok, err = ltn12.pump.step( source, sink )
472
473 -- error
474 if not ok and err then
475 return nil, err
476
477 -- eof
478 elseif not ok then
479
480 -- Process get parameters
481 if ( msg.request_method == "get" or msg.request_method == "post" ) and
482 msg.request_uri:match("?")
483 then
484 msg.params = urldecode_params( msg.request_uri )
485 else
486 msg.params = { }
487 end
488
489 -- Populate common environment variables
490 msg.env = {
491 CONTENT_LENGTH = msg.headers['Content-Length'];
492 CONTENT_TYPE = msg.headers['Content-Type'];
493 REQUEST_METHOD = msg.request_method:upper();
494 REQUEST_URI = msg.request_uri;
495 SCRIPT_NAME = msg.request_uri:gsub("?.+$","");
496 SCRIPT_FILENAME = ""; -- XXX implement me
497 SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version)
498 }
499
500 -- Populate HTTP_* environment variables
501 for i, hdr in ipairs( {
502 'Accept',
503 'Accept-Charset',
504 'Accept-Encoding',
505 'Accept-Language',
506 'Connection',
507 'Cookie',
508 'Host',
509 'Referer',
510 'User-Agent',
511 } ) do
512 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
513 local val = msg.headers[hdr]
514
515 msg.env[var] = val
516 end
517 end
518 end
519
520 return msg
521 end
522
523
524 -- Parse a http message body
525 function parse_message_body( source, msg, filecb )
526 -- Is it multipart/mime ?
527 if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
528 msg.env.CONTENT_TYPE:match("^multipart/form%-data")
529 then
530
531 return mimedecode_message_body( source, msg, filecb )
532
533 -- Is it application/x-www-form-urlencoded ?
534 elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
535 msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded"
536 then
537 return urldecode_message_body( source, msg, filecb )
538
539
540 -- Unhandled encoding
541 -- If a file callback is given then feed it chunk by chunk, else
542 -- store whole buffer in message.content
543 else
544
545 local sink
546
547 -- If we have a file callback then feed it
548 if type(filecb) == "function" then
549 sink = filecb
550
551 -- ... else append to .content
552 else
553 msg.content = ""
554 msg.content_length = 0
555
556 sink = function( chunk )
557 if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
558
559 msg.content = msg.content .. chunk
560 msg.content_length = msg.content_length + #chunk
561
562 return true
563 else
564 return nil, "POST data exceeds maximum allowed length"
565 end
566 end
567 end
568
569 -- Pump data...
570 while true do
571 local ok, err = ltn12.pump.step( source, sink )
572
573 if not ok and err then
574 return nil, err
575 elseif not err then
576 return true
577 end
578 end
579 end
580 end
581
582 -- Status codes
583 statusmsg = {
584 [200] = "OK",
585 [301] = "Moved Permanently",
586 [304] = "Not Modified",
587 [400] = "Bad Request",
588 [403] = "Forbidden",
589 [404] = "Not Found",
590 [405] = "Method Not Allowed",
591 [411] = "Length Required",
592 [412] = "Precondition Failed",
593 [500] = "Internal Server Error",
594 [503] = "Server Unavailable",
595 }