Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ad4f1e4
refactor(curl): split cli error timing and write-out modules
eareimu Jul 6, 2026
60eaa70
refactor(curl): introduce request plan
eareimu Jul 6, 2026
8b736cf
refactor(curl): split client and redirect flow
eareimu Jul 6, 2026
9c33961
feat(curl): add HTTP/3 verbose renderer
eareimu Jul 6, 2026
7efc211
feat(curl): emit verbose output from HTTP/3 stream metadata
eareimu Jul 6, 2026
3df094a
feat(curl): coordinate tracing with indicatif progress
eareimu Jul 6, 2026
31435cc
feat(curl): stream response with verbose markers and progress
eareimu Jul 6, 2026
748ba31
feat(curl): add redirect and upload verbose output
eareimu Jul 6, 2026
72bcfb0
chore(curl): format verbose refactor
eareimu Jul 6, 2026
042ad4b
fix(curl): address verbose self-review findings
eareimu Jul 6, 2026
6d11f82
fix(curl): send headers before upload body
eareimu Jul 6, 2026
51c1c2d
fix(curl): preserve verbose markers while decoding
eareimu Jul 6, 2026
0889159
fix(curl): reject invalid redirect locations
eareimu Jul 6, 2026
cf38307
fix(curl): reject unsupported redirect schemes
eareimu Jul 6, 2026
f5b2cd3
feat(curl): support fail flag clusters
eareimu Jul 6, 2026
7ff1087
feat(identity): show QR codes for checkout
eareimu Jul 6, 2026
565a5e1
feat: choose ssh primary sequence before connect
eareimu Jul 6, 2026
12282b8
feat(identity): enrich generated device names
eareimu Jul 6, 2026
3901b47
chore: prepare gmutils v0.8.0-beta.2
eareimu Jul 6, 2026
2c1ad9a
build: update Linux packaging Zig toolchain
eareimu Jul 6, 2026
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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.8.0-beta.2] - 2026-07-06

### Added

- `genmeta curl` now reports HTTP/3 verbose metadata, redirect/upload verbose output, progress coordination, streamed response handling, and `--fail` flag clusters.
- `genmeta identity` now shows QR checkout codes and enriches generated device names.
- `genmeta ssh` now selects the SSH primary sequence before connecting.

### Fixed

- `genmeta curl` now rejects unsupported redirect schemes and invalid redirect locations, preserves verbose markers while decoding, and sends headers before upload bodies.

### Dependencies

- Release manifests now target `h3x` v0.6.0-beta.2, `dhttp`
v0.5.0-beta.2, `dhttp-access` v0.3.0, `dshell` v0.6.0-beta.2,
`dyns` v0.6.0-beta.2, and `rankey` v0.2.1.

### Components

- `genmeta` v0.8.0-beta.2
- `genmeta-curl` v0.7.0-beta.2
- `genmeta-ssh` v0.7.0-beta.2
- `genmeta-access` v0.4.0-beta.1
- `genmeta-identity` v0.4.0-beta.2
- `genmeta-proxy` v0.4.0-beta.1
- `genmeta-discover` v0.4.0-beta.1
- `genmeta-doctor` v0.4.0-beta.1
- `genmeta-nat` v0.5.0-beta.1
- `genmeta-nslookup` v0.5.0-beta.1

## [0.8.0-beta.1] - 2026-07-02

### Changed
Expand Down
13 changes: 12 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ indicatif = "0.18"
whoami = "2"

# network
h3x = "0.6.0-beta.1"
h3x = "0.6.0-beta.2"
http = "1"
hyper = { version = "1", features = ["http1", "server"] }
hyper-util = { version = "0.1", features = ["server", "tokio"] }
Expand All @@ -57,6 +57,7 @@ rustls = { version = "0.23", default-features = false, features = ["ring"] }
x509-parser = "0.18"
p384 = "0.14.0-rc.7"
pkcs8 = { version = "=0.11.0-rc.11", features = ["pem"] }
qrcode = { version = "0.14.1", default-features = false }

