1pub mod actions;
5pub mod request;
6
7use crate::llm::action_helper::call_llm;
8use crate::llm::actions::protocol_trait::ActionResult;
9use crate::logging::emit::Log;
10use crate::protocol::{Event, SpawnContext};
11use crate::server::accept_bounded::{accept_bounded, ConnectionLimiter, DEFAULT_MAX_CONNECTIONS};
12use crate::server::connection::ConnectionId;
13use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
14use base64::Engine;
15use bytes::Bytes;
16use http_body_util::{BodyExt, Full, Limited};
17use hyper::{
18 body::Incoming, header, server::conn::http1, service::service_fn, Method, Request, Response,
19 StatusCode,
20};
21use hyper_util::rt::{TokioIo, TokioTimer};
22use request::{method_error, Problem};
23use serde_json::{json, Map, Value};
24use std::collections::BTreeMap;
25use std::{convert::Infallible, net::SocketAddr, sync::Arc, time::Duration};
26
27pub const MAX_BODY: usize = 1024 * 1024;
28const HEADER_TIMEOUT: Duration = Duration::from_secs(30);
29const TLS_TIMEOUT: Duration = Duration::from_secs(10);
30
31struct Shared {
32 ctx: SpawnContext,
33 accounts: Vec<(String, String)>,
35 users: BTreeMap<String, String>,
36 tokens: BTreeMap<String, String>,
37 scheme: &'static str,
38 state: String,
39}
40
41fn string_map(
42 p: Option<&crate::protocol::StartupParams>,
43 name: &str,
44) -> anyhow::Result<BTreeMap<String, String>> {
45 let mut out = BTreeMap::new();
46 if let Some(m) = p
47 .map(|p| p.get_optional_object(name))
48 .transpose()?
49 .flatten()
50 {
51 anyhow::ensure!(m.len() <= 64, "at most 64 entries in {name}");
52 for (k, v) in m {
53 let v = v
54 .as_str()
55 .ok_or_else(|| anyhow::anyhow!("{name} values are strings"))?;
56 anyhow::ensure!(
57 !k.is_empty() && !v.is_empty(),
58 "{name} entries are not empty"
59 );
60 out.insert(k.clone(), v.to_owned());
61 }
62 }
63 Ok(out)
64}
65
66pub async fn spawn(ctx: SpawnContext) -> anyhow::Result<SocketAddr> {
67 let p = ctx.startup_params.as_ref();
68 let mut accounts = Vec::new();
69 for a in p
70 .map(|p| p.get_optional_array("accounts"))
71 .transpose()?
72 .flatten()
73 .into_iter()
74 .flatten()
75 {
76 let id = a["id"]
77 .as_str()
78 .filter(|i| {
79 !i.is_empty()
80 && i.len() <= 255
81 && i.bytes()
82 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
83 })
84 .ok_or_else(|| anyhow::anyhow!("each account has an id of letters, digits, - and _"))?;
85 let name = a["name"].as_str().unwrap_or(id);
86 anyhow::ensure!(accounts.len() < 64, "at most 64 accounts");
87 accounts.push((id.to_owned(), name.to_owned()));
88 }
89 if accounts.is_empty() {
90 accounts.push(("a1".into(), "netget".into()));
91 }
92 let users = string_map(p, "users")?;
93 let tokens = string_map(p, "api_tokens")?;
94 let get = |name| {
95 p.map(|p| p.get_optional_string(name))
96 .transpose()
97 .map(Option::flatten)
98 };
99 let tls_on = p
100 .map(|p| p.get_optional_bool("tls"))
101 .transpose()?
102 .flatten()
103 .unwrap_or(true);
104 let tls = match (tls_on, get("tls_cert_file")?, get("tls_key_file")?) {
105 (false, None, None) => None,
106 (false, _, _) => anyhow::bail!("tls is false but a certificate was given"),
107 (true, Some(c), Some(k)) => Some(tokio_rustls::TlsAcceptor::from(
108 crate::server::tls_cert_manager::load_tls_config_from_files(&c, &k)?,
109 )),
110 (true, None, None) => {
111 let spec = crate::server::tls_cert_manager::CertificateSpec {
112 common_name: "localhost".into(),
113 san_dns_names: vec!["localhost".into()],
114 validity_days: 30,
115 organization: Some("NetGet".into()),
116 organizational_unit: Some("JMAP".into()),
117 };
118 let (cert, key) = crate::server::tls_cert_manager::generate_self_signed_cert(&spec)?;
119 let pem = cert.pem();
120 ctx.state
121 .with_server_mut(ctx.server_id, |s| {
122 s.set_protocol_field("certificate_pem".into(), json!(pem))
123 })
124 .await;
125 Some(tokio_rustls::TlsAcceptor::from(
126 crate::server::tls_cert_manager::create_rustls_server_config(&cert, &key)?,
127 ))
128 }
129 _ => anyhow::bail!("tls_cert_file and tls_key_file go together"),
130 };
131 let scheme = if tls.is_some() { "https" } else { "http" };
132 let state = {
133 use std::hash::{Hash, Hasher};
134 let mut h = std::collections::hash_map::DefaultHasher::new();
135 accounts.hash(&mut h);
136 format!("{:x}", h.finish())
137 };
138 let listener = tokio::net::TcpListener::bind(ctx.legacy_listen_addr()).await?;
139 let local = listener.local_addr()?;
140 Log::new(Some(&ctx.status_tx)).info(format!(
141 "JMAP session at {scheme}://{local}/.well-known/jmap"
142 ));
143 let shared = Arc::new(Shared {
144 ctx: ctx.clone(),
145 accounts,
146 users,
147 tokens,
148 scheme,
149 state,
150 });
151 let server_id = ctx.server_id;
152 let accept = tokio::spawn(async move {
153 let limiter = ConnectionLimiter::new(DEFAULT_MAX_CONNECTIONS);
154 loop {
155 let (stream, peer, permit) = match accept_bounded(&listener, &limiter, b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", "JMAP", Some(&shared.ctx.status_tx)).await {
156 Ok(v) => v,
157 Err(_) => break,
158 };
159 let id = ConnectionId::new(shared.ctx.state.get_next_unified_id().await);
160 let now = crate::utils::clock::Instant::now();
161 shared
162 .ctx
163 .state
164 .add_connection_to_server(
165 server_id,
166 ConnectionState {
167 id,
168 remote_addr: peer,
169 local_addr: local,
170 bytes_sent: 0,
171 bytes_received: 0,
172 packets_sent: 0,
173 packets_received: 0,
174 last_activity: now,
175 status: ConnectionStatus::Active,
176 status_changed_at: now,
177 protocol_info: ProtocolConnectionInfo::empty(),
178 },
179 )
180 .await;
181 let child = shared.clone();
182 let tls = tls.clone();
183 shared
184 .ctx
185 .state
186 .spawn_server_task(server_id, async move {
187 let _permit = permit;
188 let svc_shared = child.clone();
189 let service = service_fn(move |req| {
190 let shared = svc_shared.clone();
191 async move { Ok::<_, Infallible>(handle(&shared, id, req).await) }
192 });
193 let mut builder = http1::Builder::new();
194 builder
195 .timer(TokioTimer::new())
196 .header_read_timeout(HEADER_TIMEOUT)
197 .max_headers(64)
198 .max_buf_size(32 * 1024);
199 let served = match tls {
200 Some(acceptor) => {
201 match tokio::time::timeout(TLS_TIMEOUT, acceptor.accept(stream)).await {
202 Ok(Ok(s)) => builder
203 .serve_connection(TokioIo::new(s), service)
204 .await
205 .map_err(|e| e.to_string()),
206 Ok(Err(e)) => Err(format!("TLS handshake: {e}")),
207 Err(_) => Err("TLS handshake timed out".into()),
208 }
209 }
210 None => builder
211 .serve_connection(TokioIo::new(stream), service)
212 .await
213 .map_err(|e| e.to_string()),
214 };
215 if let Err(e) = served {
216 Log::new(Some(&child.ctx.status_tx))
217 .debug(format!("JMAP connection {id}: {e}"));
218 }
219 child
220 .ctx
221 .state
222 .update_connection_status(server_id, id, ConnectionStatus::Closed)
223 .await;
224 let _ = child.ctx.status_tx.send("__UPDATE_UI__".into());
225 })
226 .await;
227 }
228 });
229 ctx.state.register_server_task(server_id, accept).await;
230 Ok(local)
231}
232
233type Reply = Response<Full<Bytes>>;
234
235fn reply(status: u16, content_type: &str, body: Vec<u8>) -> Reply {
236 let mut r = Response::new(Full::new(Bytes::from(body)));
237 *r.status_mut() = StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
238 if let Ok(ct) = header::HeaderValue::from_str(content_type) {
239 r.headers_mut().insert(header::CONTENT_TYPE, ct);
240 }
241 r.headers_mut().insert(
242 header::CACHE_CONTROL,
243 header::HeaderValue::from_static("no-cache, no-store"),
244 );
245 r
246}
247
248fn json_reply(status: u16, body: &Value) -> Reply {
249 reply(
250 status,
251 "application/json",
252 serde_json::to_vec(body).unwrap_or_default(),
253 )
254}
255
256fn problem(status: u16, kind: &str, detail: &str) -> Reply {
257 reply(
258 status,
259 "application/problem+json",
260 serde_json::to_vec(&json!({"type": kind, "status": status, "detail": detail}))
261 .unwrap_or_default(),
262 )
263}
264
265fn log_decision(ctx: &SpawnContext, id: ConnectionId, what: &str, decision: &str) {
266 let line = format!("JMAP connection {id} {what} decision={decision}");
267 let log = Log::new(Some(&ctx.status_tx));
268 if decision.starts_with("fail_closed_") || decision == "model_silent" {
269 log.error(line);
270 } else {
271 log.info(line);
272 }
273}
274
275async fn handle(shared: &Shared, id: ConnectionId, req: Request<Incoming>) -> Reply {
276 let ctx = &shared.ctx;
277 ctx.state
278 .update_connection_stats(
279 ctx.server_id,
280 id,
281 Some(req.uri().to_string().len() as u64),
282 None,
283 Some(1),
284 None,
285 )
286 .await;
287 let response = route(shared, id, req).await;
288 let sent = hyper::body::Body::size_hint(response.body())
289 .exact()
290 .unwrap_or(0);
291 ctx.state
292 .update_connection_stats(ctx.server_id, id, None, Some(sent), None, Some(1))
293 .await;
294 response
295}
296
297fn authenticate(shared: &Shared, req: &Request<Incoming>) -> Option<String> {
299 let given = req
300 .headers()
301 .get(header::AUTHORIZATION)
302 .and_then(|a| a.to_str().ok());
303 if shared.users.is_empty() && shared.tokens.is_empty() {
304 return Some(
305 given
306 .and_then(basic)
307 .map(|(u, _)| u)
308 .unwrap_or_else(|| shared.accounts[0].1.clone()),
309 );
310 }
311 let given = given?;
312 if let Some(token) = given.strip_prefix("Bearer ") {
313 return shared.tokens.get(token.trim()).cloned();
314 }
315 let (user, password) = basic(given)?;
316 (shared.users.get(&user) == Some(&password)).then_some(user)
317}
318
319fn basic(header: &str) -> Option<(String, String)> {
320 let raw = header.strip_prefix("Basic ")?;
321 let decoded = base64::engine::general_purpose::STANDARD
322 .decode(raw.trim())
323 .ok()?;
324 let text = String::from_utf8(decoded).ok()?;
325 let (u, p) = text.split_once(':')?;
326 Some((u.to_owned(), p.to_owned()))
327}
328
329async fn route(shared: &Shared, id: ConnectionId, req: Request<Incoming>) -> Reply {
330 let path = req.uri().path().to_owned();
331 let Some(user) = authenticate(shared, &req) else {
332 log_decision(
333 &shared.ctx,
334 id,
335 &path,
336 "protocol_refusal reason=unauthenticated",
337 );
338 let mut r = problem(401, "about:blank", "authentication is required");
339 r.headers_mut().insert(
340 header::WWW_AUTHENTICATE,
341 header::HeaderValue::from_static("Basic realm=\"JMAP\""),
342 );
343 return r;
344 };
345 let host = req
346 .headers()
347 .get(header::HOST)
348 .and_then(|h| h.to_str().ok())
349 .filter(|h| h.len() <= 255 && !h.contains(['/', ' ']))
350 .unwrap_or("localhost")
351 .to_owned();
352 match (req.method(), path.as_str()) {
353 (&Method::GET, "/.well-known/jmap" | "/jmap/session") => {
354 json_reply(200, &session(shared, &host, &user))
355 }
356 (&Method::POST, "/jmap" | "/jmap/") => api(shared, id, &user, req).await,
357 (_, p)
358 if p.starts_with("/jmap/upload/")
359 || p.starts_with("/jmap/download/")
360 || p.starts_with("/jmap/eventsource") =>
361 {
362 problem(
363 501,
364 "about:blank",
365 "blobs and push are not implemented by this server",
366 )
367 }
368 (_, "/.well-known/jmap" | "/jmap" | "/jmap/" | "/jmap/session") => {
369 problem(405, "about:blank", "method not allowed")
370 }
371 _ => problem(404, "about:blank", "no such resource"),
372 }
373}
374
375fn session(shared: &Shared, host: &str, user: &str) -> Value {
376 let base = format!("{}://{host}", shared.scheme);
377 let mut accounts = Map::new();
378 for (id, name) in &shared.accounts {
379 accounts.insert(id.clone(), json!({
380 "name": name,
381 "isPersonal": true,
382 "isReadOnly": false,
383 "accountCapabilities": {
384 request::MAIL: {"maxMailboxesPerEmail": null, "maxMailboxDepth": 10, "maxSizeMailboxName": 255, "maxSizeAttachmentsPerEmail": 50_000_000, "emailQuerySortOptions": ["receivedAt", "sentAt", "size", "from", "to", "subject"], "mayCreateTopLevelMailbox": true},
385 request::SUBMISSION: {"maxDelayedSend": 0, "submissionExtensions": {}},
386 request::VACATION: {},
387 },
388 }));
389 }
390 let primary = &shared.accounts[0].0;
391 json!({
392 "capabilities": {
393 request::CORE: {
394 "maxSizeUpload": 0,
395 "maxConcurrentUpload": 1,
396 "maxSizeRequest": MAX_BODY,
397 "maxConcurrentRequests": 4,
398 "maxCallsInRequest": request::MAX_CALLS,
399 "maxObjectsInGet": request::MAX_OBJECTS_IN_GET,
400 "maxObjectsInSet": request::MAX_OBJECTS_IN_SET,
401 "collationAlgorithms": ["i;ascii-numeric", "i;ascii-casemap", "i;octet"],
402 },
403 request::MAIL: {},
404 request::SUBMISSION: {},
405 request::VACATION: {},
406 },
407 "accounts": accounts,
408 "primaryAccounts": {request::MAIL: primary, request::SUBMISSION: primary, request::VACATION: primary},
409 "username": user,
410 "apiUrl": format!("{base}/jmap/"),
411 "downloadUrl": format!("{base}/jmap/download/{{accountId}}/{{blobId}}/{{name}}?accept={{type}}"),
412 "uploadUrl": format!("{base}/jmap/upload/{{accountId}}/"),
413 "eventSourceUrl": format!("{base}/jmap/eventsource/?types={{types}}&closeafter={{closeafter}}&ping={{ping}}"),
414 "state": shared.state,
415 })
416}
417
418fn problem_reply(p: &Problem) -> Reply {
419 reply(
420 400,
421 "application/problem+json",
422 serde_json::to_vec(&p.body()).unwrap_or_default(),
423 )
424}
425
426async fn api(shared: &Shared, id: ConnectionId, user: &str, req: Request<Incoming>) -> Reply {
427 let bytes = match Limited::new(req.into_body(), MAX_BODY).collect().await {
428 Ok(b) => b.to_bytes(),
429 Err(_) => {
430 return problem_reply(&Problem {
431 kind: "limit",
432 detail: format!("requests are at most {MAX_BODY} bytes"),
433 limit: Some("maxSizeRequest"),
434 })
435 }
436 };
437 let request = match request::parse(&bytes) {
438 Ok(r) => r,
439 Err(p) => {
440 log_decision(
441 &shared.ctx,
442 id,
443 "request",
444 &format!("protocol_refusal reason={}", p.kind),
445 );
446 return problem_reply(&p);
447 }
448 };
449 let mut created = request.created_ids.clone().unwrap_or_default();
450 let mut responses: Vec<(String, Value, String)> = Vec::new();
451 for call in &request.calls {
452 let answers = match prepare(shared, &request.using, call, &responses, &created) {
453 Err(e) => vec![("error".to_owned(), e)],
454 Ok(args) if call.name == "Core/echo" => vec![(call.name.clone(), Value::Object(args))],
455 Ok(args) => answer(shared, id, user, call, args).await,
456 };
457 for (name, args) in answers {
458 request::record_created(&args, &mut created);
459 responses.push((name, args, call.id.clone()));
460 }
461 }
462 let mut body = json!({
463 "methodResponses": responses.into_iter().map(|(n, a, i)| json!([n, a, i])).collect::<Vec<_>>(),
464 "sessionState": shared.state,
465 });
466 if request.created_ids.is_some() {
467 body["createdIds"] = Value::Object(created);
468 }
469 json_reply(200, &body)
470}
471
472fn prepare(
474 shared: &Shared,
475 using: &[String],
476 call: &request::Call,
477 earlier: &[(String, Value, String)],
478 created: &Map<String, Value>,
479) -> Result<Map<String, Value>, Value> {
480 let args = request::resolve_references(&call.arguments, earlier)?;
481 let args = match request::substitute_creation_ids(&Value::Object(args), created) {
482 Value::Object(m) => m,
483 _ => unreachable!("an object stays an object"),
484 };
485 let Some(capability) = request::capability(&call.name) else {
486 return Err(method_error(
487 "unknownMethod",
488 &format!("{} is not a method of this server", call.name),
489 ));
490 };
491 if !using.iter().any(|u| u == capability) {
492 return Err(method_error(
493 "unknownMethod",
494 &format!("{} needs {capability} in using", call.name),
495 ));
496 }
497 if capability != request::CORE {
498 let account = args
499 .get("accountId")
500 .and_then(Value::as_str)
501 .ok_or_else(|| method_error("invalidArguments", "accountId is required"))?;
502 if !shared.accounts.iter().any(|(a, _)| a == account) {
503 return Err(method_error("accountNotFound", account));
504 }
505 }
506 if let Some(e) = request::check_limits(&call.name, &args) {
507 return Err(e);
508 }
509 Ok(args)
510}
511
512async fn answer(
514 shared: &Shared,
515 id: ConnectionId,
516 user: &str,
517 call: &request::Call,
518 args: Map<String, Value>,
519) -> Vec<(String, Value)> {
520 let ctx = &shared.ctx;
521 let account = args.get("accountId").cloned();
522 let event = Event::new(
523 &actions::METHOD_CALL_EVENT,
524 json!({"method": call.name, "account_id": account, "arguments": args, "call_id": call.id, "username": user}),
525 );
526 let result = match call_llm(
527 &ctx.llm_client,
528 &ctx.state,
529 ctx.server_id,
530 Some(id),
531 &event,
532 &actions::JmapProtocol,
533 )
534 .await
535 {
536 Ok(r) => r,
537 Err(e) => {
538 log_decision(ctx, id, &call.name, "fail_closed_llm_error");
539 let kind = if crate::utils::WireFailure::classify(&e).is_overloaded() {
540 "serverUnavailable"
541 } else {
542 "serverFail"
543 };
544 return vec![("error".into(), method_error(kind, ""))];
545 }
546 };
547 if !result.failures.is_empty() {
548 log_decision(ctx, id, &call.name, "fail_closed_invalid_reply");
549 return vec![("error".into(), method_error("serverFail", ""))];
550 }
551 let mut out = Vec::new();
552 for r in result.protocol_results {
553 let ActionResult::Custom { data, .. } = r else {
554 continue;
555 };
556 if data["type"] == "jmap_method_error" {
557 log_decision(ctx, id, &call.name, "model_reject");
558 out.push((
559 "error".to_owned(),
560 method_error(
561 data["error_type"].as_str().unwrap_or("serverFail"),
562 data["description"].as_str().unwrap_or_default(),
563 ),
564 ));
565 } else {
566 let mut a = data["arguments"].clone();
567 if let (Some(acc), Some(m)) = (&account, a.as_object_mut()) {
568 m.entry("accountId").or_insert_with(|| acc.clone());
569 }
570 let name = data["method"].as_str().unwrap_or(&call.name).to_owned();
571 out.push((name, a));
572 }
573 }
574 if out.is_empty() {
575 log_decision(ctx, id, &call.name, "model_silent");
576 return vec![("error".into(), method_error("serverFail", ""))];
577 }
578 if out.iter().all(|(n, _)| n != "error") {
579 log_decision(ctx, id, &call.name, "model_answer");
580 }
581 out
582}