Skip to main content

rns_git/
server.rs

1use std::path::PathBuf;
2use std::thread;
3use std::time::Duration;
4
5use rns_core::display::prettyb256rep;
6use rns_core::types::IdentityHash;
7use rns_crypto::identity::Identity;
8use rns_net::link_manager::ResourceStrategy;
9use rns_net::{
10    AnnouncedIdentity, Callbacks, DestHash, Destination, PacketHash, RequestResponse, RnsNode,
11};
12
13use crate::acl::{Access, Operation};
14use crate::config::ServerConfig;
15use crate::logging;
16use crate::protocol;
17use crate::util::{default_reticulum_dir, default_rngit_dir, hex, load_or_create_identity};
18use crate::{git, Error, Result};
19
20pub fn main<I>(args: I) -> Result<()>
21where
22    I: IntoIterator<Item = String>,
23{
24    let options = ServerOptions::parse(args)?;
25    git::check_git_available()?;
26
27    let rngit_dir = options.config_dir.unwrap_or_else(default_rngit_dir);
28    let rns_dir = options.rns_config_dir.or_else(default_reticulum_dir);
29    let (config, created) = ServerConfig::load_or_create(rngit_dir, rns_dir)?;
30    logging::init_file_logger(&config.dir.join("server_log"), config.log_level)?;
31    if created {
32        return Err(Error::msg(format!(
33            "created default config at {}; edit it and run rngit again",
34            config.dir.join("server_config").display()
35        )));
36    }
37
38    let identity = load_or_create_identity(&config.identity_path)?;
39    if options.print_identity {
40        let client = load_or_create_identity(&config.client_identity_path)?;
41        print_identity(&identity, &client, options.base256);
42        return Ok(());
43    }
44
45    run_server(config, identity)
46}
47
48pub fn run_server(config: ServerConfig, identity: Identity) -> Result<()> {
49    let node = RnsNode::from_config(
50        config.reticulum_dir.as_deref(),
51        Box::<ServerCallbacks>::default(),
52    )?;
53
54    let announce_interval = Duration::from_secs(config.announce_interval_secs);
55    let destination = register_repository_destination(&node, config, &identity)?;
56
57    loop {
58        thread::sleep(announce_interval);
59        let _ = node.announce(&destination, &identity, None);
60    }
61}
62
63pub fn register_repository_destination(
64    node: &RnsNode,
65    config: ServerConfig,
66    identity: &Identity,
67) -> Result<Destination> {
68    crate::util::ensure_dir(&config.repositories_dir)?;
69    let access = Access::new(
70        &config.allow_read,
71        &config.allow_write,
72        config.repositories_dir.clone(),
73    )?;
74    let destination = Destination::single_in(
75        protocol::APP_NAME,
76        &[protocol::ASPECT_REPOSITORIES],
77        IdentityHash(*identity.hash()),
78    );
79    let public_key = identity
80        .get_public_key()
81        .ok_or_else(|| Error::msg("repository identity has no public key"))?;
82    let private_key = identity
83        .get_private_key()
84        .ok_or_else(|| Error::msg("repository identity has no private key"))?;
85    let sig_prv: [u8; 32] = private_key[32..64].try_into().unwrap();
86    let sig_pub: [u8; 32] = public_key[32..64].try_into().unwrap();
87
88    node.register_link_destination(
89        destination.hash.0,
90        sig_prv,
91        sig_pub,
92        ResourceStrategy::AcceptAll as u8,
93    )
94    .map_err(|_| Error::msg("failed to register link destination"))?;
95    register_handlers(node, config, access)?;
96    node.announce(&destination, identity, None)
97        .map_err(|_| Error::msg("failed to announce rngit destination"))?;
98    Ok(destination)
99}
100
101fn register_handlers(node: &RnsNode, config: ServerConfig, access: Access) -> Result<()> {
102    let list_config = config.clone();
103    let list_access = access.clone();
104    node.register_request_handler(
105        protocol::PATH_LIST,
106        None,
107        move |_link, _path, data, remote| {
108            Some(
109                handle_list(&list_config, &list_access, data, remote)
110                    .unwrap_or_else(error_response),
111            )
112        },
113    )
114    .map_err(|_| Error::msg("failed to register list handler"))?;
115
116    let fetch_config = config.clone();
117    let fetch_access = access.clone();
118    node.register_request_handler_response(
119        protocol::PATH_FETCH,
120        None,
121        move |_link, _path, data, remote| {
122            Some(
123                handle_fetch(&fetch_config, &fetch_access, data, remote)
124                    .unwrap_or_else(|err| RequestResponse::Bytes(error_response(err))),
125            )
126        },
127    )
128    .map_err(|_| Error::msg("failed to register fetch handler"))?;
129
130    let push_config = config.clone();
131    let push_access = access.clone();
132    node.register_request_handler(
133        protocol::PATH_PUSH,
134        None,
135        move |_link, _path, data, remote| {
136            Some(
137                handle_push(&push_config, &push_access, data, remote)
138                    .unwrap_or_else(error_response),
139            )
140        },
141    )
142    .map_err(|_| Error::msg("failed to register push handler"))?;
143
144    node.register_request_handler(
145        protocol::PATH_DELETE,
146        None,
147        move |_link, _path, data, remote| {
148            Some(handle_delete(&config, &access, data, remote).unwrap_or_else(error_response))
149        },
150    )
151    .map_err(|_| Error::msg("failed to register delete handler"))?;
152
153    Ok(())
154}
155
156pub fn handle_list(
157    config: &ServerConfig,
158    access: &Access,
159    data: &[u8],
160    remote: Option<&([u8; 16], [u8; 64])>,
161) -> Result<Vec<u8>> {
162    let repo = protocol::repository_from_request(data)?;
163    let remote_hash = remote.map(|(hash, _)| hash);
164    if !access.allows(Operation::Read, &repo, remote_hash)? {
165        return Ok(protocol::status_bytes(
166            protocol::RES_DISALLOWED,
167            b"read denied",
168        ));
169    }
170    let path = git::repository_path(&config.repositories_dir, &repo)?;
171    match git::list_refs_text(&path) {
172        Ok(refs) => Ok(protocol::status_bytes(protocol::RES_OK, refs)),
173        Err(err) if err.to_string() == "repository not found" => Ok(protocol::status_bytes(
174            protocol::RES_NOT_FOUND,
175            b"repository not found",
176        )),
177        Err(err) => Ok(protocol::status_bytes(
178            protocol::RES_REMOTE_FAIL,
179            err.to_string(),
180        )),
181    }
182}
183
184pub fn handle_fetch(
185    config: &ServerConfig,
186    access: &Access,
187    data: &[u8],
188    remote: Option<&([u8; 16], [u8; 64])>,
189) -> Result<RequestResponse> {
190    let (repo, have) = protocol::parse_fetch_request(data)?;
191    let remote_hash = remote.map(|(hash, _)| hash);
192    if !access.allows(Operation::Read, &repo, remote_hash)? {
193        return Ok(RequestResponse::Bytes(protocol::status_bytes(
194            protocol::RES_DISALLOWED,
195            b"read denied",
196        )));
197    }
198    let path = git::repository_path(&config.repositories_dir, &repo)?;
199    match git::create_bundle(&path, &have) {
200        Ok(bundle) if bundle.is_empty() => Ok(RequestResponse::Bytes(protocol::status_bytes(
201            protocol::RES_OK,
202            Vec::new(),
203        ))),
204        Ok(bundle) => Ok(RequestResponse::Resource {
205            data: bundle,
206            metadata: Some(protocol::metadata_status(protocol::RES_OK)),
207            auto_compress: true,
208        }),
209        Err(err) if err.to_string() == "repository not found" => Ok(RequestResponse::Bytes(
210            protocol::status_bytes(protocol::RES_NOT_FOUND, b"repository not found"),
211        )),
212        Err(err) => Ok(RequestResponse::Bytes(protocol::status_bytes(
213            protocol::RES_REMOTE_FAIL,
214            err.to_string(),
215        ))),
216    }
217}
218
219pub fn handle_push(
220    config: &ServerConfig,
221    access: &Access,
222    data: &[u8],
223    remote: Option<&([u8; 16], [u8; 64])>,
224) -> Result<Vec<u8>> {
225    let (repo, bundle, updates) = protocol::parse_push_request(data)?;
226    let remote_hash = remote.map(|(hash, _)| hash);
227    if !access.allows(Operation::Write, &repo, remote_hash)? {
228        return Ok(protocol::status_bytes(
229            protocol::RES_DISALLOWED,
230            b"write denied",
231        ));
232    }
233    let path = git::repository_path(&config.repositories_dir, &repo)?;
234    match git::apply_push(&path, &bundle, &updates) {
235        Ok(()) => Ok(protocol::status_bytes(protocol::RES_OK, b"ok")),
236        Err(err) => Ok(protocol::status_bytes(
237            protocol::RES_REMOTE_FAIL,
238            err.to_string(),
239        )),
240    }
241}
242
243pub fn handle_delete(
244    config: &ServerConfig,
245    access: &Access,
246    data: &[u8],
247    remote: Option<&([u8; 16], [u8; 64])>,
248) -> Result<Vec<u8>> {
249    let repo = protocol::repository_from_request(data)?;
250    let remote_hash = remote.map(|(hash, _)| hash);
251    if !access.allows(Operation::Write, &repo, remote_hash)? {
252        return Ok(protocol::status_bytes(
253            protocol::RES_DISALLOWED,
254            b"write denied",
255        ));
256    }
257    let path = git::repository_path(&config.repositories_dir, &repo)?;
258    if !path.exists() {
259        return Ok(protocol::status_bytes(
260            protocol::RES_NOT_FOUND,
261            b"repository not found",
262        ));
263    }
264    std::fs::remove_dir_all(path)?;
265    Ok(protocol::status_bytes(protocol::RES_OK, b"deleted"))
266}
267
268fn error_response(err: Error) -> Vec<u8> {
269    protocol::status_bytes(protocol::RES_INVALID_REQ, err.to_string())
270}
271
272fn print_identity(identity: &Identity, client: &Identity, base256: bool) {
273    let destination = Destination::single_in(
274        protocol::APP_NAME,
275        &[protocol::ASPECT_REPOSITORIES],
276        IdentityHash(*identity.hash()),
277    );
278    println!("client_identity = {}", hex(client.hash()));
279    if base256 {
280        println!("client_identity_b256 = {}", prettyb256rep(client.hash()));
281    }
282    println!("repository_identity = {}", hex(identity.hash()));
283    if base256 {
284        println!(
285            "repository_identity_b256 = {}",
286            prettyb256rep(identity.hash())
287        );
288    }
289    println!("destination = {}", hex(&destination.hash.0));
290    if base256 {
291        println!("destination_b256 = {}", prettyb256rep(&destination.hash.0));
292    }
293}
294
295#[derive(Default)]
296struct ServerCallbacks;
297
298impl Callbacks for ServerCallbacks {
299    fn on_announce(&mut self, _announced: AnnouncedIdentity) {}
300
301    fn on_path_updated(&mut self, _dest_hash: DestHash, _hops: u8) {}
302
303    fn on_local_delivery(&mut self, _dest_hash: DestHash, _raw: Vec<u8>, _packet_hash: PacketHash) {
304    }
305}
306
307#[derive(Debug, Default)]
308struct ServerOptions {
309    config_dir: Option<PathBuf>,
310    rns_config_dir: Option<PathBuf>,
311    print_identity: bool,
312    base256: bool,
313}
314
315impl ServerOptions {
316    fn parse<I>(args: I) -> Result<Self>
317    where
318        I: IntoIterator<Item = String>,
319    {
320        let mut options = ServerOptions::default();
321        let mut args = args.into_iter();
322        while let Some(arg) = args.next() {
323            match arg.as_str() {
324                "-c" | "--config" => {
325                    options.config_dir = Some(PathBuf::from(
326                        args.next()
327                            .ok_or_else(|| Error::msg("missing config path"))?,
328                    ));
329                }
330                "--rnsconfig" => {
331                    options.rns_config_dir = Some(PathBuf::from(
332                        args.next()
333                            .ok_or_else(|| Error::msg("missing RNS config path"))?,
334                    ));
335                }
336                "--print-identity" => options.print_identity = true,
337                "-Z" | "--base256" => options.base256 = true,
338                "--service" | "--interactive" => {}
339                "-h" | "--help" => return Err(Error::msg(usage())),
340                other => {
341                    return Err(Error::msg(format!(
342                        "unknown argument: {other}\n{}",
343                        usage()
344                    )))
345                }
346            }
347        }
348        Ok(options)
349    }
350}
351
352fn usage() -> &'static str {
353    "usage: rngit [--config DIR] [--rnsconfig DIR] [--print-identity] [-Z|--base256] [--service]"
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    fn cfg(root: &std::path::Path) -> ServerConfig {
361        ServerConfig {
362            dir: root.to_path_buf(),
363            reticulum_dir: None,
364            repositories_dir: root.join("repositories"),
365            identity_path: root.join("repositories_identity"),
366            client_identity_path: root.join("client_identity"),
367            announce_interval_secs: 300,
368            allow_read: vec!["all".into()],
369            allow_write: vec!["all".into()],
370            log_level: logging::DEFAULT_LOG_LEVEL,
371        }
372    }
373
374    #[test]
375    fn parses_base256_print_identity_options() {
376        let opts = ServerOptions::parse(vec![
377            "--print-identity".to_string(),
378            "--base256".to_string(),
379        ])
380        .unwrap();
381        assert!(opts.print_identity);
382        assert!(opts.base256);
383
384        let short = ServerOptions::parse(vec!["-Z".to_string()]).unwrap();
385        assert!(short.base256);
386    }
387
388    #[test]
389    fn list_missing_repo_returns_not_found_status() {
390        let tmp = tempfile::tempdir().unwrap();
391        let config = cfg(tmp.path());
392        let access = Access::new(
393            &config.allow_read,
394            &config.allow_write,
395            config.repositories_dir.clone(),
396        )
397        .unwrap();
398        let req = protocol::repository_request("group/repo");
399        let resp = handle_list(&config, &access, &req, None).unwrap();
400        assert_eq!(resp[0], protocol::RES_NOT_FOUND);
401    }
402
403    #[test]
404    fn push_is_blocked_by_acl() {
405        let tmp = tempfile::tempdir().unwrap();
406        let mut config = cfg(tmp.path());
407        config.allow_write = vec!["none".into()];
408        let access = Access::new(
409            &config.allow_read,
410            &config.allow_write,
411            config.repositories_dir.clone(),
412        )
413        .unwrap();
414        let req = protocol::push_request("repo", Vec::new(), Vec::new());
415        let resp = handle_push(&config, &access, &req, None).unwrap();
416        assert_eq!(resp[0], protocol::RES_DISALLOWED);
417    }
418
419    #[test]
420    fn fetch_existing_repo_can_return_ok_status_or_resource() {
421        let tmp = tempfile::tempdir().unwrap();
422        let config = cfg(tmp.path());
423        let access = Access::new(
424            &config.allow_read,
425            &config.allow_write,
426            config.repositories_dir.clone(),
427        )
428        .unwrap();
429        let repo = config.repositories_dir.join("repo");
430        git::ensure_bare_repository(&repo).unwrap();
431        let req = protocol::fetch_request("repo", &[]);
432        match handle_fetch(&config, &access, &req, None).unwrap() {
433            RequestResponse::Bytes(bytes) => assert_eq!(bytes[0], protocol::RES_OK),
434            RequestResponse::Resource { metadata, .. } => assert!(metadata.is_some()),
435        }
436    }
437}