luci-base: switch to lucihttp.urldecode() and lucihttp.urlencode()
[project/luci.git] / modules / luci-base / luasrc / http / protocol.lua
1 -- Copyright 2008 Freifunk Leipzig / Jo-Philipp Wich <jow@openwrt.org>
2 -- Licensed to the public under the Apache License 2.0.
3
4 -- This class contains several functions useful for http message- and content
5 -- decoding and to retrive form data from raw http messages.
6 module("luci.http.protocol", package.seeall)
7
8 local ltn12 = require("luci.ltn12")
9 local util = require("luci.util")
10
11 HTTP_MAX_CONTENT      = 1024*8          -- 8 kB maximum content size
12
13 -- from given url or string. Returns a table with urldecoded values.
14 -- Simple parameters are stored as string values associated with the parameter
15 -- name within the table. Parameters with multiple values are stored as array
16 -- containing the corresponding values.
17 function urldecode_params( url, tbl )
18
19         local params = tbl or { }
20
21         if url:find("?") then
22                 url = url:gsub( "^.+%?([^?]+)", "%1" )
23         end
24
25         for pair in url:gmatch( "[^&;]+" ) do
26
27                 -- find key and value
28                 local key = util.urldecode( pair:match("^([^=]+)")     )
29                 local val = util.urldecode( pair:match("^[^=]+=(.+)$") )
30
31                 -- store
32                 if type(key) == "string" and key:len() > 0 then
33                         if type(val) ~= "string" then val = "" end
34
35                         if not params[key] then
36                                 params[key] = val
37                         elseif type(params[key]) ~= "table" then
38                                 params[key] = { params[key], val }
39                         else
40                                 table.insert( params[key], val )
41                         end
42                 end
43         end
44
45         return params
46 end
47
48 -- separated by "&". Tables are encoded as parameters with multiple values by
49 -- repeating the parameter name with each value.
50 function urlencode_params( tbl )
51         local enc = ""
52
53         for k, v in pairs(tbl) do
54                 if type(v) == "table" then
55                         for i, v2 in ipairs(v) do
56                                 enc = enc .. ( #enc > 0 and "&" or "" ) ..
57                                         util.urlencode(k) .. "=" .. util.urlencode(v2)
58                         end
59                 else
60                         enc = enc .. ( #enc > 0 and "&" or "" ) ..
61                                 util.urlencode(k) .. "=" .. util.urlencode(v)
62                 end
63         end
64
65         return enc
66 end
67
68 -- (Internal function)
69 -- Initialize given parameter and coerce string into table when the parameter
70 -- already exists.
71 local function __initval( tbl, key )
72         if tbl[key] == nil then
73                 tbl[key] = ""
74         elseif type(tbl[key]) == "string" then
75                 tbl[key] = { tbl[key], "" }
76         else
77                 table.insert( tbl[key], "" )
78         end
79 end
80
81 -- (Internal function)
82 -- Initialize given file parameter.
83 local function __initfileval( tbl, key, filename, fd )
84         if tbl[key] == nil then
85                 tbl[key] = { file=filename, fd=fd, name=key, "" }
86         else
87                 table.insert( tbl[key], "" )
88         end
89 end
90
91 -- (Internal function)
92 -- Append given data to given parameter, either by extending the string value
93 -- or by appending it to the last string in the parameter's value table.
94 local function __appendval( tbl, key, chunk )
95         if type(tbl[key]) == "table" then
96                 tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk
97         else
98                 tbl[key] = tbl[key] .. chunk
99         end
100 end
101
102 -- (Internal function)
103 -- Finish the value of given parameter, either by transforming the string value
104 -- or - in the case of multi value parameters - the last element in the
105 -- associated values table.
106 local function __finishval( tbl, key, handler )
107         if handler then
108                 if type(tbl[key]) == "table" then
109                         tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] )
110                 else
111                         tbl[key] = handler( tbl[key] )
112                 end
113         end
114 end
115
116
117 -- Table of our process states
118 local process_states = { }
119
120 -- Extract "magic", the first line of a http message.
121 -- Extracts the message type ("get", "post" or "response"), the requested uri
122 -- or the status code if the line descripes a http response.
123 process_states['magic'] = function( msg, chunk, err )
124
125         if chunk ~= nil then
126                 -- ignore empty lines before request
127                 if #chunk == 0 then
128                         return true, nil
129                 end
130
131                 -- Is it a request?
132                 local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$")
133
134                 -- Yup, it is
135                 if method then
136
137                         msg.type           = "request"
138                         msg.request_method = method:lower()
139                         msg.request_uri    = uri
140                         msg.http_version   = tonumber( http_ver )
141                         msg.headers        = { }
142
143                         -- We're done, next state is header parsing
144                         return true, function( chunk )
145                                 return process_states['headers']( msg, chunk )
146                         end
147
148                 -- Is it a response?
149                 else
150
151                         local http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$")
152
153                         -- Is a response
154                         if code then
155
156                                 msg.type           = "response"
157                                 msg.status_code    = code
158                                 msg.status_message = message
159                                 msg.http_version   = tonumber( http_ver )
160                                 msg.headers        = { }
161
162                                 -- We're done, next state is header parsing
163                                 return true, function( chunk )
164                                         return process_states['headers']( msg, chunk )
165                                 end
166                         end
167                 end
168         end
169
170         -- Can't handle it
171         return nil, "Invalid HTTP message magic"
172 end
173
174
175 -- Extract headers from given string.
176 process_states['headers'] = function( msg, chunk )
177
178         if chunk ~= nil then
179
180                 -- Look for a valid header format
181                 local hdr, val = chunk:match( "^([A-Za-z][A-Za-z0-9%-_]+): +(.+)$" )
182
183                 if type(hdr) == "string" and hdr:len() > 0 and
184                    type(val) == "string" and val:len() > 0
185                 then
186                         msg.headers[hdr] = val
187
188                         -- Valid header line, proceed
189                         return true, nil
190
191                 elseif #chunk == 0 then
192                         -- Empty line, we won't accept data anymore
193                         return false, nil
194                 else
195                         -- Junk data
196                         return nil, "Invalid HTTP header received"
197                 end
198         else
199                 return nil, "Unexpected EOF"
200         end
201 end
202
203
204 -- data line by line with the trailing \r\n stripped of.
205 function header_source( sock )
206         return ltn12.source.simplify( function()
207
208                 local chunk, err, part = sock:receive("*l")
209
210                 -- Line too long
211                 if chunk == nil then
212                         if err ~= "timeout" then
213                                 return nil, part
214                                         and "Line exceeds maximum allowed length"
215                                         or  "Unexpected EOF"
216                         else
217                                 return nil, err
218                         end
219
220                 -- Line ok
221                 elseif chunk ~= nil then
222
223                         -- Strip trailing CR
224                         chunk = chunk:gsub("\r$","")
225
226                         return chunk, nil
227                 end
228         end )
229 end
230
231 -- Content-Type. Stores all extracted data associated with its parameter name
232 -- in the params table within the given message object. Multiple parameter
233 -- values are stored as tables, ordinary ones as strings.
234 -- If an optional file callback function is given then it is feeded with the
235 -- file contents chunk by chunk and only the extracted file name is stored
236 -- within the params table. The callback function will be called subsequently
237 -- with three arguments:
238 --  o Table containing decoded (name, file) and raw (headers) mime header data
239 --  o String value containing a chunk of the file data
240 --  o Boolean which indicates wheather the current chunk is the last one (eof)
241 function mimedecode_message_body( src, msg, filecb )
242
243         if msg and msg.env.CONTENT_TYPE then
244                 msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
245         end
246
247         if not msg.mime_boundary then
248                 return nil, "Invalid Content-Type found"
249         end
250
251
252         local tlen   = 0
253         local inhdr  = false
254         local field  = nil
255         local store  = nil
256         local lchunk = nil
257
258         local function parse_headers( chunk, field )
259
260                 local stat
261                 repeat
262                         chunk, stat = chunk:gsub(
263                                 "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
264                                 function(k,v)
265                                         field.headers[k] = v
266                                         return ""
267                                 end
268                         )
269                 until stat == 0
270
271                 chunk, stat = chunk:gsub("^\r\n","")
272
273                 -- End of headers
274                 if stat > 0 then
275                         if field.headers["Content-Disposition"] then
276                                 if field.headers["Content-Disposition"]:match("^form%-data; ") then
277                                         field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
278                                         field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$')
279                                 end
280                         end
281
282                         if not field.headers["Content-Type"] then
283                                 field.headers["Content-Type"] = "text/plain"
284                         end
285
286                         if field.name and field.file and filecb then
287                                 __initval( msg.params, field.name )
288                                 __appendval( msg.params, field.name, field.file )
289
290                                 store = filecb
291                         elseif field.name and field.file then
292                                 local nxf = require "nixio"
293                                 local fd = nxf.mkstemp(field.name)
294                                 __initfileval ( msg.params, field.name, field.file, fd )
295                                 if fd then
296                                         store = function(hdr, buf, eof)
297                                                 fd:write(buf)
298                                                 if (eof) then
299                                                         fd:seek(0, "set")
300                                                 end
301                                         end
302                                 else
303                                         store = function( hdr, buf, eof )
304                                                 __appendval( msg.params, field.name, buf )
305                                         end
306                                 end
307                         elseif field.name then
308                                 __initval( msg.params, field.name )
309
310                                 store = function( hdr, buf, eof )
311                                         __appendval( msg.params, field.name, buf )
312                                 end
313                         else
314                                 store = nil
315                         end
316
317                         return chunk, true
318                 end
319
320                 return chunk, false
321         end
322
323         local function snk( chunk )
324
325                 tlen = tlen + ( chunk and #chunk or 0 )
326
327                 if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
328                         return nil, "Message body size exceeds Content-Length"
329                 end
330
331                 if chunk and not lchunk then
332                         lchunk = "\r\n" .. chunk
333
334                 elseif lchunk then
335                         local data = lchunk .. ( chunk or "" )
336                         local spos, epos, found
337
338                         repeat
339                                 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
340
341                                 if not spos then
342                                         spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
343                                 end
344
345
346                                 if spos then
347                                         local predata = data:sub( 1, spos - 1 )
348
349                                         if inhdr then
350                                                 predata, eof = parse_headers( predata, field )
351
352                                                 if not eof then
353                                                         return nil, "Invalid MIME section header"
354                                                 elseif not field.name then
355                                                         return nil, "Invalid Content-Disposition header"
356                                                 end
357                                         end
358
359                                         if store then
360                                                 store( field, predata, true )
361                                         end
362
363
364                                         field = { headers = { } }
365                                         found = found or true
366
367                                         data, eof = parse_headers( data:sub( epos + 1, #data ), field )
368                                         inhdr = not eof
369                                 end
370                         until not spos
371
372                         if found then
373                                 -- We found at least some boundary. Save
374                                 -- the unparsed remaining data for the
375                                 -- next chunk.
376                                 lchunk, data = data, nil
377                         else
378                                 -- There was a complete chunk without a boundary. Parse it as headers or
379                                 -- append it as data, depending on our current state.
380                                 if inhdr then
381                                         lchunk, eof = parse_headers( data, field )
382                                         inhdr = not eof
383                                 else
384                                         -- We're inside data, so append the data. Note that we only append
385                                         -- lchunk, not all of data, since there is a chance that chunk
386                                         -- contains half a boundary. Assuming that each chunk is at least the
387                                         -- boundary in size, this should prevent problems
388                                         store( field, lchunk, false )
389                                         lchunk, chunk = chunk, nil
390                                 end
391                         end
392                 end
393
394                 return true
395         end
396
397         return ltn12.pump.all( src, snk )
398 end
399
400 -- Content-Type. Stores all extracted data associated with its parameter name
401 -- in the params table within the given message object. Multiple parameter
402 -- values are stored as tables, ordinary ones as strings.
403 function urldecode_message_body( src, msg )
404
405         local tlen   = 0
406         local lchunk = nil
407
408         local function snk( chunk )
409
410                 tlen = tlen + ( chunk and #chunk or 0 )
411
412                 if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
413                         return nil, "Message body size exceeds Content-Length"
414                 elseif tlen > HTTP_MAX_CONTENT then
415                         return nil, "Message body size exceeds maximum allowed length"
416                 end
417
418                 if not lchunk and chunk then
419                         lchunk = chunk
420
421                 elseif lchunk then
422                         local data = lchunk .. ( chunk or "&" )
423                         local spos, epos
424
425                         repeat
426                                 spos, epos = data:find("^.-[;&]")
427
428                                 if spos then
429                                         local pair = data:sub( spos, epos - 1 )
430                                         local key  = pair:match("^(.-)=")
431                                         local val  = pair:match("=([^%s]*)%s*$")
432
433                                         if key and #key > 0 then
434                                                 __initval( msg.params, key )
435                                                 __appendval( msg.params, key, val )
436                                                 __finishval( msg.params, key, urldecode )
437                                         end
438
439                                         data = data:sub( epos + 1, #data )
440                                 end
441                         until not spos
442
443                         lchunk = data
444                 end
445
446                 return true
447         end
448
449         return ltn12.pump.all( src, snk )
450 end
451
452 -- version, message headers and resulting CGI environment variables from the
453 -- given ltn12 source.
454 function parse_message_header( src )
455
456         local ok   = true
457         local msg  = { }
458
459         local sink = ltn12.sink.simplify(
460                 function( chunk )
461                         return process_states['magic']( msg, chunk )
462                 end
463         )
464
465         -- Pump input data...
466         while ok do
467
468                 -- get data
469                 ok, err = ltn12.pump.step( src, sink )
470
471                 -- error
472                 if not ok and err then
473                         return nil, err
474
475                 -- eof
476                 elseif not ok then
477
478                         -- Process get parameters
479                         if ( msg.request_method == "get" or msg.request_method == "post" ) and
480                            msg.request_uri:match("?")
481                         then
482                                 msg.params = urldecode_params( msg.request_uri )
483                         else
484                                 msg.params = { }
485                         end
486
487                         -- Populate common environment variables
488                         msg.env = {
489                                 CONTENT_LENGTH    = msg.headers['Content-Length'];
490                                 CONTENT_TYPE      = msg.headers['Content-Type'] or msg.headers['Content-type'];
491                                 REQUEST_METHOD    = msg.request_method:upper();
492                                 REQUEST_URI       = msg.request_uri;
493                                 SCRIPT_NAME       = msg.request_uri:gsub("?.+$","");
494                                 SCRIPT_FILENAME   = "";         -- XXX implement me
495                                 SERVER_PROTOCOL   = "HTTP/" .. string.format("%.1f", msg.http_version);
496                                 QUERY_STRING      = msg.request_uri:match("?")
497                                         and msg.request_uri:gsub("^.+?","") or ""
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 -- This function will examine the Content-Type within the given message object
524 -- to select the appropriate content decoder.
525 -- Currently the application/x-www-urlencoded and application/form-data
526 -- mime types are supported. If the encountered content encoding can't be
527 -- handled then the whole message body will be stored unaltered as "content"
528 -- property within the given message object.
529 function parse_message_body( src, msg, filecb )
530         -- Is it multipart/mime ?
531         if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
532            msg.env.CONTENT_TYPE:match("^multipart/form%-data")
533         then
534
535                 return mimedecode_message_body( src, msg, filecb )
536
537         -- Is it application/x-www-form-urlencoded ?
538         elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
539                msg.env.CONTENT_TYPE:match("^application/x%-www%-form%-urlencoded")
540         then
541                 return urldecode_message_body( src, msg, filecb )
542
543
544         -- Unhandled encoding
545         -- If a file callback is given then feed it chunk by chunk, else
546         -- store whole buffer in message.content
547         else
548
549                 local sink
550
551                 -- If we have a file callback then feed it
552                 if type(filecb) == "function" then
553                         local meta = {
554                                 name = "raw",
555                                 encoding = msg.env.CONTENT_TYPE
556                         }
557                         sink = function( chunk )
558                                 if chunk then
559                                         return filecb(meta, chunk, false)
560                                 else
561                                         return filecb(meta, nil, true)
562                                 end
563                         end
564                 -- ... else append to .content
565                 else
566                         msg.content = ""
567                         msg.content_length = 0
568
569                         sink = function( chunk )
570                                 if chunk then
571                                         if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
572                                                 msg.content        = msg.content        .. chunk
573                                                 msg.content_length = msg.content_length + #chunk
574                                                 return true
575                                         else
576                                                 return nil, "POST data exceeds maximum allowed length"
577                                         end
578                                 end
579                                 return true
580                         end
581                 end
582
583                 -- Pump data...
584                 while true do
585                         local ok, err = ltn12.pump.step( src, sink )
586
587                         if not ok and err then
588                                 return nil, err
589                         elseif not ok then -- eof
590                                 return true
591                         end
592                 end
593
594                 return true
595         end
596 end
597
598 statusmsg = {
599         [200] = "OK",
600         [206] = "Partial Content",
601         [301] = "Moved Permanently",
602         [302] = "Found",
603         [304] = "Not Modified",
604         [400] = "Bad Request",
605         [403] = "Forbidden",
606         [404] = "Not Found",
607         [405] = "Method Not Allowed",
608         [408] = "Request Time-out",
609         [411] = "Length Required",
610         [412] = "Precondition Failed",
611         [416] = "Requested range not satisfiable",
612         [500] = "Internal Server Error",
613         [503] = "Server Unavailable",
614 }