Skip to content

Commit acb2606

Browse files
committed
feat: add token-protected web sharing
1 parent e2dd312 commit acb2606

8 files changed

Lines changed: 516 additions & 40 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ exclude = ["vendor/iroh", "vendor/suppaftp"]
44
resolver = "3"
55

66
[workspace.package]
7-
version = "0.1.17"
7+
version = "0.1.18"
88
edition = "2024"
99
rust-version = "1.91"
1010
license = "MIT"

README.en.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ Open a temporary LAN download page:
130130
```powershell
131131
ii send .\video.mp4 --web
132132
ii send .\my-folder --web
133+
ii send .\video.mp4 --web --token A1b2C3d4E5f6G7h8
133134
```
134135

135-
The command displays a QR code directly above the primary LAN URL for opening the download page, then lists the remaining physical and virtual adapter IPv4 URLs under `other:`. The QR code at the top of that page points directly to `/download` for phone downloads. Folders download as `.tar` archives. Press `Ctrl+C` to stop the server. This mode has no authentication and is intended only for short-lived, trusted LAN sharing.
136+
The command displays a QR code directly above the primary LAN URL for opening the download page, then lists the remaining physical and virtual adapter IPv4 URLs under `other:`. The QR code at the top of that page points directly to `/download` for phone downloads. The page can also upload multiple files into `./ii/` under the directory where the command started; directory uploads are not supported. Folders download as `.tar` archives. Press `Ctrl+C` to stop the server. Optional `--token <value>` adds a path access token to the page, download, and upload URLs; it must be 16 to 128 ASCII letters, digits, `-`, or `_`, and omitting it keeps the unprotected URLs. This mode has no account authentication and is intended only for short-lived, trusted LAN sharing.
136137

137138
Prefer local network paths and avoid public relays:
138139

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,10 @@ ii recv ii1k7v...x9a --stdout > project.tar.gz
133133
```powershell
134134
ii send .\video.mp4 --web
135135
ii send .\my-folder --web
136+
ii send .\video.mp4 --web --token A1b2C3d4E5f6G7h8
136137
```
137138

138-
命令行会在主局域网 URL 上方显示进入下载页的二维码,并在 `other:` 下列出其余物理和虚拟网卡的 IPv4 URL;下载页顶部的二维码则直达 `/download`,方便手机扫码下载。目录会下载为 `.tar`。按 `Ctrl+C` 关闭服务。该模式没有鉴权,只适合临时、可信的局域网。
139+
命令行会在主局域网 URL 上方显示进入下载页的二维码,并在 `other:` 下列出其余物理和虚拟网卡的 IPv4 URL;下载页顶部的二维码则直达 `/download`,方便手机扫码下载。网页也可以一次上传多个文件,接收文件写到启动命令当前目录的 `./ii/`,不支持上传目录。目录会下载为 `.tar`。按 `Ctrl+C` 关闭服务。可选 `--token <value>` 为网页、下载和上传加路径访问令牌,令牌只能是 16 到 128 个 ASCII 字母、数字、`-``_`;不带时保持无令牌 URL。该模式没有账号鉴权,只适合临时、可信的局域网。
139140

140141
局域网优先,不走公网中继:
141142

