root/ff-luci/trunk/libs/http/luasrc/http/protocol.lua @ 3553

Revision 3553, 17.6 KB (checked in by jow, 5 years ago)

* luci/libs/httpprotocol: store QUERY_STRING in environment too, fixes query strings with luci-httpd and propably others

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