Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions crates/hypercolor-cli/src/commands/cloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::time::{Duration, Instant};

use anyhow::{Context, Result, bail};
use clap::{Args, Subcommand};
use reqwest::Url;

use crate::client::DaemonClient;
use crate::output::{OutputContext, OutputFormat, extract_str};
Expand Down Expand Up @@ -427,10 +428,14 @@ fn render_login_start(
})
.context("daemon response omitted verification_uri")?;

if !args.no_open
&& let Err(error) = open::that_detached(verification_uri)
{
ctx.warning(&format!("Could not open browser: {error}"));
if !args.no_open {
if is_allowed_browser_uri(verification_uri) {
if let Err(error) = open::that_detached(verification_uri) {
ctx.warning(&format!("Could not open browser: {error}"));
}
} else {
ctx.warning("Refusing to auto-open non-HTTP(S) verification URL; use --no-open and open it manually if trusted");
}
}

match ctx.format {
Expand All @@ -457,6 +462,12 @@ fn render_login_start(
Ok(())
}

fn is_allowed_browser_uri(uri: &str) -> bool {
Url::parse(uri)
.ok()
.is_some_and(|parsed| matches!(parsed.scheme(), "https" | "http"))
}

fn render_login_success(
start: &serde_json::Value,
poll: &serde_json::Value,
Expand Down
61 changes: 61 additions & 0 deletions crates/hypercolor-cli/tests/request_shape_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1456,3 +1456,64 @@ async fn capture_profile_apply(
fn test_device_id() -> &'static str {
"00000000-0000-0000-0000-000000000001"
}

#[tokio::test]
async fn cloud_login_refuses_auto_open_for_non_http_uri() -> Result<()> {
let router = Router::new()
.route(
"/api/v1/cloud/login/start",
post(|| async {
Json(serde_json::json!({
"data": {
"login_id": "018f4c36-4a44-7cc9-9f57-0d2e9224d2f1",
"user_code": "HC-1234",
"verification_uri": "hypercolor-test://activate",
"verification_uri_complete": null,
"expires_in": 900,
"interval": 1,
"retry_after_ms": 1
}
}))
}),
)
.route(
"/api/v1/cloud/login/{login_id}/poll",
post(|Path(login_id): Path<String>| async move {
Json(serde_json::json!({
"data": {
"login_id": login_id,
"status": "authorized",
"retry_after_ms": null,
"refresh_token_stored": true,
"daemon_id": "018f4c36-4a44-7cc9-9f57-0d2e9224d2f1",
"identity_pubkey": "pubkey",
"device_install_id": "018f4c36-4a44-7cc9-9f57-0d2e9224d2f2",
"device_registered": true,
"registration_token_issued": true,
"error": null
}
}))
}),
);
let (port, shutdown_tx, task) = spawn_server(router).await?;

let output = run_hyper_output(port, &["cloud", "login", "--timeout-seconds", "2"]).await?;

let _ = shutdown_tx.send(());
task.await.context("test server task join failed")?;
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"hyper CLI failed (status={}):\nstdout:\n{}\nstderr:\n{}",
output.status,
stdout,
stderr
);
}

let stderr = String::from_utf8(output.stderr).context("stderr should be utf8")?;
assert!(stderr.contains("Refusing to auto-open non-HTTP(S) verification URL"));

Ok(())
}
Loading