* luci/libs/http: replace url decoder in http.protocol
[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 > 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 > 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    = tonumber(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 }