<commit_before>mod color;
mod middlewares;
mod util;

use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::env;
use std::error::Error;
use std::fs;
use std::io::{self, Read, Seek, SeekFrom};
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use clap::crate_version;
use htmlescape::encode_minimal;
use iron::headers;
use iron::headers::{AcceptEncoding, ContentEncoding, Encoding, QualityItem};
use iron::method;
use iron::modifiers::Redirect;
use iron::status;
use iron::{Chain, Handler, Iron, IronError, IronResult, Request, Response, Set};
use iron_cors::CorsMiddleware;
use lazy_static::lazy_static;
use mime_guess as mime_types;
use multipart::server::{Multipart, SaveResult};
use path_dedot::*;
use percent_encoding::percent_decode;
use pretty_bytes::converter::convert;
use termcolor::{Color, ColorSpec};

use color::{build_spec, Printer};
use util::{
    enable_string, encode_link_path, error_io2iron, error_resp, now_string,
    system_time_to_date_time, StringError, ROOT_LINK,
};

use middlewares::{AuthChecker, CompressionHandler, RequestLogger};

const ORDER_ASC: &str = "asc";
const ORDER_DESC: &str = "desc";
const DEFAULT_ORDER: &str = ORDER_DESC;

lazy_static! {
    static ref SORT_FIELDS: Vec<&'static str> = vec!["name", "modified", "size"];
}

