You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
686 lines
23 KiB
686 lines
23 KiB
mod config;
|
|
mod proxy;
|
|
mod tasks;
|
|
mod tpl;
|
|
mod util;
|
|
|
|
pub use config::*;
|
|
|
|
use std::{future, io, net, sync};
|
|
|
|
use crate::error::unexpected::{self, Mappable};
|
|
use crate::{domain, service, task_stack};
|
|
|
|
use http_body_util::combinators::UnsyncBoxBody as BoxBody;
|
|
use hyper::{
|
|
body::{Body, Incoming as RequestBody},
|
|
Method,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
type Request = hyper::Request<RequestBody>;
|
|
pub type Response = hyper::Response<BoxBody<bytes::Bytes, io::Error>>;
|
|
|
|
fn bytes_body(b: bytes::Bytes) -> impl Body<Data = bytes::Bytes, Error = io::Error> {
|
|
use http_body_util::BodyExt;
|
|
http_body_util::Full::new(b).map_err(io::Error::other)
|
|
}
|
|
|
|
fn empty_body() -> impl Body<Data = bytes::Bytes, Error = io::Error> {
|
|
use http_body_util::BodyExt;
|
|
http_body_util::Empty::<bytes::Bytes>::new().map_err(io::Error::other)
|
|
}
|
|
|
|
fn stream_body<S>(s: S) -> impl Body<Data = bytes::Bytes, Error = io::Error>
|
|
where
|
|
S: futures::stream::Stream<Item = io::Result<bytes::Bytes>>,
|
|
{
|
|
use futures::stream::TryStreamExt;
|
|
http_body_util::StreamBody::new(s.map_ok(http_body::Frame::data))
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct BasePresenter<'a, T> {
|
|
page_name: &'a str,
|
|
form_method: &'a str,
|
|
http_scheme: &'a str,
|
|
data: T,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct DomainArgs {
|
|
domain: domain::Name,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct DomainInitArgs {
|
|
domain: domain::Name,
|
|
|
|
#[serde(flatten)]
|
|
url_encoded_domain_settings: util::UrlEncodedDomainSettings,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct DomainSyncArgs {
|
|
domain: domain::Name,
|
|
passphrase: String,
|
|
|
|
#[serde(flatten)]
|
|
url_encoded_domain_settings: util::UrlEncodedDomainSettings,
|
|
}
|
|
|
|
pub struct Service {
|
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
|
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
|
handlebars: handlebars::Handlebars<'static>,
|
|
config: service::Config,
|
|
}
|
|
|
|
impl Service {
|
|
pub fn new<CertResolver>(
|
|
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
|
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
|
cert_resolver: CertResolver,
|
|
config: service::Config,
|
|
) -> sync::Arc<Service>
|
|
where
|
|
CertResolver: rustls::server::ResolvesServerCert + 'static,
|
|
{
|
|
let service = sync::Arc::new(Service {
|
|
domain_manager: domain_manager.clone(),
|
|
cert_resolver: sync::Arc::from(cert_resolver),
|
|
handlebars: tpl::get(),
|
|
config,
|
|
});
|
|
|
|
task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller));
|
|
|
|
if service.https_enabled() {
|
|
task_stack.push_spawn(|canceller| tasks::listen_https(service.clone(), canceller));
|
|
}
|
|
|
|
service
|
|
}
|
|
|
|
fn https_enabled(&self) -> bool {
|
|
self.config.http.https_addr.is_some()
|
|
}
|
|
|
|
fn serve<B>(&self, status_code: u16, path: &str, body: B) -> Response
|
|
where
|
|
B: Body<Data = bytes::Bytes, Error = io::Error> + Send + 'static,
|
|
{
|
|
hyper::Response::builder()
|
|
.status(status_code)
|
|
.header("Content-Type", service::guess_mime(path))
|
|
.body(BoxBody::new(body))
|
|
.unwrap_or_else(|err| panic!("failed to build {}: {}", path, err))
|
|
}
|
|
|
|
fn render<T>(&self, status_code: u16, name: &str, value: T) -> Response
|
|
where
|
|
T: Serialize,
|
|
{
|
|
let rendered = match self.handlebars.render(name, &value) {
|
|
Ok(res) => res,
|
|
Err(handlebars::RenderError {
|
|
template_name: None,
|
|
..
|
|
}) => return self.render_error_page(404, "Static asset not found"),
|
|
Err(err) => {
|
|
return self.render_error_page(500, format!("template error: {err}").as_str());
|
|
}
|
|
};
|
|
|
|
self.serve(status_code, name, bytes_body(rendered.into()))
|
|
}
|
|
|
|
fn presenter_http_scheme(&self) -> &str {
|
|
if self.https_enabled() {
|
|
return "https";
|
|
}
|
|
"http"
|
|
}
|
|
|
|
fn render_error_page(&self, status_code: u16, e: &str) -> Response {
|
|
#[derive(Serialize)]
|
|
struct Response<'a> {
|
|
error_msg: &'a str,
|
|
}
|
|
|
|
self.render(
|
|
status_code,
|
|
"/base.html",
|
|
BasePresenter {
|
|
page_name: "/error.html",
|
|
form_method: self.config.http.form_method.as_str(),
|
|
http_scheme: self.presenter_http_scheme(),
|
|
data: &Response { error_msg: e },
|
|
},
|
|
)
|
|
}
|
|
|
|
fn internal_error(&self, e: &str) -> Response {
|
|
log::error!("Internal error: {e}");
|
|
self.render_error_page(
|
|
500,
|
|
"An unexpected error occurred. The server administrator will be able to help.",
|
|
)
|
|
}
|
|
|
|
fn render_redirect(&self, status_code: u16, target_uri: &str) -> Response {
|
|
hyper::Response::builder()
|
|
.status(status_code)
|
|
.header("Location", target_uri.to_string())
|
|
.body(BoxBody::new(empty_body()))
|
|
.unwrap_or_else(|err| {
|
|
panic!("failed to render {status_code} redirect to {target_uri}: {err}",)
|
|
})
|
|
}
|
|
|
|
fn https_redirect(&self, domain: domain::Name, req: Request) -> Response {
|
|
let https_addr = self.config.http.https_addr.unwrap();
|
|
|
|
(|| {
|
|
let mut uri_parts = http::uri::Parts::default();
|
|
uri_parts.scheme = Some(http::uri::Scheme::HTTPS);
|
|
uri_parts.authority = Some(
|
|
http::uri::Authority::from_maybe_shared(format!(
|
|
"{}:{}",
|
|
&domain,
|
|
https_addr.port()
|
|
))
|
|
.or_unexpected_while("constructing authority")?,
|
|
);
|
|
uri_parts.path_and_query = req.uri().path_and_query().cloned();
|
|
|
|
let uri: http::uri::Uri = uri_parts
|
|
.try_into()
|
|
.or_unexpected_while("constructing new URI")?;
|
|
|
|
Ok(self.render_redirect(
|
|
http::status::StatusCode::PERMANENT_REDIRECT.into(),
|
|
uri.to_string().as_str(),
|
|
))
|
|
})()
|
|
.unwrap_or_else(|err: unexpected::Error| {
|
|
self.internal_error(
|
|
format!("failed to redirect from {} to https: {}", req.uri(), err).as_str(),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn render_page<T>(&self, name: &str, data: T) -> Response
|
|
where
|
|
T: Serialize,
|
|
{
|
|
self.render(
|
|
200,
|
|
"/base.html",
|
|
BasePresenter {
|
|
page_name: name,
|
|
form_method: self.config.http.form_method.as_str(),
|
|
http_scheme: self.presenter_http_scheme(),
|
|
data,
|
|
},
|
|
)
|
|
}
|
|
|
|
async fn serve_origin(&self, settings: domain::Settings, req: Request) -> Response {
|
|
let path = service::append_index_to_path(req.uri().path(), "index.html");
|
|
|
|
use domain::manager::GetFileError;
|
|
match self.domain_manager.get_file(&settings, &path).await {
|
|
Ok(f) => self.serve(200, &path, stream_body(f.into_stream())),
|
|
Err(GetFileError::FileNotFound) => self.render_error_page(404, "File not found"),
|
|
Err(GetFileError::Unavailable) => self.render_error_page(502, "Content unavailable"),
|
|
Err(GetFileError::DescrNotSynced) => self.internal_error(
|
|
format!(
|
|
"Backend for {:?} has not yet been synced",
|
|
settings.origin_descr
|
|
)
|
|
.as_str(),
|
|
),
|
|
Err(GetFileError::PathIsDirectory) => {
|
|
// redirect so that the path has '/' appended to it, which will cause the server to
|
|
// check index.html within the path on the new page load.
|
|
let mut path = path.into_owned();
|
|
path.push('/');
|
|
self.render_redirect(
|
|
http::status::StatusCode::TEMPORARY_REDIRECT.into(),
|
|
path.as_str(),
|
|
)
|
|
}
|
|
Err(GetFileError::Unexpected(e)) => {
|
|
self.internal_error(format!("failed to fetch file {path}: {e}").as_str())
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn with_query_req<'a, F, In, Out>(
|
|
&self,
|
|
req: &'a hyper::http::request::Parts,
|
|
body: RequestBody,
|
|
f: F,
|
|
) -> Response
|
|
where
|
|
In: for<'d> Deserialize<'d>,
|
|
F: FnOnce(In) -> Out,
|
|
Out: future::Future<Output = Response>,
|
|
{
|
|
let res = match self.config.http.form_method {
|
|
ConfigFormMethod::GET => {
|
|
serde_urlencoded::from_str::<In>(req.uri.query().unwrap_or(""))
|
|
}
|
|
ConfigFormMethod::POST => {
|
|
use http_body_util::BodyExt;
|
|
let body = match body.collect().await {
|
|
Ok(res) => res.to_bytes(),
|
|
Err(e) => {
|
|
return self.internal_error(format!("failed to read body: {e}").as_str())
|
|
}
|
|
};
|
|
serde_urlencoded::from_bytes::<In>(body.as_ref())
|
|
}
|
|
};
|
|
|
|
match res {
|
|
Ok(args) => f(args).await,
|
|
Err(err) => {
|
|
self.render_error_page(400, format!("failed to parse query args: {}", err).as_str())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn domain(&self, args: DomainArgs) -> Response {
|
|
#[derive(Serialize)]
|
|
struct Data {
|
|
domain: domain::Name,
|
|
settings: Option<domain::Settings>,
|
|
}
|
|
|
|
let settings = match self.domain_manager.get_settings(&args.domain) {
|
|
Ok(domain::manager::GetSettingsResult::Stored(settings)) => Some(settings),
|
|
Ok(domain::manager::GetSettingsResult::Builtin(config)) => {
|
|
config.public.then_some(config.settings)
|
|
}
|
|
Ok(domain::manager::GetSettingsResult::Proxied(_)) => None,
|
|
Ok(domain::manager::GetSettingsResult::Interface) => None,
|
|
Ok(domain::manager::GetSettingsResult::External(_)) => None,
|
|
Err(domain::manager::GetSettingsError::NotFound) => None,
|
|
Err(domain::manager::GetSettingsError::Unexpected(e)) => {
|
|
return self.internal_error(
|
|
format!("retrieving settings for domain {}: {}", &args.domain, e).as_str(),
|
|
);
|
|
}
|
|
};
|
|
|
|
self.render_page(
|
|
"/domain.html",
|
|
Data {
|
|
domain: args.domain,
|
|
settings,
|
|
},
|
|
)
|
|
}
|
|
|
|
fn domain_init(&self, args: DomainInitArgs) -> Response {
|
|
#[derive(Serialize)]
|
|
struct Data<'a> {
|
|
domain: domain::Name,
|
|
url_encoded_domain_settings: util::UrlEncodedDomainSettings,
|
|
dns_records: &'a [service::ConfigDNSRecord],
|
|
challenge_token: String,
|
|
|
|
dns_records_have_more_than_one: bool,
|
|
dns_records_have_cname: bool,
|
|
}
|
|
|
|
let settings: domain::Settings = match args.url_encoded_domain_settings.try_into() {
|
|
Ok(settings) => settings,
|
|
Err(e) => {
|
|
return self
|
|
.render_error_page(400, format!("invalid domain settings: {e}").as_str())
|
|
}
|
|
};
|
|
|
|
let settings_hash = match settings.hash() {
|
|
Ok(hash) => hash,
|
|
Err(e) => {
|
|
return self.internal_error(
|
|
format!("failed to hash domain settings {settings:?}: {e}").as_str(),
|
|
)
|
|
}
|
|
};
|
|
|
|
let dns_records_have_cname = self
|
|
.config
|
|
.dns_records
|
|
.iter()
|
|
.any(|r| matches!(r, service::ConfigDNSRecord::CNAME { .. }));
|
|
|
|
let url_encoded_domain_settings = match settings.try_into() {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
return self
|
|
.internal_error(format!("failed to flatten domains settings: {e}").as_str())
|
|
}
|
|
};
|
|
|
|
self.render_page(
|
|
"/domain_init.html",
|
|
Data {
|
|
domain: args.domain,
|
|
url_encoded_domain_settings,
|
|
dns_records: &self.config.dns_records,
|
|
challenge_token: settings_hash,
|
|
|
|
dns_records_have_more_than_one: self.config.dns_records.len() > 1,
|
|
dns_records_have_cname,
|
|
},
|
|
)
|
|
}
|
|
|
|
async fn domain_sync(&self, args: DomainSyncArgs) -> Response {
|
|
if args.passphrase != self.config.passphrase.as_str() {
|
|
return self.render_error_page(401, "Incorrect passphrase");
|
|
}
|
|
|
|
let settings: domain::Settings = match args.url_encoded_domain_settings.clone().try_into() {
|
|
Ok(settings) => settings,
|
|
Err(e) => {
|
|
return self
|
|
.render_error_page(400, format!("invalid domain settings: {e}").as_str())
|
|
}
|
|
};
|
|
|
|
let sync_result = self
|
|
.domain_manager
|
|
.sync_with_settings(args.domain.clone(), settings)
|
|
.await;
|
|
|
|
#[derive(Serialize)]
|
|
struct Data {
|
|
domain: domain::Name,
|
|
url_encoded_domain_settings: util::UrlEncodedDomainSettings,
|
|
passphrase: String,
|
|
|
|
error_msg: Option<String>,
|
|
retryable: bool,
|
|
}
|
|
|
|
let (error_msg, retryable) = match sync_result {
|
|
Ok(_) => (None, false),
|
|
|
|
Err(domain::manager::SyncWithSettingsError::NotModifiable) => {
|
|
(Some("This domain is not allowed to be configured.".to_string()), false)
|
|
}
|
|
|
|
Err(domain::manager::SyncWithSettingsError::InvalidURL) => (Some(
|
|
"Fetching the git repository failed; please double check that you input the correct
|
|
URL."
|
|
.to_string(),
|
|
), false),
|
|
|
|
Err(domain::manager::SyncWithSettingsError::Unavailable) => (Some(
|
|
"Fetching the git repository failed; the server is not available or is not corectly serving the repository."
|
|
.to_string(),
|
|
), false),
|
|
|
|
Err(domain::manager::SyncWithSettingsError::InvalidBranchName) => (Some(
|
|
"The git repository does not have a branch of the given name; please double check
|
|
that you input the correct name."
|
|
.to_string(),
|
|
), false),
|
|
|
|
Err(domain::manager::SyncWithSettingsError::AlreadyInProgress) => {
|
|
(Some("The configuration of your domain is still in progress.".to_string()), true)
|
|
}
|
|
|
|
Err(domain::manager::SyncWithSettingsError::ServiceDNSRecordsNotSet) => (Some(
|
|
"No routing record (A, AAAA, CNAME, etc...) was present on the nameserver."
|
|
.to_string(),
|
|
), true),
|
|
|
|
Err(domain::manager::SyncWithSettingsError::ChallengeTokenNotSet) => (Some(
|
|
"The challenge record (TXT) was not present on the nameserver."
|
|
.to_string(),
|
|
), true),
|
|
|
|
Err(domain::manager::SyncWithSettingsError::Unexpected(e)) => {
|
|
(Some(format!("An unexpected error occurred: {e}")), false)
|
|
}
|
|
};
|
|
|
|
self.render_page(
|
|
"/domain_sync.html",
|
|
Data {
|
|
domain: args.domain,
|
|
url_encoded_domain_settings: args.url_encoded_domain_settings,
|
|
passphrase: args.passphrase,
|
|
error_msg,
|
|
retryable,
|
|
},
|
|
)
|
|
}
|
|
|
|
fn domains(&self) -> Response {
|
|
#[derive(Serialize)]
|
|
struct Response {
|
|
domains: Vec<String>,
|
|
}
|
|
|
|
let domains = match self.domain_manager.all_domains() {
|
|
Ok(domains) => domains,
|
|
Err(e) => return self.internal_error(format!("failed get all domains: {e}").as_str()),
|
|
};
|
|
|
|
let mut domains: Vec<String> = domains
|
|
.into_iter()
|
|
.filter(|d| d.public)
|
|
.map(|d| d.domain.as_str().to_string())
|
|
.collect();
|
|
|
|
domains.sort();
|
|
|
|
self.render_page("/domains.html", Response { domains })
|
|
}
|
|
|
|
async fn serve_interface(&self, req: Request) -> Response {
|
|
let (req, body) = req.into_parts();
|
|
let path = req.uri.path();
|
|
|
|
if req.method == Method::GET && path.starts_with("/static/") {
|
|
return self.render(200, path, ());
|
|
}
|
|
|
|
let config_form_method = self.config.http.form_method.as_ref();
|
|
|
|
match (&req.method, path) {
|
|
(&Method::GET, "/") | (&Method::GET, "/index.html") => {
|
|
self.render_page("/index.html", ())
|
|
}
|
|
(form_method, "/domain.html") if form_method == config_form_method => {
|
|
self.with_query_req(&req, body, |args: DomainArgs| async { self.domain(args) })
|
|
.await
|
|
}
|
|
(form_method, "/domain_init.html") if form_method == config_form_method => {
|
|
self.with_query_req(&req, body, |args: DomainInitArgs| async {
|
|
self.domain_init(args)
|
|
})
|
|
.await
|
|
}
|
|
(form_method, "/domain_sync.html") if form_method == config_form_method => {
|
|
self.with_query_req(&req, body, |args: DomainSyncArgs| async {
|
|
self.domain_sync(args).await
|
|
})
|
|
.await
|
|
}
|
|
(&Method::GET, "/domains.html") => self.domains(),
|
|
_ => self.render_error_page(
|
|
404,
|
|
"This is not the page you're looking for. This page doesn't even exist!",
|
|
),
|
|
}
|
|
}
|
|
|
|
fn domain_from_req(req: &Request) -> Option<domain::Name> {
|
|
let host_header = req
|
|
.headers()
|
|
.get("Host")
|
|
.and_then(|v| v.to_str().ok())
|
|
.map(strip_port);
|
|
|
|
host_header
|
|
// if host_header isn't present, try the host from the URI
|
|
.or_else(|| req.uri().host().map(strip_port))
|
|
.and_then(|h| h.parse().ok())
|
|
}
|
|
|
|
async fn handle_request(
|
|
&self,
|
|
client_ip: net::IpAddr,
|
|
req: Request,
|
|
req_is_https: bool,
|
|
) -> Response {
|
|
let domain = match Self::domain_from_req(&req) {
|
|
Some(domain) => {
|
|
log::debug!("[{client_ip}] Serving request to {domain}{}", req.uri());
|
|
domain
|
|
}
|
|
None => {
|
|
log::debug!("[{client_ip}] Domain not found on request");
|
|
return self.render_error_page(400, "Cannot serve page without domain");
|
|
}
|
|
};
|
|
|
|
let method = req.method();
|
|
let path = req.uri().path();
|
|
|
|
// Serving acme challenges always takes priority. We serve them from the same store no
|
|
// matter the domain, presumably they are cryptographically random enough that it doesn't
|
|
// matter.
|
|
if method == Method::GET && path.starts_with("/.well-known/acme-challenge/") {
|
|
let token = path.trim_start_matches("/.well-known/acme-challenge/");
|
|
|
|
if let Ok(key) = self.domain_manager.get_acme_http01_challenge_key(token) {
|
|
return self.serve(200, "token.txt", bytes_body(key.into()));
|
|
}
|
|
}
|
|
|
|
// Serving domani challenges similarly takes priority.
|
|
if method == Method::GET && path == "/.well-known/domani-challenge" {
|
|
match self
|
|
.domain_manager
|
|
.get_domain_checker_challenge_token(&domain)
|
|
{
|
|
Ok(Some(token)) => return self.serve(200, "token.txt", bytes_body(token.into())),
|
|
Ok(None) => return self.render_error_page(404, "Token not found"),
|
|
Err(e) => {
|
|
return self.internal_error(
|
|
format!("failed to get token for domain {}: {e}", domain).as_str(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// We only allow HTTP requests when HTTPS is enabled in specific cases:
|
|
// - /.well-known urls
|
|
// - proxied domains with https_disabled set on them
|
|
// everything else must use https if possible.
|
|
let https_upgradable = self.https_enabled() && !req_is_https;
|
|
|
|
let settings = {
|
|
use domain::manager::{GetSettingsError, GetSettingsResult};
|
|
match self.domain_manager.get_settings(&domain) {
|
|
Ok(GetSettingsResult::Stored(settings)) => settings,
|
|
Ok(GetSettingsResult::Builtin(config)) => config.settings,
|
|
Ok(GetSettingsResult::Proxied(config)) => {
|
|
if config.http_url.is_none() {
|
|
return self.render_error_page(404, "Domain not found");
|
|
} else if https_upgradable && !config.https_disabled {
|
|
return self.https_redirect(domain, req);
|
|
}
|
|
|
|
let http_url = config.http_url.as_ref().unwrap();
|
|
|
|
return service::http::proxy::serve_http_request(
|
|
http_url.original_url.as_str(),
|
|
&config.http_request_headers.0,
|
|
client_ip,
|
|
req,
|
|
req_is_https,
|
|
)
|
|
.await
|
|
.map(|res| {
|
|
res.map(|body| {
|
|
use http_body_util::BodyExt;
|
|
body.map_err(io::Error::other).boxed_unsync()
|
|
})
|
|
})
|
|
.unwrap_or_else(|e| {
|
|
self.internal_error(
|
|
format!("serving {domain} via proxy {}: {e}", http_url.original_url)
|
|
.as_str(),
|
|
)
|
|
});
|
|
}
|
|
Ok(GetSettingsResult::Interface) => {
|
|
if https_upgradable {
|
|
return self.https_redirect(domain, req);
|
|
}
|
|
return self.serve_interface(req).await;
|
|
}
|
|
Ok(GetSettingsResult::External(_)) => {
|
|
return self.render_error_page(404, "Unknown domain name")
|
|
}
|
|
Err(GetSettingsError::NotFound) => {
|
|
return self.render_error_page(404, "Unknown domain name")
|
|
}
|
|
Err(GetSettingsError::Unexpected(e)) => {
|
|
return self.internal_error(
|
|
format!("failed to fetch settings for domain {domain}: {e}").as_str(),
|
|
)
|
|
}
|
|
}
|
|
};
|
|
|
|
self.serve_origin(settings, req).await
|
|
}
|
|
}
|
|
|
|
fn strip_port(host: &str) -> &str {
|
|
match host.rfind(':') {
|
|
None => host,
|
|
Some(i) => &host[..i],
|
|
}
|
|
}
|
|
|
|
struct HyperServiceImpl {
|
|
service: sync::Arc<Service>,
|
|
client_ip: net::IpAddr,
|
|
is_https: bool,
|
|
}
|
|
|
|
impl HyperServiceImpl {
|
|
pub fn new(service: sync::Arc<Service>, client_ip: net::IpAddr, is_https: bool) -> Self {
|
|
Self {
|
|
service,
|
|
client_ip,
|
|
is_https,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl hyper::service::Service<Request> for HyperServiceImpl {
|
|
type Response = Response;
|
|
type Error = std::io::Error;
|
|
type Future = crate::util::BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
|
|
|
fn call(&self, req: Request) -> Self::Future {
|
|
let service = self.service.clone();
|
|
let client_ip = self.client_ip;
|
|
let is_https = self.is_https;
|
|
Box::pin(async move { Ok(service.handle_request(client_ip, req, is_https).await) })
|
|
}
|
|
}
|
|
|