Skip to main content

mobius_gateway/command/
init.rs

1use 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_default_agent(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.default_agent.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_default_agent(current.revision, composition)?;
68        store.save(&config)?;
69        println!("reset möbius gateway defaults for new chats");
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
114/// Initializes one gateway with an account-free Cloudflare Quick Tunnel.
115pub 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
124/// Initializes one gateway against a user-owned named Cloudflare Tunnel.
125pub 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
138/// Permanently removes previously confirmed gateway state after stopping its process.
139///
140/// # Errors
141///
142/// Returns an error unless the target is an empty real directory or contains a regular
143/// `gateway.toml` marker. Lifecycle or filesystem failures are also returned.
144pub 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}