Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{HttpContext, HttpRequest, HttpSessionData};
|
||||
use common::{
|
||||
Server,
|
||||
expr::{functions::ResolveVariable, *},
|
||||
};
|
||||
use compact_str::{ToCompactString, format_compact};
|
||||
use hyper::StatusCode;
|
||||
use registry::schema::enums::ExpressionVariable;
|
||||
|
||||
impl<'x> HttpContext<'x> {
|
||||
pub fn new(session: &'x HttpSessionData, req: &'x HttpRequest) -> Self {
|
||||
Self { session, req }
|
||||
}
|
||||
|
||||
pub async fn has_endpoint_access(&self, server: &Server) -> StatusCode {
|
||||
server
|
||||
.eval_if(
|
||||
&server.core.network.http.allowed_endpoint,
|
||||
self,
|
||||
self.session.session_id,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(StatusCode::OK)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveVariable for HttpContext<'_> {
|
||||
fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_> {
|
||||
match variable {
|
||||
ExpressionVariable::RemoteIp => self.session.remote_ip.to_compact_string().into(),
|
||||
ExpressionVariable::RemotePort => self.session.remote_port.into(),
|
||||
ExpressionVariable::LocalIp => self.session.local_ip.to_compact_string().into(),
|
||||
ExpressionVariable::LocalPort => self.session.local_port.into(),
|
||||
ExpressionVariable::IsTls => self.session.is_tls.into(),
|
||||
ExpressionVariable::Protocol => {
|
||||
if self.session.is_tls { "https" } else { "http" }.into()
|
||||
}
|
||||
ExpressionVariable::Listener => self.session.instance.id.as_str().into(),
|
||||
ExpressionVariable::Url => self.req.uri().to_compact_string().into(),
|
||||
ExpressionVariable::Path => self.req.uri().path().into(),
|
||||
ExpressionVariable::Method => self.req.method().as_str().into(),
|
||||
ExpressionVariable::Headers => self
|
||||
.req
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(h, v)| {
|
||||
Variable::String(
|
||||
format_compact!("{}: {}", h.as_str(), v.to_str().unwrap_or_default())
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
_ => Variable::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_global(&self, _: &str) -> Variable<'_> {
|
||||
Variable::Integer(0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#![warn(clippy::large_futures)]
|
||||
|
||||
pub mod context;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub use form_urlencoded;
|
||||
|
||||
use common::network::ServerInstance;
|
||||
use hyper::StatusCode;
|
||||
use std::{net::IpAddr, sync::Arc};
|
||||
|
||||
pub type HttpRequest = hyper::Request<hyper::body::Incoming>;
|
||||
|
||||
pub struct JsonResponse<T: serde::Serialize> {
|
||||
status: StatusCode,
|
||||
inner: T,
|
||||
no_cache: bool,
|
||||
}
|
||||
|
||||
pub struct HtmlResponse {
|
||||
status: StatusCode,
|
||||
body: String,
|
||||
}
|
||||
|
||||
pub enum HttpResponseBody {
|
||||
Text(String),
|
||||
Binary(Vec<u8>),
|
||||
Stream(http_body_util::combinators::BoxBody<hyper::body::Bytes, hyper::Error>),
|
||||
WebsocketUpgrade(String),
|
||||
Empty,
|
||||
}
|
||||
|
||||
pub struct HttpResponse {
|
||||
status: StatusCode,
|
||||
builder: hyper::http::response::Builder,
|
||||
body: HttpResponseBody,
|
||||
}
|
||||
|
||||
pub struct HttpContext<'x> {
|
||||
pub session: &'x HttpSessionData,
|
||||
pub req: &'x HttpRequest,
|
||||
}
|
||||
|
||||
pub struct HttpSessionData {
|
||||
pub instance: Arc<ServerInstance>,
|
||||
pub local_ip: IpAddr,
|
||||
pub local_port: u16,
|
||||
pub remote_ip: IpAddr,
|
||||
pub remote_port: u16,
|
||||
pub is_tls: bool,
|
||||
pub session_id: u64,
|
||||
}
|
||||
|
||||
pub struct DownloadResponse {
|
||||
pub filename: String,
|
||||
pub content_type: String,
|
||||
pub blob: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct JsonProblemResponse(pub StatusCode);
|
||||
|
||||
impl<T: serde::Serialize> JsonResponse<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
JsonResponse {
|
||||
inner,
|
||||
status: StatusCode::OK,
|
||||
no_cache: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_status(status: StatusCode, inner: T) -> Self {
|
||||
JsonResponse {
|
||||
inner,
|
||||
status,
|
||||
no_cache: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_cache(mut self) -> Self {
|
||||
self.no_cache = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl HtmlResponse {
|
||||
pub fn new(body: String) -> Self {
|
||||
HtmlResponse {
|
||||
body,
|
||||
status: StatusCode::OK,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_status(status: StatusCode, body: String) -> Self {
|
||||
HtmlResponse { body, status }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToHttpResponse {
|
||||
fn into_http_response(self) -> HttpResponse;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use compact_str::ToCompactString;
|
||||
use http_body_util::BodyExt;
|
||||
|
||||
use crate::HttpRequest;
|
||||
|
||||
#[inline]
|
||||
pub fn decode_path_element(item: &str) -> Cow<'_, str> {
|
||||
percent_encoding::percent_decode_str(item)
|
||||
.decode_utf8()
|
||||
.unwrap_or_else(|_| item.into())
|
||||
}
|
||||
|
||||
pub async fn fetch_body(
|
||||
req: &mut HttpRequest,
|
||||
max_size: usize,
|
||||
session_id: u64,
|
||||
) -> Option<Vec<u8>> {
|
||||
let mut bytes = Vec::with_capacity(1024);
|
||||
while let Some(Ok(frame)) = req.frame().await {
|
||||
if let Some(data) = frame.data_ref() {
|
||||
if bytes.len() + data.len() <= max_size || max_size == 0 {
|
||||
bytes.extend_from_slice(data);
|
||||
} else {
|
||||
trc::event!(
|
||||
Http(trc::HttpEvent::RequestBody),
|
||||
SpanId = session_id,
|
||||
Details = req
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| trc::Value::Array(vec![
|
||||
k.as_str().to_compact_string().into(),
|
||||
v.to_str().unwrap_or_default().to_compact_string().into()
|
||||
]))
|
||||
.collect::<Vec<_>>(),
|
||||
Contents = std::str::from_utf8(&bytes)
|
||||
.unwrap_or("[binary data]")
|
||||
.to_string(),
|
||||
Size = bytes.len(),
|
||||
Limit = max_size,
|
||||
);
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Http(trc::HttpEvent::RequestBody),
|
||||
SpanId = session_id,
|
||||
Details = req
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| trc::Value::Array(vec![
|
||||
k.as_str().to_compact_string().into(),
|
||||
v.to_str().unwrap_or_default().to_compact_string().into()
|
||||
]))
|
||||
.collect::<Vec<_>>(),
|
||||
Contents = std::str::from_utf8(&bytes)
|
||||
.unwrap_or("[binary data]")
|
||||
.to_string(),
|
||||
Size = bytes.len(),
|
||||
);
|
||||
|
||||
bytes.into()
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::manager::application::Resource;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::{
|
||||
StatusCode,
|
||||
body::Bytes,
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
DownloadResponse, HtmlResponse, HttpResponse, HttpResponseBody, JsonProblemResponse,
|
||||
JsonResponse, ToHttpResponse,
|
||||
};
|
||||
|
||||
impl HttpResponse {
|
||||
pub fn new(status: StatusCode) -> Self {
|
||||
HttpResponse {
|
||||
status,
|
||||
builder: hyper::Response::builder().status(status),
|
||||
body: HttpResponseBody::Empty,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redirect(location: String) -> Self {
|
||||
let mut response = HttpResponse::new(StatusCode::FOUND);
|
||||
response.builder = response
|
||||
.builder
|
||||
.status(StatusCode::FOUND)
|
||||
.header(header::LOCATION, location);
|
||||
response
|
||||
}
|
||||
|
||||
pub fn with_content_type<V>(mut self, content_type: V) -> Self
|
||||
where
|
||||
V: TryInto<HeaderValue>,
|
||||
<V as TryInto<HeaderValue>>::Error: Into<hyper::http::Error>,
|
||||
{
|
||||
self.builder = self.builder.header(header::CONTENT_TYPE, content_type);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_status_code(mut self, status: StatusCode) -> Self {
|
||||
self.status = status;
|
||||
self.builder = self.builder.status(status);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_content_length(mut self, content_length: usize) -> Self {
|
||||
self.builder = self.builder.header(header::CONTENT_LENGTH, content_length);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_content_range(mut self, content_range: String) -> Self {
|
||||
self.builder = self.builder.header(header::CONTENT_RANGE, content_range);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_accept_ranges(mut self) -> Self {
|
||||
self.builder = self.builder.header(header::ACCEPT_RANGES, "bytes");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_etag(mut self, etag: String) -> Self {
|
||||
self.builder = self.builder.header(header::ETAG, etag);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_etag_opt(self, etag: Option<String>) -> Self {
|
||||
if let Some(etag) = etag {
|
||||
self.with_etag(etag)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_schedule_tag_opt(mut self, tag: Option<u32>) -> Self {
|
||||
if let Some(tag) = tag {
|
||||
self.builder = self.builder.header("Schedule-Tag", format!("\"{tag}\""));
|
||||
self
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_last_modified(mut self, last_modified: String) -> Self {
|
||||
self.builder = self.builder.header(header::LAST_MODIFIED, last_modified);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_lock_token(mut self, token_uri: &str) -> Self {
|
||||
self.builder = self.builder.header("Lock-Token", format!("<{token_uri}>"));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_header<K, V>(mut self, name: K, value: V) -> Self
|
||||
where
|
||||
K: TryInto<HeaderName>,
|
||||
<K as TryInto<HeaderName>>::Error: Into<hyper::http::Error>,
|
||||
V: TryInto<HeaderValue>,
|
||||
<V as TryInto<HeaderValue>>::Error: Into<hyper::http::Error>,
|
||||
{
|
||||
self.builder = self.builder.header(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_xml_body(self, body: impl Into<String>) -> Self {
|
||||
self.with_text_body(body)
|
||||
.with_content_type("application/xml; charset=utf-8")
|
||||
}
|
||||
|
||||
pub fn with_text_body(mut self, body: impl Into<String>) -> Self {
|
||||
let body = body.into();
|
||||
let body_len = body.len();
|
||||
self.body = HttpResponseBody::Text(body);
|
||||
self.with_content_length(body_len)
|
||||
}
|
||||
|
||||
pub fn with_binary_body(mut self, body: impl Into<Vec<u8>>) -> Self {
|
||||
let body = body.into();
|
||||
let body_len = body.len();
|
||||
self.body = HttpResponseBody::Binary(body);
|
||||
self.with_content_length(body_len)
|
||||
}
|
||||
|
||||
pub fn with_stream_body(
|
||||
mut self,
|
||||
stream: http_body_util::combinators::BoxBody<hyper::body::Bytes, hyper::Error>,
|
||||
) -> Self {
|
||||
self.body = HttpResponseBody::Stream(stream);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_websocket_upgrade(mut self, derived_key: String) -> Self {
|
||||
self.body = HttpResponseBody::WebsocketUpgrade(derived_key);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_content_disposition<V>(mut self, content_disposition: V) -> Self
|
||||
where
|
||||
V: TryInto<HeaderValue>,
|
||||
<V as TryInto<HeaderValue>>::Error: Into<hyper::http::Error>,
|
||||
{
|
||||
self.builder = self
|
||||
.builder
|
||||
.header(header::CONTENT_DISPOSITION, content_disposition);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cache_control<V>(mut self, cache_control: V) -> Self
|
||||
where
|
||||
V: TryInto<HeaderValue>,
|
||||
<V as TryInto<HeaderValue>>::Error: Into<hyper::http::Error>,
|
||||
{
|
||||
self.builder = self.builder.header(header::CACHE_CONTROL, cache_control);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_no_store(mut self) -> Self {
|
||||
self.builder = self
|
||||
.builder
|
||||
.header(header::CACHE_CONTROL, "no-store, no-cache, must-revalidate");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_no_cache(mut self) -> Self {
|
||||
self.builder = self.builder.header(header::CACHE_CONTROL, "no-cache");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_immutable_cache(mut self) -> Self {
|
||||
self.builder = self
|
||||
.builder
|
||||
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_location<V>(mut self, location: V) -> Self
|
||||
where
|
||||
V: TryInto<HeaderValue>,
|
||||
<V as TryInto<HeaderValue>>::Error: Into<hyper::http::Error>,
|
||||
{
|
||||
self.builder = self.builder.header(header::LOCATION, location);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cors_unrestricted(mut self) -> Self {
|
||||
self.builder = self
|
||||
.builder
|
||||
.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.header(
|
||||
header::ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
"Authorization, Content-Type, Accept, X-Requested-With",
|
||||
)
|
||||
.header(
|
||||
header::ACCESS_CONTROL_ALLOW_METHODS,
|
||||
"POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONS",
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
match &self.body {
|
||||
HttpResponseBody::Text(value) => value.len(),
|
||||
HttpResponseBody::Binary(value) => value.len(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(
|
||||
self,
|
||||
) -> hyper::Response<http_body_util::combinators::BoxBody<hyper::body::Bytes, hyper::Error>>
|
||||
{
|
||||
match self.body {
|
||||
HttpResponseBody::Text(body) => self.builder.body(
|
||||
Full::new(Bytes::from(body))
|
||||
.map_err(|never| match never {})
|
||||
.boxed(),
|
||||
),
|
||||
HttpResponseBody::Binary(body) => self.builder.body(
|
||||
Full::new(Bytes::from(body))
|
||||
.map_err(|never| match never {})
|
||||
.boxed(),
|
||||
),
|
||||
HttpResponseBody::Empty => {
|
||||
let has_content_length = self
|
||||
.builder
|
||||
.headers_ref()
|
||||
.is_some_and(|headers| headers.contains_key(header::CONTENT_LENGTH));
|
||||
let builder = if has_content_length {
|
||||
self.builder
|
||||
} else {
|
||||
self.builder.header(header::CONTENT_LENGTH, 0)
|
||||
};
|
||||
|
||||
builder.body(
|
||||
Full::new(Bytes::new())
|
||||
.map_err(|never| match never {})
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
HttpResponseBody::Stream(stream) => self.builder.body(stream),
|
||||
HttpResponseBody::WebsocketUpgrade(derived_key) => self
|
||||
.builder
|
||||
.header(header::CONNECTION, "upgrade")
|
||||
.header(header::UPGRADE, "websocket")
|
||||
.header("Sec-WebSocket-Accept", &derived_key)
|
||||
.header("Sec-WebSocket-Protocol", "jmap")
|
||||
.body(
|
||||
Full::new(Bytes::from("Switching to WebSocket protocol"))
|
||||
.map_err(|never| match never {})
|
||||
.boxed(),
|
||||
),
|
||||
}
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn body(&self) -> &HttpResponseBody {
|
||||
&self.body
|
||||
}
|
||||
|
||||
pub fn status(&self) -> StatusCode {
|
||||
self.status
|
||||
}
|
||||
|
||||
pub fn headers(&self) -> Option<&hyper::HeaderMap<HeaderValue>> {
|
||||
self.builder.headers_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: serde::Serialize> ToHttpResponse for JsonResponse<T> {
|
||||
fn into_http_response(self) -> HttpResponse {
|
||||
let response = HttpResponse::new(self.status)
|
||||
.with_content_type("application/json; charset=utf-8")
|
||||
.with_text_body(serde_json::to_string(&self.inner).unwrap_or_default());
|
||||
|
||||
if self.no_cache {
|
||||
response.with_no_store()
|
||||
} else {
|
||||
response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHttpResponse for DownloadResponse {
|
||||
fn into_http_response(self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type(self.content_type)
|
||||
.with_content_disposition(format!(
|
||||
"attachment; filename=\"{}\"",
|
||||
self.filename.replace('\"', "\\\"")
|
||||
))
|
||||
.with_cache_control("private, immutable, max-age=31536000")
|
||||
.with_binary_body(self.blob)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHttpResponse for Resource<Vec<u8>> {
|
||||
fn into_http_response(self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type(self.content_type.as_ref())
|
||||
.with_binary_body(self.contents)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHttpResponse for HtmlResponse {
|
||||
fn into_http_response(self) -> HttpResponse {
|
||||
HttpResponse::new(self.status)
|
||||
.with_content_type("text/html; charset=utf-8")
|
||||
.with_text_body(self.body)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHttpResponse for JsonProblemResponse {
|
||||
fn into_http_response(self) -> HttpResponse {
|
||||
HttpResponse::new(self.0)
|
||||
.with_content_type("application/problem+json")
|
||||
.with_text_body(
|
||||
serde_json::to_string(&json!(
|
||||
{
|
||||
"type": "about:blank",
|
||||
"title": self.0.canonical_reason().unwrap_or_default(),
|
||||
"status": self.0.as_u16(),
|
||||
"detail": self.0.canonical_reason().unwrap_or_default(),
|
||||
}
|
||||
))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user