Oregami
Repositories/oxedyne/ore

oxedyne/ore/relay/src/main.rs

12.6 KiB, 31 runs

created by r2848102244:147, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! `ore-relay` -- the always-on half of a sync.
2//!
3//! A separate program rather than a verb of `ore`, and deliberately so. `ore` has
4//! ten verbs and that is the budget; more to the point, a relay is a different
5//! thing from a working copy -- it has no files, authors nothing, and is run by
6//! whoever owns the machine rather than by whoever is writing.
7//!
8//! ```text
9//! ore-relay serve <data> [--port <n>] [--host <addr>] [--reply-bytes <n>]
10//! [--post-bytes <n>] [--reading-bytes <n>]
11//! ore-relay create <account>/<name> --owner <key> [--public]
12//! ore-relay grant <account>/<name> <key> <pull|push|admin>
13//! ore-relay list
14//! ```
15//!
16//! A key is written the way `ore init` and `ore key` print it. Creation is an
17//! administrator's act at this rung: a client that pushes to a repository nobody
18//! made is told so rather than making one.
19
20use ore_relay::acl::{
21 Acl,
22 Role,
23};
24use ore_relay::host::Host;
25use ore_relay::serve;
26
27use ore_store::keys::{
28 public_of,
29 text_of,
30};
31
32use oxedyne_fe2o3_core::prelude::*;
33
34use std::env;
35use std::str::FromStr;
36use std::net::{
37 IpAddr,
38 SocketAddr,
39};
40use std::path::PathBuf;
41
42
43/// What the program prints when it is asked for nothing it knows.
44const USAGE: &str = "\
45ore-relay -- a machine that holds Ore histories and authors none
46
47usage:
48 ore-relay serve <data-dir> [--host <addr>] [--port <n>] [--log <level>]
49 [--reply-bytes <n>] [--post-bytes <n>] [--reading-bytes <n>]
50 answer syncs for the repositories under <data-dir>
51 ore-relay create <account>/<name> --owner <key> [--public]
52 make an empty hosted repository
53 ore-relay grant <account>/<name> <key> <pull|push|admin>
54 let a key read or write one
55 ore-relay list <data-dir>
56 say what this relay holds
57
58create and grant take the data directory from --data, or the working directory.
59
60The relay is never an authority. It verifies nothing on the way in, because a
61signature it could check is one the puller must check anyway, and it carries key
62bindings without being able to mint them: a binding is signed by the key it
63binds, so a fabricated one fails its own signature. A relay that fails, lies or
64withholds delays convergence exactly as a partition would, and can corrupt no
65history and block no local work.";
66
67
68/// Sets how much of the library's own logging is printed.
69///
70/// A relay is a server and a server watching itself is useful, but the default
71/// is to trace every byte of every exchange, which drowns the lines this program
72/// writes about what it is holding. Errors only, unless somebody asks for more.
73fn logging(level: &str)
74 -> Outcome<()>
75{
76 let mut cfg = log_get_config!();
77 cfg.file = None;
78 log_set_config!(cfg);
79 log_set_level!(fmt!("{}", level));
80 Ok(())
81}
82
83/// Runs the subcommand the arguments name.
84fn run(args: &[String])
85 -> Outcome<()>
86{
87 let verb = match args.first() {
88 Some(v) => v.as_str(),
89 None => {
90 println!("{}", USAGE);
91 return Ok(());
92 },
93 };
94 let rest = &args[1..];
95 match verb {
96 "serve" => serve_at(rest),
97 "create" => create(rest),
98 "grant" => grant(rest),
99 "list" => list(rest),
100 "help" | "-h" | "--help" => {
101 println!("{}", USAGE);
102 Ok(())
103 },
104 other => Err(err!(
105 "There is no command {:?}. There are four: serve, create, grant, list.",
106 other;
107 Invalid, Input)),
108 }
109}
110
111/// Returns the value of a named option, where it was given.
112fn option(args: &[String], name: &str)
113 -> Outcome<Option<String>>
114{
115 let mut at = 0usize;
116 while at < args.len() {
117 if args[at] == name {
118 return match args.get(at + 1) {
119 Some(v) => Ok(Some(v.clone())),
120 None => Err(err!(
121 "{} needs a value after it.", name;
122 Invalid, Input, Missing)),
123 };
124 }
125 at += 1;
126 }
127 Ok(None)
128}
129
130/// Returns the positional arguments, which are those not part of an option.
131fn plain(args: &[String], taking: &[&str]) -> Vec<String> {
132 let mut out = Vec::new();
133 let mut at = 0usize;
134 while at < args.len() {
135 let arg = &args[at];
136 if taking.contains(&arg.as_str()) {
137 at += 2;
138 continue;
139 }
140 if arg.starts_with("--") {
141 at += 1;
142 continue;
143 }
144 out.push(arg.clone());
145 at += 1;
146 }
147 out
148}
149
150/// Returns the data directory a command was pointed at.
151fn data_of(args: &[String], positional: Option<&String>)
152 -> Outcome<PathBuf>
153{
154 if let Some(named) = res!(option(args, "--data")) {
155 return Ok(PathBuf::from(named));
156 }
157 match positional {
158 Some(p) => Ok(PathBuf::from(p)),
159 None => match env::current_dir() {
160 Ok(d) => Ok(d),
161 Err(e) => Err(err!(e,
162 "The current directory could not be read.";
163 IO, File, Read)),
164 },
165 }
166}
167
168/// Splits `<account>/<name>` into its two labels.
169fn label_of(said: &str)
170 -> Outcome<(String, String)>
171{
172 let parts: Vec<&str> = said.split('/').collect();
173 if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
174 return Err(err!(
175 "A repository is named <account>/<name>, and {:?} is not.", said;
176 Invalid, Input));
177 }
178 Ok((fmt!("{}", parts[0]), fmt!("{}", parts[1])))
179}
180
181/// `ore-relay serve` -- answers syncs until it is stopped.
182fn serve_at(args: &[String])
183 -> Outcome<()>
184{
185 let positional = plain(args, &[
186 "--host", "--port", "--data", "--log", "--reply-bytes", "--post-bytes",
187 "--reading-bytes",
188 ]);
189 let dir = res!(data_of(args, positional.first()));
190 if let Some(level) = res!(option(args, "--log")) {
191 res!(logging(&level));
192 }
193 let host = match res!(option(args, "--host")) {
194 Some(h) => h,
195 None => fmt!("127.0.0.1"),
196 };
197 let port = match res!(option(args, "--port")) {
198 Some(p) => match p.parse::<u16>() {
199 Ok(n) => n,
200 Err(e) => return Err(err!(e,
201 "The port {:?} is not a number between 0 and 65535.", p;
202 Invalid, Input)),
203 },
204 None => 8420,
205 };
206 let ip = match host.parse::<IpAddr>() {
207 Ok(a) => a,
208 Err(e) => return Err(err!(e,
209 "The address {:?} is not one this relay can bind.", host;
210 Invalid, Input)),
211 };
212 let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
213 Ok(r) => r,
214 Err(e) => return Err(err!(e,
215 "The relay could not start a runtime.";
216 IO, Init)),
217 };
218 // What one reply may carry. An operator serving callers with less memory than
219 // this relay has may lower it; what it costs a caller is further visits, and
220 // what it saves the caller is materialising a whole clone as one body.
221 //
222 // Not a way to make a clone cheap on the receiving end. Measured 2026-08-20,
223 // bounding a 58 MB clone into fourteen replies moved the receiving peak by
224 // under one percent, because that peak is the per-operation cost of holding the
225 // history rather than the body.
226 //
227 // It is a way to make one cheap on this end, which it was not until 2026-08-23:
228 // the owed turn was built whole and truncated after, so a lower bound cost this
229 // relay more work rather than less. `serve::outgoing` stops at the bound now,
230 // and a clone of fe2o3's history at six mebibytes went from 2.48 s of this
231 // relay's processor to 0.96 s.
232 let mut host = Host::at(&dir);
233 if let Some(b) = res!(option(args, "--reply-bytes")) {
234 host = match b.parse::<usize>() {
235 Ok(n) if n > 0 => host.with_reply_bytes(n),
236 _ => return Err(err!(
237 "The reply bound {:?} is not a positive number of bytes.", b;
238 Invalid, Input)),
239 };
240 }
241 // What one REQUEST may carry, which is the number a proxy in front of this
242 // relay decides and this relay could only guess at until now. It is published
243 // on `GET /ore` and beside the bindings, a client posts the smaller of it and
244 // its own ceiling, and a body over it is refused here with a sentence rather
245 // than by the proxy with a closed connection. Set it to what the proxy takes,
246 // less room for the headers.
247 if let Some(b) = res!(option(args, "--post-bytes")) {
248 host = match b.parse::<usize>() {
249 Ok(n) if n > 0 => host.with_post_bytes(n),
250 _ => return Err(err!(
251 "The request bound {:?} is not a positive number of bytes.", b;
252 Invalid, Input)),
253 };
254 }
255 // What the logs this relay is holding may come to. It holds each hosted
256 // repository as it last read it and extends that reading over whatever has been
257 // appended since, so that serving a read costs the bytes since the last request
258 // rather than the whole history: measured on a copy of fe2o3, a thirty-two
259 // request clone went from 2,801 MB of reads and 16.2 s of this relay's processor
260 // to 1.6 MB and 2.5 s.
261 //
262 // Lower it on a host with less memory than `reading::READING_BUDGET` assumes;
263 // zero holds nothing, which is this relay as it stood before, reading the whole
264 // history on every request.
265 if let Some(b) = res!(option(args, "--reading-bytes")) {
266 host = match b.parse::<u64>() {
267 Ok(n) => host.with_reading_bytes(n),
268 Err(e) => return Err(err!(e,
269 "The reading bound {:?} is not a number of bytes.", b;
270 Invalid, Input)),
271 };
272 }
273 runtime.block_on(serve::listen(host, SocketAddr::new(ip, port)))
274}
275
276/// `ore-relay create` -- makes an empty hosted repository.
277fn create(args: &[String])
278 -> Outcome<()>
279{
280 let positional = plain(args, &["--owner", "--data"]);
281 let said = match positional.first() {
282 Some(s) => s,
283 None => return Err(err!(
284 "`ore-relay create` needs one argument: the repository, as \
285 <account>/<name>.";
286 Invalid, Input, Missing)),
287 };
288 let (account, name) = res!(label_of(said));
289 let owner = match res!(option(args, "--owner")) {
290 Some(k) => res!(public_of(&k)),
291 None => return Err(err!(
292 "`ore-relay create` needs --owner, the public key of whoever owns the \
293 repository. `ore init` and `ore key` print it.";
294 Invalid, Input, Missing)),
295 };
296 let public = args.iter().any(|a| a == "--public");
297 let dir = res!(data_of(args, positional.get(1)));
298 let host = Host::at(&dir);
299 let hosted = res!(host.create(&account, &name, &Acl::new(owner.clone(), public)));
300 println!("created {} at {}", hosted.label, hosted.dir.display());
301 println!("owner {}", text_of(&owner));
302 println!("access {}", if public {
303 "anyone may pull; the owner and whoever is granted may push"
304 } else {
305 "the owner, and whoever is granted"
306 });
307 Ok(())
308}
309
310/// `ore-relay grant` -- lets a key read or write a hosted repository.
311fn grant(args: &[String])
312 -> Outcome<()>
313{
314 let positional = plain(args, &["--data"]);
315 if positional.len() < 3 {
316 return Err(err!(
317 "`ore-relay grant` needs three arguments: the repository as \
318 <account>/<name>, the public key, and the role.";
319 Invalid, Input, Missing));
320 }
321 let (account, name) = res!(label_of(&positional[0]));
322 let key = res!(public_of(&positional[1]));
323 let role = res!(Role::of(positional[2].trim()));
324 let dir = res!(data_of(args, positional.get(3)));
325 let host = Host::at(&dir);
326 let hosted = match res!(host.open(&account, &name)) {
327 Some(h) => h,
328 None => return Err(err!(
329 "This relay does not hold {}/{}.", account, name;
330 Invalid, Input, Missing)),
331 };
332 let mut acl = hosted.acl;
333 acl.grant(key.clone(), role);
334 res!(acl.write(&hosted.dir));
335 println!("granted {} on {} to {}", role.name(), hosted.label, text_of(&key));
336 Ok(())
337}
338
339/// `ore-relay list` -- says what this relay holds.
340fn list(args: &[String])
341 -> Outcome<()>
342{
343 let positional = plain(args, &["--data"]);
344 let dir = res!(data_of(args, positional.first()));
345 let host = Host::at(&dir);
346 let accounts = match std::fs::read_dir(&host.dir) {
347 Ok(e) => e,
348 Err(e) => return Err(err!(e,
349 "The data directory {:?} could not be read.", host.dir;
350 IO, File, Read)),
351 };
352 let mut found = 0usize;
353 let mut names: Vec<String> = Vec::new();
354 for account in accounts {
355 let account = res!(account);
356 if !res!(account.file_type()).is_dir() {
357 continue;
358 }
359 let label = account.file_name().to_string_lossy().into_owned();
360 for repo in res!(std::fs::read_dir(account.path())) {
361 let repo = res!(repo);
362 if !res!(repo.file_type()).is_dir() {
363 continue;
364 }
365 names.push(fmt!("{}/{}", label, repo.file_name().to_string_lossy()));
366 }
367 }
368 names.sort();
369 for name in &names {
370 let (account, repo) = res!(label_of(name));
371 match res!(host.open(&account, &repo)) {
372 Some(hosted) => {
373 let replayed = res!(hosted.store.replay(
374 ore_store::store::Verify::Nothing,
375 ore_store::store::Keep::Nothing,
376 ));
377 println!("{:<32} {} operation{}, {} binding{}",
378 hosted.label,
379 replayed.log.len(),
380 if replayed.log.len() == 1 { "" } else { "s" },
381 res!(hosted.bindings()).len(),
382 if res!(hosted.bindings()).len() == 1 { "" } else { "s" },
383 );
384 found += 1;
385 },
386 None => (),
387 }
388 }
389 if found == 0 {
390 println!("this relay holds nothing yet; `ore-relay create` makes a repository");
391 }
392 Ok(())
393}
394
395/// Runs the command and reports a failure on standard error.
396fn main() {
397 let args: Vec<String> = env::args().skip(1).collect();
398 if let Err(e) = logging("error") {
399 eprintln!("ore-relay: {}", e.plain());
400 std::process::exit(1);
401 }
402 match run(&args) {
403 Ok(()) => (),
404 Err(e) => {
405 eprintln!("ore-relay: {}", e.plain());
406 std::process::exit(1);
407 },
408 }
409}