ethersync/
editor.rs

1// SPDX-FileCopyrightText: 2024 blinry <mail@blinry.org>
2// SPDX-FileCopyrightText: 2024 zormit <nt4u@kpvn.de>
3//
4// SPDX-License-Identifier: AGPL-3.0-or-later
5
6//! This module is all about daemon to editor communication.
7use crate::cli::ask;
8use crate::daemon::{DocMessage, DocumentActorHandle};
9use crate::sandbox;
10use crate::types::EditorProtocolObject;
11use anyhow::{bail, Context, Result};
12use futures::StreamExt;
13use std::{fs, os::unix::fs::PermissionsExt, path::Path};
14use tokio::{
15    io::WriteHalf,
16    net::{UnixListener, UnixStream},
17};
18use tokio_util::{
19    bytes::BytesMut,
20    codec::{Encoder, FramedRead, FramedWrite, LinesCodec},
21};
22use tracing::{debug, info};
23
24pub type EditorId = usize;
25
26pub type EditorWriter = FramedWrite<WriteHalf<UnixStream>, EditorProtocolCodec>;
27
28#[derive(Debug)]
29pub struct EditorProtocolCodec;
30
31impl Encoder<EditorProtocolObject> for EditorProtocolCodec {
32    type Error = anyhow::Error;
33
34    fn encode(
35        &mut self,
36        item: EditorProtocolObject,
37        dst: &mut BytesMut,
38    ) -> Result<(), Self::Error> {
39        let payload = item.to_jsonrpc()?;
40        dst.extend_from_slice(format!("{payload}\n").as_bytes());
41        Ok(())
42    }
43}
44
45fn is_user_readable_only(socket_path: &Path) -> Result<()> {
46    let parent_dir = socket_path
47        .parent()
48        .context("The socket path should not be the root directory")?;
49    let current_permissions = fs::metadata(parent_dir)
50        .context("Expected to have access to metadata of the socket path's parent")?
51        .permissions()
52        .mode();
53    // Group and others should not have any permissions.
54    let allowed_permissions = 0o77700u32;
55    if current_permissions | allowed_permissions != allowed_permissions {
56        bail!("For security reasons, the parent directory of the socket must only be accessible by the current user. Please run `chmod go-rwx {:?}`", parent_dir);
57    }
58    Ok(())
59}
60
61/// # Panics
62///
63/// Will panic if we fail to listen on the socket, or if we fail to accept an incoming connection.
64pub fn spawn_socket_listener(
65    socket_path: &Path,
66    document_handle: DocumentActorHandle,
67) -> Result<()> {
68    // Make sure the parent directory of the socket is only accessible by the current user.
69    if let Err(description) = is_user_readable_only(socket_path) {
70        panic!("{}", description);
71    }
72
73    // Using the sandbox method here is technically unnecessary,
74    // but we want to really run all path operations through the sandbox module.
75    // TODO: Use correct directory as guard.
76    if sandbox::exists(Path::new("/"), Path::new(&socket_path))
77        .expect("Failed to check existence of path")
78    {
79        let socket_path_display = socket_path.display();
80        let remove_socket = ask(&format!("Detected an existing socket '{socket_path_display}'. There might be a daemon running already for this directory, or the previous one crashed. Do you want to continue?"));
81        if remove_socket? {
82            sandbox::remove_file(Path::new("/"), socket_path).expect("Could not remove socket");
83        } else {
84            bail!("Not continuing, make sure to stop all other daemons on this directory");
85        }
86    }
87
88    let listener = UnixListener::bind(socket_path)?;
89    debug!("Listening on UNIX socket: {}", socket_path.display());
90
91    tokio::spawn(async move {
92        loop {
93            match listener.accept().await {
94                Ok((stream, _addr)) => {
95                    let id = document_handle.clone().next_editor_id();
96                    let document_handle_clone = document_handle.clone();
97                    tokio::spawn(async move {
98                        handle_editor_connection(stream, document_handle_clone.clone(), id).await;
99                    })
100                }
101                Err(err) => {
102                    panic!("Error while accepting socket connection: {err}");
103                }
104            };
105        }
106    });
107
108    Ok(())
109}
110
111async fn handle_editor_connection(
112    stream: UnixStream,
113    document_handle: DocumentActorHandle,
114    editor_id: EditorId,
115) {
116    let (stream_read, stream_write) = tokio::io::split(stream);
117    let mut reader = FramedRead::new(stream_read, LinesCodec::new());
118    let writer = FramedWrite::new(stream_write, EditorProtocolCodec);
119
120    document_handle
121        .send_message(DocMessage::NewEditorConnection(editor_id, writer))
122        .await;
123    info!("Editor #{editor_id} connected.");
124
125    while let Some(Ok(line)) = reader.next().await {
126        document_handle
127            .send_message(DocMessage::FromEditor(editor_id, line))
128            .await;
129    }
130
131    document_handle
132        .send_message(DocMessage::CloseEditorConnection(editor_id))
133        .await;
134    info!("Editor #{editor_id} disconnected.");
135}