SIZE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ All measurements use `cargo build -p ii --release --locked` followed by UPX
2626
| LAN web-share QR SVG and terminal QR | 5,970,944 | 2,161,152 | +12,288 | `cargo test -p ii --locked` (76 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
2727
| Web download QR and responsive share page | 5,975,040 | 2,159,616 | -1,536 | `cargo test -p ii --locked` (76 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
2828
| Web-share other IPv4 URL list | 5,979,648 | 2,160,640 | +1,024 | `cargo test -p ii --locked` (77 tests), isolated `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
29+
| Web-share bidirectional file upload | 5,992,960 | 2,166,784 | +5,632 | `cargo test -p ii --locked` (78 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
30+
| Web-share path access token | 6,001,664 | 2,169,344 | +2,560 | `cargo test -p ii --locked` (83 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
2931

3032
Equivalent release rebuilds have varied by up to `1,536` UPX bytes; the latest
3133
measurement is recorded with the pinned `5.1.0` packer.

ii.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ii 用户手册
22

3-
`ii` 是唯一对外品牌和唯一用户入口。用户只需要记 `ii`,不用记 `sendme``provide/get``iroh-relay`,也不用接触 `hash``peer id``token` 这些底层词。
3+
`ii` 是唯一对外品牌和唯一用户入口。用户只需要记 `ii`,不用记 `sendme``provide/get``iroh-relay`,也不用接触 `hash``peer id` 这些底层词。
44

55
## 一句话
66

@@ -9,7 +9,7 @@
99
## 命令总览
1010

1111
```text
12-
ii send [<path>] [--name <name>] [-t] [-c] [-o <path>] [--web | --s3 | --webdav | --ftp | --sftp] [--profile <name>] [-d] [-p] [--local] [--relay <https-url> [-k]] [--no-relay]
12+
ii send [<path>] [--name <name>] [-t] [-c] [-o <path>] [--web [--token <value>] | --s3 | --webdav | --ftp | --sftp] [--profile <name>] [-d] [-p] [--local] [--relay <https-url> [-k]] [--no-relay]
1313
ii recv <ticket> [-o <dir>] [--stdout] [--overwrite] [--resume] [--local] [--trace]
1414
ii relay (--public <https-url> | --tls <domain> --cert <path> --key <path>) [-H <bind-port>]
1515
ii doctor
@@ -78,7 +78,10 @@ tar czf - .\project | ii send --name project.tar.gz
7878
如果文件已存在,会覆盖。这个 `-o` 属于 `ii send`,不影响 `ii recv -o <dir>` 的保存目录语义。
7979

8080
`--web`
81-
: 在局域网内临时开放一个无鉴权 HTTP 下载页。执行后会在主 URL 上方展示进入下载页的二维码,随后在 `other:` 下列出其余物理和虚拟网卡的 IPv4 URL;下载页顶部二维码直达 `/download`。按 `Ctrl+C` 停止服务。文件直接下载;文件夹会按原目录名打包为 `.tar` 下载。它不生成 ticket,不能和 `-c``-o``--s3``--webdav``--ftp``--sftp``--local``--relay``--no-relay` 同时使用。
81+
: 在局域网内临时开放一个无账号鉴权 HTTP 下载页。执行后会在主 URL 上方展示进入下载页的二维码,随后在 `other:` 下列出其余物理和虚拟网卡的 IPv4 URL;下载页顶部二维码直达 `/download`。网页可一次上传多个文件,接收文件写到启动命令当前目录的 `./ii/`,不支持上传目录。按 `Ctrl+C` 停止服务。文件直接下载;文件夹会按原目录名打包为 `.tar` 下载。它不生成 ticket,不能和 `-c``-o``--s3``--webdav``--ftp``--sftp``--local``--relay``--no-relay` 同时使用。
82+
83+
`--token <value>`
84+
: 仅和 `--web` 同用,把网页、下载和上传 URL 固定到 `/<value>/` 路径下;遗漏或写错路径会返回 `404``value` 必须为 16 到 128 个 ASCII 字母、数字、`-``_`。不提供时仍使用原来的无令牌 URL。
8285

8386
`--local`
8487
: 只走局域网优先路径,不走公网发现,不走公网 relay。

src/cli.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub struct SendArgs {
3030
pub webdav: bool,
3131
pub sftp: bool,
3232
pub web: bool,
33+
pub web_token: Option<String>,
3334
pub portable_webdav: bool,
3435
pub local: bool,
3536
pub relay: Option<iroh::RelayUrl>,
@@ -156,6 +157,7 @@ fn parse_send(args: Vec<String>) -> Result<SendArgs, ParseAction> {
156157
Some(("output", value)) => out.output = Some(PathBuf::from(value)),
157158
Some(("profile", value)) => out.profile = Some(value.to_string()),
158159
Some(("relay", value)) => out.relay = Some(parse_relay_url(value)?),
160+
Some(("token", value)) => out.web_token = Some(value.to_string()),
159161
Some((flag, _)) => {
160162
return Err(ParseAction::error(format!("unknown option `--{flag}`")));
161163
}
@@ -172,6 +174,7 @@ fn parse_send(args: Vec<String>) -> Result<SendArgs, ParseAction> {
172174
"--webdav" => out.webdav = true,
173175
"--sftp" => out.sftp = true,
174176
"--web" => out.web = true,
177+
"--token" => out.web_token = Some(iter.value("--token")?),
175178
"-p" => out.portable_webdav = true,
176179
"--local" => out.local = true,
177180
"--relay" => out.relay = Some(parse_relay_url(&iter.value("--relay")?)?),
@@ -344,10 +347,27 @@ fn validate_send(args: &SendArgs) -> Result<(), ParseAction> {
344347
if args.web && (args.copy || args.output.is_some()) {
345348
return Err(ParseAction::error("--web cannot be used with -c or -o"));
346349
}
350+
if args.web_token.is_some() && !args.web {
351+
return Err(ParseAction::error("--token requires --web"));
352+
}
353+
if let Some(token) = args.web_token.as_deref()
354+
&& !is_valid_web_token(token)
355+
{
356+
return Err(ParseAction::error(
357+
"--token must contain 16 to 128 ASCII letters, digits, '-' or '_'",
358+
));
359+
}
347360

348361
Ok(())
349362
}
350363

364+
fn is_valid_web_token(token: &str) -> bool {
365+
(16..=128).contains(&token.len())
366+
&& token
367+
.bytes()
368+
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
369+
}
370+
351371
fn reject_extra(command: &str, args: Vec<String>) -> Result<(), ParseAction> {
352372
if args.iter().any(|arg| is_help(arg)) {
353373
return Err(ParseAction::help(match command {
@@ -473,6 +493,7 @@ Options:
473493
--ftp
474494
--sftp
475495
--web
496+
--token <value>
476497
-p
477498
-d
478499
--profile <name>
@@ -604,6 +625,62 @@ mod tests {
604625
));
605626
}
606627

628+
#[test]
629+
fn send_accepts_web_token() {
630+
let token = "A1b2C3d4E5f6G7h8";
631+
for args in [
632+
vec!["ii", "send", "file.txt", "--web", "--token", token],
633+
vec![
634+
"ii",
635+
"send",
636+
"file.txt",
637+
"--web",
638+
"--token=A1b2C3d4E5f6G7h8",
639+
],
640+
] {
641+
let cli = Cli::parse_from(args);
642+
match cli.command {
643+
Command::Send(args) => assert_eq!(args.web_token.as_deref(), Some(token)),
644+
_ => panic!("expected send command"),
645+
}
646+
}
647+
}
648+
649+
#[test]
650+
fn web_token_accepts_hyphen_and_underscore() {
651+
assert!(is_valid_web_token("A1b2C3d4E5f6G_-h"));
652+
}
653+
654+
#[test]
655+
fn send_rejects_invalid_web_tokens() {
656+
for args in [
657+
vec!["ii", "send", "file.txt", "--token", "A1b2C3d4E5f6G7h8"],
658+
vec!["ii", "send", "file.txt", "--web", "--token"],
659+
vec!["ii", "send", "file.txt", "--web", "--token", "too-short"],
660+
vec![
661+
"ii",
662+
"send",
663+
"file.txt",
664+
"--web",
665+
"--token",
666+
"A1b2C3d4E5f6G7h!",
667+
],
668+
vec![
669+
"ii",
670+
"send",
671+
"file.txt",
672+
"--web",
673+
"--token",
674+
&"a".repeat(129),
675+
],
676+
] {
677+
assert!(matches!(
678+
parse_args(args),
679+
Err(ParseAction::Print { code: 2, .. })
680+
));
681+
}
682+
}
683+
607684
#[test]
608685
fn send_accepts_ftp_and_sftp_storage_options() {
609686
let ftp = Cli::parse_from(["ii", "send", "--ftp", "-p", "-d", "file.txt"]);

0 commit comments

Comments
 (0)