| 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 | --- 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. |
|---|
| 19 | module("luci.http.protocol", package.seeall) |
|---|
| 20 | |
|---|
| 21 | local ltn12 = require("luci.ltn12") |
|---|
| 22 | |
|---|
| 23 | HTTP_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 |
|---|
| 31 | function 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 |
|---|
| 46 | end |
|---|
| 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 |
|---|
| 57 | function 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 |
|---|
| 86 | end |
|---|
| 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 |
|---|
| 92 | function 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 |
|---|
| 108 | end |
|---|
| 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 |
|---|
| 116 | function 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 |
|---|
| 132 | end |
|---|
| 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 |
|---|
| 140 | local 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 |
|---|
| 148 | end |
|---|
| 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 |
|---|
| 158 | local 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 |
|---|
| 164 | end |
|---|
| 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 |
|---|
| 176 | local 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 |
|---|
| 184 | end |
|---|
| 185 | |
|---|
| 186 | |
|---|
| 187 | -- Table of our process states |
|---|
| 188 | local 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. |
|---|
| 193 | process_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" |
|---|
| 242 | end |
|---|
| 243 | |
|---|
| 244 | |
|---|
| 245 | -- Extract headers from given string. |
|---|
| 246 | process_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 |
|---|
| 271 | end |
|---|
| 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 |
|---|
| 278 | function 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 ) |
|---|
| 302 | end |
|---|
| 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 |
|---|
| 321 | function 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 ) |
|---|
| 464 | end |
|---|
| 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 |
|---|
| 475 | function 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 ) |
|---|
| 522 | end |
|---|
| 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 |
|---|
| 530 | function 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 |
|---|
| 597 | end |
|---|
| 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 |
|---|
| 612 | function 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:match("^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 |
|---|
| 670 | end |
|---|
| 671 | |
|---|
| 672 | --- Table containing human readable messages for several http status codes. |
|---|
| 673 | -- @class table |
|---|
| 674 | statusmsg = { |
|---|
| 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 | } |
|---|