* luci/libs: drop depency to luci.bits use tonumber() for hex decoding
[oweals/luci.git] / libs / web / 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 require("luci.util")
19
20
21 HTTP_MAX_CONTENT     = 1048576          -- 1 MB
22 HTTP_DEFAULT_CTYPE   = "text/html"      -- default content type
23 HTTP_DEFAULT_VERSION = "1.0"            -- HTTP default version
24
25
26 -- Decode an urlencoded string.
27 -- Returns the decoded value.
28 function urldecode( str )
29
30         local function __chrdec( hex )
31                 return string.char( tonumber( hex, 16 ) )
32         end
33
34         if type(str) == "string" then
35                 str = str:gsub( "+", " " ):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 )
45
46         local params = { }
47
48         if url:find("?") then
49                 url = url:gsub( "^.+%?([^?]+)", "%1" )
50         end
51
52         for i, pair in ipairs(luci.util.split( url, "[&;]+", nil, true )) 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 -- Decode MIME encoded data.
113 -- Returns a table with decoded values.
114 function mimedecode( data, boundary, filecb )
115
116         local params = { }
117
118         -- create a line reader
119         local reader = _linereader( data )
120
121         -- state variables
122         local in_part = false
123         local in_file = false
124         local in_fbeg = false
125         local in_size = true
126
127         local filename
128         local buffer
129         local field
130         local clen = 0
131
132
133         -- try to read all mime parts
134         for line in reader do
135
136                 -- update content length
137                 clen = clen + line:len()
138
139                 if clen >= HTTP_MAX_CONTENT then
140                         in_size = false
141                 end
142
143                 -- when no boundary is given, try to find it
144                 if not boundary then
145                         boundary = line:match("^%-%-([^\r\n]+)\r?\n$")
146                 end
147
148                 -- Got a valid boundary line or reached max allowed size.
149                 if ( boundary and line:sub(1,2) == "--" and line:len() > #boundary + 2 and
150                      line:sub( 3, 2 + #boundary ) == boundary ) or not in_size
151                 then
152                         -- Flush the data of the previous mime part.
153                         -- When field and/or buffer are set to nil we should discard
154                         -- the previous section entirely due to format violations.
155                         if type(field)  == "string" and field:len() > 0 and
156                            type(buffer) == "string"
157                         then
158                                 -- According to the rfc the \r\n preceeding a boundary
159                                 -- is assumed to be part of the boundary itself.
160                                 -- Since we are reading line by line here, this crlf
161                                 -- is part of the last line of our section content,
162                                 -- so strip it before storing the buffer.
163                                 buffer = buffer:gsub("\r?\n$","")
164
165                                 -- If we're in a file part and a file callback has been provided
166                                 -- then do a final call and send eof.
167                                 if in_file and type(filecb) == "function" then
168                                         filecb( field, filename, buffer, true )
169                                         params[field] = filename
170
171                                 -- Store buffer.
172                                 else
173                                         params[field] = buffer
174                                 end
175                         end
176
177                         -- Reset vars
178                         buffer   = ""
179                         filename = nil
180                         field    = nil
181                         in_file  = false
182
183                         -- Abort here if we reached maximum allowed size
184                         if not in_size then break end
185
186                         -- Do we got the last boundary?
187                         if line:len() > #boundary + 4 and
188                            line:sub( #boundary + 2, #boundary + 4 ) == "--"
189                         then
190                                 -- No more processing
191                                 in_part = false
192
193                         -- It's a middle boundary
194                         else
195
196                                 -- Read headers
197                                 local hlen, headers = extract_headers( reader )
198
199                                 -- Check for valid headers
200                                 if headers['Content-Disposition'] then
201
202                                         -- Got no content type header, assume content-type "text/plain"
203                                         if not headers['Content-Type'] then
204                                                 headers['Content-Type'] = 'text/plain'
205                                         end
206
207                                         -- Find field name
208                                         local hdrvals = luci.util.split(
209                                                 headers['Content-Disposition'], '; '
210                                         )
211
212                                         -- Valid form data part?
213                                         if hdrvals[1] == "form-data" and hdrvals[2]:match("^name=") then
214
215                                                 -- Store field identifier
216                                                 field = hdrvals[2]:match('^name="(.+)"$')
217
218                                                 -- Do we got a file upload field?
219                                                 if #hdrvals == 3 and hdrvals[3]:match("^filename=") then
220                                                         in_file  = true
221                                                         if_fbeg  = true
222                                                         filename = hdrvals[3]:match('^filename="(.+)"$')
223                                                 end
224
225                                                 -- Entering next part processing
226                                                 in_part = true
227                                         end
228                                 end
229                         end
230
231                 -- Processing content
232                 elseif in_part then
233
234                         -- XXX: Would be really good to switch from line based to
235                         --      buffered reading here.
236
237
238                         -- If we're in a file part and a file callback has been provided
239                         -- then call the callback and reset the buffer.
240                         if in_file and type(filecb) == "function" then
241
242                                 -- If we're not processing the first chunk, then call 
243                                 if not in_fbeg then
244                                         filecb( field, filename, buffer, false )
245                                         buffer = ""
246                                 
247                                 -- Clear in_fbeg flag after first run
248                                 else
249                                         in_fbeg = false
250                                 end
251                         end
252
253                         -- Append date to buffer
254                         buffer = buffer .. line
255                 end
256         end
257
258         return params
259 end
260
261
262 -- Extract "magic", the first line of a http message.
263 -- Returns the message type ("get", "post" or "response"), the requested uri
264 -- if it is a valid http request or the status code if the line descripes a 
265 -- http response. For requests the third parameter is nil, for responses it
266 -- contains the human readable status description.
267 function extract_magic( reader )
268
269         for line in reader do
270                 -- Is it a request?
271                 local method, uri = line:match("^([A-Z]+) ([^ ]+) HTTP/[01]%.[019]\r?\n$")
272
273                 -- Yup, it is
274                 if method then
275                         return method:lower(), uri, nil
276
277                 -- Is it a response?
278                 else
279                         local code, message = line:match("^HTTP/[01]%.[019] ([0-9]+) ([^\r\n]+)\r?\n$")
280
281                         -- Is a response
282                         if code then
283                                 return "response", code + 0, message
284
285                         -- Can't handle it
286                         else
287                                 return nil
288                         end
289                 end
290         end
291 end
292
293
294 -- Extract headers from given string.
295 -- Returns a table of extracted headers and the remainder of the parsed data.
296 function extract_headers( reader, tbl )
297
298         local headers = tbl or { }
299         local count   = 0
300
301         -- Iterate line by line
302         for line in reader do
303
304                 -- Look for a valid header format
305                 local hdr, val = line:match( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r?\n$" )
306
307                 if type(hdr) == "string" and hdr:len() > 0 and
308                    type(val) == "string" and val:len() > 0
309                 then
310                         count = count + line:len()
311                         headers[hdr] = val
312
313                 elseif line:match("^\r?\n$") then
314                         
315                         return count + line:len(), headers
316
317                 else
318                         -- junk data, don't add length
319                         return count, headers
320                 end
321         end
322
323         return count, headers
324 end
325
326
327 -- Parse a http message
328 function parse_message( data, filecb )
329
330         -- Create a line reader
331         local reader  = _linereader( data )
332         local message = { }
333
334         -- Try to extract magic
335         local method, arg1, arg2 = extract_magic( reader )
336
337         -- Does it looks like a valid message?
338         if method then
339
340                 message.request_method = method
341                 message.status_code    = arg2 and arg1 or 200
342                 message.status_message = arg2 or nil
343                 message.request_uri    = arg2 and nil or arg1
344
345                 if method == "response" then
346                         message.type = "response"
347                 else
348                         message.type = "request"
349                 end
350
351                 -- Parse headers?
352                 local hlen, hdrs = extract_headers( reader )
353
354                 -- Valid headers?
355                 if hlen > 2 and type(hdrs) == "table" then
356
357                         message.headers = hdrs
358
359                         -- Get content
360                         local clen = ( hdrs['Content-Length'] or HTTP_MAX_CONTENT ) + 0
361
362                         -- Process get parameters
363                         if ( method == "get" or method == "post" ) and
364                            message.request_uri:match("?")
365                         then
366                                 message.params = urldecode_params( message.request_uri )
367                         else
368                                 message.params = { }
369                         end
370
371                         -- Process post method
372                         if method == "post" and hdrs['Content-Type'] then
373
374                                 -- Is it multipart/form-data ?
375                                 if hdrs['Content-Type']:match("^multipart/form%-data") then
376                                         for k, v in pairs( mimedecode(
377                                                 reader,
378                                                 hdrs['Content-Type']:match("boundary=(.+)"),
379                                                 filecb
380                                         ) ) do
381                                                 message.params[k] = v
382                                         end
383
384                                 -- Is it x-www-urlencoded?
385                                 elseif hdrs['Content-Type'] == 'application/x-www-urlencoded' then
386
387                                         -- XXX: readline isn't the best solution here
388                                         for chunk in reader do
389                                                 for k, v in pairs( urldecode_params( chunk ) ) do
390                                                         message.params[k] = v
391                                                 end
392
393                                                 -- XXX: unreliable (undefined line length)
394                                                 if clen + chunk:len() >= HTTP_MAX_CONTENT then
395                                                         break
396                                                 end
397
398                                                 clen = clen + chunk:len()
399                                         end
400
401                                 -- Unhandled encoding
402                                 -- If a file callback is given then feed it line by line, else
403                                 -- store whole buffer in message.content
404                                 else
405
406                                         for chunk in reader do
407
408                                                 -- We have a callback, feed it.
409                                                 if type(filecb) == "function" then
410
411                                                         filecb( "_post", nil, chunk, false )
412
413                                                 -- Append to .content buffer.
414                                                 else
415                                                         message.content = 
416                                                                 type(message.content) == "string"
417                                                                         and message.content .. chunk
418                                                                         or chunk
419                                                 end
420
421                                                 -- XXX: unreliable
422                                                 if clen + chunk:len() >= HTTP_MAX_CONTENT then
423                                                         break
424                                                 end
425
426                                                 clen = clen + chunk:len()
427                                         end
428
429                                         -- Send eof to callback
430                                         if type(filecb) == "function" then
431                                                 filecb( "_post", nil, "", true )
432                                         end
433                                 end
434                         end
435
436                         -- Populate common environment variables
437                         message.env = {
438                                 CONTENT_LENGTH    = hdrs['Content-Length'];
439                                 CONTENT_TYPE      = hdrs['Content-Type'];
440                                 REQUEST_METHOD    = message.request_method;
441                                 REQUEST_URI       = message.request_uri;
442                                 SCRIPT_NAME       = message.request_uri:gsub("?.+$","");
443                                 SCRIPT_FILENAME   = ""          -- XXX implement me
444                         }
445
446                         -- Populate HTTP_* environment variables
447                         for i, hdr in ipairs( {
448                                 'Accept',
449                                 'Accept-Charset',
450                                 'Accept-Encoding',
451                                 'Accept-Language',
452                                 'Connection',
453                                 'Cookie',
454                                 'Host',
455                                 'Referer',
456                                 'User-Agent',
457                         } ) do
458                                 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
459                                 local val = hdrs[hdr]
460
461                                 message.env[var] = val
462                         end
463
464
465                         return message
466                 end
467         end
468 end
469
470 function _linereader( obj )
471
472         -- object is string
473         if type(obj) == "string" then
474
475                 return obj:gmatch( "[^\r\n]*\r?\n" )
476
477         -- object is a function
478         elseif type(obj) == "function" then
479
480                 return obj
481
482         -- object is a table and implements a readline() function
483         elseif type(obj) == "table" and type(obj.readline) == "function" then
484
485                 return obj.readline
486
487         -- object is a table and has a lines property
488         elseif type(obj) == "table" and obj.lines then
489
490                 -- decide wheather to use "lines" as function or table
491                 local _lns = ( type(obj.lines) == "function" ) and obj.lines() or obj.lines
492                 local _pos = 1
493                 
494                 return function()
495                         if _pos <= #_lns then
496                                 _pos = _pos + 1
497                                 return _lns[_pos]
498                         end
499                 end
500
501         -- no usable data type
502         else
503
504                 -- dummy iterator
505                 return function()
506                         return nil
507                 end
508         end
509 end