qair 0.7.0

Send/receive files with builtin HTTP server and terminal QR code.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! HTTP handler to store files uploaded to the webserver via e.g. HTTP PUT.
//!
//! Does not deal with the the upload HTML form.

extern crate hyper;
extern crate iron;
extern crate mount;
extern crate percent_encoding;
extern crate router;

use crate::noclobber::*;
use iron::request::Request;
use iron::response::Response;
use iron::{status, IronResult};
use percent_encoding::percent_decode_str;
use std::path::Path;

/// Filename to store files uploaded to this server if client requested invalid
/// filename.
static DEFAULT_UPLOAD_FILENAME: &'static str = "received_file";

/// Handler for HTTP requests that upload a file via e.g. HTTP PUT
///
/// On error we do not show an informative error message to the client because
/// we have not investigated that this never leaks too much info on all platforms.
pub fn upload_receive_handler(request: &mut Request) -> IronResult<Response> {
    let target_filename =
        trusted_upload_filename_for(request).unwrap_or(DEFAULT_UPLOAD_FILENAME.to_string());

    let create_ret = create_and_adapt_filename_if_exists(&target_filename);
    match create_ret {
        Err(_) => Ok(Response::with((
            status::InternalServerError,
            "Error creating file",
        ))),
        Ok(mut file) => {
            let copy_ret = std::io::copy(&mut request.body, &mut file);
            match copy_ret {
                Err(_) => Ok(Response::with((
                    status::InternalServerError,
                    "Error writing file",
                ))),
                Ok(_) => Ok(Response::with((status::Ok, "ok"))),
            }
        }
    }
}

/// Given a HTTP request, returns where to put the file uploaded to this server
///
/// There might already exist a file with the returned filename
///
/// Returns "None" if no (allowed) filename was asked for in the request
fn trusted_upload_filename_for(request: &Request) -> Option<String> {
    // Iron's opinion about paths can be different from the OS.
    // So after Iron chopped off the directory part on the web level,
    // we do it again on the filesystem level.
    let untrusted_path_encoded_parts = request.url.path();
    let untrusted_path_encoded = untrusted_path_encoded_parts.last()?;
    let untrusted_path_cow = percent_decode_str(untrusted_path_encoded)
        .decode_utf8()
        .ok()?;
    let untrusted_path = Path::new(untrusted_path_cow.as_ref());
    let potentially_dotfilename = untrusted_path.file_name()?.to_str()?;

    // Disallow files starting with dot, to avoid people receiving for
    // example a new ".bash_profile" because that would be a security problem.
    let trusted_path = if potentially_dotfilename.starts_with(".") {
        "_".to_owned() + potentially_dotfilename
    } else {
        potentially_dotfilename.to_string()
    };

    Some(trusted_path)
}