# serialization
serde = { version = "1", features = ["derive"] }
Expand Down Expand Up @@ -97,18 +98,18 @@ async-compression = { version = "0.4", features = [
# workspace
dhttp-access = "0.3.0"
genmeta-access = { path = "genmeta-access", version = "0.4.0-beta.1" }
dhttp = "0.5.0-beta.1"
genmeta-curl = { path = "genmeta-curl", version = "0.7.0-beta.1" }
dhttp = "0.5.0-beta.2"
genmeta-curl = { path = "genmeta-curl", version = "0.7.0-beta.2" }
genmeta-discover = { path = "genmeta-discover", version = "0.4.0-beta.1" }
genmeta-doctor = { path = "genmeta-doctor", version = "0.4.0-beta.1" }
genmeta-identity = { path = "genmeta-identity", version = "0.4.0-beta.1" }
genmeta-identity = { path = "genmeta-identity", version = "0.4.0-beta.2", default-features = false }
genmeta-nat = { path = "genmeta-nat", version = "0.5.0-beta.1" }
genmeta-nslookup = { path = "genmeta-nslookup", version = "0.5.0-beta.1" }
genmeta-ssh = { path = "genmeta-ssh", version = "0.7.0-beta.1" }
genmeta-ssh = { path = "genmeta-ssh", version = "0.7.0-beta.2" }
genmeta-proxy = { path = "genmeta-proxy", version = "0.4.0-beta.1" }

# DShell
dshell = { version = "0.6.0-beta.1", features = [
dshell = { version = "0.6.0-beta.2", features = [
"config",
] }

Expand Down
4 changes: 3 additions & 1 deletion genmeta-curl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "genmeta-curl"
description = "curl-like DHTTP/3 client"
version = "0.7.0-beta.1"
version = "0.7.0-beta.2"
edition.workspace = true
license.workspace = true
repository.workspace = true
Expand All @@ -16,9 +16,11 @@ bytes = { workspace = true }
clap = { workspace = true }
dhttp = { workspace = true }
http = { workspace = true }
indicatif = { workspace = true }
snafu = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-appender = { workspace = true }
tracing-indicatif = { workspace = true }
url = "2"
144 changes: 144 additions & 0 deletions genmeta-curl/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use std::path::PathBuf;

use clap::Parser;
use dhttp::{ddns::resolvers::DnsScheme, dquic::binds::BindPattern, home, name::DhttpName as Name};
use http::{HeaderName, HeaderValue, Method, Uri};
use snafu::{OptionExt, ResultExt};

use crate::error::{ParseHeaderError, parse_header_error};

/// Maximum number of redirects to follow (same default as curl since 8.3.0)
pub(crate) const MAX_REDIRS_DEFAULT: u32 = 30;
pub(crate) const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 5;

/// Supported content encodings for --compressed
pub(crate) const ACCEPT_ENCODING: &str = "deflate, gzip, zstd";

#[derive(Parser, Debug, Clone)]
#[command(version, about)]
pub struct Options {
/// URL to request
pub(crate) uri: Uri,

/// Specify request method to use
#[arg(short = 'X', long)]
pub(crate) request: Option<Method>,

/// Send data in a POST request
#[arg(short, long, conflicts_with("upload_file"))]
pub(crate) data: Option<String>,

/// Transfer local file to destination
#[arg(short = 'T', long, conflicts_with("data"))]
pub(crate) upload_file: Option<PathBuf>,

/// Pass custom header(s) to server
#[arg(short = 'H', long, value_parser = parse_header)]
pub(crate) header: Vec<(String, String)>,

/// Follow redirects
#[arg(short = 'L', long)]
pub(crate) location: bool,

/// Fail on HTTP response status 400 or greater
#[arg(short = 'f', long)]
pub(crate) fail: bool,

/// Maximum number of redirects to follow
#[arg(long, default_value_t = MAX_REDIRS_DEFAULT)]
pub(crate) max_redirs: u32,

/// Write output to file instead of stdout
#[arg(short, long)]
pub(crate) output: Option<PathBuf>,

/// Define output format for response metadata
///
/// Supported: %{response_code}, %{http_code}, %{url}, %{method},
/// %{scheme}, %{http_version}, %{time_total}, %{time_connect},
/// %{time_starttransfer}, %{size_download}, %{header{name}}
#[arg(short = 'w', long = "write-out")]
pub(crate) write_out: Option<String>,

/// Request compressed response and decompress it
#[arg(long)]
pub(crate) compressed: bool,

/// Disable content decoding; pass raw bytes through
#[arg(long, conflicts_with("compressed"))]
pub(crate) raw: bool,

/// Maximum time allowed for connection in seconds
///
/// Use 0 to disable the timeout.
#[arg(long, default_value_t = DEFAULT_CONNECT_TIMEOUT_SECS)]
pub(crate) connect_timeout: u64,

/// Client identity for DHTTP/3 connections
#[arg(short, long, value_name = "client_identity")]
pub(crate) id: Option<Name<'static>>,

/// Use the global dhttp home instead of the default user home
#[arg(long)]
pub(crate) global: bool,

/// Skip identity loading and use anonymous mode
#[arg(long, conflicts_with = "id")]
pub(crate) anonymous: bool,

/// Resolve names to IPv4 addresses only
#[arg(short = '4', long = "ipv4")]
pub(crate) ipv4: bool,

/// Resolve names to IPv6 addresses only
#[arg(short = '6', long = "ipv6")]
pub(crate) ipv6: bool,

/// DNS resolution schemes
#[arg(long, value_name = "scheme", default_value = "mdns,h3", value_delimiter = ',', hide = cfg!(not(debug_assertions)))]
pub(crate) dns: Vec<DnsScheme>,

/// Bind patterns for DHTTP/3 connections
#[arg(long = "interface", value_name = "bind", default_value = "*", hide = cfg!(not(debug_assertions)))]
pub(crate) binds: Vec<BindPattern>,

/// Make the operation more talkative
#[arg(short, long)]
pub(crate) verbose: bool,

/// Suppress progress and error messages
#[arg(short = 's', long)]
pub(crate) silent: bool,

/// Show error messages even when --silent is active
#[arg(short = 'S', long = "show-error")]
pub(crate) show_error: bool,
}

impl Options {
pub(crate) fn home_scope(&self) -> home::HomeScope {
if self.global {
home::HomeScope::Global
} else {
home::HomeScope::User
}
}
}

fn parse_header(s: &str) -> Result<(String, String), ParseHeaderError> {
let mut parts = s.splitn(2, ':');
let key = parts
.next()
.context(parse_header_error::MissingKeySnafu { input: s })?
.trim()
.to_string();
let value = parts
.next()
.context(parse_header_error::MissingValueSnafu { input: s })?
.trim()
.to_string();
HeaderName::from_bytes(key.as_bytes())
.context(parse_header_error::InvalidNameSnafu { input: s })?;
HeaderValue::from_str(&value).context(parse_header_error::InvalidValueSnafu { input: s })?;
Ok((key, value))
}
Loading