| 872 | /// `--remote user@host` with the gateway endpoint derived from the URL. |
| 873 | #[allow(clippy::too_many_arguments)] |
| 874 | pub async fn gateway_add( |
| 875 | endpoint: &str, |
| 876 | name: Option<&str>, |
| 877 | remote: Option<&str>, |
| 878 | local: bool, |
| 879 | oidc_issuer: Option<&str>, |
| 880 | oidc_client_id: &str, |
| 881 | oidc_audience: Option<&str>, |
| 882 | oidc_scopes: Option<&str>, |
| 883 | gateway_insecure: bool, |
| 884 | ) -> Result<()> { |
| 885 | // If the endpoint starts with ssh://, parse it into an SSH destination |
| 886 | // and a gateway endpoint automatically. The host is resolved via |
| 887 | // `ssh -G` so that SSH config aliases map to the real hostname/IP. |
| 888 | // e.g. ssh://drew@spark:8080 -> remote="drew@spark", endpoint="https://<resolved>:8080" |
| 889 | let (endpoint, remote) = if endpoint.starts_with("ssh://") { |
| 890 | if local { |
| 891 | return Err(miette::miette!( |
| 892 | "Cannot use --local with an ssh:// endpoint.\n\ |
| 893 | ssh:// implies a remote gateway." |
| 894 | )); |
| 895 | } |
| 896 | if remote.is_some() { |
| 897 | return Err(miette::miette!( |
| 898 | "Cannot use --remote with an ssh:// endpoint.\n\ |
| 899 | The SSH destination is already embedded in the URL." |
| 900 | )); |
| 901 | } |
| 902 | let parsed = url::Url::parse(endpoint) |
| 903 | .map_err(|e| miette::miette!("Invalid ssh:// URL '{endpoint}': {e}"))?; |
| 904 | let host = parsed |
| 905 | .host_str() |
| 906 | .ok_or_else(|| miette::miette!("ssh:// URL must include a hostname: {endpoint}"))?; |
| 907 | let port = parsed |
| 908 | .port() |
| 909 | .ok_or_else(|| miette::miette!("ssh:// URL must include a port: {endpoint}"))?; |
| 910 | |
| 911 | let ssh_dest = if parsed.username().is_empty() { |
| 912 | host.to_string() |
| 913 | } else { |
| 914 | format!("{}@{host}", parsed.username()) |
| 915 | }; |
| 916 | // Resolve the SSH host alias (e.g. ~/.ssh/config HostName) so the |
| 917 | // endpoint uses the actual hostname/IP that matches the TLS certificate |
| 918 | // SANs. |
| 919 | let resolved = resolve_ssh_hostname(host); |
| 920 | let https_endpoint = format!("https://{resolved}:{port}"); |
| 921 | |
| 922 | (https_endpoint, Some(ssh_dest)) |
| 923 | } else { |
| 924 | // Normalise the endpoint: ensure it has a scheme. |
| 925 | let endpoint = if endpoint.contains("://") { |
| 926 | endpoint.to_string() |
| 927 | } else { |
| 928 | format!("https://{endpoint}") |
| 929 | }; |
| 930 | (endpoint, remote.map(String::from)) |
| 931 | }; |