forks/sorcery

Mirror of https://git.t4t.associates/char/sorcery

git clone https://git.gae.moe/forks/sorcery

Charlotte Somdaemon: share test state setup via Inner::for_tests64292c7

grace-patches
4.6 KiB143 linesraw
1//! Control socket for local pushers (sorcery-ssh, post-receive hooks).
2//!
3//! Unauthenticated: the socket is only reachable by its owner, which is the
4//! same user that owns the repositories. One request per connection:
5//!
6//! ```text
7//! client:  <absolute repository path>\n
8//! server:  ok[ <site url>]\n  |  error <message>\n
9//! ```
10//!
11//! Paths outside the published root are `ok` with no URL, so private
12//! repositories can be pushed without noise.
13
14use std::io;
15use std::path::Path;
16
17use anyhow::{Context, Result};
18use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
19use tokio::net::{UnixListener, UnixStream};
20
21use super::cache::RepoEntry;
22use super::{AppState, Inner};
23
24pub(super) async fn serve(state: AppState, listener: UnixListener) {
25    loop {
26        match listener.accept().await {
27            Ok((stream, _)) => {
28                let state = state.clone();
29                tokio::spawn(async move {
30                    if let Err(error) = handle(&state, stream).await {
31                        eprintln!("sorceryd: control connection: {error}");
32                    }
33                });
34            }
35            Err(error) => eprintln!("sorceryd: control accept: {error}"),
36        }
37    }
38}
39
40async fn handle(state: &Inner, mut stream: UnixStream) -> io::Result<()> {
41    let mut line = String::new();
42    BufReader::new(&mut stream).read_line(&mut line).await?;
43    let reply = match refresh(state, Path::new(line.trim_end_matches('\n'))).await {
44        Ok(Some(url)) => format!("ok {url}\n"),
45        Ok(None) => "ok\n".to_owned(),
46        Err(error) => format!("error {error:#}\n"),
47    };
48    let result = stream.write_all(reply.as_bytes()).await;
49    state.rescan_requested.notify_one();
50    result
51}
52
53/// Rebuild the published repository at `path`, if there is one.
54async fn refresh(state: &Inner, path: &Path) -> Result<Option<String>> {
55    let path = path
56        .canonicalize()
57        .with_context(|| format!("resolving {}", path.display()))?;
58    let entry = match entry_at(state, &path) {
59        Some(entry) => Some(entry),
60        None => {
61            let root = state.config.repositories.canonicalize()?;
62            let Ok(relative) = path.strip_prefix(&root) else {
63                return Ok(None);
64            };
65            if relative.iter().count() != 2
66                || relative
67                    .iter()
68                    .any(|part| part.to_str().is_none_or(|name| name.starts_with('.')))
69            {
70                return Ok(None);
71            }
72            // First pushes need discovery before returning a working site URL.
73            state.rescan().await?;
74            entry_at(state, &path)
75        }
76    };
77    let Some(entry) = entry else {
78        return Ok(None);
79    };
80    state.build(&entry, true).await?;
81    Ok(state.config.site_url(&entry.repository))
82}
83
84fn entry_at(state: &Inner, path: &Path) -> Option<RepoEntry> {
85    state
86        .snapshot
87        .load()
88        .repos
89        .values()
90        .find(|entry| entry.repository.path == path)
91        .cloned()
92}
93
94#[cfg(test)]
95mod tests {
96    use std::fs;
97    use std::path::Path;
98
99    use anyhow::Result;
100    use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
101    use tokio::net::{UnixListener, UnixStream};
102
103    use crate::daemon::Inner;
104    use crate::generate::STATE_FILE;
105    use crate::testutil::{TempDir, commit, init_repo};
106
107    async fn request(socket: &Path, path: &Path) -> Result<String> {
108        let mut stream = UnixStream::connect(socket).await?;
109        stream
110            .write_all(format!("{}\n", path.display()).as_bytes())
111            .await?;
112        let mut reply = String::new();
113        BufReader::new(stream).read_line(&mut reply).await?;
114        Ok(reply)
115    }
116
117    #[tokio::test]
118    async fn refreshes_published_repositories_and_reports_their_urls() -> Result<()> {
119        let root = TempDir::new("control");
120        let repo = root.join("repos/alice/thing");
121        init_repo(&repo)?;
122        fs::write(repo.join("README"), "hello")?;
123        commit(&repo, "initial")?;
124
125        let socket = root.join("control");
126        let listener = UnixListener::bind(&socket)?;
127        tokio::spawn(super::serve(Inner::for_tests(&root), listener));
128
129        assert_eq!(
130            request(&socket, &repo).await?,
131            "ok https://git.example/alice/thing\n"
132        );
133        assert!(root.join("cache/alice/thing").join(STATE_FILE).is_file());
134
135        assert_eq!(request(&socket, &root.join("repos/alice")).await?, "ok\n");
136        assert!(
137            request(&socket, &root.join("missing"))
138                .await?
139                .starts_with("error ")
140        );
141        Ok(())
142    }
143}