fn main() {
    let matches = clap::App::new("Simple HTTP(s) Server")
        .setting(clap::AppSettings::ColoredHelp)
        .version(crate_version!())
        .arg(clap::Arg::with_name("root")
             .index(1)
             .validator(|s| {
                 match fs::metadata(s) {
                     Ok(metadata) => {
                         if metadata.is_dir() { Ok(()) } else {
                             Err("Not directory".to_owned())
                         }
                     },
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("Root directory"))
        .arg(clap::Arg::with_name("index")
             .short("i")
             .long("index")
             .help("Enable automatic render index page [index.html, index.htm]"))
        .arg(clap::Arg::with_name("upload")
             .short("u")
             .long("upload")
             .help("Enable upload files (multiple select)"))
        .arg(clap::Arg::with_name("redirect").long("redirect")
             .takes_value(true)
             .validator(|url_string| iron::Url::parse(url_string.as_str()).map(|_| ()))
             .help("takes a URL to redirect to using HTTP 301 Moved Permanently"))
        .arg(clap::Arg::with_name("nosort")
             .long("nosort")
             .help("Disable directory entries sort (by: name, modified, size)"))
        .arg(clap::Arg::with_name("nocache")
             .long("nocache")
             .help("Disable http cache"))
        .arg(clap::Arg::with_name("norange")
             .long("norange")
             .help("Disable header::Range support (partial request)"))
        .arg(clap::Arg::with_name("cert")
             .long("cert")
             .takes_value(true)
             .validator(|s| {
                 match fs::metadata(s) {
                     Ok(metadata) => {
                         if metadata.is_file() { Ok(()) } else {
                             Err("Not a regular file".to_owned())
                         }
                     },
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("TLS/SSL certificate (pkcs#12 format)"))
        .arg(clap::Arg::with_name("cors")
             .long("cors")
             .help("Enable CORS via the \"Access-Control-Allow-Origin\" header"))
        .arg(clap::Arg::with_name("certpass").
             long("certpass")
             .takes_value(true)
             .help("TLS/SSL certificate password"))
        .arg(clap::Arg::with_name("ip")
             .long("ip")
             .takes_value(true)
             .default_value("0.0.0.0")
             .validator(|s| {
                 match IpAddr::from_str(&s) {
                     Ok(_) => Ok(()),
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("IP address to bind"))
        .arg(clap::Arg::with_name("port")
             .short("p")
             .long("port")
             .takes_value(true)
             .default_value("8000")
             .validator(|s| {
                 match s.parse::<u16>() {
                     Ok(_) => Ok(()),
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("Port number"))
        .arg(clap::Arg::with_name("auth")
             .short("a")
             .long("auth")
             .takes_value(true)
             .validator(|s| {
                 let parts = s.splitn(2, ':').collect::<Vec<&str>>();
                 if parts.len() < 2 || parts.len() >= 2 && parts[1].is_empty() {
                     Err("no password found".to_owned())
                 } else if parts[0].is_empty() {
                     Err("no username found".to_owned())
                 } else {
                     Ok(())
                 }
             })
             .help("HTTP Basic Auth (username:password)"))
        .arg(clap::Arg::with_name("compress")
             .short("c")
             .long("compress")
             .multiple(true)
             .value_delimiter(",")
             .takes_value(true)
             .help("Enable file compression: gzip/deflate\n    Example: -c=js,d.ts\n    Note: disabled on partial request!"))
        .arg(clap::Arg::with_name("threads")
             .short("t")
             .long("threads")
             .takes_value(true)
             .default_value("3")
             .validator(|s| {
                 match s.parse::<u8>() {
                     Ok(v) => {
                         if v > 0 { Ok(()) } else {
                             Err("Not positive number".to_owned())
                         }
                     }
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("How many worker threads"))
        .arg(clap::Arg::with_name("try-file-404")
             .long("try-file")
             .visible_alias("try-file-404")
             .takes_value(true)
             .value_name("PATH")
             .validator(|s| {
                 match fs::metadata(s) {
                     Ok(metadata) => {
                         if metadata.is_file() { Ok(()) } else {
                             Err("Not a file".to_owned())
                         }
                     },
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("serve this file (server root relative) in place of missing files (useful for single page apps)"))
        .arg(clap::Arg::with_name("silent")
             .long("silent")
             .short("s")
             .takes_value(false)
             .help("Disable all outputs"))
        .get_matches();

    let root = matches
        .value_of("root")
        .map(|s| PathBuf::from(s).canonicalize().unwrap())
        .unwrap_or_else(|| env::current_dir().unwrap());
    let index = matches.is_present("index");
    let upload = matches.is_present("upload");
    let redirect_to = matches
        .value_of("redirect")
        .map(iron::Url::parse)
        .map(Result::unwrap);
    let sort = !matches.is_present("nosort");
    let cache = !matches.is_present("nocache");
    let range = !matches.is_present("norange");
    let cert = matches.value_of("cert");
    let certpass = matches.value_of("certpass");
    let cors = matches.is_present("cors");
    let ip = matches.value_of("ip").unwrap();
    let port = matches.value_of("port").unwrap().parse::<u16>().unwrap();
    let auth = matches.value_of("auth");
    let compress = matches.values_of_lossy("compress");
    let threads = matches.value_of("threads").unwrap().parse::<u8>().unwrap();
    let try_file_404 = matches.value_of("try-file-404");

    let printer = Printer::new();
    let color_blue = Some(build_spec(Some(Color::Blue), false));
    let addr = format!("{}:{}", ip, port);
    let compression_exts = compress
        .clone()
        .unwrap_or_default()
        .iter()
        .map(|s| format!("*.{}", s))
        .collect::<Vec<String>>();
    let compression_string = if compression_exts.is_empty() {
        "disabled".to_owned()
    } else {
        format!("{:?}", compression_exts)
    };
    let silent = matches.is_present("silent");

    if !silent {
        printer.println_out(
            r#"     Index: {}, Upload: {}, Cache: {}, Cors: {}, Range: {}, Sort: {}, Threads: {}
          Auth: {}, Compression: {}
         https: {}, Cert: {}, Cert-Password: {}
          Root: {},
    TryFile404: {}
       Address: {}
    ======== [{}] ========"#,
            &vec![
                enable_string(index),
                enable_string(upload),
                enable_string(cache),
                enable_string(cors),
                enable_string(range),
                enable_string(sort),
                threads.to_string(),
                auth.unwrap_or("disabled").to_string(),
                compression_string,
                (if cert.is_some() { "enabled" } else { "disabled" }).to_string(),
                cert.unwrap_or("").to_owned(),
                certpass.unwrap_or("").to_owned(),
                root.to_str().unwrap().to_owned(),
                try_file_404.unwrap_or("").to_owned(),
                format!("{}://{}", if cert.is_some() {"https"} else {"http"}, addr),
                now_string()
            ].iter()
                .map(|s| (s.as_str(), &color_blue))
                .collect::<Vec<(&str, &Option<ColorSpec>)>>()
        ).unwrap();
    }

    let mut chain = Chain::new(MainHandler {
        root,
        index,
        upload,
        cache,
        range,
        redirect_to,
        sort,
        compress: compress
            .clone()
            .map(|exts| exts.iter().map(|s| format!(".{}", s)).collect()),
        try_file_404: try_file_404.map(PathBuf::from),
    });
    if cors {
        chain.link_around(CorsMiddleware::with_allow_any());
    }
    if let Some(auth) = auth {
        chain.link_before(AuthChecker::new(auth));
    }
    if let Some(ref exts) = compress {
        if !exts.is_empty() {
            chain.link_after(CompressionHandler);
        }
    }
    if !silent {
        chain.link_after(RequestLogger {
            printer: Printer::new(),
        });
    }
    let mut server = Iron::new(chain);
    server.threads = threads as usize;
    let rv = if let Some(cert) = cert {
        use hyper_native_tls::NativeTlsServer;
        let ssl = NativeTlsServer::new(cert, certpass.unwrap_or("")).unwrap();
        server.https(&addr, ssl)
    } else {
        server.http(&addr)
    };
    if let Err(e) = rv {
        printer
            .println_err(
                "{}: Can not bind on {}, {}",
                &[
                    ("ERROR", &Some(build_spec(Some(Color::Red), true))),
                    (addr.as_str(), &None),
                    (e.to_string().as_str(), &None),
                ],
            )
            .unwrap();
        std::process::exit(1);
    };
}

struct MainHandler {
    root: PathBuf,
    index: bool,
    upload: bool,
    cache: bool,
    range: bool,
    redirect_to: Option<iron::Url>,
    sort: bool,
    compress: Option<Vec<String>>,
    try_file_404: Option<PathBuf>,
}

impl Handler for MainHandler {
    fn handle(&self, req: &mut Request) -> IronResult<Response> {
        let mut fs_path = self.root.clone();
        if let Some(url) = &self.redirect_to {
            return Ok(Response::with((
                status::PermanentRedirect,
                Redirect(url.clone()),
            )));
        }
        let path_prefix = req
            .url
            .path()
            .into_iter()
            .filter(|s| !s.is_empty())
            .map(|s| PathBuf::from(&*percent_decode(s.as_bytes()).decode_utf8().unwrap()))
            .collect::<PathBuf>();
        fs_path.push(&path_prefix);
        let fs_path = dbg!(fs_path.parse_dot().unwrap());

        if !fs_path.starts_with(&self.root) {
            return Err(IronError::new(
                io::Error::new(io::ErrorKind::PermissionDenied, "Permission Denied"),
                status::Forbidden,
            ));
        }

        if self.upload && req.method == method::Post {
            if let Err((s, msg)) = self.save_files(req, &fs_path) {
                return Ok(error_resp(s, &msg));
            } else {
                return Ok(Response::with((status::Found, Redirect(req.url.clone()))));
            }
        }

        let path_metadata = match fs::metadata(&fs_path) {
            Ok(value) => value,
            Err(err) => {
                let status = match err.kind() {
                    io::ErrorKind::PermissionDenied => status::Forbidden,
                    io::ErrorKind::NotFound => {
                        if let Some(ref p) = self.try_file_404 {
                            if Some(true) == fs::metadata(p).ok().map(|meta| meta.is_file()) {
                                return self.send_file(req, p);
                            }<commit_msg>Minor adjustments<commit_after>mod color;
mod middlewares;
mod util;

use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::env;
use std::error::Error;
use std::fs;
use std::io::{self, Read, Seek, SeekFrom};
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use clap::crate_version;
use htmlescape::encode_minimal;
use iron::headers;
use iron::headers::{AcceptEncoding, ContentEncoding, Encoding, QualityItem};
use iron::method;
use iron::modifiers::Redirect;
use iron::status;
use iron::{Chain, Handler, Iron, IronError, IronResult, Request, Response, Set};
use iron_cors::CorsMiddleware;
use lazy_static::lazy_static;
use mime_guess as mime_types;
use multipart::server::{Multipart, SaveResult};
use path_dedot::ParseDot;
use percent_encoding::percent_decode;
use pretty_bytes::converter::convert;
use termcolor::{Color, ColorSpec};

use color::{build_spec, Printer};
use util::{
    enable_string, encode_link_path, error_io2iron, error_resp, now_string,
    system_time_to_date_time, StringError, ROOT_LINK,
};

use middlewares::{AuthChecker, CompressionHandler, RequestLogger};

const ORDER_ASC: &str = "asc";
const ORDER_DESC: &str = "desc";
const DEFAULT_ORDER: &str = ORDER_DESC;

lazy_static! {
    static ref SORT_FIELDS: Vec<&'static str> = vec!["name", "modified", "size"];
}

fn main() {
    let matches = clap::App::new("Simple HTTP(s) Server")
        .setting(clap::AppSettings::ColoredHelp)
        .version(crate_version!())
        .arg(clap::Arg::with_name("root")
             .index(1)
             .validator(|s| {
                 match fs::metadata(s) {
                     Ok(metadata) => {
                         if metadata.is_dir() { Ok(()) } else {
                             Err("Not directory".to_owned())
                         }
                     },
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("Root directory"))
        .arg(clap::Arg::with_name("index")
             .short("i")
             .long("index")
             .help("Enable automatic render index page [index.html, index.htm]"))
        .arg(clap::Arg::with_name("upload")
             .short("u")
             .long("upload")
             .help("Enable upload files (multiple select)"))
        .arg(clap::Arg::with_name("redirect").long("redirect")
             .takes_value(true)
             .validator(|url_string| iron::Url::parse(url_string.as_str()).map(|_| ()))
             .help("takes a URL to redirect to using HTTP 301 Moved Permanently"))
        .arg(clap::Arg::with_name("nosort")
             .long("nosort")
             .help("Disable directory entries sort (by: name, modified, size)"))
        .arg(clap::Arg::with_name("nocache")
             .long("nocache")
             .help("Disable http cache"))
        .arg(clap::Arg::with_name("norange")
             .long("norange")
             .help("Disable header::Range support (partial request)"))
        .arg(clap::Arg::with_name("cert")
             .long("cert")
             .takes_value(true)
             .validator(|s| {
                 match fs::metadata(s) {
                     Ok(metadata) => {
                         if metadata.is_file() { Ok(()) } else {
                             Err("Not a regular file".to_owned())
                         }
                     },
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("TLS/SSL certificate (pkcs#12 format)"))
        .arg(clap::Arg::with_name("cors")
             .long("cors")
             .help("Enable CORS via the \"Access-Control-Allow-Origin\" header"))
        .arg(clap::Arg::with_name("certpass").
             long("certpass")
             .takes_value(true)
             .help("TLS/SSL certificate password"))
        .arg(clap::Arg::with_name("ip")
             .long("ip")
             .takes_value(true)
             .default_value("0.0.0.0")
             .validator(|s| {
                 match IpAddr::from_str(&s) {
                     Ok(_) => Ok(()),
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("IP address to bind"))
        .arg(clap::Arg::with_name("port")
             .short("p")
             .long("port")
             .takes_value(true)
             .default_value("8000")
             .validator(|s| {
                 match s.parse::<u16>() {
                     Ok(_) => Ok(()),
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("Port number"))
        .arg(clap::Arg::with_name("auth")
             .short("a")
             .long("auth")
             .takes_value(true)
             .validator(|s| {
                 let parts = s.splitn(2, ':').collect::<Vec<&str>>();
                 if parts.len() < 2 || parts.len() >= 2 && parts[1].is_empty() {
                     Err("no password found".to_owned())
                 } else if parts[0].is_empty() {
                     Err("no username found".to_owned())
                 } else {
                     Ok(())
                 }
             })
             .help("HTTP Basic Auth (username:password)"))
        .arg(clap::Arg::with_name("compress")
             .short("c")
             .long("compress")
             .multiple(true)
             .value_delimiter(",")
             .takes_value(true)
             .help("Enable file compression: gzip/deflate\n    Example: -c=js,d.ts\n    Note: disabled on partial request!"))
        .arg(clap::Arg::with_name("threads")
             .short("t")
             .long("threads")
             .takes_value(true)
             .default_value("3")
             .validator(|s| {
                 match s.parse::<u8>() {
                     Ok(v) => {
                         if v > 0 { Ok(()) } else {
                             Err("Not positive number".to_owned())
                         }
                     }
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("How many worker threads"))
        .arg(clap::Arg::with_name("try-file-404")
             .long("try-file")
             .visible_alias("try-file-404")
             .takes_value(true)
             .value_name("PATH")
             .validator(|s| {
                 match fs::metadata(s) {
                     Ok(metadata) => {
                         if metadata.is_file() { Ok(()) } else {
                             Err("Not a file".to_owned())
                         }
                     },
                     Err(e) => Err(e.description().to_string())
                 }
             })
             .help("serve this file (server root relative) in place of missing files (useful for single page apps)"))
        .arg(clap::Arg::with_name("silent")
             .long("silent")
             .short("s")
             .takes_value(false)
             .help("Disable all outputs"))
        .get_matches();

    let root = matches
        .value_of("root")
        .map(|s| PathBuf::from(s).canonicalize().unwrap())
        .unwrap_or_else(|| env::current_dir().unwrap());
    let index = matches.is_present("index");
    let upload = matches.is_present("upload");
    let redirect_to = matches
        .value_of("redirect")
        .map(iron::Url::parse)
        .map(Result::unwrap);
    let sort = !matches.is_present("nosort");
    let cache = !matches.is_present("nocache");
    let range = !matches.is_present("norange");
    let cert = matches.value_of("cert");
    let certpass = matches.value_of("certpass");
    let cors = matches.is_present("cors");
    let ip = matches.value_of("ip").unwrap();
    let port = matches.value_of("port").unwrap().parse::<u16>().unwrap();
    let auth = matches.value_of("auth");
    let compress = matches.values_of_lossy("compress");
    let threads = matches.value_of("threads").unwrap().parse::<u8>().unwrap();
    let try_file_404 = matches.value_of("try-file-404");

    let printer = Printer::new();
    let color_blue = Some(build_spec(Some(Color::Blue), false));
    let addr = format!("{}:{}", ip, port);
    let compression_exts = compress
        .clone()
        .unwrap_or_default()
        .iter()
        .map(|s| format!("*.{}", s))
        .collect::<Vec<String>>();
    let compression_string = if compression_exts.is_empty() {
        "disabled".to_owned()
    } else {
        format!("{:?}", compression_exts)
    };
    let silent = matches.is_present("silent");

    if !silent {
        printer.println_out(
            r#"     Index: {}, Upload: {}, Cache: {}, Cors: {}, Range: {}, Sort: {}, Threads: {}
          Auth: {}, Compression: {}
         https: {}, Cert: {}, Cert-Password: {}
          Root: {},
    TryFile404: {}
       Address: {}
    ======== [{}] ========"#,
            &vec![
                enable_string(index),
                enable_string(upload),
                enable_string(cache),
                enable_string(cors),
                enable_string(range),
                enable_string(sort),
                threads.to_string(),
                auth.unwrap_or("disabled").to_string(),
                compression_string,
                (if cert.is_some() { "enabled" } else { "disabled" }).to_string(),
                cert.unwrap_or("").to_owned(),
                certpass.unwrap_or("").to_owned(),
                root.to_str().unwrap().to_owned(),
                try_file_404.unwrap_or("").to_owned(),
                format!("{}://{}", if cert.is_some() {"https"} else {"http"}, addr),
                now_string()
            ].iter()
                .map(|s| (s.as_str(), &color_blue))
                .collect::<Vec<(&str, &Option<ColorSpec>)>>()
        ).unwrap();
    }

    let mut chain = Chain::new(MainHandler {
        root,
        index,
        upload,
        cache,
        range,
        redirect_to,
        sort,
        compress: compress
            .clone()
            .map(|exts| exts.iter().map(|s| format!(".{}", s)).collect()),
        try_file_404: try_file_404.map(PathBuf::from),
    });
    if cors {
        chain.link_around(CorsMiddleware::with_allow_any());
    }
    if let Some(auth) = auth {
        chain.link_before(AuthChecker::new(auth));
    }
    if let Some(ref exts) = compress {
        if !exts.is_empty() {
            chain.link_after(CompressionHandler);
        }
    }
    if !silent {
        chain.link_after(RequestLogger {
            printer: Printer::new(),
        });
    }
    let mut server = Iron::new(chain);
    server.threads = threads as usize;
    let rv = if let Some(cert) = cert {
        use hyper_native_tls::NativeTlsServer;
        let ssl = NativeTlsServer::new(cert, certpass.unwrap_or("")).unwrap();
        server.https(&addr, ssl)
    } else {
        server.http(&addr)
    };
    if let Err(e) = rv {
        printer
            .println_err(
                "{}: Can not bind on {}, {}",
                &[
                    ("ERROR", &Some(build_spec(Some(Color::Red), true))),
                    (addr.as_str(), &None),
                    (e.to_string().as_str(), &None),
                ],
            )
            .unwrap();
        std::process::exit(1);
    };
}

struct MainHandler {
    root: PathBuf,
    index: bool,
    upload: bool,
    cache: bool,
    range: bool,
    redirect_to: Option<iron::Url>,
    sort: bool,
    compress: Option<Vec<String>>,
    try_file_404: Option<PathBuf>,
}

impl Handler for MainHandler {
    fn handle(&self, req: &mut Request) -> IronResult<Response> {
        let mut fs_path = self.root.clone();
        if let Some(url) = &self.redirect_to {
            return Ok(Response::with((
                status::PermanentRedirect,
                Redirect(url.clone()),
            )));
        }
        let path_prefix = req
            .url
            .path()
            .into_iter()
            .filter(|s| !s.is_empty())
            .map(|s| PathBuf::from(&*percent_decode(s.as_bytes()).decode_utf8().unwrap()))
            .collect::<PathBuf>();
        fs_path.push(&path_prefix);
        let fs_path = fs_path.parse_dot().unwrap();

        if !fs_path.starts_with(&self.root) {
            return Err(IronError::new(
                io::Error::new(io::ErrorKind::PermissionDenied, "Permission Denied"),
                status::Forbidden,
            ));
        }

        if self.upload && req.method == method::Post {
            if let Err((s, msg)) = self.save_files(req, &fs_path) {
                return Ok(error_resp(s, &msg));
            } else {
                return Ok(Response::with((status::Found, Redirect(req.url.clone()))));
            }
        }

        let path_metadata = match fs::metadata(&fs_path) {
            Ok(value) => value,
            Err(err) => {
                let status = match err.kind() {
                    io::ErrorKind::PermissionDenied => status::Forbidden,
                    io::ErrorKind::NotFound => {
                        if let Some(ref p) = self.try_file_404 {
                            if Some(true) == fs::metadata(p).ok().map(|meta| meta.is_file()) {
                                return self.send_file(req, p);
                            }
{'fraction_non_alphanumeric': 0.15968753520862283, 'fraction_numerical': 0.004506703721786157, 'mean_word_length': 1.7160342717258261, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 77, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 2, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1006854', 'n_tokens_mistral': 7833, 'n_tokens_neox': 7732, 'n_words': 1407}