Repository: hatoo/oha (rust)

Issue:
Support [Ipv6] format `requested_host` in `--connect-to`
Because `curl` supports it.

```sh
❯ curl -I --connect-to [::0]:80:www.google.com:80 [::0]
HTTP/1.1 404 Not Found
Transfer-Encoding: chunked
Date: Wed, 12 Oct 2022 07:37:31 GMT
Content-Type: text/html; charset=UTF-8
```

```sh
❯ oha --connect-to [::0]:80:www.google.com:80 [::0]
error: Invalid value "[::0]:80:www.google.com:80" for '--connect-to <CONNECT_TO>': requested port must be an u16, but got : cannot parse integer from empty string

For more information try --help
```

Just in case if the parse is hard, it may be good to use separate arguments like..
```sh
❯ oha --connect-to [::0]:80 www.google.com:80 --connect-to host2:port2 host3:port3  [::0]
```


Pull request description:
 Support [Ipv6] format requested_host in --connect-to
closes #197 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 601dd52..5d056d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
 # Unreleased
 
+-  Support [Ipv6] format requested_host in --connect-to #197
+
 # 0.5.5 (2022-09-19)
 
 - Add colors to the tui view #64
diff --git a/src/main.rs b/src/main.rs
index 0a9e145..62c8166 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -133,21 +133,17 @@ impl FromStr for ConnectToEntry {
     type Err = String;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let expected_syntax: String =
-            "syntax for --connect-to is host:port:target_host:target_port".into();
-
-        let (requested_host, s) = s.split_once(':').ok_or_else(|| expected_syntax.clone())?;
-        let (requested_port, s) = s.split_once(':').ok_or_else(|| expected_syntax.clone())?;
-
-        let (target_host, target_port) = if s.starts_with('[') {
-            // parse bracketed IPv6 address
-            let end = s.find("]:").ok_or(expected_syntax)?;
-            let target_host = &s[..end + 1];
-            let target_port = &s[end + 2..]; // shouldn't panic because we looked for `]:`, not just `]`
-            (target_host, target_port)
+        let expected_syntax: &str = "syntax for --connect-to is host:port:target_host:target_port";
+
+        let (s, target_port) = s.rsplit_once(':').ok_or(expected_syntax)?;
+        let (s, target_host) = if s.ends_with(']') {
+            // ipv6
+            let i = s.rfind(":[").ok_or(expected_syntax)?;
+            (&s[..i], &s[i + 1..])
         } else {
-            s.split_once(':').ok_or_else(|| expected_syntax.clone())?
+            s.rsplit_once(':').ok_or(expected_syntax)?
         };
+        let (requested_host, requested_port) = s.rsplit_once(':').ok_or(expected_syntax)?;
 
         Ok(ConnectToEntry {
             requested_host: requested_host.into(),
diff --git a/src/monitor.rs b/src/monitor.rs
index 8af8473..c458d8b 100644
--- a/src/monitor.rs
+++ b/src/monitor.rs
@@ -116,10 +116,10 @@ impl Monitor {
 
             let now = std::time::Instant::now();
             let progress = match &self.end_line {
-                EndLine::Duration(d) => ((now - self.start).as_secs_f64() / d.as_secs_f64())
-                    .max(0.0)
-                    .min(1.0),
-                EndLine::NumQuery(n) => (all.len() as f64 / *n as f64).max(0.0).min(1.0),
+                EndLine::Duration(d) => {
+                    ((now - self.start).as_secs_f64() / d.as_secs_f64()).clamp(0.0, 1.0)
+                }
+                EndLine::NumQuery(n) => (all.len() as f64 / *n as f64).clamp(0.0, 1.0),
             };
 
             let count = 32;


Test patch:
diff --git a/tests/tests.rs b/tests/tests.rs
index dd9f350..e2159e9 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -176,7 +176,7 @@ async fn get_host_with_connect_to(host: &'static str) -> String {
     rx.try_recv().unwrap()
 }
 
-async fn get_host_with_connect_to_ipv6(host: &'static str) -> String {
+async fn get_host_with_connect_to_ipv6_target(host: &'static str) -> String {
     let (tx, rx) = flume::unbounded();
     let report_host =
         warp::get()
@@ -211,6 +211,40 @@ async fn get_host_with_connect_to_ipv6(host: &'static str) -> String {
     rx.try_recv().unwrap()
 }
 
+async fn get_host_with_connect_to_ipv6_requested() -> String {
+    let (tx, rx) = flume::unbounded();
+    let report_host =
+        warp::get()
+            .and(warp::filters::header::header("host"))
+            .map(move |host: String| {
+                tx.send(host).unwrap();
+                "Hello World"
+            });
+
+    let _guard = PORT_LOCK.lock().await;
+    // sic. the `get_port` crate doesn't support IpV6 addresses, so we check
+    // with 127.0.0.1 even though we bind on ::1 later.
+    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
+    tokio::spawn(warp::serve(report_host).run(([127, 0, 0, 1], port)));
+    // It's not guaranteed that the port is used here.
+    // So we can't drop guard here.
+
+    tokio::task::spawn_blocking(move || {
+        Command::cargo_bin("oha")
+            .unwrap()
+            .args(&["-n", "1", "--no-tui"])
+            .arg("http://[::1]/")
+            .arg("--connect-to")
+            .arg(format!("[::1]:80:localhost:{port}"))
+            .assert()
+            .success();
+    })
+    .await
+    .unwrap();
+
+    rx.try_recv().unwrap()
+}
+
 async fn get_host_with_connect_to_redirect(host: &'static str) -> String {
     let (tx, rx) = flume::unbounded();
     let redirect = warp::get().and(warp::path!("source")).map(move || {
@@ -360,13 +394,18 @@ async fn test_connect_to() {
 }
 
 #[tokio::test]
-async fn test_connect_to_ipv6() {
+async fn test_connect_to_ipv6_target() {
     assert_eq!(
-        get_host_with_connect_to_ipv6("invalid.example.org").await,
+        get_host_with_connect_to_ipv6_target("invalid.example.org").await,
         "invalid.example.org"
     )
 }
 
+#[tokio::test]
+async fn test_connect_to_ipv6_requested() {
+    assert_eq!(get_host_with_connect_to_ipv6_requested().await, "[::1]")
+}
+
 #[tokio::test]
 async fn test_connect_to_redirect() {
     assert_eq!(