1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
//! What the daemon was started with.
//!
//! The daemon serves the MACHINE, so it takes no worktree: it registers them
//! as commands arrive and as the operator adds them. `--worktree` remains as
//! the one a start may name so that the first page has something on it, and it
//! is optional for the same reason — a daemon started at login belongs to no
//! directory in particular.
//!
//! Everything else is required. The command line carries the policy — which
//! port the workbench is on, which port peers dial — and a daemon that guessed
//! one of them would be running under settings nobody chose.
use std::net::{IpAddr, Ipv4Addr};
use std::path::PathBuf;
/// A fully-validated daemon configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
/// A worktree to register at startup, absolute.
///
/// `None` for a daemon started without one: it registers worktrees as
/// commands are run in them. Not "the daemon's worktree" — there is no
/// such thing any more.
pub worktree_root: Option<PathBuf>,
/// The address the workbench is served on.
///
/// Loopback unless something asked otherwise. The workbench has no
/// authentication: it reads and edits the worktree and runs git on it —
/// so reaching it is reaching this machine, and that is not
/// something to be one default away from.
pub listen: IpAddr,
/// The port the workbench is served on.
pub http_port: u16,
/// The address PEERS reach this machine on.
///
/// Every interface by default, which is the opposite of the workbench and
/// deliberately so — the two listeners protect different things:
///
/// * The workbench is unauthenticated HTTP. Anyone who reaches it has this
/// machine, so it stays on loopback until somebody says otherwise.
/// * The peer port answers only machines whose ed25519 identity this one
/// has been paired with by hand, over a session that is key-exchanged
/// and encrypted. An unpaired connection gets no further than the
/// handshake, and there is nothing in it to read.
///
/// A peer listener on loopback can be reached by no peer at all, which
/// makes the feature it exists for impossible without a tunnel. So it
/// binds where peers are, and `--peer-listen` narrows it.
pub peer_listen: IpAddr,
/// The port peers connect to. Fixed rather than floating: a peer is
/// reached at an address a person typed once, and a port that moved with
/// every restart would have to be re-typed.
pub peer_port: u16,
/// Whether to log what the workbench is doing.
pub debug: bool,
/// A process to die with, when something embeds this daemon.
///
/// `None` for the ordinary daemon, which outlives every command line —
/// that is what it is for. An application that spawns its own (the desktop
/// clients) passes its pid so the daemon does not survive it, since
/// nothing would ever stop it afterwards.
pub parent_pid: Option<u32>,
}
impl Config {
/// Parse `--worktree`, `--port`, `--listen`, `--peer-listen`,
/// `--peer-port`, `--debug` and `--parent-pid`.
///
/// A missing flag, a missing value, a relative worktree, a port of zero
/// and an unknown flag are all refusals. A daemon is started by a program
/// (see `wsb::daemon::launch`), so a malformed argument is a defect in the
/// caller rather than a typo to be lenient about.
pub fn from_args<I>(args: I) -> Result<Self, String>
where
I: IntoIterator<Item = String>,
{
let mut worktree: Option<String> = None;
let mut port: Option<String> = None;
let mut peer_port: Option<String> = None;
let mut listen: Option<String> = None;
let mut peer_listen: Option<String> = None;
let mut debug = false;
let mut parent: Option<String> = None;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--worktree" => worktree = Some(expect_value(&mut iter, "--worktree")?),
"--port" => port = Some(expect_value(&mut iter, "--port")?),
"--peer-port" => peer_port = Some(expect_value(&mut iter, "--peer-port")?),
"--listen" => listen = Some(expect_value(&mut iter, "--listen")?),
"--peer-listen" => {
peer_listen = Some(expect_value(&mut iter, "--peer-listen")?)
}
"--debug" => debug = true,
"--parent-pid" => parent = Some(expect_value(&mut iter, "--parent-pid")?),
other => return Err(format!("unknown argument: {other}")),
}
}
let worktree_root = match worktree {
None => None,
Some(raw) => {
let root = PathBuf::from(raw);
if !root.is_absolute() {
return Err(format!("--worktree must be absolute, got {}", root.display()));
}
if !root.is_dir() {
return Err(format!(
"--worktree names {}, which is not a directory",
root.display()
));
}
Some(root)
}
};
let port = port.ok_or_else(|| "--port is required".to_string())?;
let http_port: u16 = port
.parse()
.map_err(|e| format!("--port must be a TCP port number (1-65535): {e}"))?;
if http_port == 0 {
return Err("--port must not be 0: the workbench is served at a known port".to_string());
}
// The one argument with a default, and the default is the safe end of
// it: a daemon started with no opinion about who may reach it is
// reachable only from this machine.
let listen: IpAddr = match listen {
Some(address) => address.parse().map_err(|e| {
format!("--listen must be an IP address to bind (e.g. 127.0.0.1 or 0.0.0.0): {e}")
})?,
None => IpAddr::V4(Ipv4Addr::LOCALHOST),
};
// The peer listener's default is the other end: every interface. See
// `Config::peer_listen` — this one is authenticated and encrypted, and
// one that only loopback can reach is one no peer can.
let peer_listen: IpAddr = match peer_listen {
Some(address) => address.parse().map_err(|e| {
format!(
"--peer-listen must be an IP address to bind (e.g. 0.0.0.0 or 192.168.1.10): {e}"
)
})?,
None => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
};
// Zero means "do not listen for peers", which is a deliberate choice a
// machine that is not part of a peer relationship can make.
let peer_port: u16 = match peer_port {
Some(value) => value
.parse()
.map_err(|e| format!("--peer-port must be a TCP port number (0-65535): {e}"))?,
None => crate::peer::service::DEFAULT_PEER_PORT,
};
if peer_port == http_port {
return Err(format!(
"--peer-port and --port are both {peer_port}; the workbench and the peer listener cannot share one port"
));
}
let parent_pid = match parent {
Some(pid) => {
let pid: u32 = pid
.parse()
.map_err(|e| format!("--parent-pid must be a process id: {e}"))?;
if pid == 0 {
return Err("--parent-pid must not be 0".to_string());
}
Some(pid)
}
None => None,
};
Ok(Self {
worktree_root,
peer_port,
peer_listen,
listen,
http_port,
debug,
parent_pid,
})
}
}
fn expect_value<I>(iter: &mut I, flag: &str) -> Result<String, String>
where
I: Iterator<Item = String>,
{
iter.next()
.ok_or_else(|| format!("{flag} requires a value"))
}
#[cfg(test)]
#[path = "config_tests.rs"]
mod tests;