1use anyhow::{anyhow, Result};
2use axum::{
3 body::Bytes,
4 extract::{
5 ws::{Message, WebSocket},
6 DefaultBodyLimit, Path, Query, Request, State, WebSocketUpgrade,
7 },
8 http::{
9 header::{HeaderMap, HeaderName},
10 StatusCode,
11 },
12 middleware::{self, Next},
13 response::{IntoResponse, Response},
14 routing::{get, post},
15 Json, Router,
16};
17use axum_extra::typed_header::TypedHeader;
18use dashmap::{mapref::one::MappedRef, DashMap};
19use futures::{SinkExt, StreamExt};
20use serde::Deserialize;
21use serde_json::{json, Value};
22use std::{
23 sync::{Arc, RwLock},
24 time::Duration,
25};
26use tokio::{
27 net::TcpListener,
28 sync::mpsc::{channel, Receiver},
29};
30use tokio_util::{sync::CancellationToken, task::TaskTracker};
31use tracing::{span, Instrument, Level};
32use url::Url;
33use y_sweet_core::{
34 api_types::{
35 validate_doc_name, AuthDocRequest, Authorization, ClientToken, DocCreationRequest,
36 NewDocResponse,
37 },
38 auth::{Authenticator, ExpirationTimeEpochMillis, DEFAULT_EXPIRATION_SECONDS},
39 doc_connection::DocConnection,
40 doc_sync::DocWithSyncKv,
41 store::Store,
42 sync::awareness::Awareness,
43 sync_kv::SyncKv,
44};
45
46const PLANE_VERIFIED_USER_DATA_HEADER: &str = "x-verified-user-data";
47
48const PING_EVERY: Duration = Duration::from_secs(20);
50const PONG_TIMEOUT: Duration = Duration::from_secs(40);
53
54fn current_time_epoch_millis() -> u64 {
55 let now = std::time::SystemTime::now();
56 let duration_since_epoch = now.duration_since(std::time::UNIX_EPOCH).unwrap();
57 duration_since_epoch.as_millis() as u64
58}
59
60#[derive(Debug)]
61pub struct AppError(StatusCode, anyhow::Error);
62impl std::error::Error for AppError {}
63impl IntoResponse for AppError {
64 fn into_response(self) -> Response {
65 (self.0, format!("Something went wrong: {}", self.1)).into_response()
66 }
67}
68impl<E> From<(StatusCode, E)> for AppError
69where
70 E: Into<anyhow::Error>,
71{
72 fn from((status_code, err): (StatusCode, E)) -> Self {
73 Self(status_code, err.into())
74 }
75}
76impl std::fmt::Display for AppError {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 write!(f, "Status code: {} {}", self.0, self.1)?;
79 Ok(())
80 }
81}
82
83pub struct Server {
84 docs: Arc<DashMap<String, DocWithSyncKv>>,
85 doc_worker_tracker: TaskTracker,
86 store: Option<Arc<Box<dyn Store>>>,
87 checkpoint_freq: Duration,
88 authenticator: Option<Authenticator>,
89 url_prefix: Option<Url>,
90 cancellation_token: CancellationToken,
91 doc_gc: bool,
94 max_body_size: Option<usize>,
95}
96
97impl Server {
98 pub async fn new(
99 store: Option<Box<dyn Store>>,
100 checkpoint_freq: Duration,
101 authenticator: Option<Authenticator>,
102 url_prefix: Option<Url>,
103 cancellation_token: CancellationToken,
104 doc_gc: bool,
105 max_body_size: Option<usize>,
106 ) -> Result<Self> {
107 Ok(Self {
108 docs: Arc::new(DashMap::new()),
109 doc_worker_tracker: TaskTracker::new(),
110 store: store.map(Arc::new),
111 checkpoint_freq,
112 authenticator,
113 url_prefix,
114 cancellation_token,
115 doc_gc,
116 max_body_size,
117 })
118 }
119
120 pub async fn doc_exists(&self, doc_id: &str) -> bool {
121 if self.docs.contains_key(doc_id) {
122 return true;
123 }
124 if let Some(store) = &self.store {
125 store
126 .exists(&format!("{}/data.ysweet", doc_id))
127 .await
128 .unwrap_or_default()
129 } else {
130 false
131 }
132 }
133
134 pub async fn create_doc(&self) -> Result<String> {
135 let doc_id = nanoid::nanoid!();
136 self.load_doc(&doc_id).await?;
137 tracing::info!(doc_id=?doc_id, "Created doc");
138 Ok(doc_id)
139 }
140
141 pub async fn load_doc(&self, doc_id: &str) -> Result<()> {
142 let (send, recv) = channel(1024);
143
144 let dwskv = DocWithSyncKv::new(doc_id, self.store.clone(), move || {
145 send.try_send(()).unwrap();
146 })
147 .await?;
148
149 dwskv
150 .sync_kv()
151 .persist()
152 .await
153 .map_err(|e| anyhow!("Error persisting: {:?}", e))?;
154
155 {
156 let sync_kv = dwskv.sync_kv();
157 let checkpoint_freq = self.checkpoint_freq;
158 let doc_id = doc_id.to_string();
159 let cancellation_token = self.cancellation_token.clone();
160
161 self.doc_worker_tracker.spawn(
163 Self::doc_persistence_worker(
164 recv,
165 sync_kv,
166 checkpoint_freq,
167 doc_id.clone(),
168 cancellation_token.clone(),
169 )
170 .instrument(span!(Level::INFO, "save_loop", doc_id=?doc_id)),
171 );
172
173 if self.doc_gc {
174 self.doc_worker_tracker.spawn(
175 Self::doc_gc_worker(
176 self.docs.clone(),
177 doc_id.clone(),
178 checkpoint_freq,
179 cancellation_token,
180 )
181 .instrument(span!(Level::INFO, "gc_loop", doc_id=?doc_id)),
182 );
183 }
184 }
185
186 self.docs.insert(doc_id.to_string(), dwskv);
187 Ok(())
188 }
189
190 async fn doc_gc_worker(
191 docs: Arc<DashMap<String, DocWithSyncKv>>,
192 doc_id: String,
193 checkpoint_freq: Duration,
194 cancellation_token: CancellationToken,
195 ) {
196 let mut checkpoints_without_refs = 0;
197
198 loop {
199 tokio::select! {
200 _ = tokio::time::sleep(checkpoint_freq) => {
201 if let Some(doc) = docs.get(&doc_id) {
202 let awareness = Arc::downgrade(&doc.awareness());
203 if awareness.strong_count() > 1 {
204 checkpoints_without_refs = 0;
205 tracing::debug!("doc is still alive - it has {} references", awareness.strong_count());
206 } else {
207 checkpoints_without_refs += 1;
208 tracing::info!("doc has only one reference, candidate for GC. checkpoints_without_refs: {}", checkpoints_without_refs);
209 }
210 } else {
211 break;
212 }
213
214 if checkpoints_without_refs >= 2 {
215 tracing::info!("GCing doc");
216 if let Some(doc) = docs.get(&doc_id) {
217 doc.sync_kv().shutdown();
218 }
219
220 docs.remove(&doc_id);
221 break;
222 }
223 }
224 _ = cancellation_token.cancelled() => {
225 break;
226 }
227 };
228 }
229 tracing::info!("Exiting gc_loop");
230 }
231
232 async fn doc_persistence_worker(
233 mut recv: Receiver<()>,
234 sync_kv: Arc<SyncKv>,
235 checkpoint_freq: Duration,
236 doc_id: String,
237 cancellation_token: CancellationToken,
238 ) {
239 let mut last_save = std::time::Instant::now();
240
241 loop {
242 let is_done = tokio::select! {
243 v = recv.recv() => v.is_none(),
244 _ = cancellation_token.cancelled() => true,
245 _ = tokio::time::sleep(checkpoint_freq) => {
246 sync_kv.is_shutdown()
247 }
248 };
249
250 tracing::info!("Received signal. done: {}", is_done);
251 let now = std::time::Instant::now();
252 if !is_done && now - last_save < checkpoint_freq {
253 let sleep = tokio::time::sleep(checkpoint_freq - (now - last_save));
254 tokio::pin!(sleep);
255 tracing::info!("Throttling.");
256
257 loop {
258 tokio::select! {
259 _ = &mut sleep => {
260 break;
261 }
262 v = recv.recv() => {
263 tracing::info!("Received dirty while throttling.");
264 if v.is_none() {
265 break;
266 }
267 }
268 _ = cancellation_token.cancelled() => {
269 tracing::info!("Received cancellation while throttling.");
270 break;
271 }
272
273 }
274 tracing::info!("Done throttling.");
275 }
276 }
277 tracing::info!("Persisting.");
278 if let Err(e) = sync_kv.persist().await {
279 tracing::error!(?e, "Error persisting.");
280 } else {
281 tracing::info!("Done persisting.");
282 }
283 last_save = std::time::Instant::now();
284
285 if is_done {
286 break;
287 }
288 }
289 tracing::info!("Terminating loop for {}", doc_id);
290 }
291
292 pub async fn get_or_create_doc(
293 &self,
294 doc_id: &str,
295 ) -> Result<MappedRef<String, DocWithSyncKv, DocWithSyncKv>> {
296 if !self.docs.contains_key(doc_id) {
297 tracing::info!(doc_id=?doc_id, "Loading doc");
298 self.load_doc(doc_id).await?;
299 }
300
301 Ok(self
302 .docs
303 .get(doc_id)
304 .ok_or_else(|| anyhow!("Failed to get-or-create doc"))?
305 .map(|d| d))
306 }
307
308 pub fn check_auth(
309 &self,
310 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
311 ) -> Result<(), AppError> {
312 if let Some(auth) = &self.authenticator {
313 if let Some(TypedHeader(headers::Authorization(bearer))) = auth_header {
314 if let Ok(()) =
315 auth.verify_server_token(bearer.token(), current_time_epoch_millis())
316 {
317 return Ok(());
318 }
319 }
320 Err((StatusCode::UNAUTHORIZED, anyhow!("Unauthorized.")))?
321 } else {
322 Ok(())
323 }
324 }
325
326 pub async fn redact_error_middleware(req: Request, next: Next) -> impl IntoResponse {
327 let resp = next.run(req).await;
328 if resp.status().is_server_error() || resp.status().is_client_error() {
329 return resp.status().into_response();
332 }
333 resp
334 }
335
336 pub fn routes(self: &Arc<Self>) -> Router {
337 Router::new()
338 .route("/ready", get(ready))
339 .route("/check_store", post(check_store))
340 .route("/check_store", get(check_store_deprecated))
341 .route("/doc/ws/:doc_id", get(handle_socket_upgrade_deprecated))
342 .route("/doc/new", post(new_doc))
343 .route("/doc/:doc_id/auth", post(auth_doc))
344 .route("/doc/:doc_id/as-update", get(get_doc_as_update_deprecated))
345 .route("/doc/:doc_id/update", post(update_doc_deprecated))
346 .route("/d/:doc_id/as-update", get(get_doc_as_update))
347 .route("/d/:doc_id/update", post(update_doc))
348 .route(
349 "/d/:doc_id/ws/:doc_id2",
350 get(handle_socket_upgrade_full_path),
351 )
352 .with_state(self.clone())
353 }
354
355 pub fn single_doc_routes(self: &Arc<Self>) -> Router {
356 Router::new()
357 .route("/ws/:doc_id", get(handle_socket_upgrade_single))
358 .route("/as-update", get(get_doc_as_update_single))
359 .route("/update", post(update_doc_single))
360 .with_state(self.clone())
361 }
362
363 async fn serve_internal(
364 self: Arc<Self>,
365 listener: TcpListener,
366 redact_errors: bool,
367 routes: Router,
368 ) -> Result<()> {
369 let token = self.cancellation_token.clone();
370
371 let mut app = if let Some(max_body_size) = self.max_body_size {
372 routes.layer(DefaultBodyLimit::max(max_body_size))
373 } else {
374 routes
375 };
376
377 app = if redact_errors {
378 app
379 } else {
380 app.layer(middleware::from_fn(Self::redact_error_middleware))
381 };
382
383 axum::serve(listener, app.into_make_service())
384 .with_graceful_shutdown(async move { token.cancelled().await })
385 .await?;
386
387 self.doc_worker_tracker.close();
388 self.doc_worker_tracker.wait().await;
389
390 Ok(())
391 }
392
393 pub async fn serve(self, listener: TcpListener, redact_errors: bool) -> Result<()> {
394 let s = Arc::new(self);
395 let routes = s.routes();
396 s.serve_internal(listener, redact_errors, routes).await
397 }
398
399 pub async fn serve_doc(self, listener: TcpListener, redact_errors: bool) -> Result<()> {
400 let s = Arc::new(self);
401 let routes = s.single_doc_routes();
402 s.serve_internal(listener, redact_errors, routes).await
403 }
404
405 fn verify_doc_token(&self, token: Option<&str>, doc: &str) -> Result<Authorization, AppError> {
406 if let Some(authenticator) = &self.authenticator {
407 if let Some(token) = token {
408 let authorization = authenticator
409 .verify_doc_token(token, doc, current_time_epoch_millis())
410 .map_err(|e| (StatusCode::UNAUTHORIZED, e))?;
411 Ok(authorization)
412 } else {
413 Err((StatusCode::UNAUTHORIZED, anyhow!("No token provided.")))?
414 }
415 } else {
416 Ok(Authorization::Full)
417 }
418 }
419
420 fn get_single_doc_id(&self) -> Result<String, AppError> {
421 self.docs
422 .iter()
423 .next()
424 .map(|entry| entry.key().clone())
425 .ok_or_else(|| AppError(StatusCode::NOT_FOUND, anyhow!("No document found")))
426 }
427}
428
429#[derive(Deserialize)]
430struct HandlerParams {
431 token: Option<String>,
432}
433
434async fn get_doc_as_update(
435 State(server_state): State<Arc<Server>>,
436 Path(doc_id): Path<String>,
437 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
438) -> Result<Response, AppError> {
439 let token = get_token_from_header(auth_header);
441 let _ = server_state.verify_doc_token(token.as_deref(), &doc_id)?;
442
443 let dwskv = server_state
444 .get_or_create_doc(&doc_id)
445 .await
446 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
447
448 let update = dwskv.as_update();
449 tracing::debug!("update: {:?}", update);
450 Ok(update.into_response())
451}
452
453async fn get_doc_as_update_deprecated(
454 Path(doc_id): Path<String>,
455 State(server_state): State<Arc<Server>>,
456 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
457) -> Result<Response, AppError> {
458 tracing::warn!("/doc/:doc_id/as-update is deprecated; call /doc/:doc_id/auth instead and then call as-update on the returned base URL.");
459 get_doc_as_update(State(server_state), Path(doc_id), auth_header).await
460}
461
462async fn update_doc_deprecated(
463 Path(doc_id): Path<String>,
464 State(server_state): State<Arc<Server>>,
465 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
466 body: Bytes,
467) -> Result<Response, AppError> {
468 tracing::warn!("/doc/:doc_id/update is deprecated; call /doc/:doc_id/auth instead and then call update on the returned base URL.");
469 update_doc(Path(doc_id), State(server_state), auth_header, body).await
470}
471
472async fn get_doc_as_update_single(
473 State(server_state): State<Arc<Server>>,
474 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
475) -> Result<Response, AppError> {
476 let doc_id = server_state.get_single_doc_id()?;
477 get_doc_as_update(State(server_state), Path(doc_id), auth_header).await
478}
479
480async fn update_doc(
481 Path(doc_id): Path<String>,
482 State(server_state): State<Arc<Server>>,
483 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
484 body: Bytes,
485) -> Result<Response, AppError> {
486 let token = get_token_from_header(auth_header);
487 let authorization = server_state.verify_doc_token(token.as_deref(), &doc_id)?;
488 update_doc_inner(doc_id, server_state, authorization, body).await
489}
490
491async fn update_doc_inner(
492 doc_id: String,
493 server_state: Arc<Server>,
494 authorization: Authorization,
495 body: Bytes,
496) -> Result<Response, AppError> {
497 if !matches!(authorization, Authorization::Full) {
498 return Err(AppError(StatusCode::FORBIDDEN, anyhow!("Unauthorized.")));
499 }
500
501 let dwskv = server_state
502 .get_or_create_doc(&doc_id)
503 .await
504 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
505
506 if let Err(err) = dwskv.apply_update(&body) {
507 tracing::error!(?err, "Failed to apply update");
508 return Err(AppError(StatusCode::INTERNAL_SERVER_ERROR, err));
509 }
510
511 Ok(StatusCode::OK.into_response())
512}
513
514async fn update_doc_single(
515 State(server_state): State<Arc<Server>>,
516 headers: HeaderMap,
517 body: Bytes,
518) -> Result<Response, AppError> {
519 let doc_id = server_state.get_single_doc_id()?;
520 let authorization = get_authorization_from_plane_header(headers)?;
523 update_doc_inner(doc_id, server_state, authorization, body).await
524}
525
526async fn handle_socket_upgrade(
527 ws: WebSocketUpgrade,
528 Path(doc_id): Path<String>,
529 authorization: Authorization,
530 State(server_state): State<Arc<Server>>,
531) -> Result<Response, AppError> {
532 if !matches!(authorization, Authorization::Full) && !server_state.docs.contains_key(&doc_id) {
533 return Err(AppError(
534 StatusCode::NOT_FOUND,
535 anyhow!("Doc {} not found", doc_id),
536 ));
537 }
538
539 let dwskv = server_state
540 .get_or_create_doc(&doc_id)
541 .await
542 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
543 let awareness = dwskv.awareness();
544 let cancellation_token = server_state.cancellation_token.clone();
545
546 Ok(ws.on_upgrade(move |socket| {
547 handle_socket(socket, awareness, authorization, cancellation_token)
548 }))
549}
550
551async fn handle_socket_upgrade_deprecated(
552 ws: WebSocketUpgrade,
553 Path(doc_id): Path<String>,
554 Query(params): Query<HandlerParams>,
555 State(server_state): State<Arc<Server>>,
556) -> Result<Response, AppError> {
557 tracing::warn!(
558 "/doc/ws/:doc_id is deprecated; call /doc/:doc_id/auth instead and use the returned URL."
559 );
560 let authorization = server_state.verify_doc_token(params.token.as_deref(), &doc_id)?;
561 handle_socket_upgrade(ws, Path(doc_id), authorization, State(server_state)).await
562}
563
564async fn handle_socket_upgrade_full_path(
565 ws: WebSocketUpgrade,
566 Path((doc_id, doc_id2)): Path<(String, String)>,
567 Query(params): Query<HandlerParams>,
568 State(server_state): State<Arc<Server>>,
569) -> Result<Response, AppError> {
570 if doc_id != doc_id2 {
571 return Err(AppError(
572 StatusCode::BAD_REQUEST,
573 anyhow!("For Yjs compatibility, the doc_id appears twice in the URL. It must be the same in both places, but we got {} and {}.", doc_id, doc_id2),
574 ));
575 }
576 let authorization = server_state.verify_doc_token(params.token.as_deref(), &doc_id)?;
577 handle_socket_upgrade(ws, Path(doc_id), authorization, State(server_state)).await
578}
579
580async fn handle_socket_upgrade_single(
581 ws: WebSocketUpgrade,
582 Path(doc_id): Path<String>,
583 headers: HeaderMap,
584 State(server_state): State<Arc<Server>>,
585) -> Result<Response, AppError> {
586 let single_doc_id = server_state.get_single_doc_id()?;
587 if doc_id != single_doc_id {
588 return Err(AppError(
589 StatusCode::NOT_FOUND,
590 anyhow!("Document not found"),
591 ));
592 }
593
594 let authorization = get_authorization_from_plane_header(headers)?;
597 handle_socket_upgrade(ws, Path(single_doc_id), authorization, State(server_state)).await
598}
599
600async fn handle_socket(
601 socket: WebSocket,
602 awareness: Arc<RwLock<Awareness>>,
603 authorization: Authorization,
604 cancellation_token: CancellationToken,
605) {
606 let (mut sink, mut stream) = socket.split();
607 let (send, mut recv) = channel(1024);
608
609 let last_pong = Arc::new(RwLock::new(tokio::time::Instant::now()));
610 let last_pong_clone = last_pong.clone();
611
612 tokio::spawn(async move {
613 let mut ticker = tokio::time::interval(PING_EVERY);
614 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
615
616 loop {
617 tokio::select! {
618 msg = recv.recv() => {
619 let Some(msg) = msg else {
620 break;
621 };
622 let _ = sink.send(Message::Binary(msg)).await;
623 }
624 _ = ticker.tick() => {
625 if last_pong_clone.read().expect("Failed to get read lock on last_pong").elapsed() > PONG_TIMEOUT {
626 tracing::info!("Pong timeout, closing connection");
627 break;
628 }
629 let _ = sink.send(Message::Ping(vec![])).await;
630 }
631 }
632 }
633 });
634
635 let connection = DocConnection::new(awareness, authorization, move |bytes| {
636 if let Err(e) = send.try_send(bytes.to_vec()) {
637 tracing::warn!(?e, "Error sending message");
638 }
639 });
640
641 loop {
642 tokio::select! {
643 msg = stream.next() => {
644 let Some(msg) = msg else {
645 break;
646 };
647 let msg = match msg {
648 Ok(Message::Binary(bytes)) => bytes,
649 Ok(Message::Close(_)) => break,
650 Ok(Message::Pong(_)) => {
651 *last_pong.write().expect("Failed to get write lock on last_pong") = tokio::time::Instant::now();
652 continue;
653 }
654 Err(_e) => {
655 continue;
658 }
659 msg => {
660 tracing::warn!(?msg, "Received non-binary message");
661 continue;
662 }
663 };
664
665 if let Err(e) = connection.send(&msg).await {
666 tracing::warn!(?e, "Error handling message");
667 }
668 }
669 _ = cancellation_token.cancelled() => {
670 tracing::debug!("Closing doc connection due to server cancel...");
671 break;
672 }
673 }
674 }
675}
676
677async fn check_store(
678 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
679 State(server_state): State<Arc<Server>>,
680) -> Result<Json<Value>, AppError> {
681 server_state.check_auth(auth_header)?;
682
683 if server_state.store.is_none() {
684 return Ok(Json(json!({"ok": false, "error": "No store set."})));
685 };
686
687 Ok(Json(json!({"ok": true})))
690}
691
692async fn check_store_deprecated(
693 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
694 State(server_state): State<Arc<Server>>,
695) -> Result<Json<Value>, AppError> {
696 tracing::warn!(
697 "GET check_store is deprecated, use POST check_store with an empty body instead."
698 );
699 check_store(auth_header, State(server_state)).await
700}
701
702async fn ready() -> Result<Json<Value>, AppError> {
704 Ok(Json(json!({"ok": true})))
705}
706
707async fn new_doc(
708 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
709 State(server_state): State<Arc<Server>>,
710 Json(body): Json<DocCreationRequest>,
711) -> Result<Json<NewDocResponse>, AppError> {
712 server_state.check_auth(auth_header)?;
713
714 let doc_id = if let Some(doc_id) = body.doc_id {
715 if !validate_doc_name(doc_id.as_str()) {
716 Err((StatusCode::BAD_REQUEST, anyhow!("Invalid document name")))?
717 }
718
719 server_state
720 .get_or_create_doc(doc_id.as_str())
721 .await
722 .map_err(|e| {
723 tracing::error!(?e, "Failed to create doc");
724 (StatusCode::INTERNAL_SERVER_ERROR, e)
725 })?;
726
727 doc_id
728 } else {
729 server_state.create_doc().await.map_err(|d| {
730 tracing::error!(?d, "Failed to create doc");
731 (StatusCode::INTERNAL_SERVER_ERROR, d)
732 })?
733 };
734
735 Ok(Json(NewDocResponse { doc_id }))
736}
737
738async fn auth_doc(
739 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
740 TypedHeader(host): TypedHeader<headers::Host>,
741 State(server_state): State<Arc<Server>>,
742 Path(doc_id): Path<String>,
743 body: Option<Json<AuthDocRequest>>,
744) -> Result<Json<ClientToken>, AppError> {
745 server_state.check_auth(auth_header)?;
746
747 let Json(AuthDocRequest {
748 authorization,
749 valid_for_seconds,
750 ..
751 }) = body.unwrap_or_default();
752
753 if !server_state.doc_exists(&doc_id).await {
754 Err((StatusCode::NOT_FOUND, anyhow!("Doc {} not found", doc_id)))?;
755 }
756
757 let valid_for_seconds = valid_for_seconds.unwrap_or(DEFAULT_EXPIRATION_SECONDS);
758 let expiration_time =
759 ExpirationTimeEpochMillis(current_time_epoch_millis() + valid_for_seconds * 1000);
760
761 let token = if let Some(auth) = &server_state.authenticator {
762 let token = auth.gen_doc_token(&doc_id, authorization, expiration_time);
763 Some(token)
764 } else {
765 None
766 };
767
768 let url = if let Some(url_prefix) = &server_state.url_prefix {
769 let mut url = url_prefix.clone();
770 let scheme = if url.scheme() == "https" { "wss" } else { "ws" };
771 url.set_scheme(scheme).unwrap();
772 url = url.join(&format!("/d/{doc_id}/ws")).unwrap();
773 url.to_string()
774 } else {
775 format!("ws://{host}/d/{doc_id}/ws")
776 };
777
778 let base_url = if let Some(url_prefix) = &server_state.url_prefix {
779 let mut url_prefix = url_prefix.to_string();
780 if !url_prefix.ends_with('/') {
781 url_prefix = format!("{url_prefix}/");
782 }
783
784 format!("{url_prefix}d/{doc_id}")
785 } else {
786 format!("http://{host}/d/{doc_id}")
787 };
788
789 Ok(Json(ClientToken {
790 url,
791 base_url: Some(base_url),
792 doc_id,
793 token,
794 authorization,
795 }))
796}
797
798fn get_token_from_header(
799 auth_header: Option<TypedHeader<headers::Authorization<headers::authorization::Bearer>>>,
800) -> Option<String> {
801 if let Some(TypedHeader(headers::Authorization(bearer))) = auth_header {
802 Some(bearer.token().to_string())
803 } else {
804 None
805 }
806}
807
808#[derive(Deserialize)]
809struct PlaneVerifiedUserData {
810 authorization: Authorization,
811}
812
813fn get_authorization_from_plane_header(headers: HeaderMap) -> Result<Authorization, AppError> {
814 if let Some(token) = headers.get(HeaderName::from_static(PLANE_VERIFIED_USER_DATA_HEADER)) {
815 let token_str = token.to_str().map_err(|e| (StatusCode::BAD_REQUEST, e))?;
816 let user_data: PlaneVerifiedUserData =
817 serde_json::from_str(token_str).map_err(|e| (StatusCode::BAD_REQUEST, e))?;
818 Ok(user_data.authorization)
819 } else {
820 Err((StatusCode::UNAUTHORIZED, anyhow!("No token provided.")))?
821 }
822}
823
824#[cfg(test)]
825mod test {
826 use super::*;
827 use y_sweet_core::api_types::Authorization;
828
829 #[tokio::test]
830 async fn test_auth_doc() {
831 let server_state = Server::new(
832 None,
833 Duration::from_secs(60),
834 None,
835 None,
836 CancellationToken::new(),
837 true,
838 None,
839 )
840 .await
841 .unwrap();
842
843 let doc_id = server_state.create_doc().await.unwrap();
844
845 let token = auth_doc(
846 None,
847 TypedHeader(headers::Host::from(http::uri::Authority::from_static(
848 "localhost",
849 ))),
850 State(Arc::new(server_state)),
851 Path(doc_id.clone()),
852 Some(Json(AuthDocRequest {
853 authorization: Authorization::Full,
854 user_id: None,
855 valid_for_seconds: None,
856 })),
857 )
858 .await
859 .unwrap();
860
861 let expected_url = format!("ws://localhost/d/{doc_id}/ws");
862 assert_eq!(token.url, expected_url);
863 assert_eq!(token.doc_id, doc_id);
864 assert!(token.token.is_none());
865 }
866
867 #[tokio::test]
868 async fn test_auth_doc_with_prefix() {
869 let prefix: Url = "https://foo.bar".parse().unwrap();
870 let server_state = Server::new(
871 None,
872 Duration::from_secs(60),
873 None,
874 Some(prefix),
875 CancellationToken::new(),
876 true,
877 None,
878 )
879 .await
880 .unwrap();
881
882 let doc_id = server_state.create_doc().await.unwrap();
883
884 let token = auth_doc(
885 None,
886 TypedHeader(headers::Host::from(http::uri::Authority::from_static(
887 "localhost",
888 ))),
889 State(Arc::new(server_state)),
890 Path(doc_id.clone()),
891 None,
892 )
893 .await
894 .unwrap();
895
896 let expected_url = format!("wss://foo.bar/d/{doc_id}/ws");
897 assert_eq!(token.url, expected_url);
898 assert_eq!(token.doc_id, doc_id);
899 assert!(token.token.is_none());
900 }
901}