mobius_gateway/command/
init.rs1use super::*;
2
3pub(super) fn initialize(options: InitOptions) -> Result<()> {
4 let (store, config) = match options.cloudflare {
5 Some(CloudflareInit::Quick) => {
6 ConfigStore::initialize_quick_cloudflare(options.state_dir, options.listen)?
7 }
8 Some(CloudflareInit::Named { hostname, token }) => {
9 ConfigStore::initialize_named_cloudflare(
10 options.state_dir,
11 options.listen,
12 &hostname,
13 &token,
14 )?
15 }
16 None if options.tls.is_none() => {
17 ConfigStore::initialize_quick_cloudflare(options.state_dir, options.listen)?
18 }
19 None => ConfigStore::initialize(options.state_dir, options.listen, options.tls)?,
20 };
21 initialize_auth(&store)?;
22 println!("initialized möbius gateway");
23 print_listener(&config, None);
24 println!("run `mobius-gateway connect` to pair a client");
25 Ok(())
26}
27
28pub(super) fn initialize_auth(store: &ConfigStore) -> Result<()> {
29 if let Err(error) = AuthStore::initialize(store.auth_path()) {
30 return cleanup_failed_initialization(store, error);
31 }
32 Ok(())
33}
34
35pub(super) fn initialize_bootstrap(
36 state_dir: PathBuf,
37 save_local_client: fn(&Endpoint, String) -> Result<()>,
38) -> Result<()> {
39 let (store, config) = ConfigStore::initialize(state_dir, DEFAULT_LISTEN, None)?;
40 let initialized = AuthStore::initialize(store.auth_path()).and_then(|(auth, _)| {
41 let endpoint = direct_loopback_endpoint(&config)?;
42 let issued = auth.provision_local_client()?;
43 save_local_client(&endpoint, issued.token)
44 });
45 if let Err(error) = initialized {
46 return cleanup_failed_initialization(&store, error);
47 }
48 println!("initialized möbius gateway bootstrap");
49 print_listener(&config, None);
50 Ok(())
51}
52
53pub(super) fn reset_bot_defaults(state_dir: PathBuf) -> Result<()> {
54 #[cfg(unix)]
55 {
56 let (store, _) = ConfigStore::open(state_dir)?;
57 let _startup = StartupGuard::create(store.state_dir())?;
58 stop_gateway(store.state_dir(), None)?;
59 let (store, config) = ConfigStore::open(store.state_dir().to_path_buf())?;
60 let current = config.bot_defaults.as_ref().ok_or_else(|| {
61 Error::Config("configure a provider before resetting defaults".into())
62 })?;
63 let composition = crate::wire::AgentComposition {
64 provider: current.config.provider.clone(),
65 ..crate::wire::AgentComposition::default()
66 };
67 let config = config.replacing_bot_defaults(current.revision, composition)?;
68 store.save(&config)?;
69 println!("reset möbius gateway Bot defaults");
70 Ok(())
71 }
72 #[cfg(not(unix))]
73 {
74 let _ = state_dir;
75 Err(unsupported_lifecycle())
76 }
77}
78
79pub(super) fn direct_loopback_endpoint(config: &GatewayConfig) -> Result<Endpoint> {
80 if !config.listen.ip().is_loopback() || config.tls.is_some() || config.cloudflare.is_some() {
81 return Err(Error::Config(
82 "bootstrap commands require a direct plaintext loopback gateway".into(),
83 ));
84 }
85 loopback_endpoint(config)
86}
87
88fn cleanup_failed_initialization<T>(store: &ConfigStore, error: Error) -> Result<T> {
89 std::fs::remove_dir_all(store.state_dir()).map_err(|cleanup| {
90 Error::Config(format!(
91 "{error}; failed to remove incomplete gateway state at {}: {cleanup}",
92 store.state_dir().display()
93 ))
94 })?;
95 Err(error)
96}
97
98pub(super) fn provision_cloudflare_local_client(
99 auth: &AuthStore,
100 config: &GatewayConfig,
101) -> Result<Option<(Endpoint, String)>> {
102 if config.cloudflare.is_none() {
103 return Ok(None);
104 }
105 let endpoint = loopback_endpoint(config)?;
106 let issued = auth.provision_local_client()?;
107 Ok(Some((endpoint, issued.token)))
108}
109
110pub(super) fn loopback_endpoint(config: &GatewayConfig) -> Result<Endpoint> {
111 format!("tcp://{}", config.listen).parse()
112}
113
114pub fn initialize_quick_cloudflare(state_dir: PathBuf) -> Result<()> {
116 initialize(InitOptions {
117 state_dir,
118 listen: DEFAULT_LISTEN,
119 tls: None,
120 cloudflare: Some(CloudflareInit::Quick),
121 })
122}
123
124pub fn initialize_named_cloudflare(
126 state_dir: PathBuf,
127 hostname: String,
128 token: String,
129) -> Result<()> {
130 initialize(InitOptions {
131 state_dir,
132 listen: DEFAULT_LISTEN,
133 tls: None,
134 cloudflare: Some(CloudflareInit::Named { hostname, token }),
135 })
136}
137
138pub fn reset_gateway_state(state_dir: PathBuf) -> Result<()> {
145 #[cfg(unix)]
146 {
147 let had_config = validate_reset_target(&state_dir, false)?;
148 let state_dir = fs::canonicalize(state_dir)?;
149 let _startup = StartupGuard::create(&state_dir)?;
150 if validate_reset_target(&state_dir, true)? != had_config {
151 return Err(invalid_reset_target(&state_dir));
152 }
153 stop_gateway(&state_dir, None)?;
154 fs::remove_dir_all(state_dir)?;
155 Ok(())
156 }
157 #[cfg(not(unix))]
158 {
159 let _ = state_dir;
160 Err(unsupported_lifecycle())
161 }
162}
163
164#[cfg(unix)]
165pub(super) fn validate_reset_target(path: &Path, ignore_startup_lock: bool) -> Result<bool> {
166 let metadata = fs::symlink_metadata(path)?;
167 if metadata.file_type().is_symlink() || !metadata.is_dir() {
168 return Err(invalid_reset_target(path));
169 }
170 let mut empty = true;
171 for entry in fs::read_dir(path)? {
172 let entry = entry?;
173 if ignore_startup_lock && entry.file_name() == STARTUP_FILE {
174 continue;
175 }
176 empty = false;
177 }
178 if empty {
179 return Ok(false);
180 }
181 let marker = fs::symlink_metadata(path.join(STATE_MARKER_FILE))
182 .map_err(|_| invalid_reset_target(path))?;
183 if !marker.is_file() || marker.file_type().is_symlink() {
184 return Err(invalid_reset_target(path));
185 }
186 Ok(true)
187}
188
189#[cfg(unix)]
190pub(super) fn invalid_reset_target(path: &Path) -> Error {
191 Error::Config(format!(
192 "refusing to reset {}: expected an empty directory or möbius gateway state with a regular {STATE_MARKER_FILE}",
193 path.display()
194 ))
195}