From f1c8a55b7a7ae533236564a195037128804445e6 Mon Sep 17 00:00:00 2001 From: Dean Li Date: Sun, 21 Feb 2021 16:55:17 +0800 Subject: Allow set custom headers from CLI --- Cargo.lock | 1 + Cargo.toml | 1 + src/args.rs | 5 +++++ src/main.rs | 28 ++++++++++++++++++++++++++++ 4 files changed, 35 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index de0952f..98a000e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1597,6 +1597,7 @@ dependencies = [ "futures", "grass", "hex", + "httparse", "libflate", "log", "maud", diff --git a/Cargo.toml b/Cargo.toml index c0af4f3..537b215 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ actix-files = "0.5" actix-multipart = "0.3.0" actix-web-httpauth = "0.5.0" mime = "0.3" +httparse = "1.3.5" [dev-dependencies] assert_cmd = "1.0" diff --git a/src/args.rs b/src/args.rs index f736941..53f1638 100644 --- a/src/args.rs +++ b/src/args.rs @@ -117,6 +117,10 @@ struct CliArgs { /// Shown instead of host in page title and heading #[structopt(short = "t", long = "title")] title: Option, + + /// Custom header from user + #[structopt(long = "header")] + header: Option, } /// Checks wether an interface is valid, i.e. it can be parsed into an IP address @@ -225,6 +229,7 @@ pub fn parse_args() -> crate::MiniserveConfig { zip_enabled: args.enable_zip, dirs_first: args.dirs_first, title: args.title, + header: args.header, } } diff --git a/src/main.rs b/src/main.rs index c55e77f..149381f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,6 +88,9 @@ pub struct MiniserveConfig { /// Shown instead of host in page title and heading pub title: Option, + + /// If specified, header will be added + pub header: Option, } fn main() { @@ -248,6 +251,7 @@ async fn run() -> Result<(), ContextualError> { let srv = actix_web::HttpServer::new(move || { App::new() + .wrap(configure_header(&inside_config.clone())) .app_data(inside_config.clone()) .wrap(middleware::Condition::new( !inside_config.auth.is_empty(), @@ -279,6 +283,30 @@ async fn run() -> Result<(), ContextualError> { .map_err(|e| ContextualError::IoError("".to_owned(), e)) } +fn configure_header(conf: &MiniserveConfig) -> middleware::DefaultHeaders { + let mut headers = [httparse::EMPTY_HEADER; 16]; + + match conf.clone().header { + Some(mut header) => { + // parse_headers need header newline ends + header.push('\n'); + httparse::parse_headers(header.as_bytes(), &mut headers).expect("Bad header"); + + let mut header_middleware = middleware::DefaultHeaders::new(); + + for h in headers.iter() { + if h.name != httparse::EMPTY_HEADER.name { + println!("h={:?}", h); + header_middleware = header_middleware.header(h.name, h.value); + } + } + + header_middleware + } + None => middleware::DefaultHeaders::new(), + } +} + /// Configures the Actix application fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) { let random_route = conf.random_route.clone().unwrap_or_default(); -- cgit v1.2.3 From 7b4402238a19f187321a68088e6542d8d5fa8572 Mon Sep 17 00:00:00 2001 From: Dean Li Date: Mon, 22 Feb 2021 12:11:11 +0800 Subject: Move the parsing header logic to args.rs --- Cargo.toml | 2 +- src/args.rs | 37 +++++++++++++++++++++++++++++++++++-- src/main.rs | 26 +++++++------------------- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 537b215..cc4d5fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ actix-files = "0.5" actix-multipart = "0.3.0" actix-web-httpauth = "0.5.0" mime = "0.3" -httparse = "1.3.5" +httparse = "1" [dev-dependencies] assert_cmd = "1.0" diff --git a/src/args.rs b/src/args.rs index 53f1638..ecfe151 100644 --- a/src/args.rs +++ b/src/args.rs @@ -119,8 +119,8 @@ struct CliArgs { title: Option, /// Custom header from user - #[structopt(long = "header")] - header: Option, + #[structopt(long = "header", parse(try_from_str = parse_header))] + header: Option
, } /// Checks wether an interface is valid, i.e. it can be parsed into an IP address @@ -174,6 +174,39 @@ fn parse_auth(src: &str) -> Result { }) } +/// A own header modified from [httparse](https://docs.rs/httparse/1.3.5/src/httparse/lib.rs.html#415-425) +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct Header { + /// The name portion of a header. + /// + /// A header name must be valid ASCII-US, so it's safe to store as a `String`. + pub name: String, + /// The value portion of a header. + /// + /// While headers **should** be ASCII-US, the specification allows for + /// values that may not be, and so the value is stored as bytes. + pub value: Vec, +} + +impl Header { + fn new(header: &httparse::Header) -> Self { + Header { + name: header.name.to_string(), + value: header.value.to_vec(), + } + } +} + +fn parse_header(src: &str) -> Result { + let mut headers = [httparse::EMPTY_HEADER; 1]; + let mut header = src.to_string(); + header.push('\n'); + httparse::parse_headers(header.as_bytes(), &mut headers)?; + + let header = Header::new(&headers[0]); + Ok(header) +} + /// Parses the command line arguments pub fn parse_args() -> crate::MiniserveConfig { let args = CliArgs::from_args(); diff --git a/src/main.rs b/src/main.rs index 149381f..0dd692e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use actix_web::{ }; use actix_web::{middleware, App, HttpRequest, HttpResponse}; use actix_web_httpauth::middleware::HttpAuthentication; +use args::Header; use std::io::{self, Write}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::thread; @@ -90,7 +91,7 @@ pub struct MiniserveConfig { pub title: Option, /// If specified, header will be added - pub header: Option, + pub header: Option
, } fn main() { @@ -284,26 +285,13 @@ async fn run() -> Result<(), ContextualError> { } fn configure_header(conf: &MiniserveConfig) -> middleware::DefaultHeaders { - let mut headers = [httparse::EMPTY_HEADER; 16]; + let header = conf.clone().header; - match conf.clone().header { - Some(mut header) => { - // parse_headers need header newline ends - header.push('\n'); - httparse::parse_headers(header.as_bytes(), &mut headers).expect("Bad header"); - - let mut header_middleware = middleware::DefaultHeaders::new(); - - for h in headers.iter() { - if h.name != httparse::EMPTY_HEADER.name { - println!("h={:?}", h); - header_middleware = header_middleware.header(h.name, h.value); - } - } - - header_middleware + match header { + Some(header) if header.name != httparse::EMPTY_HEADER.name => { + middleware::DefaultHeaders::new().header(&header.name, header.value) } - None => middleware::DefaultHeaders::new(), + _ => middleware::DefaultHeaders::new(), } } -- cgit v1.2.3 From da97e91f2eb02ace7f3e6cb3522d4bd55aa45caf Mon Sep 17 00:00:00 2001 From: Dean Li Date: Mon, 22 Feb 2021 13:13:53 +0800 Subject: Add test for custom header --- tests/header.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/header.rs diff --git a/tests/header.rs b/tests/header.rs new file mode 100644 index 0000000..a0734e5 --- /dev/null +++ b/tests/header.rs @@ -0,0 +1,33 @@ +mod fixtures; + +use assert_cmd::prelude::*; +use assert_fs::fixture::TempDir; +use fixtures::{port, tmpdir, Error}; +use rstest::rstest; +use std::process::{Command, Stdio}; +use std::thread::sleep; +use std::time::Duration; + +#[rstest] +fn custom_header_set(tmpdir: TempDir, port: u16) -> Result<(), Error> { + let header_name = "x-info"; + let header_value = "123"; + let header_str = format!("{}: {}", header_name, header_value); + + let _ = Command::cargo_bin("miniserve")? + .arg(tmpdir.path()) + .arg("-p") + .arg(port.to_string()) + .arg("--header") + .arg(header_str) + .stdout(Stdio::null()) + .spawn()?; + + sleep(Duration::from_secs(1)); + + let resp = reqwest::blocking::get(format!("http://localhost:{}", port).as_str())?; + + assert_eq!(resp.headers().get(header_name).unwrap(), header_value); + + Ok(()) +} -- cgit v1.2.3 From 956ce204b4bda191c441fe5c4d385baa92c82b3e Mon Sep 17 00:00:00 2001 From: Dean Li Date: Mon, 22 Feb 2021 14:11:18 +0800 Subject: Multiple headers support for custom headers --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ src/args.rs | 47 +++++++++++++++++++---------------------------- src/main.rs | 20 +++++++++++++------- tests/header.rs | 28 +++++++++++++++++++--------- 5 files changed, 55 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98a000e..55ba636 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1590,6 +1590,7 @@ dependencies = [ "alphanumeric-sort", "assert_cmd", "assert_fs", + "bytes 1.0.1", "bytesize", "chrono", "chrono-humanize", @@ -1597,6 +1598,7 @@ dependencies = [ "futures", "grass", "hex", + "http", "httparse", "libflate", "log", diff --git a/Cargo.toml b/Cargo.toml index cc4d5fc..4d7201a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,8 @@ actix-multipart = "0.3.0" actix-web-httpauth = "0.5.0" mime = "0.3" httparse = "1" +http = "0.2.3" +bytes = "1" [dev-dependencies] assert_cmd = "1.0" diff --git a/src/args.rs b/src/args.rs index ecfe151..7710cce 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,3 +1,5 @@ +use bytes::Bytes; +use http::header::{HeaderMap, HeaderName, HeaderValue}; use port_check::free_local_port; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::path::PathBuf; @@ -120,7 +122,7 @@ struct CliArgs { /// Custom header from user #[structopt(long = "header", parse(try_from_str = parse_header))] - header: Option
, + header: Option, } /// Checks wether an interface is valid, i.e. it can be parsed into an IP address @@ -174,37 +176,26 @@ fn parse_auth(src: &str) -> Result { }) } -/// A own header modified from [httparse](https://docs.rs/httparse/1.3.5/src/httparse/lib.rs.html#415-425) -#[derive(Clone, Eq, PartialEq, Debug)] -pub struct Header { - /// The name portion of a header. - /// - /// A header name must be valid ASCII-US, so it's safe to store as a `String`. - pub name: String, - /// The value portion of a header. - /// - /// While headers **should** be ASCII-US, the specification allows for - /// values that may not be, and so the value is stored as bytes. - pub value: Vec, -} - -impl Header { - fn new(header: &httparse::Header) -> Self { - Header { - name: header.name.to_string(), - value: header.value.to_vec(), - } - } -} - -fn parse_header(src: &str) -> Result { - let mut headers = [httparse::EMPTY_HEADER; 1]; +/// Custom header parser (allow multiple headers input) +pub fn parse_header(src: &str) -> Result { + // Max customized header is limitted to 16 + let mut headers = [httparse::EMPTY_HEADER; 16]; let mut header = src.to_string(); header.push('\n'); httparse::parse_headers(header.as_bytes(), &mut headers)?; - let header = Header::new(&headers[0]); - Ok(header) + let mut header_map = HeaderMap::new(); + + for h in headers.iter() { + if h.name != httparse::EMPTY_HEADER.name { + header_map.insert( + HeaderName::from_bytes(&Bytes::copy_from_slice(h.name.as_bytes())).unwrap(), + HeaderValue::from_bytes(h.value).unwrap(), + ); + } + } + + Ok(header_map) } /// Parses the command line arguments diff --git a/src/main.rs b/src/main.rs index 0dd692e..44298d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use actix_web::{ }; use actix_web::{middleware, App, HttpRequest, HttpResponse}; use actix_web_httpauth::middleware::HttpAuthentication; -use args::Header; +use http::header::HeaderMap; use std::io::{self, Write}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::thread; @@ -91,7 +91,7 @@ pub struct MiniserveConfig { pub title: Option, /// If specified, header will be added - pub header: Option
, + pub header: Option, } fn main() { @@ -285,11 +285,17 @@ async fn run() -> Result<(), ContextualError> { } fn configure_header(conf: &MiniserveConfig) -> middleware::DefaultHeaders { - let header = conf.clone().header; - - match header { - Some(header) if header.name != httparse::EMPTY_HEADER.name => { - middleware::DefaultHeaders::new().header(&header.name, header.value) + let headers = conf.clone().header; + + match headers { + Some(headers) => { + let mut default_headers = middleware::DefaultHeaders::new(); + for (header_name, header_value) in headers.into_iter() { + if let Some(header_name) = header_name { + default_headers = default_headers.header(&header_name, header_value); + } + } + default_headers } _ => middleware::DefaultHeaders::new(), } diff --git a/tests/header.rs b/tests/header.rs index a0734e5..187730f 100644 --- a/tests/header.rs +++ b/tests/header.rs @@ -8,18 +8,17 @@ use std::process::{Command, Stdio}; use std::thread::sleep; use std::time::Duration; -#[rstest] -fn custom_header_set(tmpdir: TempDir, port: u16) -> Result<(), Error> { - let header_name = "x-info"; - let header_value = "123"; - let header_str = format!("{}: {}", header_name, header_value); - - let _ = Command::cargo_bin("miniserve")? +#[rstest(header, + case("x-info: 123".to_string()), + case("x-info1: 123\r\nx-info2: 345".to_string()) +)] +fn custom_header_set(tmpdir: TempDir, port: u16, header: String) -> Result<(), Error> { + let mut child = Command::cargo_bin("miniserve")? .arg(tmpdir.path()) .arg("-p") .arg(port.to_string()) .arg("--header") - .arg(header_str) + .arg(header.clone()) .stdout(Stdio::null()) .spawn()?; @@ -27,7 +26,18 @@ fn custom_header_set(tmpdir: TempDir, port: u16) -> Result<(), Error> { let resp = reqwest::blocking::get(format!("http://localhost:{}", port).as_str())?; - assert_eq!(resp.headers().get(header_name).unwrap(), header_value); + let mut headers = [httparse::EMPTY_HEADER; 4]; + let mut header = header.clone(); + header.push('\n'); + httparse::parse_headers(header.as_bytes(), &mut headers)?; + + for h in headers.iter() { + if h.name != httparse::EMPTY_HEADER.name { + assert_eq!(resp.headers().get(h.name).unwrap(), h.value); + } + } + + child.kill()?; Ok(()) } -- cgit v1.2.3