mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-29 05:45:54 +08:00
DAV skeleton
This commit is contained in:
parent
b0a486106e
commit
1c460c7f3b
15 changed files with 515 additions and 298 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
|
@ -1669,6 +1669,25 @@ checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dav"
|
name = "dav"
|
||||||
version = "0.11.5"
|
version = "0.11.5"
|
||||||
|
dependencies = [
|
||||||
|
"common",
|
||||||
|
"dav-proto",
|
||||||
|
"groupware",
|
||||||
|
"hashify",
|
||||||
|
"http_proto",
|
||||||
|
"hyper 1.6.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dav-proto"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"calcard",
|
||||||
|
"hashify",
|
||||||
|
"hyper 1.6.0",
|
||||||
|
"mail-parser",
|
||||||
|
"quick-xml 0.37.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dbl"
|
name = "dbl"
|
||||||
|
|
@ -2988,6 +3007,7 @@ dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"common",
|
"common",
|
||||||
|
"dav",
|
||||||
"directory",
|
"directory",
|
||||||
"email",
|
"email",
|
||||||
"form-data",
|
"form-data",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@ edition = "2024"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
dav-proto = { path = "/Users/me/code/dav-proto" }
|
||||||
|
common = { path = "../common" }
|
||||||
|
groupware = { path = "../groupware" }
|
||||||
|
http_proto = { path = "../http-proto" }
|
||||||
|
hashify = { version = "0.2" }
|
||||||
|
hyper = { version = "1.0.1", features = ["server", "http1", "http2"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,90 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
pub mod request;
|
||||||
|
|
||||||
|
use http_proto::HttpResponse;
|
||||||
|
use hyper::{Method, StatusCode};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum DavResource {
|
||||||
|
Card,
|
||||||
|
Cal,
|
||||||
|
File,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum DavMethod {
|
||||||
|
GET,
|
||||||
|
PUT,
|
||||||
|
POST,
|
||||||
|
DELETE,
|
||||||
|
PATCH,
|
||||||
|
PROPFIND,
|
||||||
|
PROPPATCH,
|
||||||
|
REPORT,
|
||||||
|
MKCOL,
|
||||||
|
COPY,
|
||||||
|
MOVE,
|
||||||
|
LOCK,
|
||||||
|
UNLOCK,
|
||||||
|
OPTIONS,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DavResource {
|
||||||
|
pub fn parse(service: &str) -> Option<Self> {
|
||||||
|
hashify::tiny_map!(service.as_bytes(),
|
||||||
|
"card" => DavResource::Card,
|
||||||
|
"cal" => DavResource::Cal,
|
||||||
|
"file" => DavResource::File
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_options_response(self) -> HttpResponse {
|
||||||
|
let todo = "true";
|
||||||
|
HttpResponse::new(StatusCode::OK)
|
||||||
|
.with_header("DAV", "1, 2, 3, access-control, calendar-access")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DavMethod {
|
||||||
|
pub fn parse(method: &Method) -> Option<Self> {
|
||||||
|
match *method {
|
||||||
|
Method::GET => Some(DavMethod::GET),
|
||||||
|
Method::PUT => Some(DavMethod::PUT),
|
||||||
|
Method::DELETE => Some(DavMethod::DELETE),
|
||||||
|
Method::OPTIONS => Some(DavMethod::OPTIONS),
|
||||||
|
Method::POST => Some(DavMethod::POST),
|
||||||
|
Method::PATCH => Some(DavMethod::PATCH),
|
||||||
|
_ => {
|
||||||
|
hashify::tiny_map!(method.as_str().as_bytes(),
|
||||||
|
"PROPFIND" => DavMethod::PROPFIND,
|
||||||
|
"PROPPATCH" => DavMethod::PROPPATCH,
|
||||||
|
"REPORT" => DavMethod::REPORT,
|
||||||
|
"MKCOL" => DavMethod::MKCOL,
|
||||||
|
"COPY" => DavMethod::COPY,
|
||||||
|
"MOVE" => DavMethod::MOVE,
|
||||||
|
"LOCK" => DavMethod::LOCK,
|
||||||
|
"UNLOCK" => DavMethod::UNLOCK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn has_body(self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
DavMethod::PUT
|
||||||
|
| DavMethod::POST
|
||||||
|
| DavMethod::PATCH
|
||||||
|
| DavMethod::PROPPATCH
|
||||||
|
| DavMethod::PROPFIND
|
||||||
|
| DavMethod::REPORT
|
||||||
|
| DavMethod::MKCOL
|
||||||
|
| DavMethod::COPY
|
||||||
|
| DavMethod::MOVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
38
crates/dav/src/request.rs
Normal file
38
crates/dav/src/request.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use common::{Server, auth::AccessToken};
|
||||||
|
use http_proto::{HttpRequest, HttpResponse, HttpSessionData};
|
||||||
|
|
||||||
|
use crate::{DavMethod, DavResource};
|
||||||
|
|
||||||
|
pub trait DavRequestHandler: Sync + Send {
|
||||||
|
fn handle_dav_request(
|
||||||
|
&self,
|
||||||
|
request: HttpRequest,
|
||||||
|
access_token: Arc<AccessToken>,
|
||||||
|
session: &HttpSessionData,
|
||||||
|
resource: DavResource,
|
||||||
|
method: DavMethod,
|
||||||
|
body: Vec<u8>,
|
||||||
|
) -> impl Future<Output = HttpResponse> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DavRequestHandler for Server {
|
||||||
|
async fn handle_dav_request(
|
||||||
|
&self,
|
||||||
|
request: HttpRequest,
|
||||||
|
access_token: Arc<AccessToken>,
|
||||||
|
session: &HttpSessionData,
|
||||||
|
resource: DavResource,
|
||||||
|
method: DavMethod,
|
||||||
|
body: Vec<u8>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ pub mod response;
|
||||||
|
|
||||||
pub use form_urlencoded;
|
pub use form_urlencoded;
|
||||||
|
|
||||||
use std::{borrow::Cow, net::IpAddr, sync::Arc};
|
use std::{net::IpAddr, sync::Arc};
|
||||||
|
|
||||||
use common::listener::ServerInstance;
|
use common::listener::ServerInstance;
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
|
|
@ -37,11 +37,9 @@ pub enum HttpResponseBody {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HttpResponse {
|
pub struct HttpResponse {
|
||||||
pub status: StatusCode,
|
status: StatusCode,
|
||||||
pub content_type: Cow<'static, str>,
|
builder: hyper::http::response::Builder,
|
||||||
pub content_disposition: Cow<'static, str>,
|
body: HttpResponseBody,
|
||||||
pub cache_control: Cow<'static, str>,
|
|
||||||
pub body: HttpResponseBody,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HttpContext<'x> {
|
pub struct HttpContext<'x> {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use common::manager::webadmin::Resource;
|
use common::manager::webadmin::Resource;
|
||||||
use http_body_util::{BodyExt, Full};
|
use http_body_util::{BodyExt, Full};
|
||||||
use hyper::{StatusCode, body::Bytes, header};
|
use hyper::{
|
||||||
|
StatusCode,
|
||||||
|
body::Bytes,
|
||||||
|
header::{self, HeaderName, HeaderValue},
|
||||||
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -17,42 +19,91 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
impl HttpResponse {
|
impl HttpResponse {
|
||||||
pub fn new_empty(status: StatusCode) -> Self {
|
pub fn new(status: StatusCode) -> Self {
|
||||||
HttpResponse {
|
HttpResponse {
|
||||||
status,
|
status,
|
||||||
content_type: "".into(),
|
builder: hyper::Response::builder().status(status),
|
||||||
content_disposition: "".into(),
|
|
||||||
cache_control: "".into(),
|
|
||||||
body: HttpResponseBody::Empty,
|
body: HttpResponseBody::Empty,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_text(
|
pub fn with_content_type<V>(mut self, content_type: V) -> Self
|
||||||
status: StatusCode,
|
where
|
||||||
content_type: impl Into<Cow<'static, str>>,
|
V: TryInto<HeaderValue>,
|
||||||
body: impl Into<String>,
|
<V as TryInto<HeaderValue>>::Error: Into<hyper::http::Error>,
|
||||||
) -> Self {
|
{
|
||||||
HttpResponse {
|
self.builder = self.builder.header(header::CONTENT_TYPE, content_type);
|
||||||
status,
|
self
|
||||||
content_type: content_type.into(),
|
|
||||||
content_disposition: "".into(),
|
|
||||||
cache_control: "".into(),
|
|
||||||
body: HttpResponseBody::Text(body.into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_binary(
|
pub fn with_header<K, V>(mut self, name: K, value: V) -> Self
|
||||||
status: StatusCode,
|
where
|
||||||
content_type: impl Into<Cow<'static, str>>,
|
K: TryInto<HeaderName>,
|
||||||
body: impl Into<Vec<u8>>,
|
<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_text_body(mut self, body: impl Into<String>) -> Self {
|
||||||
|
self.body = HttpResponseBody::Text(body.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_binary_body(mut self, body: impl Into<Vec<u8>>) -> Self {
|
||||||
|
self.body = HttpResponseBody::Binary(body.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_stream_body(
|
||||||
|
mut self,
|
||||||
|
stream: http_body_util::combinators::BoxBody<hyper::body::Bytes, hyper::Error>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
HttpResponse {
|
self.body = HttpResponseBody::Stream(stream);
|
||||||
status,
|
self
|
||||||
content_type: content_type.into(),
|
}
|
||||||
content_disposition: "".into(),
|
|
||||||
cache_control: "".into(),
|
pub fn with_websocket_upgrade(mut self, derived_key: String) -> Self {
|
||||||
body: HttpResponseBody::Binary(body.into()),
|
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_cache(mut self) -> Self {
|
||||||
|
self.builder = self
|
||||||
|
.builder
|
||||||
|
.header(header::CACHE_CONTROL, "no-store, no-cache, must-revalidate");
|
||||||
|
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 size(&self) -> usize {
|
pub fn size(&self) -> usize {
|
||||||
|
|
@ -67,46 +118,25 @@ impl HttpResponse {
|
||||||
self,
|
self,
|
||||||
) -> hyper::Response<http_body_util::combinators::BoxBody<hyper::body::Bytes, hyper::Error>>
|
) -> hyper::Response<http_body_util::combinators::BoxBody<hyper::body::Bytes, hyper::Error>>
|
||||||
{
|
{
|
||||||
let builder = hyper::Response::builder().status(self.status);
|
|
||||||
|
|
||||||
match self.body {
|
match self.body {
|
||||||
HttpResponseBody::Text(body) => builder
|
HttpResponseBody::Text(body) => self.builder.body(
|
||||||
.header(header::CONTENT_TYPE, self.content_type.as_ref())
|
Full::new(Bytes::from(body))
|
||||||
.body(
|
.map_err(|never| match never {})
|
||||||
Full::new(Bytes::from(body))
|
.boxed(),
|
||||||
.map_err(|never| match never {})
|
),
|
||||||
.boxed(),
|
HttpResponseBody::Binary(body) => self.builder.body(
|
||||||
),
|
Full::new(Bytes::from(body))
|
||||||
HttpResponseBody::Binary(body) => {
|
.map_err(|never| match never {})
|
||||||
let mut builder = builder.header(header::CONTENT_TYPE, self.content_type.as_ref());
|
.boxed(),
|
||||||
|
),
|
||||||
if !self.content_disposition.is_empty() {
|
HttpResponseBody::Empty => self.builder.body(
|
||||||
builder = builder.header(
|
|
||||||
header::CONTENT_DISPOSITION,
|
|
||||||
self.content_disposition.as_ref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.cache_control.is_empty() {
|
|
||||||
builder = builder.header(header::CACHE_CONTROL, self.cache_control.as_ref());
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.body(
|
|
||||||
Full::new(Bytes::from(body))
|
|
||||||
.map_err(|never| match never {})
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
HttpResponseBody::Empty => builder.body(
|
|
||||||
Full::new(Bytes::new())
|
Full::new(Bytes::new())
|
||||||
.map_err(|never| match never {})
|
.map_err(|never| match never {})
|
||||||
.boxed(),
|
.boxed(),
|
||||||
),
|
),
|
||||||
HttpResponseBody::Stream(stream) => builder
|
HttpResponseBody::Stream(stream) => self.builder.body(stream),
|
||||||
.header(header::CONTENT_TYPE, self.content_type.as_ref())
|
HttpResponseBody::WebsocketUpgrade(derived_key) => self
|
||||||
.header(header::CACHE_CONTROL, self.cache_control.as_ref())
|
.builder
|
||||||
.body(stream),
|
|
||||||
HttpResponseBody::WebsocketUpgrade(derived_key) => builder
|
|
||||||
.header(header::CONNECTION, "upgrade")
|
.header(header::CONNECTION, "upgrade")
|
||||||
.header(header::UPGRADE, "websocket")
|
.header(header::UPGRADE, "websocket")
|
||||||
.header("Sec-WebSocket-Accept", &derived_key)
|
.header("Sec-WebSocket-Accept", &derived_key)
|
||||||
|
|
@ -119,67 +149,73 @@ impl HttpResponse {
|
||||||
}
|
}
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn body(&self) -> &HttpResponseBody {
|
||||||
|
&self.body
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status(&self) -> StatusCode {
|
||||||
|
self.status
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: serde::Serialize> ToHttpResponse for JsonResponse<T> {
|
impl<T: serde::Serialize> ToHttpResponse for JsonResponse<T> {
|
||||||
fn into_http_response(self) -> HttpResponse {
|
fn into_http_response(self) -> HttpResponse {
|
||||||
HttpResponse {
|
let response = HttpResponse::new(self.status)
|
||||||
status: self.status,
|
.with_content_type("application/json; charset=utf-8")
|
||||||
content_type: "application/json; charset=utf-8".into(),
|
.with_text_body(serde_json::to_string(&self.inner).unwrap_or_default());
|
||||||
content_disposition: "".into(),
|
|
||||||
cache_control: if !self.no_cache {
|
if self.no_cache {
|
||||||
""
|
response.with_no_cache()
|
||||||
} else {
|
} else {
|
||||||
"no-store, no-cache, must-revalidate"
|
response
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
body: HttpResponseBody::Text(serde_json::to_string(&self.inner).unwrap_or_default()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToHttpResponse for DownloadResponse {
|
impl ToHttpResponse for DownloadResponse {
|
||||||
fn into_http_response(self) -> HttpResponse {
|
fn into_http_response(self) -> HttpResponse {
|
||||||
HttpResponse {
|
HttpResponse::new(StatusCode::OK)
|
||||||
status: StatusCode::OK,
|
.with_content_type(self.content_type)
|
||||||
content_type: self.content_type.into(),
|
.with_content_disposition(format!(
|
||||||
content_disposition: format!(
|
|
||||||
"attachment; filename=\"{}\"",
|
"attachment; filename=\"{}\"",
|
||||||
self.filename.replace('\"', "\\\"")
|
self.filename.replace('\"', "\\\"")
|
||||||
)
|
))
|
||||||
.into(),
|
.with_cache_control("private, immutable, max-age=31536000")
|
||||||
cache_control: "private, immutable, max-age=31536000".into(),
|
.with_binary_body(self.blob)
|
||||||
body: HttpResponseBody::Binary(self.blob),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToHttpResponse for Resource<Vec<u8>> {
|
impl ToHttpResponse for Resource<Vec<u8>> {
|
||||||
fn into_http_response(self) -> HttpResponse {
|
fn into_http_response(self) -> HttpResponse {
|
||||||
HttpResponse::new_binary(StatusCode::OK, self.content_type, self.contents)
|
HttpResponse::new(StatusCode::OK)
|
||||||
|
.with_content_type(self.content_type.as_ref())
|
||||||
|
.with_binary_body(self.contents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToHttpResponse for HtmlResponse {
|
impl ToHttpResponse for HtmlResponse {
|
||||||
fn into_http_response(self) -> HttpResponse {
|
fn into_http_response(self) -> HttpResponse {
|
||||||
HttpResponse::new_text(self.status, "text/html; charset=utf-8", self.body)
|
HttpResponse::new(self.status)
|
||||||
|
.with_content_type("text/html; charset=utf-8")
|
||||||
|
.with_text_body(self.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToHttpResponse for JsonProblemResponse {
|
impl ToHttpResponse for JsonProblemResponse {
|
||||||
fn into_http_response(self) -> HttpResponse {
|
fn into_http_response(self) -> HttpResponse {
|
||||||
HttpResponse::new_text(
|
HttpResponse::new(self.0)
|
||||||
self.0,
|
.with_content_type("application/problem+json")
|
||||||
"application/problem+json",
|
.with_text_body(
|
||||||
serde_json::to_string(&json!(
|
serde_json::to_string(&json!(
|
||||||
{
|
{
|
||||||
"type": "about:blank",
|
"type": "about:blank",
|
||||||
"title": self.0.canonical_reason().unwrap_or_default(),
|
"title": self.0.canonical_reason().unwrap_or_default(),
|
||||||
"status": self.0.as_u16(),
|
"status": self.0.as_u16(),
|
||||||
"detail": self.0.canonical_reason().unwrap_or_default(),
|
"detail": self.0.canonical_reason().unwrap_or_default(),
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ trc = { path = "../trc" }
|
||||||
email = { path = "../email" }
|
email = { path = "../email" }
|
||||||
smtp = { path = "../smtp" }
|
smtp = { path = "../smtp" }
|
||||||
jmap = { path = "../jmap" }
|
jmap = { path = "../jmap" }
|
||||||
|
dav = { path = "../dav" }
|
||||||
spam-filter = { path = "../spam-filter" }
|
spam-filter = { path = "../spam-filter" }
|
||||||
http_proto = { path = "../http-proto" }
|
http_proto = { path = "../http-proto" }
|
||||||
jmap_proto = { path = "../jmap-proto" }
|
jmap_proto = { path = "../jmap-proto" }
|
||||||
|
|
|
||||||
|
|
@ -203,107 +203,104 @@ impl TelemetryApi for Server {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
let mut active_span_ids = AHashSet::new();
|
let mut active_span_ids = AHashSet::new();
|
||||||
|
|
||||||
Ok(HttpResponse {
|
Ok(HttpResponse::new(StatusCode::OK)
|
||||||
status: StatusCode::OK,
|
.with_content_type("text/event-stream")
|
||||||
content_type: "text/event-stream".into(),
|
.with_cache_control("no-store")
|
||||||
content_disposition: "".into(),
|
.with_stream_body(BoxBody::new(StreamBody::new(
|
||||||
cache_control: "no-store".into(),
|
async_stream::stream! {
|
||||||
body: HttpResponseBody::Stream(BoxBody::new(StreamBody::new(
|
let mut last_message = Instant::now() - throttle;
|
||||||
async_stream::stream! {
|
let mut timeout = ping_interval;
|
||||||
let mut last_message = Instant::now() - throttle;
|
|
||||||
let mut timeout = ping_interval;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match tokio::time::timeout(timeout, rx.recv()).await {
|
match tokio::time::timeout(timeout, rx.recv()).await {
|
||||||
Ok(Some(event_batch)) => {
|
Ok(Some(event_batch)) => {
|
||||||
for event in event_batch {
|
for event in event_batch {
|
||||||
if (filter.is_none() && key_filters.is_empty())
|
if (filter.is_none() && key_filters.is_empty())
|
||||||
|| event
|
|| event
|
||||||
.span_id()
|
.span_id()
|
||||||
.is_some_and(|span_id| active_span_ids.contains(&span_id))
|
.is_some_and(|span_id| active_span_ids.contains(&span_id))
|
||||||
|
{
|
||||||
|
events.push(event);
|
||||||
|
} else {
|
||||||
|
let mut matched_keys = AHashSet::new();
|
||||||
|
for (key, value) in event
|
||||||
|
.keys
|
||||||
|
.iter()
|
||||||
|
.chain(event.inner.span.as_ref().map_or(([]).iter(), |s| s.keys.iter()))
|
||||||
{
|
{
|
||||||
events.push(event);
|
if let Some(needle) = key_filters.get(key).or(filter.as_ref()) {
|
||||||
} else {
|
let matches = match value {
|
||||||
let mut matched_keys = AHashSet::new();
|
Value::Static(haystack) => haystack.contains(needle),
|
||||||
for (key, value) in event
|
Value::String(haystack) => haystack.contains(needle),
|
||||||
.keys
|
Value::Timestamp(haystack) => {
|
||||||
.iter()
|
DateTime::from_timestamp(*haystack as i64)
|
||||||
.chain(event.inner.span.as_ref().map_or(([]).iter(), |s| s.keys.iter()))
|
.to_rfc3339()
|
||||||
{
|
.contains(needle)
|
||||||
if let Some(needle) = key_filters.get(key).or(filter.as_ref()) {
|
}
|
||||||
let matches = match value {
|
Value::Bool(true) => needle == "true",
|
||||||
Value::Static(haystack) => haystack.contains(needle),
|
Value::Bool(false) => needle == "false",
|
||||||
Value::String(haystack) => haystack.contains(needle),
|
Value::Ipv4(haystack) => haystack.to_string().contains(needle),
|
||||||
Value::Timestamp(haystack) => {
|
Value::Ipv6(haystack) => haystack.to_string().contains(needle),
|
||||||
DateTime::from_timestamp(*haystack as i64)
|
Value::Event(_) |
|
||||||
.to_rfc3339()
|
Value::Array(_) |
|
||||||
.contains(needle)
|
Value::UInt(_) |
|
||||||
}
|
Value::Int(_) |
|
||||||
Value::Bool(true) => needle == "true",
|
Value::Float(_) |
|
||||||
Value::Bool(false) => needle == "false",
|
Value::Duration(_) |
|
||||||
Value::Ipv4(haystack) => haystack.to_string().contains(needle),
|
Value::Bytes(_) |
|
||||||
Value::Ipv6(haystack) => haystack.to_string().contains(needle),
|
Value::None => false,
|
||||||
Value::Event(_) |
|
};
|
||||||
Value::Array(_) |
|
|
||||||
Value::UInt(_) |
|
|
||||||
Value::Int(_) |
|
|
||||||
Value::Float(_) |
|
|
||||||
Value::Duration(_) |
|
|
||||||
Value::Bytes(_) |
|
|
||||||
Value::None => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if matches {
|
if matches {
|
||||||
matched_keys.insert(*key);
|
matched_keys.insert(*key);
|
||||||
if filter.is_some() || matched_keys.len() == key_filters.len() {
|
if filter.is_some() || matched_keys.len() == key_filters.len() {
|
||||||
if let Some(span_id) = event.span_id() {
|
if let Some(span_id) = event.span_id() {
|
||||||
active_span_ids.insert(span_id);
|
active_span_ids.insert(span_id);
|
||||||
}
|
|
||||||
events.push(event);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
events.push(event);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(_) => (),
|
|
||||||
}
|
}
|
||||||
|
Ok(None) => {
|
||||||
timeout = if !events.is_empty() {
|
break;
|
||||||
let elapsed = last_message.elapsed();
|
}
|
||||||
if elapsed >= throttle {
|
Err(_) => (),
|
||||||
last_message = Instant::now();
|
|
||||||
yield Ok(Frame::data(Bytes::from(format!(
|
|
||||||
"event: trace\ndata: {}\n\n",
|
|
||||||
serde_json::to_string(
|
|
||||||
&JsonEventSerializer::new(std::mem::take(&mut events))
|
|
||||||
.with_description()
|
|
||||||
.with_explanation()).unwrap_or_default()
|
|
||||||
))));
|
|
||||||
|
|
||||||
ping_interval
|
|
||||||
} else {
|
|
||||||
throttle - elapsed
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let elapsed = last_ping.elapsed();
|
|
||||||
if elapsed >= ping_interval {
|
|
||||||
last_ping = Instant::now();
|
|
||||||
yield Ok(Frame::data(ping_payload.clone()));
|
|
||||||
ping_interval
|
|
||||||
} else {
|
|
||||||
ping_interval - elapsed
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
))),
|
timeout = if !events.is_empty() {
|
||||||
})
|
let elapsed = last_message.elapsed();
|
||||||
|
if elapsed >= throttle {
|
||||||
|
last_message = Instant::now();
|
||||||
|
yield Ok(Frame::data(Bytes::from(format!(
|
||||||
|
"event: trace\ndata: {}\n\n",
|
||||||
|
serde_json::to_string(
|
||||||
|
&JsonEventSerializer::new(std::mem::take(&mut events))
|
||||||
|
.with_description()
|
||||||
|
.with_explanation()).unwrap_or_default()
|
||||||
|
))));
|
||||||
|
|
||||||
|
ping_interval
|
||||||
|
} else {
|
||||||
|
throttle - elapsed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let elapsed = last_ping.elapsed();
|
||||||
|
if elapsed >= ping_interval {
|
||||||
|
last_ping = Instant::now();
|
||||||
|
yield Ok(Frame::data(ping_payload.clone()));
|
||||||
|
ping_interval
|
||||||
|
} else {
|
||||||
|
ping_interval - elapsed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))))
|
||||||
}
|
}
|
||||||
("trace", id, &Method::GET) => {
|
("trace", id, &Method::GET) => {
|
||||||
// Validate the access token
|
// Validate the access token
|
||||||
|
|
@ -474,73 +471,69 @@ impl TelemetryApi for Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(HttpResponse {
|
Ok(HttpResponse::new(StatusCode::OK)
|
||||||
status: StatusCode::OK,
|
.with_content_type("text/event-stream")
|
||||||
content_type: "text/event-stream".into(),
|
.with_cache_control("no-store")
|
||||||
content_disposition: "".into(),
|
.with_stream_body(BoxBody::new(StreamBody::new(
|
||||||
cache_control: "no-store".into(),
|
async_stream::stream! {
|
||||||
body: HttpResponseBody::Stream(BoxBody::new(StreamBody::new(
|
loop {
|
||||||
async_stream::stream! {
|
let mut metrics = String::with_capacity(512);
|
||||||
|
metrics.push_str("event: metrics\ndata: [");
|
||||||
|
let mut is_first = true;
|
||||||
|
|
||||||
loop {
|
for counter in Collector::collect_counters(true) {
|
||||||
let mut metrics = String::with_capacity(512);
|
if event_types.is_empty() || event_types.contains(&counter.id()) {
|
||||||
metrics.push_str("event: metrics\ndata: [");
|
if !is_first {
|
||||||
let mut is_first = true;
|
metrics.push(',');
|
||||||
|
} else {
|
||||||
for counter in Collector::collect_counters(true) {
|
is_first = false;
|
||||||
if event_types.is_empty() || event_types.contains(&counter.id()) {
|
|
||||||
if !is_first {
|
|
||||||
metrics.push(',');
|
|
||||||
} else {
|
|
||||||
is_first = false;
|
|
||||||
}
|
|
||||||
let _ = write!(
|
|
||||||
&mut metrics,
|
|
||||||
"{{\"id\":\"{}\",\"type\":\"counter\",\"value\":{}}}",
|
|
||||||
counter.id().name(),
|
|
||||||
counter.value()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
let _ = write!(
|
||||||
|
&mut metrics,
|
||||||
|
"{{\"id\":\"{}\",\"type\":\"counter\",\"value\":{}}}",
|
||||||
|
counter.id().name(),
|
||||||
|
counter.value()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for gauge in Collector::collect_gauges(true) {
|
|
||||||
if metric_types.is_empty() || metric_types.contains(&gauge.id()) {
|
|
||||||
if !is_first {
|
|
||||||
metrics.push(',');
|
|
||||||
} else {
|
|
||||||
is_first = false;
|
|
||||||
}
|
|
||||||
let _ = write!(
|
|
||||||
&mut metrics,
|
|
||||||
"{{\"id\":\"{}\",\"type\":\"gauge\",\"value\":{}}}",
|
|
||||||
gauge.id().name(),
|
|
||||||
gauge.get()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for histogram in Collector::collect_histograms(true) {
|
|
||||||
if metric_types.is_empty() || metric_types.contains(&histogram.id()) {
|
|
||||||
if !is_first {
|
|
||||||
metrics.push(',');
|
|
||||||
} else {
|
|
||||||
is_first = false;
|
|
||||||
}
|
|
||||||
let _ = write!(
|
|
||||||
&mut metrics,
|
|
||||||
"{{\"id\":\"{}\",\"type\":\"histogram\",\"count\":{},\"sum\":{}}}",
|
|
||||||
histogram.id().name(),
|
|
||||||
histogram.count(),
|
|
||||||
histogram.sum()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
metrics.push_str("]\n\n");
|
|
||||||
|
|
||||||
yield Ok(Frame::data(Bytes::from(metrics)));
|
|
||||||
tokio::time::sleep(interval).await;
|
|
||||||
}
|
}
|
||||||
},
|
for gauge in Collector::collect_gauges(true) {
|
||||||
))),
|
if metric_types.is_empty() || metric_types.contains(&gauge.id()) {
|
||||||
})
|
if !is_first {
|
||||||
|
metrics.push(',');
|
||||||
|
} else {
|
||||||
|
is_first = false;
|
||||||
|
}
|
||||||
|
let _ = write!(
|
||||||
|
&mut metrics,
|
||||||
|
"{{\"id\":\"{}\",\"type\":\"gauge\",\"value\":{}}}",
|
||||||
|
gauge.id().name(),
|
||||||
|
gauge.get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for histogram in Collector::collect_histograms(true) {
|
||||||
|
if metric_types.is_empty() || metric_types.contains(&histogram.id()) {
|
||||||
|
if !is_first {
|
||||||
|
metrics.push(',');
|
||||||
|
} else {
|
||||||
|
is_first = false;
|
||||||
|
}
|
||||||
|
let _ = write!(
|
||||||
|
&mut metrics,
|
||||||
|
"{{\"id\":\"{}\",\"type\":\"histogram\",\"count\":{},\"sum\":{}}}",
|
||||||
|
histogram.id().name(),
|
||||||
|
histogram.count(),
|
||||||
|
histogram.sum()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metrics.push_str("]\n\n");
|
||||||
|
|
||||||
|
yield Ok(Frame::data(Bytes::from(metrics)));
|
||||||
|
tokio::time::sleep(interval).await;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))))
|
||||||
}
|
}
|
||||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,20 +89,15 @@ impl TroubleshootApi for Server {
|
||||||
timeout,
|
timeout,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(HttpResponse {
|
Ok(HttpResponse::new(StatusCode::OK)
|
||||||
status: StatusCode::OK,
|
.with_content_type("text/event-stream")
|
||||||
content_type: "text/event-stream".into(),
|
.with_cache_control("no-store")
|
||||||
content_disposition: "".into(),
|
.with_stream_body(BoxBody::new(StreamBody::new(async_stream::stream! {
|
||||||
cache_control: "no-store".into(),
|
while let Some(stage) = rx.recv().await {
|
||||||
body: HttpResponseBody::Stream(BoxBody::new(StreamBody::new(
|
yield Ok(stage.to_frame());
|
||||||
async_stream::stream! {
|
}
|
||||||
while let Some(stage) = rx.recv().await {
|
yield Ok(DeliveryStage::Completed.to_frame());
|
||||||
yield Ok(stage.to_frame());
|
}))))
|
||||||
}
|
|
||||||
yield Ok(DeliveryStage::Completed.to_frame());
|
|
||||||
},
|
|
||||||
))),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
("dmarc", None, &Method::POST) => {
|
("dmarc", None, &Method::POST) => {
|
||||||
let request = serde_json::from_slice::<DmarcTroubleshootRequest>(
|
let request = serde_json::from_slice::<DmarcTroubleshootRequest>(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||||
|
*/
|
||||||
|
|
||||||
use std::{net::IpAddr, sync::Arc};
|
use std::{net::IpAddr, sync::Arc};
|
||||||
|
|
||||||
use common::{
|
use common::{
|
||||||
|
|
@ -8,6 +14,7 @@ use common::{
|
||||||
listener::{SessionData, SessionManager, SessionStream},
|
listener::{SessionData, SessionManager, SessionStream},
|
||||||
manager::webadmin::Resource,
|
manager::webadmin::Resource,
|
||||||
};
|
};
|
||||||
|
use dav::{DavMethod, DavResource, request::DavRequestHandler};
|
||||||
use directory::Permission;
|
use directory::Permission;
|
||||||
use http_proto::{
|
use http_proto::{
|
||||||
DownloadResponse, HttpContext, HttpRequest, HttpResponse, HttpResponseBody, HttpSessionData,
|
DownloadResponse, HttpContext, HttpRequest, HttpResponse, HttpResponseBody, HttpSessionData,
|
||||||
|
|
@ -107,7 +114,7 @@ impl ParseHttp for Server {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
return Ok(self
|
return Ok(self
|
||||||
.handle_request(request, access_token, &session)
|
.handle_jmap_request(request, access_token, &session)
|
||||||
.await
|
.await
|
||||||
.into_http_response());
|
.into_http_response());
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +204,41 @@ impl ParseHttp for Server {
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"dav" => {
|
||||||
|
let response = match (
|
||||||
|
path.next().and_then(DavResource::parse),
|
||||||
|
DavMethod::parse(req.method()),
|
||||||
|
) {
|
||||||
|
(Some(resource), Some(DavMethod::OPTIONS)) => resource.into_options_response(),
|
||||||
|
(Some(resource), Some(method)) => {
|
||||||
|
// Authenticate request
|
||||||
|
let (_in_flight, access_token) =
|
||||||
|
self.authenticate_headers(&req, &session, false).await?;
|
||||||
|
let body = if method.has_body() {
|
||||||
|
fetch_body(
|
||||||
|
&mut req,
|
||||||
|
if !access_token.has_permission(Permission::UnlimitedUploads) {
|
||||||
|
self.core.jmap.upload_max_size
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
session.session_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| trc::LimitEvent::SizeRequest.into_err())?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.handle_dav_request(req, access_token, &session, resource, method, body)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
(_, None) => HttpResponse::new(StatusCode::METHOD_NOT_ALLOWED),
|
||||||
|
(None, _) => HttpResponse::new(StatusCode::NOT_FOUND),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
".well-known" => match (path.next().unwrap_or_default(), req.method()) {
|
".well-known" => match (path.next().unwrap_or_default(), req.method()) {
|
||||||
("jmap", &Method::GET) => {
|
("jmap", &Method::GET) => {
|
||||||
// Authenticate request
|
// Authenticate request
|
||||||
|
|
@ -208,6 +250,18 @@ impl ParseHttp for Server {
|
||||||
.await
|
.await
|
||||||
.map(|s| s.into_http_response());
|
.map(|s| s.into_http_response());
|
||||||
}
|
}
|
||||||
|
("caldav", &Method::GET) => {
|
||||||
|
let base_url = ctx.resolve_response_url(self).await;
|
||||||
|
return Ok(HttpResponse::new(StatusCode::TEMPORARY_REDIRECT)
|
||||||
|
.with_no_cache()
|
||||||
|
.with_location(format!("{base_url}/dav/cal")));
|
||||||
|
}
|
||||||
|
("carddav", &Method::GET) => {
|
||||||
|
let base_url = ctx.resolve_response_url(self).await;
|
||||||
|
return Ok(HttpResponse::new(StatusCode::TEMPORARY_REDIRECT)
|
||||||
|
.with_no_cache()
|
||||||
|
.with_location(format!("{base_url}/dav/card")));
|
||||||
|
}
|
||||||
("oauth-authorization-server", &Method::GET) => {
|
("oauth-authorization-server", &Method::GET) => {
|
||||||
// Limit anonymous requests
|
// Limit anonymous requests
|
||||||
self.is_http_anonymous_request_allowed(&session.remote_ip)
|
self.is_http_anonymous_request_allowed(&session.remote_ip)
|
||||||
|
|
@ -690,13 +744,13 @@ async fn handle_session<T: SessionStream>(inner: Arc<Inner>, session: SessionDat
|
||||||
trc::event!(
|
trc::event!(
|
||||||
Http(trc::HttpEvent::ResponseBody),
|
Http(trc::HttpEvent::ResponseBody),
|
||||||
SpanId = session.session_id,
|
SpanId = session.session_id,
|
||||||
Contents = match &response.body {
|
Contents = match response.body() {
|
||||||
HttpResponseBody::Text(value) => trc::Value::String(value.clone()),
|
HttpResponseBody::Text(value) => trc::Value::String(value.clone()),
|
||||||
HttpResponseBody::Binary(_) => trc::Value::Static("[binary data]"),
|
HttpResponseBody::Binary(_) => trc::Value::Static("[binary data]"),
|
||||||
HttpResponseBody::Stream(_) => trc::Value::Static("[stream]"),
|
HttpResponseBody::Stream(_) => trc::Value::Static("[stream]"),
|
||||||
_ => trc::Value::None,
|
_ => trc::Value::None,
|
||||||
},
|
},
|
||||||
Code = response.status.as_u16(),
|
Code = response.status().as_u16(),
|
||||||
Size = response.size(),
|
Size = response.size(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,12 +105,10 @@ impl EventSourceHandler for Server {
|
||||||
.subscribe_state_manager(access_token.primary_id(), types)
|
.subscribe_state_manager(access_token.primary_id(), types)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse {
|
Ok(HttpResponse::new(StatusCode::OK)
|
||||||
status: StatusCode::OK,
|
.with_content_type("text/event-stream")
|
||||||
content_type: "text/event-stream".into(),
|
.with_cache_control("no-store")
|
||||||
content_disposition: "".into(),
|
.with_stream_body(BoxBody::new(StreamBody::new(async_stream::stream! {
|
||||||
cache_control: "no-store".into(),
|
|
||||||
body: HttpResponseBody::Stream(BoxBody::new(StreamBody::new(async_stream::stream! {
|
|
||||||
let mut last_message = Instant::now() - throttle;
|
let mut last_message = Instant::now() - throttle;
|
||||||
let mut timeout =
|
let mut timeout =
|
||||||
ping.as_ref().map(|p| p.interval).unwrap_or(LONG_1D_SLUMBER);
|
ping.as_ref().map(|p| p.interval).unwrap_or(LONG_1D_SLUMBER);
|
||||||
|
|
@ -162,7 +160,6 @@ impl EventSourceHandler for Server {
|
||||||
LONG_1D_SLUMBER
|
LONG_1D_SLUMBER
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}))),
|
}))))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,9 @@ impl ToJmapHttpResponse for Session {
|
||||||
|
|
||||||
impl ToJmapHttpResponse for RequestError<'_> {
|
impl ToJmapHttpResponse for RequestError<'_> {
|
||||||
fn into_http_response(self) -> HttpResponse {
|
fn into_http_response(self) -> HttpResponse {
|
||||||
HttpResponse::new_text(
|
HttpResponse::new(StatusCode::from_u16(self.status).unwrap_or(StatusCode::BAD_REQUEST))
|
||||||
StatusCode::from_u16(self.status).unwrap_or(StatusCode::BAD_REQUEST),
|
.with_content_type("application/problem+json")
|
||||||
"application/problem+json",
|
.with_text_body(serde_json::to_string(&self).unwrap_or_default())
|
||||||
serde_json::to_string(&self).unwrap_or_default(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ use crate::{
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
pub trait RequestHandler: Sync + Send {
|
pub trait RequestHandler: Sync + Send {
|
||||||
fn handle_request(
|
fn handle_jmap_request(
|
||||||
&self,
|
&self,
|
||||||
request: Request,
|
request: Request,
|
||||||
access_token: Arc<AccessToken>,
|
access_token: Arc<AccessToken>,
|
||||||
|
|
@ -61,7 +61,7 @@ pub trait RequestHandler: Sync + Send {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestHandler for Server {
|
impl RequestHandler for Server {
|
||||||
async fn handle_request(
|
async fn handle_jmap_request(
|
||||||
&self,
|
&self,
|
||||||
request: Request,
|
request: Request,
|
||||||
access_token: Arc<AccessToken>,
|
access_token: Arc<AccessToken>,
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ impl WebSocketHandler for Server {
|
||||||
) {
|
) {
|
||||||
Ok(WebSocketMessage::Request(request)) => {
|
Ok(WebSocketMessage::Request(request)) => {
|
||||||
let response = self
|
let response = self
|
||||||
.handle_request(
|
.handle_jmap_request(
|
||||||
request.request,
|
request.request,
|
||||||
access_token.clone(),
|
access_token.clone(),
|
||||||
&session,
|
&session,
|
||||||
|
|
|
||||||
|
|
@ -102,12 +102,6 @@ impl WebSocketUpgrade for Server {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(HttpResponse {
|
Ok(HttpResponse::new(StatusCode::SWITCHING_PROTOCOLS).with_websocket_upgrade(derived_key))
|
||||||
status: StatusCode::SWITCHING_PROTOCOLS,
|
|
||||||
content_type: "".into(),
|
|
||||||
content_disposition: "".into(),
|
|
||||||
cache_control: "".into(),
|
|
||||||
body: HttpResponseBody::WebsocketUpgrade(derived_key),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue