* luci/libs/http: use aliased ltn12. instead of luci.ltn12.
[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 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 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 }