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}