# HG changeset patch # User Matthew Wild # Date 1522663037 -3600 # Node ID ac99a04231b17038ecd32f2724def175ecb677a4 # Parent 7036e82f83f56c71f8067c44eb863eb483d4b2b4 mod_http_upload_external: Add newer 'v2' protocol (and share_v2.php) which supports content-type preservation diff -r 7036e82f83f5 -r ac99a04231b1 mod_http_upload_external/mod_http_upload_external.lua --- a/mod_http_upload_external/mod_http_upload_external.lua Mon Apr 02 10:52:32 2018 +0100 +++ b/mod_http_upload_external/mod_http_upload_external.lua Mon Apr 02 10:57:17 2018 +0100 @@ -14,8 +14,12 @@ -- config local file_size_limit = module:get_option_number(module.name .. "_file_size_limit", 100 * 1024 * 1024); -- 100 MB -local base_url = assert(module:get_option_string(module.name .. "_base_url"), module.name .. "_base_url is a required option"); -local secret = assert(module:get_option_string(module.name .. "_secret"), module.name .. "_secret is a required option"); +local base_url = assert(module:get_option_string(module.name .. "_base_url"), + module.name .. "_base_url is a required option"); +local secret = assert(module:get_option_string(module.name .. "_secret"), + module.name .. "_secret is a required option"); + +local token_protocol = module:get_option_string(module.name .. "_protocol", "v1"); -- depends module:depends("disco"); @@ -39,14 +43,19 @@ { name = "max-file-size", type = "text-single" }, }:form({ ["max-file-size"] = tostring(file_size_limit) }, "result")); -local function magic_crypto_dust(random, filename, filesize) - local message = string.format("%s/%s %d", random, filename, filesize); +local function magic_crypto_dust(random, filename, filesize, filetype) + local param, message; + if token_protocol == "v1" then + param, message = "v", string.format("%s/%s %d", random, filename, filesize); + else + param, message = "v2", string.format("%s/%s\0%d\0%s", random, filename, filesize, filetype); + end local digest = HMAC(secret, message, true); random, filename = http.urlencode(random), http.urlencode(filename); - return base_url .. random .. "/" .. filename, "?v=" .. digest; + return base_url .. random .. "/" .. filename, "?"..param.."=" .. digest; end -local function handle_request(origin, stanza, xmlns, filename, filesize) +local function handle_request(origin, stanza, xmlns, filename, filesize, filetype) -- local clients only if origin.type ~= "c2s" then module:log("debug", "Request for upload slot from a %s", origin.type); @@ -71,7 +80,7 @@ return nil, nil; end local random = uuid(); - local get_url, verify = magic_crypto_dust(random, filename, filesize); + local get_url, verify = magic_crypto_dust(random, filename, filesize, filetype); local put_url = get_url .. verify; module:log("info", "Handing out upload slot %s to %s@%s", get_url, origin.username, origin.host); @@ -85,9 +94,10 @@ local request = stanza.tags[1]; local filename = request:get_child_text("filename"); local filesize = tonumber(request:get_child_text("size")); + local filetype = request.attr["content-type"] or "application/octet-stream"; local get_url, put_url = handle_request( - origin, stanza, legacy_namespace, filename, filesize); + origin, stanza, legacy_namespace, filename, filesize, filetype); if not get_url then -- error was already sent @@ -108,8 +118,10 @@ local request = stanza.tags[1]; local filename = request.attr.filename; local filesize = tonumber(request.attr.size); + local filetype = request.attr["content-type"] or "application/octet-stream"; + local get_url, put_url = handle_request( - origin, stanza, namespace, filename, filesize); + origin, stanza, namespace, filename, filesize, filetype); if not get_url then -- error was already sent diff -r 7036e82f83f5 -r ac99a04231b1 mod_http_upload_external/share_v2.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_http_upload_external/share_v2.php Mon Apr 02 10:57:17 2018 +0100 @@ -0,0 +1,145 @@ + + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial + portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +/*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/ +/* CONFIGURATION OPTIONS */ +/*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/ + +/* Change this to a directory that is writable by your web server, but is outside your web root */ +$CONFIG_STORE_DIR = '/tmp'; + +/* This must be the same as 'http_upload_external_secret' that you set in Prosody's config file */ +$CONFIG_SECRET = 'this is your secret string'; + +/* For people who need options to tweak that they don't understand... here you are */ +$CONFIG_CHUNK_SIZE = 4096; + +/*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/ +/* END OF CONFIGURATION */ +/*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/ + +/* Do not edit below this line unless you know what you are doing (spoiler: nobody does) */ + +$upload_file_name = substr($_SERVER['PHP_SELF'], strlen($_SERVER['SCRIPT_NAME'])+1); +$store_file_name = $CONFIG_STORE_DIR . '/store-' . hash('sha256', $upload_file_name); + +$request_method = $_SERVER['REQUEST_METHOD']; + +if(array_key_exists('v2', $_GET) === TRUE && $request_method === 'PUT') { + $upload_file_size = $_SERVER['HTTP_CONTENT_LENGTH']; + $upload_token = $_SERVER['HTTP_UPLOAD_SIGNATURE']; + + if(array_key_exists('HTTP_CONTENT_TYPE', $_SERVER) === TRUE) { + $upload_file_type = $_SERVER['HTTP_CONTENT_TYPE']; + } else { + $upload_file_type = 'application/octet-stream'; + } + + // Imagine being able to store the file data in the content-type! + if(strlen($upload_file_type) > 255) { + header('HTTP/1.0 400 Bad Request'); + exit; + } + + $calculated_token = hash_hmac('sha256', "$upload_file_name\0$upload_file_size\0$upload_file_type", $CONFIG_SECRET); + if(function_exists('hash_equals') { + if(hash_equals($calculated_token, $upload_token) !== TRUE) { + header('HTTP/1.0 403 Forbidden'); + exit; + } + } + else { + if($upload_token !== $calculated_token) { + header('HTTP/1.0 403 Forbidden'); + exit; + } + } + /* Open a file for writing */ + $store_file = fopen($store_file_name, 'x'); + + if($store_file === FALSE) { + header('HTTP/1.0 409 Conflict'); + exit; + } + + /* PUT data comes in on the stdin stream */ + $incoming_data = fopen('php://input', 'r'); + + /* Read the data a chunk at a time and write to the file */ + while ($data = fread($incoming_data, $CONFIG_CHUNK_SIZE)) { + fwrite($store_file, $data); + } + + /* Close the streams */ + fclose($incoming_data); + fclose($store_file); + file_put_contents($store_file_name.'-type', $upload_file_type); +} else if($request_method === 'GET' || $request_method === 'HEAD') { + // Send file (using X-Sendfile would be nice here...) + if(file_exists($store_file_name)) { + $mime_type = file_get_contents($store_file_name.'-type'); + if($mime_type === FALSE) { + $mime_type = 'application/octet-stream'; + } + header('Content-Disposition: attachment'); + header('Content-Type: '.$mime_type); + header('Content-Length: '.filesize($store_file_name)); + header('Content-Security-Policy: "default-src \'none\'"'); + header('X-Content-Security-Policy: "default-src \'none\'"'); + header('X-WebKit-CSP: "default-src 'none'"'); + if($request_method !== 'HEAD') { + readfile($store_file_name); + } + } else { + header('HTTP/1.0 404 Not Found'); + } +} else { + header('HTTP/1.0 400 Bad Request'); +} + +exit;