diff --git a/.github/workflows/spa-server-ci.yml b/.github/workflows/spa-server-ci.yml index 90d1baa..ac42390 100644 --- a/.github/workflows/spa-server-ci.yml +++ b/.github/workflows/spa-server-ci.yml @@ -17,7 +17,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: run integration test # --show-output - run: cargo test -p tests --test starter -j 1 -- --test-threads 1 + run: cargo test -p tests --test http_test -j 1 -- --test-threads 1 - name: run pebble run: ./run_pebble.sh working-directory: ./tests/bash/ diff --git a/config.release.toml b/config.release.toml index 8478825..b9a5813 100644 --- a/config.release.toml +++ b/config.release.toml @@ -13,8 +13,9 @@ addr = "0.0.0.0" ## port when serving public PI,default is http port. external_port should not be 0. # external_port = 80 -## optional, when https enabled, redirect_https default value is true -# redirect_https = false +## optional, when https enabled, redirect_https default value true +## it would the port would be https.external_port(https.external_port should be defined), otherwise is false +# redirect_https = true # [https] # port = 443 # https bind address @@ -89,8 +90,9 @@ addr = "0.0.0.0" # alias = ["example.com"] # cors = false # [domains.https] -## optional, default is `http.redirect_https` value. -# redirect_https = false +## optional, when https enabled, redirect_https default value true +## it would the port would be https.external_port(https.external_port should be defined), otherwise is false +# redirect_https = 443 ## this would be usefully when set https.acme # disable_acme = false diff --git a/server/src/admin_server.rs b/server/src/admin_server.rs index c35ed1a..642f6a0 100644 --- a/server/src/admin_server.rs +++ b/server/src/admin_server.rs @@ -1,9 +1,10 @@ +use std::collections::HashMap; use crate::acme::ACMEManager; use crate::admin_server::request::{ DeleteDomainVersionOption, DomainWithOptVersionOption, DomainWithVersionOption, GetDomainOption, GetDomainPositionOption, UpdateUploadingStatusOption, UploadFileOption, }; -use crate::config::AdminConfig; +use crate::config::{AdminConfig, get_host_path_from_domain}; use crate::domain_storage::DomainStorage; use crate::hot_reload::HotReloadManager; use crate::with; @@ -15,7 +16,7 @@ use std::str::FromStr; use std::sync::Arc; use warp::multipart::FormData; use warp::reply::Response; -use warp::{Filter, Rejection}; +use warp::{Filter, Rejection, Reply}; pub struct AdminServer { conf: Arc, @@ -23,6 +24,7 @@ pub struct AdminServer { reload_manager: Arc, acme_manager: Arc, delay_timer: DelayTimer, + host_alias: Arc>, } impl AdminServer { @@ -32,6 +34,7 @@ impl AdminServer { reload_manager: HotReloadManager, acme_manager: Arc, delay_timer: DelayTimer, + host_alias: Arc> ) -> Self { AdminServer { conf: Arc::new(conf.clone()), @@ -39,6 +42,7 @@ impl AdminServer { reload_manager: Arc::new(reload_manager), acme_manager, delay_timer, + host_alias } } @@ -86,7 +90,7 @@ impl AdminServer { fn get_domain_info( &self, - ) -> impl Filter + Clone { + ) -> impl Filter + Clone { warp::path("status") .and(warp::query::()) .and(with(self.domain_storage.clone())) @@ -99,6 +103,7 @@ impl AdminServer { warp::path!("upload" / "position") .and(warp::query::()) .and(with(self.domain_storage.clone())) + .and(with(self.host_alias.clone())) .map(service::get_upload_position) } @@ -140,15 +145,28 @@ impl AdminServer { warp::body::content_length_limit(1024 * 16) .and(warp::body::json::()), ) + .and(with(self.host_alias.clone())) .and_then(service::change_upload_status) } - fn upload_file(&self) -> impl Filter + Clone { + fn check_alias(domain:&str, host_alias: Arc>) -> Option { + let (host,_) = get_host_path_from_domain(domain); + if let Some(original_host) = host_alias.get(host) { + return Some(bad_resp(format!("should not use alias domain, please use {original_host}"))) + } + None + } + + fn upload_file(&self) -> impl Filter + Clone { async fn handler( query: UploadFileOption, form: FormData, storage: Arc, + host_alias: Arc>, ) -> Result { + if let Some(resp) = AdminServer::check_alias(&query.domain, host_alias) { + return Ok(resp) + } let resp = service::update_file(query, form, storage) .await .unwrap_or_else(|e| { @@ -163,12 +181,13 @@ impl AdminServer { .and(warp::query::()) .and(warp::multipart::form().max_length(self.conf.max_upload_size)) .and(with(self.domain_storage.clone())) + .and(with(self.host_alias.clone())) .and_then(handler) } fn get_files_metadata( &self, - ) -> impl Filter + Clone { + ) -> impl Filter + Clone { warp::path!("files" / "metadata") .and(with(self.domain_storage.clone())) .and(warp::query::()) @@ -204,6 +223,7 @@ impl AdminServer { } pub mod service { + use std::collections::HashMap; use crate::acme::ACMEManager; use crate::admin_server::request::{ DeleteDomainVersionOption, DomainWithOptVersionOption, DomainWithVersionOption, @@ -274,7 +294,11 @@ pub mod service { pub(super) fn get_upload_position( option: GetDomainPositionOption, storage: Arc, + host_alias: Arc>, ) -> Response { + if let Some(resp) = super::AdminServer::check_alias(&option.domain, host_alias) { + return resp + } if URI_REGEX.is_match(&option.domain) { match storage.get_upload_position(&option.domain) { Ok(ret) => { @@ -317,7 +341,11 @@ pub mod service { storage: Arc, acme_manager: Arc, param: UpdateUploadingStatusOption, + host_alias: Arc>, ) -> Result { + if let Some(resp) = super::AdminServer::check_alias(¶m.domain, host_alias) { + return Ok(resp) + } let resp = match storage .update_uploading_status(param.domain, param.version, param.status, &acme_manager) .await @@ -473,6 +501,12 @@ pub mod service { } } +fn bad_resp(text:String) -> Response { + let mut resp = StatusCode::BAD_REQUEST.into_response(); + *resp.body_mut() = Body::from(text); + resp +} + pub mod request { use crate::domain_storage::UploadingStatus; use serde::{Deserialize, Serialize}; diff --git a/server/src/config.rs b/server/src/config.rs index 5a36f63..1ded1e8 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -105,12 +105,12 @@ pub struct DomainConfig { pub cache: Option, pub https: Option, pub alias: Option>, + pub redirect_https: Option, } #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct DomainHttpsConfig { pub ssl: Option, - pub http_redirect_to_https: Option, #[serde(default)] pub disable_acme: bool, } @@ -162,6 +162,7 @@ pub struct HttpConfig { pub addr: String, pub port: u16, pub external_port: Option, + pub redirect_https: Option, } #[derive(Deserialize, Debug, Clone, PartialEq)] @@ -172,7 +173,7 @@ pub struct HttpsConfig { pub external_port: Option, pub addr: String, #[serde(default)] - pub http_redirect_to_https: u32, + pub http_redirect_to_https: u16, } // should write Deserialize by hand. #[derive(Deserialize, Debug, Clone, PartialEq)] diff --git a/server/src/lib.rs b/server/src/lib.rs index 46afc56..81d2659 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -14,6 +14,7 @@ pub mod cors; pub mod service; pub mod static_file_filter; +use std::collections::HashMap; use crate::acme::{ACMEManager, RefreshDomainMessage, ReloadACMEState}; use crate::admin_server::AdminServer; use crate::config::{AdminConfig, Config}; @@ -47,6 +48,7 @@ async fn run_admin_server( reload_manager: HotReloadManager, acme_manager: Arc, delay_timer: DelayTimer, + host_alias: Arc> ) -> anyhow::Result<()> { let admin_server = AdminServer::new( config, @@ -54,6 +56,7 @@ async fn run_admin_server( reload_manager, acme_manager, delay_timer, + host_alias, ); admin_server.run().await } @@ -71,7 +74,7 @@ pub async fn reload_server( let domain_storage = Arc::new(DomainStorage::init(&config.file_dir, cache)?); let (state, http_rx, https_rx) = OneShotReloadState::init(&config); - let server = Server::new(config.clone(), domain_storage.clone()); + let server = Server::new(config.clone(), domain_storage.clone())?; let acme_config = config.https.as_ref().and_then(|x| x.acme.clone()); let reload_acme_state: Option = if let Some(acme_config) = acme_config { Some(ACMEManager::init_acme_provider_and_certificate( @@ -96,10 +99,10 @@ pub async fn reload_server( tokio::task::spawn(async move { join( server - .init_http_server(http_rx, challenge_path.clone()) + .init_http_server(http_rx, challenge_path) .map_err(|error| error!("reload http server error:{error}")), server - .init_https_server(https_rx, tls_server_config, challenge_path.clone()) + .init_https_server(https_rx, tls_server_config) .map_err(|error| error!("reload https server error:{error}")), ) .await @@ -125,7 +128,7 @@ pub async fn run_server() -> anyhow::Result<()> { pub async fn run_server_with_config(config: Config) -> anyhow::Result<()> { let cache = FileCache::new(&config); let domain_storage = Arc::new(DomainStorage::init(&config.file_dir, cache)?); - let server = Server::new(config.clone(), domain_storage.clone()); + let server = Server::new(config.clone(), domain_storage.clone())?; if let Some(admin_config) = &config.admin_config { tracing::info!("admin server enabled"); @@ -154,16 +157,19 @@ pub async fn run_server_with_config(config: Config) -> anyhow::Result<()> { )?); let challenge_path = acme_manager.challenge_dir.clone(); - let tls_server_config = load_ssl_server_config(&config, acme_manager.clone(), server.get_host_alias())?; + let host_alias = server.get_host_alias(); + + + let tls_server_config = load_ssl_server_config(&config, acme_manager.clone(), host_alias.clone())?; let _ = tokio::join!( server - .init_https_server(https_rx, tls_server_config, challenge_path.clone()) + .init_https_server(https_rx, tls_server_config) .map_err(|error| { error!("init https server error: {error}"); error }), server - .init_http_server(http_rx, challenge_path.clone()) + .init_http_server(http_rx, challenge_path) .map_err(|error| { error!("init http server error: {error}"); error @@ -174,6 +180,7 @@ pub async fn run_server_with_config(config: Config) -> anyhow::Result<()> { reload_manager, acme_manager.clone(), delay_timer, + host_alias, ) .map_err(|error| { error!("init admin server error: {error}"); @@ -197,7 +204,7 @@ pub async fn run_server_with_config(config: Config) -> anyhow::Result<()> { let tls_server_config = load_ssl_server_config(&config, acme_manager.clone(), server.get_host_alias())?; let _ = tokio::join!( server - .init_https_server(None, tls_server_config, challenge_path.clone()) + .init_https_server(None, tls_server_config) .map_err(|error| { error!("init https server error: {error}"); panic!("init https server error: {error}") diff --git a/server/src/service.rs b/server/src/service.rs index 0b8fd43..c93841e 100644 --- a/server/src/service.rs +++ b/server/src/service.rs @@ -12,6 +12,7 @@ use std::str::FromStr; use std::sync::Arc; use tracing::warn; use warp::fs::{ArcPath, Conditionals}; +use warp::http::Uri; use warp::Reply; use crate::static_file_filter::{cache_or_file_reply, get_cache_file}; @@ -24,7 +25,7 @@ pub struct ServiceConfig { pub struct DomainServiceConfig { pub cors: bool, - pub http_redirect_to_https: Option, + pub redirect_https: Option, pub enable_acme: bool, } @@ -34,36 +35,87 @@ impl ServiceConfig { } } -pub struct ServiceContext { - pub is_https: bool, - pub external_port: u16, - pub redirect_url: Arc>, // - pub challenge_path: ChallengePath, +fn alias_redirect(uri: &Uri, https:bool, host:&str, external_port:u16) -> warp::reply::Response { // cors? + let mut resp = Response::default(); + let schema = if https {"https://"} else {"http://"}; + + let mut path = format!("{schema}{host}"); + + if https && external_port != 443 || !https && external_port != 80 { + path.push_str(&format!(":{external_port}")) + } + + path = format!("{path}{uri}"); + let path = path.parse().unwrap(); + resp.headers_mut().insert(LOCATION, path); + *resp.status_mut() = StatusCode::MOVED_PERMANENTLY; + resp } -pub async fn create_service( - req: Request, - service_config: Arc, - domain_storage: Arc, - context: ServiceContext, -) -> Result { - let ServiceContext { - is_https, - external_port, - redirect_url, - challenge_path, - } = context; +// static file reply +async fn file_resp(req: &Request,uri:&Uri, host:&str, domain_storage: Arc, origin_opt: Option) -> Result, Infallible> { + let path = uri.path(); + let mut resp = match get_cache_file(path, host, domain_storage.clone()).await { + Some(item) => { + let headers = req.headers(); + let conditionals = Conditionals { + if_modified_since: headers.typed_get(), + if_unmodified_since: headers.typed_get(), + if_range: headers.typed_get(), + range: headers.typed_get(), + }; + let accept_encoding = headers + .get("accept-encoding") + .and_then(|x| x.to_str().map(|x| x.to_string()).ok()); + cache_or_file_reply(item, conditionals, accept_encoding).await + } + None => { + // path: "" => "/" + if domain_storage.check_if_empty_index(host, path) { + let mut resp = Response::default(); + //Attention: alias would be twice + let mut path = format!("{path}/"); + if let Some(query) = uri.query() { + path.push('?'); + path.push_str(query); + } + let path = path.parse().unwrap(); + resp.headers_mut().insert(LOCATION, path); + *resp.status_mut() = StatusCode::MOVED_PERMANENTLY; + Ok(resp) + } else { + Ok(not_found()) + } + } + }; + if let Some(Validated::Simple(origin)) = origin_opt { + resp = resp.map(|r| cors_resp(r, origin)); + } + resp +} +fn get_authority(req:&Request) -> Option { let uri = req.uri(); let from_uri = uri.authority().cloned(); // trick, need more check - let authority_opt = from_uri.or_else(|| { + from_uri.or_else(|| { req.headers().get("host").and_then(|value| { value .to_str() .ok() .and_then(|x| Authority::from_str(x).ok()) }) - }); + }) +} + + +pub async fn create_http_service( + req: Request, + service_config: Arc, + domain_storage: Arc, + challenge_path: ChallengePath, + external_port: u16, +) -> Result { + let authority_opt = get_authority(&req); if let Some(authority) = authority_opt { let origin_host = authority.host(); @@ -79,95 +131,77 @@ pub async fn create_service( Either::Left(x) => Some(x), Either::Right(v) => return Ok(v), }; + let uri = req.uri(); let path = uri.path(); - // redirect to https - if !is_https { - // check if acme challenge - if service_config.enable_acme && path.starts_with(ACME_CHALLENGE) { - let token = &path[ACME_CHALLENGE.len()..]; - { - if let Some(path) = challenge_path.read().await.as_ref() { - let path = get_challenge_path(path, host, token); - let headers = req.headers(); - let conditionals = Conditionals { - if_modified_since: headers.typed_get(), - if_unmodified_since: headers.typed_get(), - if_range: headers.typed_get(), - range: headers.typed_get(), - }; - return match warp::fs::file_reply(ArcPath(Arc::new(path)), conditionals) - .await - { - Ok(resp) => Ok(resp.into_response()), - Err(_err) => { - warn!("known challenge error:{_err:?}"); - Ok(not_found()) - } - }; - } + if service_config.enable_acme && path.starts_with(ACME_CHALLENGE) { + let token = &path[ACME_CHALLENGE.len()..]; + { + if let Some(path) = challenge_path.read().await.as_ref() { + let path = get_challenge_path(path, host, token); + let headers = req.headers(); + let conditionals = Conditionals { + if_modified_since: headers.typed_get(), + if_unmodified_since: headers.typed_get(), + if_range: headers.typed_get(), + range: headers.typed_get(), + }; + return match warp::fs::file_reply(ArcPath(Arc::new(path)), conditionals) + .await + { + Ok(resp) => Ok(resp.into_response()), + Err(_err) => { + warn!("known challenge error:{_err:?}"); + Ok(not_found()) + } + }; } - return Ok(not_found()); - } - if let Some(port) = service_config.http_redirect_to_https { - let mut resp = Response::default(); - let redirect_path = if port != 443 { - format!("https://{host}:{port}{uri}") - } else { - format!("https://{host}{uri}") - }; - resp.headers_mut() - .insert(LOCATION, redirect_path.parse().unwrap()); - *resp.status_mut() = StatusCode::MOVED_PERMANENTLY; - return Ok(resp); } + return Ok(not_found()); } - if is_alias { - //TODO: needs external port + if is_alias || service_config.redirect_https.is_some() { + let (https, external_port) = match service_config.redirect_https { + Some(external_port) => (true, external_port), + None => (false, external_port) + }; + return Ok(alias_redirect(uri,https, host, external_port)); } + file_resp(&req, uri, host, domain_storage, origin_opt).await + } else { + Ok(forbid()) + } - // static file - let mut resp = match get_cache_file(path, host, domain_storage.clone()).await { - Some(item) => { - let headers = req.headers(); - let conditionals = Conditionals { - if_modified_since: headers.typed_get(), - if_unmodified_since: headers.typed_get(), - if_range: headers.typed_get(), - range: headers.typed_get(), - }; - let accept_encoding = headers - .get("accept-encoding") - .and_then(|x| x.to_str().map(|x| x.to_string()).ok()); - cache_or_file_reply(item, conditionals, accept_encoding).await - } - None => { - // path: "" => "/" - if domain_storage.check_if_empty_index(host, path) { - let mut resp = Response::default(); - //TODO: alias would be twice. otherwise needs to add outer bind port config. - let mut path = format!("{path}/"); - if let Some(query) = uri.query() { - path.push('?'); - path.push_str(query); - } - let path = path.parse().unwrap(); - resp.headers_mut().insert(LOCATION, path); - *resp.status_mut() = StatusCode::MOVED_PERMANENTLY; - Ok(resp) - } else { - Ok(not_found()) - } - } +} + + +pub async fn create_https_service( + req: Request, + service_config: Arc, + domain_storage: Arc, + external_port: u16, +) -> Result { + let authority_opt = get_authority(&req); + + if let Some(authority) = authority_opt { + let origin_host = authority.host(); + let (is_alias, host) = if let Some(alias) = service_config.host_alias.get(origin_host) { + (true, alias.as_str()) + } else { + (false, origin_host) }; - if let Some(Validated::Simple(origin)) = origin_opt { - resp = resp.map(|r| cors_resp(r, origin)); + let service_config = service_config.get_domain_service_config(host); + // cors + let origin_opt = match resp_cors_request(req.method(), req.headers(), service_config.cors) { + Either::Left(x) => Some(x), + Either::Right(v) => return Ok(v), + }; + let uri = req.uri(); + if is_alias { + return Ok(alias_redirect(uri,true, host, external_port)); } - resp + file_resp(&req, uri,host, domain_storage, origin_opt).await } else { - let mut resp = Response::default(); - *resp.status_mut() = StatusCode::FORBIDDEN; - Ok(resp) + Ok(forbid()) } } @@ -176,6 +210,11 @@ pub fn not_found() -> Response { *resp.status_mut() = StatusCode::NOT_FOUND; resp } +pub fn forbid()-> Response { + let mut resp = Response::default(); + *resp.status_mut() = StatusCode::FORBIDDEN; + resp +} pub fn resp(code: StatusCode, str: &'static str) -> Response { let mut resp = Response::new(Body::from(str)); diff --git a/server/src/web_server.rs b/server/src/web_server.rs index 6098765..78c6ee6 100644 --- a/server/src/web_server.rs +++ b/server/src/web_server.rs @@ -1,7 +1,7 @@ use crate::acme::ChallengePath; use chrono::{DateTime, Local}; use hyper::server::conn::AddrIncoming; -use hyper::server::Server as HServer; +use hyper::server::{Server as HServer}; use hyper::service::service_fn; use rustls::ServerConfig; use socket2::{Domain, Socket, Type}; @@ -10,12 +10,14 @@ use std::convert::Infallible; use std::net::{SocketAddr, TcpListener}; use std::str::FromStr; use std::sync::Arc; +use anyhow::bail; +use futures_util::future::Either; use tokio::net::TcpListener as TKTcpListener; use tokio::sync::oneshot::Receiver; -use crate::config::Config; +use crate::config::{Config, HttpConfig, HttpsConfig}; use crate::domain_storage::DomainStorage; -use crate::service::{create_service, DomainServiceConfig, ServiceConfig, ServiceContext}; +use crate::service::{create_http_service, create_https_service, DomainServiceConfig, ServiceConfig}; use crate::tls::TlsAcceptor; async fn handler(rx: Receiver<()>, time: DateTime, http_or_https: &'static str) { @@ -55,35 +57,59 @@ pub struct Server { } impl Server { - pub fn new(conf: Config, storage: Arc) -> Self { - let default_http_redirect_to_https = conf.https.as_ref().map(|x| x.http_redirect_to_https); + pub fn new(conf: Config, storage: Arc) -> anyhow::Result { + let default_http_redirect_to_https:Option> = conf.http.as_ref().and_then(|x| { + if x.redirect_https.is_some_and(|x|x) { + let external_port = conf.https.as_ref().and_then(|https| https.external_port); + if external_port.is_none() { + Some(Either::Left("when redirect_https is undefined or true, https.external_port should be set")) + } else { + external_port.map(|x|Either::Right(x)) + } + } else { + None + } + }); + let default_http_redirect_to_https = match default_http_redirect_to_https { + Some(Either::Right(v)) => Some(v), + None => None, + Some(Either::Left(s)) => bail!(s) + }; let default = DomainServiceConfig { cors: conf.cors, - http_redirect_to_https: default_http_redirect_to_https, + redirect_https: default_http_redirect_to_https, enable_acme: conf.https.as_ref().and_then(|x| x.acme.as_ref()).is_some(), }; - let service_config: HashMap = conf - .domains - .iter() - .map(|domain| { - let domain_service_config: DomainServiceConfig = DomainServiceConfig { - cors: domain.cors.unwrap_or(default.cors), - http_redirect_to_https: domain - .https - .as_ref() - .and_then(|x| x.http_redirect_to_https) - .or(default_http_redirect_to_https), - enable_acme: domain - .https - .as_ref() - .map(|x| x.disable_acme) - .unwrap_or(default.enable_acme), - }; - - (domain.domain.clone(), domain_service_config) - }) - .collect(); + let mut service_config: HashMap = HashMap::new(); + for domain in conf.domains.iter() { + let redirect_https = match domain.redirect_https { + None => default_http_redirect_to_https, + Some(true) => { + match default_http_redirect_to_https { + Some(port) => Some(port), + None => { + let external_port = conf.https.as_ref().and_then(|https| https.external_port); + if external_port.is_none() { + bail!("when domains.redirect_https is true, https.external_port should be set") + } + external_port + } + } + }, + Some(false) => None + }; + let domain_service_config: DomainServiceConfig = DomainServiceConfig { + cors: domain.cors.unwrap_or(default.cors), + redirect_https, + enable_acme: domain + .https + .as_ref() + .map(|x| x.disable_acme) + .unwrap_or(default.enable_acme), + }; + service_config.insert(domain.domain.clone(), domain_service_config); + } let mut alias_map = HashMap::new(); for domain in conf.domains.iter() { @@ -99,51 +125,54 @@ impl Server { inner: service_config, host_alias: Arc::new(alias_map), }); - Server { + Ok(Server { conf, storage, service_config, - } + }) + } + pub fn init_http_tcp(http_config: &HttpConfig) -> anyhow::Result { + let bind_address = + SocketAddr::from_str(&format!("{}:{}", &http_config.addr, &http_config.port))?; + let socket= get_socket(bind_address)?; + Ok(socket) + } + + fn init_https_tcp(config:&HttpsConfig, tls_server_config:Arc) -> anyhow::Result<(SocketAddr, TlsAcceptor)> { + let bind_address = + SocketAddr::from_str(&format!("{}:{}", &config.addr, &config.port))?; + + let incoming = + AddrIncoming::from_listener(TKTcpListener::from_std(get_socket(bind_address)?)?)?; + let local_addr = incoming.local_addr(); + Ok((local_addr, TlsAcceptor::new(tls_server_config, incoming))) } pub async fn init_https_server( &self, rx: Option>, tls_server_config: Option>, - challenge_path: ChallengePath, ) -> anyhow::Result<()> { if let Some(config) = &self.conf.https { // This has checked by load_ssl_server_config + let tls_server_config = tls_server_config.unwrap(); - let bind_address = - SocketAddr::from_str(&format!("{}:{}", &config.addr, &config.port)).unwrap(); - let external_port = config.external_port.unwrap_or(bind_address.port()); + let (local_addr, acceptor ) = Self::init_https_tcp(config, tls_server_config)?; + tracing::info!("listening on https://{}", local_addr); + let external_port = config.external_port.unwrap_or(local_addr.port()); let make_svc = hyper::service::make_service_fn(|_| { let service_config = self.service_config.clone(); let storage = self.storage.clone(); - let challenge_path = challenge_path.clone(); async move { Ok::<_, Infallible>(service_fn(move |req| { - create_service( - req, - service_config.clone(), - storage.clone(), - ServiceContext { - challenge_path: challenge_path.clone(), - is_https: true, - external_port, - }, - ) + create_https_service(req, service_config.clone(), storage.clone(), external_port) })) } }); - tracing::info!("listening on https://{}", &bind_address); - let incoming = - AddrIncoming::from_listener(TKTcpListener::from_std(get_socket(bind_address)?)?)?; let server = - HServer::builder(TlsAcceptor::new(tls_server_config, incoming)).serve(make_svc); + HServer::builder(acceptor).serve(make_svc); run_server!(tls: server, rx); } Ok(()) @@ -155,31 +184,22 @@ impl Server { challenge_path: ChallengePath, ) -> anyhow::Result<()> { if let Some(http_config) = &self.conf.http { - let bind_address = - SocketAddr::from_str(&format!("{}:{}", &http_config.addr, &http_config.port)) - .unwrap(); - let external_port = http_config.external_port.unwrap_or(bind_address.port()); + let listener = Self::init_http_tcp(http_config)?; + let local_addr = listener.local_addr()?; + tracing::info!("listening on http://{}", &local_addr); + let external_port = http_config.external_port.unwrap_or(local_addr.port()); let make_svc = hyper::service::make_service_fn(|_| { let service_config = self.service_config.clone(); let storage = self.storage.clone(); let challenge_path = challenge_path.clone(); async move { Ok::<_, Infallible>(service_fn(move |req| { - create_service( - req, - service_config.clone(), - storage.clone(), - ServiceContext { - challenge_path: challenge_path.clone(), - is_https: true, - external_port, - }, - ) + create_http_service(req, service_config.clone(), storage.clone(), challenge_path.clone(), external_port) + })) } }); - tracing::info!("listening on http://{}", &bind_address); - let server = HServer::from_tcp(get_socket(bind_address)?)?.serve(make_svc); + let server = HServer::from_tcp(listener)?.serve(make_svc); run_server!(server, rx); } Ok(()) diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 0399b45..4a280de 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [[test]] -name = "starter" +name = "http_test" [[test]] name = "acme_test" diff --git a/tests/bash/run_pebble_dev.sh b/tests/bash/run_pebble_dev.sh index ca5e64a..fb6b6b4 100755 --- a/tests/bash/run_pebble_dev.sh +++ b/tests/bash/run_pebble_dev.sh @@ -1,3 +1,4 @@ +#!/bin/bash docker run --network=host --rm -e PEBBLE_WFE_NONCEREJECT=0 \ --name pebble \ ghcr.io/letsencrypt/pebble:2.6.0 diff --git a/tests/data/server_config.conf b/tests/data/server_config.conf index 4c621f5..2eacebb 100644 --- a/tests/data/server_config.conf +++ b/tests/data/server_config.conf @@ -11,27 +11,7 @@ file_dir = "./data/web" # Access-Control-Allow-Origin: $ORIGIN # Access-Control-Allow-Methods: OPTION,GET,HEAD # Access-Control-Max-Age: 3600 -// cors = true - -# https config, optional -//https { -// # default value for https ssl -// ssl { -// # private ssl key -// private = "private.key path", -// # public ssl cert -// public = "public.cert path" -// } - -// # https bind address -// port = 443 -// addr = "0.0.0.0" - -// # if set port, http server(80) will send client -// # status code:301(Moved Permanently) to tell client redirect to https -// http_redirect_to_https = 443 -//} - +cors = true # default cache config @@ -82,23 +62,3 @@ admin_config { max_preserve: 2, } } - - -# optional, domains specfic config, it will use the default config if not set -//domains = [{ -// # domain name -// domain: "www.example.com", -// // optional, same with cache config, if not set, will use default cache config. -// cache: { -// client_cache:${cache.client_cache} -// max_size: ${cache.max_size} -// client_cache = ${cache.client_cache} -// }, -// # cors -// cors: ${cors}, -// # domain https config, if not set, will use default https config. -// https: { -// ssl: ${https.ssl} -// http_redirect_to_https: ${https.http_redirect_to_https} -// } -//}] \ No newline at end of file diff --git a/tests/data/server_config_acme.conf b/tests/data/server_config_acme.conf index d3bcafd..3a6ee53 100644 --- a/tests/data/server_config_acme.conf +++ b/tests/data/server_config_acme.conf @@ -16,7 +16,6 @@ cors = true # https config, optional https { - # acme config, it doest not support run with https.ssl config. acme { emails = ["mailto:zsy.evan@gmail.com"] @@ -30,10 +29,7 @@ https { // # https bind address port = 8443 addr = "0.0.0.0" - - # if set port, http server(80) will send client - # status code:301(Moved Permanently) to tell client redirect to https - http_redirect_to_https = 8443 + external_port = 8443 } diff --git a/tests/data/server_config_acme_alias.toml b/tests/data/server_config_acme_alias.toml new file mode 100644 index 0000000..8c2c173 --- /dev/null +++ b/tests/data/server_config_acme_alias.toml @@ -0,0 +1,54 @@ +file_dir = "./data/web" +cors = true + +# http bind, if set port <= 0, will disable http server(need set https config) +[http] +port = 5002 +addr = "0.0.0.0" + +# https config, optional +[https] +port = 8443 +addr = "0.0.0.0" +external_port = 8443 + +[https.acme] +emails = ["mailto:zsy.evan@gmail.com"] + # directory to store account and certificate + # optional, default is ${file_dir}/acme + #dir = "/data/acme" + # optional ,default is false +type = "ci" +ci_ca_path = "./data/pebble/certs/pebble.minica.pem" + +[cache] + +# if file size > max_size, it will not be cached. default is (10MB). +max_size = 20 +compression = true + +[[cache.client_cache]] +expire = '30d' +extension_names = ['icon','gif','jpg','jpeg','png','js'] + +[[cache.client_cache]] +expire = '0' +extension_names = ['html'] + + +# admin server config +# admin server don't support hot reload. the config should not change. +# optional, and it's disabled by default. +# if you use spa-client to upload files, control version. Need to open it +[admin_config] + # bind host +port = 9000 +addr = "127.0.0.1" + + # this is used to check client request + # put it in http header, Authorization: Bearer $token +token = "token" + +[[domains]] +domain = "local.fornetcode.com" +alias = ["local2.fornetcode.com"] diff --git a/tests/data/server_config_alias.toml b/tests/data/server_config_alias.toml new file mode 100644 index 0000000..1b0dae1 --- /dev/null +++ b/tests/data/server_config_alias.toml @@ -0,0 +1,41 @@ +cors = true +file_dir = "./data/web" + + +# http bind, if set port <= 0, will disable http server(need set https config) +[http] +port = 8080 +addr = "0.0.0.0" + + + +[cache] +# if file size > max_size, it will not be cached. default is (10MB). +max_size = 20 +compression = true + +[[cache.client_cache]] +expire = '30d' +extension_names = ['icon','gif','jpg','jpeg','png','js'] + +[[cache.client_cache]] +expire = '0' +extension_names = ['html'] + + +# admin server config +# admin server don't support hot reload. the config should not change. +# optional, and it's disabled by default. +# if you use spa-client to upload files, control version. Need to open it +[admin_config] +# bind host +port = 9000 +addr = "127.0.0.1" + +# this is used to check client request +# put it in http header, Authorization: Bearer $token +token = "token" + +[[domains]] +domain = "local.fornetcode.com" +alias = ["local2.fornetcode.com"] diff --git a/tests/data/server_config_https.conf b/tests/data/server_config_https.conf index a8b07a3..48b779f 100644 --- a/tests/data/server_config_https.conf +++ b/tests/data/server_config_https.conf @@ -26,10 +26,7 @@ https { // # https bind address port = 8443 addr = "0.0.0.0" - - # if set port, http server(80) will send client - # status code:301(Moved Permanently) to tell client redirect to https - http_redirect_to_https = 8443 + external_port = 8443 } @@ -69,17 +66,6 @@ admin_config { # this is used to check client request # put it in http header, Authorization: Bearer $token token = "token" - -// # max file size allowed to be uploaded, -// max_upload_size = 31457280 - -// # delete deprecated version by cron -// deprecated_version_delete { -// # default value: every day at 3am. -// cron: "0 0 3 * * *", -// # default value is 2 -// max_preserve: 2, -// } } @@ -87,13 +73,4 @@ admin_config { domains = [{ # domain name domain: "local.fornetcode.com", - # optional, same with cache config, if not set, will use default cache config. -// cache: { -// client_cache:${cache.client_cache} -// max_size: ${cache.max_size} -// client_cache = ${cache.client_cache} -// }, - # cors -// cors: true, -// # domain https config, if not set, will use default https config. }] diff --git a/tests/tests/acme_test.rs b/tests/tests/acme_test.rs index e606c2d..ae5dadd 100644 --- a/tests/tests/acme_test.rs +++ b/tests/tests/acme_test.rs @@ -86,7 +86,7 @@ async fn simple_acme_test() { sleep(Duration::from_secs(2)).await; assert_files(domain, request_prefix, 1, vec!["", "index.html"]).await; } -#[ignore] + #[tokio::test] async fn simple_acme_test2() { let domain = LOCAL_HOST.to_owned(); @@ -115,3 +115,34 @@ async fn simple_acme_test2() { } assert_files(domain, request_prefix, 1, vec!["", "index.html"]).await; } + +#[tokio::test] +async fn alias_acme() { + let domain = LOCAL_HOST.to_owned(); + let domain = &domain; + let request_prefix = format!("https://{LOCAL_HOST2}:8443"); + let request_prefix = &request_prefix; + clean_web_domain_dir(LOCAL_HOST); + clean_cert(); + run_server_with_config("server_config_acme_alias.toml"); + sleep(Duration::from_secs(2)).await; + upload_file_and_check(domain, request_prefix, 1, vec![]).await; + + let (api, _) = get_client_api("client_config.conf"); + let mut wait_count = 0; + loop { + assert!(wait_count < 60, "60 seconds doest not have cert"); + sleep(Duration::from_secs(1)).await; + let cert_info = api + .get_acme_cert_info(Some(get_host_path_from_domain(domain).0.to_string())) + .await + .unwrap(); + if !cert_info.is_empty() { + break; + } + wait_count += 1; + } + assert_files(domain, request_prefix, 1, vec!["index.html"]).await; + assert_redirects(request_prefix, vec![format!("https://{LOCAL_HOST}:8443/27"), "/27/".to_owned()]).await +} + diff --git a/tests/tests/common.rs b/tests/tests/common.rs index 35646ee..46380d5 100644 --- a/tests/tests/common.rs +++ b/tests/tests/common.rs @@ -1,15 +1,16 @@ use reqwest::header::{CACHE_CONTROL, LOCATION}; use reqwest::redirect::Policy; -use reqwest::{ClientBuilder, StatusCode, Url}; +use reqwest::{Certificate, ClientBuilder, StatusCode, Url}; use spa_client::api::API; use std::path::{Path, PathBuf}; use std::{env, fs, io}; //use tokio::sync::oneshot; use tokio::task::JoinHandle; -use tracing::{debug, error, Level}; +use tracing::{debug, error}; use tracing_subscriber::EnvFilter; pub const LOCAL_HOST: &str = "local.fornetcode.com"; +pub const LOCAL_HOST2: &str = "local2.fornetcode.com"; pub fn get_test_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data") @@ -37,6 +38,11 @@ pub fn get_server_data_path(domain: &str, version: u32) -> PathBuf { .join(version.to_string()) } +fn get_root_cert() -> Certificate { + let path = get_test_dir().join("pebble/certs/pebble.minica.pem"); + Certificate::from_pem(&fs::read(&path).unwrap()).unwrap() +} + pub fn run_server_with_config(config_file_name: &str) -> JoinHandle<()> { env::set_var( "SPA_CONFIG", @@ -115,6 +121,24 @@ pub async fn upload_file_and_check( assert_files(domain, request_prefix, version, check_path).await; } + +pub async fn assert_redirects(request:&str, redirect_urls: Vec) { + let mut request = request.to_string(); + for redirect_url in redirect_urls { + let target= assert_redirect_correct(request.as_str(), &redirect_url).await; + match Url::parse(&target) { + Ok(_) => { + request = target; + } + Err(_) => { + let mut url = Url::parse(&request).unwrap(); + url.set_path(&redirect_url); + request = url.to_string(); + } + } + + } +} pub async fn assert_files( domain: &str, request_prefix: &str, @@ -160,9 +184,9 @@ pub async fn assert_files( } } } -pub async fn assert_redirect_correct(request_prefix: &str, target_prefix: &str) { +pub async fn assert_redirect_correct(request_prefix: &str, target_prefix: &str) -> String { let client = ClientBuilder::new() - .danger_accept_invalid_certs(true) + .add_root_certificate(get_root_cert()) .redirect(Policy::none()) .build() .unwrap(); @@ -170,11 +194,14 @@ pub async fn assert_redirect_correct(request_prefix: &str, target_prefix: &str) let url = Url::parse_with_params(request_prefix, &query).unwrap(); let query = url.query().unwrap(); let response = client.get(url.clone()).send().await.unwrap(); + + let location = response.headers().get(LOCATION).unwrap().to_str().unwrap().to_string(); assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY); assert_eq!( - response.headers().get(LOCATION).unwrap().to_str().unwrap(), + location, format!("{target_prefix}?{query}") //format!("{path}/?{query}") ); + location } pub async fn assert_files_no_exists(request_prefix: &str, check_path: Vec<&'static str>) { for file in check_path { diff --git a/tests/tests/starter.rs b/tests/tests/http_test.rs similarity index 94% rename from tests/tests/starter.rs rename to tests/tests/http_test.rs index 46ecde8..4333dd9 100644 --- a/tests/tests/starter.rs +++ b/tests/tests/http_test.rs @@ -315,4 +315,18 @@ async fn revoke_version() { assert_files(domain, request_prefix, 2, vec!["index.html", "2.html"]).await; assert_files_no_exists(request_prefix, vec!["3.html"]).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] +async fn alias_start_server_and_client_upload_file() { + let domain = LOCAL_HOST.to_owned() + "/27"; + let domain = &domain; + let request_prefix = format!("http://{LOCAL_HOST2}:8080/27"); + let request_prefix = &request_prefix; + + clean_web_domain_dir(LOCAL_HOST); + run_server_with_config("server_config_alias.toml"); + tokio::time::sleep(Duration::from_secs(1)).await; + upload_file_and_check(domain, request_prefix, 1, vec!["index.html"]).await; + assert_redirects(request_prefix, vec![format!("http://{LOCAL_HOST}:8080/27"), "/27/".to_owned()]).await } \ No newline at end of file