4cb89779be1652f92a60d6f3b228cf95667fba3c
[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 function parse_headers( chunk, field )
270
271 local stat
272 repeat
273 chunk, stat = chunk:gsub(
274 "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
275 function(k,v)
276 field.headers[k] = v
277 return ""
278 end
279 )
280 until stat == 0
281
282 chunk, stat = chunk:gsub("^\r\n","")
283
284 -- End of headers
285 if stat > 0 then
286 if field.headers["Content-Disposition"] then
287 if field.headers["Content-Disposition"]:match("^form%-data; ") then
288 field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
289 field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$')
290 end
291 end
292
293 if not field.headers["Content-Type"] then
294 field.headers["Content-Type"] = "text/plain"
295 end
296
297 return chunk, true
298 end
299
300 return chunk, false
301 end
302
303
304 local tlen = 0
305 local inhdr = false
306 local field = nil
307 local store = nil
308 local lchunk = nil
309
310 local function snk( chunk )
311
312 tlen = tlen + ( chunk and #chunk or 0 )
313
314 if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) then
315 return nil, "Message body size exceeds Content-Length"
316 end
317
318 if chunk and not lchunk then
319 lchunk = "\r\n" .. chunk
320
321 elseif lchunk then
322 local data = lchunk .. ( chunk or "" )
323 local spos, epos, found
324
325 repeat
326 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
327
328 if not spos then
329 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
330 end
331
332
333 if spos then
334 local predata = data:sub( 1, spos - 1 )
335
336 if inhdr then
337 predata, eof = parse_headers( predata, field )
338
339 if not eof then
340 return nil, "Invalid MIME section header"
341 end
342
343 if not field.name then
344 return nil, "Invalid Content-Disposition header"
345 end
346 end
347
348 if store then
349 store( field.headers, predata, true )
350 end
351
352
353 field = { headers = { } }
354 found = found or true
355
356 data, eof = parse_headers( data:sub( epos + 1, #data ), field )
357 inhdr = not eof
358
359 if eof then
360 if field.file and filecb then
361 msg.params[field.name] = field.file
362 store = filecb
363 else
364 __initval( msg.params, field.name )
365
366 store = function( hdr, buf, eof )
367 __appendval( msg.params, field.name, buf )
368 end
369 end
370 end
371 end
372 until not spos
373
374
375 if found then
376 if #data > 78 then
377 lchunk = data:sub( #data - 78 + 1, #data )
378 data = data:sub( 1, #data - 78 )
379
380 if store and field and field.name then
381 store( field.headers, data, false )
382 else
383 return nil, "Invalid MIME section header"
384 end
385 else
386 lchunk, data = data, nil
387 end
388 else
389 if inhdr then
390 lchunk, eof = parse_headers( data, field )
391 inhdr = not eof
392 else
393 store( field.headers, lchunk, false )
394 lchunk, chunk = chunk, nil
395 end
396 end
397 end
398
399 return true
400 end
401
402 return luci.ltn12.pump.all( src, snk )
403 end
404
405
406 -- Decode urlencoded data.
407 function urldecode_message_body( src, msg )
408
409 local tlen = 0
410 local lchunk = nil
411
412 local function snk( chunk )
413
414 tlen = tlen + ( chunk and #chunk or 0 )
415
416 if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) then
417 return nil, "Message body size exceeds Content-Length"
418 elseif tlen > HTTP_MAX_CONTENT then
419 return nil, "Message body size exceeds maximum allowed length"
420 end
421
422 if not lchunk and chunk then
423 lchunk = chunk
424
425 elseif lchunk then
426 local data = lchunk .. ( chunk or "&" )
427 local spos, epos
428
429 repeat
430 spos, epos = data:find("^.-[;&]")
431
432 if spos then
433 local pair = data:sub( spos, epos - 1 )
434 local key = pair:match("^(.-)=")
435 local val = pair:match("=(.*)$")
436
437 if key and #key > 0 then
438 __initval( msg.params, key )
439 __appendval( msg.params, key, val )
440 __finishval( msg.params, key, urldecode )
441 end
442
443 data = data:sub( epos + 1, #data )
444 end
445 until not spos
446
447 lchunk = data
448 end
449
450 return true
451 end
452
453 return luci.ltn12.pump.all( src, snk )
454 end
455
456
457 -- Parse a http message header
458 function parse_message_header( source )
459
460 local ok = true
461 local msg = { }
462
463 local sink = ltn12.sink.simplify(
464 function( chunk )
465 return process_states['magic']( msg, chunk )
466 end
467 )
468
469 -- Pump input data...
470 while ok do
471
472 -- get data
473 ok, err = ltn12.pump.step( source, sink )
474
475 -- error
476 if not ok and err then
477 return nil, err
478
479 -- eof
480 elseif not ok then
481
482 -- Process get parameters
483 if ( msg.request_method == "get" or msg.request_method == "post" ) and
484 msg.request_uri:match("?")
485 then
486 msg.params = urldecode_params( msg.request_uri )
487 else
488 msg.params = { }
489 end
490
491 -- Populate common environment variables
492 msg.env = {
493 CONTENT_LENGTH = msg.headers['Content-Length'];
494 CONTENT_TYPE = msg.headers['Content-Type'];
495 REQUEST_METHOD = msg.request_method:upper();
496 REQUEST_URI = msg.request_uri;
497 SCRIPT_NAME = msg.request_uri:gsub("?.+$","");
498 SCRIPT_FILENAME = ""; -- XXX implement me
499 SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version)
500 }
501
502 -- Populate HTTP_* environment variables
503 for i, hdr in ipairs( {
504 'Accept',
505 'Accept-Charset',
506 'Accept-Encoding',
507 'Accept-Language',
508 'Connection',
509 'Cookie',
510 'Host',
511 'Referer',
512 'User-Agent',
513 } ) do
514 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
515 local val = msg.headers[hdr]
516
517 msg.env[var] = val
518 end
519 end
520 end
521
522 return msg
523 end
524
525
526 -- Parse a http message body
527 function parse_message_body( source, msg, filecb )
528 -- Is it multipart/mime ?
529 if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
530 msg.env.CONTENT_TYPE:match("^multipart/form%-data")
531 then
532
533 return mimedecode_message_body( source, msg, filecb )
534
535 -- Is it application/x-www-form-urlencoded ?
536 elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
537 msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded"
538 then
539 return urldecode_message_body( source, msg, filecb )
540
541
542 -- Unhandled encoding
543 -- If a file callback is given then feed it chunk by chunk, else
544 -- store whole buffer in message.content
545 else
546
547 local sink
548
549 -- If we have a file callback then feed it
550 if type(filecb) == "function" then
551 sink = filecb
552
553 -- ... else append to .content
554 else
555 msg.content = ""
556 msg.content_length = 0
557
558 sink = function( chunk )
559 if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
560
561 msg.content = msg.content .. chunk
562 msg.content_length = msg.content_length + #chunk
563
564 return true
565 else
566 return nil, "POST data exceeds maximum allowed length"
567 end
568 end
569 end
570
571 -- Pump data...
572 while true do
573 local ok, err = ltn12.pump.step( source, sink )
574
575 if not ok and err then
576 return nil, err
577 elseif not err then
578 return true
579 end
580 end
581 end
582 end
583
584 -- Status codes
585 statusmsg = {
586 [200] = "OK",
587 [301] = "Moved Permanently",
588 [304] = "Not Modified",
589 [400] = "Bad Request",
590 [403] = "Forbidden",
591 [404] = "Not Found",
592 [405] = "Method Not Allowed",
593 [411] = "Length Required",
594 [412] = "Precondition Failed",
595 [500] = "Internal Server Error",
596 [503] = "Server Unavailable",
597 }