mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-28 09:29:58 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a82b18fb8 | |||
| 5fada3f929 | |||
| 828a604c9d | |||
| 02328e59a2 | |||
| 577ab79fd0 | |||
| 8c221d02fe |
@@ -31,10 +31,10 @@ jobs:
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Set up pnpm package manager
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- name: Contribute List
|
||||
uses: akhilmhdh/contributors-readme-action@83ea0b4f1ac928fbfe88b9e8460a932a528eb79f #v2.3.11
|
||||
env:
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 #v4.1.0
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@a6f7623b2e2401f485f1eead77ced45bd99b09b0 #v31
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Gather context
|
||||
env:
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Check if first-time contributor
|
||||
id: check-first-time
|
||||
@@ -479,7 +479,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Check if first-time contributor
|
||||
id: check-first-time
|
||||
@@ -617,10 +617,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Run opencode
|
||||
uses: anomalyco/opencode/github@abda3515f444c4d28a98953d153c5a3e1892d3d4 #v1.17.4
|
||||
uses: anomalyco/opencode/github@11e47f91496005aab4d7c5a2d0a7da5d2651b4ac #v1.17.8
|
||||
env:
|
||||
ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }}
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -34,10 +34,10 @@ jobs:
|
||||
run: git config --global core.autocrlf false
|
||||
|
||||
- name: Checkout repository code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Set up pnpm package manager
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
|
||||
@@ -41,10 +41,10 @@ jobs:
|
||||
run: git config --global core.autocrlf false
|
||||
|
||||
- name: Checkout repository code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Set up pnpm package manager
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
github.event.workflow_run.conclusion == 'success')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Determine release tag
|
||||
id: tag
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -105,10 +105,10 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
@@ -288,7 +288,7 @@ jobs:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
@@ -454,7 +454,7 @@ jobs:
|
||||
needs: [release, changelog]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
@@ -552,7 +552,7 @@ jobs:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
with:
|
||||
ref: main
|
||||
|
||||
|
||||
@@ -104,10 +104,10 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
@@ -284,7 +284,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
|
||||
- name: Generate nightly tag
|
||||
id: tag
|
||||
|
||||
@@ -21,6 +21,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- name: Spell Check Repo
|
||||
uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 #v1.47.2
|
||||
|
||||
@@ -32,10 +32,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: Start MinIO
|
||||
run: |
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## v0.27.0 (2026-06-17)
|
||||
|
||||
### Features
|
||||
|
||||
- amek window resizable
|
||||
|
||||
### Refactoring
|
||||
|
||||
- better tray icon
|
||||
- simplify socks connection
|
||||
- switch local proxy from http to socks
|
||||
|
||||
### Documentation
|
||||
|
||||
- readme
|
||||
- readme
|
||||
|
||||
### Maintenance
|
||||
|
||||
- chore: version bump
|
||||
- ci(deps): bump anomalyco/opencode in the github-actions group (#437)
|
||||
- chore: update flake.nix for v0.26.0 [skip ci] (#428)
|
||||
|
||||
|
||||
## v0.26.0 (2026-06-08)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
| | Apple Silicon | Intel |
|
||||
|---|---|---|
|
||||
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut_0.26.0_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut_0.26.0_x64.dmg) |
|
||||
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut_0.27.0_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut_0.27.0_x64.dmg) |
|
||||
|
||||
Or install via Homebrew:
|
||||
|
||||
@@ -56,15 +56,15 @@ brew install --cask donut
|
||||
|
||||
### Windows
|
||||
|
||||
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut_0.26.0_x64-setup.exe) · [Portable (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut_0.26.0_x64-portable.zip)
|
||||
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut_0.27.0_x64-setup.exe) · [Portable (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut_0.27.0_x64-portable.zip)
|
||||
|
||||
### Linux
|
||||
|
||||
| Format | x86_64 | ARM64 |
|
||||
|---|---|---|
|
||||
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut_0.26.0_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut_0.26.0_arm64.deb) |
|
||||
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut-0.26.0-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut-0.26.0-1.aarch64.rpm) |
|
||||
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut_0.26.0_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut_0.26.0_aarch64.AppImage) |
|
||||
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut_0.27.0_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut_0.27.0_arm64.deb) |
|
||||
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut-0.27.0-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut-0.27.0-1.aarch64.rpm) |
|
||||
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut_0.27.0_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut_0.27.0_aarch64.AppImage) |
|
||||
<!-- install-links-end -->
|
||||
|
||||
Or install via package manager:
|
||||
|
||||
@@ -96,17 +96,17 @@
|
||||
pkgConfigPath = lib.makeSearchPath "lib/pkgconfig" (
|
||||
pkgConfigLibs ++ map lib.getDev pkgConfigLibs
|
||||
);
|
||||
releaseVersion = "0.26.0";
|
||||
releaseVersion = "0.27.0";
|
||||
releaseAppImage =
|
||||
if system == "x86_64-linux" then
|
||||
pkgs.fetchurl {
|
||||
url = "https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut_0.26.0_amd64.AppImage";
|
||||
hash = "sha256-uwt8T+BeGf5NTFOj3D1gc8I9wkF02X2bJRpU3Yn5E2E=";
|
||||
url = "https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut_0.27.0_amd64.AppImage";
|
||||
hash = "sha256-b9jY+SPw+5UvvTKgXmvxLJjIbrLW6kHTVeZywJA6DFE=";
|
||||
}
|
||||
else if system == "aarch64-linux" then
|
||||
pkgs.fetchurl {
|
||||
url = "https://github.com/zhom/donutbrowser/releases/download/v0.26.0/Donut_0.26.0_aarch64.AppImage";
|
||||
hash = "sha256-aLXoN5S+gNQJOXrLrTYeBUAckITcTNJUGTk/ZfGhpJA=";
|
||||
url = "https://github.com/zhom/donutbrowser/releases/download/v0.27.0/Donut_0.27.0_aarch64.AppImage";
|
||||
hash = "sha256-UyK3p88kx3JkJmQ9Jv1hQGmfLbG1YZDuF2pZ1h529sQ=";
|
||||
}
|
||||
else
|
||||
null;
|
||||
|
||||
+193
-23
@@ -326,19 +326,15 @@ async fn handle_connect(
|
||||
let port = upstream.port().unwrap_or(1080);
|
||||
let socks_addr = format!("{}:{}", host, port);
|
||||
|
||||
let username = upstream.username();
|
||||
let password = upstream.password().unwrap_or("");
|
||||
let (username, password) = upstream_userpass(&upstream);
|
||||
let auth = (!username.is_empty()).then_some((username.as_str(), password.as_str()));
|
||||
|
||||
match connect_via_socks(
|
||||
&socks_addr,
|
||||
target_host,
|
||||
target_port,
|
||||
scheme == "socks5",
|
||||
if !username.is_empty() {
|
||||
Some((username, password))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
auth,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -386,10 +382,9 @@ async fn connect_via_http_proxy(
|
||||
target_host, target_port, target_host, target_port
|
||||
);
|
||||
|
||||
if !upstream.username().is_empty() {
|
||||
let (username, password) = upstream_userpass(upstream);
|
||||
if !username.is_empty() {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let username = upstream.username();
|
||||
let password = upstream.password().unwrap_or("");
|
||||
let auth = general_purpose::STANDARD.encode(format!("{}:{}", username, password));
|
||||
connect_req.push_str(&format!("Proxy-Authorization: Basic {}\r\n", auth));
|
||||
}
|
||||
@@ -409,6 +404,96 @@ async fn connect_via_http_proxy(
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract percent-decoded (username, password) from the upstream URL.
|
||||
///
|
||||
/// `url::Url::username()` / `Url::password()` return percent-encoded ASCII
|
||||
/// strings per the WHATWG spec. `build_proxy_url` on the producer side
|
||||
/// already percent-encodes the credentials with `urlencoding::encode`, so
|
||||
/// we must decode here — otherwise the upstream SOCKS5 / HTTP CONNECT
|
||||
/// receives `%40` instead of `@`, breaking RFC1929 user/password
|
||||
/// authentication or HTTP Basic-Auth
|
||||
fn upstream_userpass(upstream: &Url) -> (String, String) {
|
||||
let username = urlencoding::decode(upstream.username())
|
||||
.map(|cow| cow.into_owned())
|
||||
.unwrap_or_default();
|
||||
let password = urlencoding::decode(upstream.password().unwrap_or(""))
|
||||
.map(|cow| cow.into_owned())
|
||||
.unwrap_or_default();
|
||||
(username, password)
|
||||
}
|
||||
|
||||
/// Transparent AsyncRead/AsyncWrite wrapper that logs every read/write
|
||||
/// byte of the SOCKS5 handshake. Used only during the handshake — the
|
||||
/// inner stream is taken back via `into_inner` once the handshake
|
||||
/// completes, so the tunnel phase pays no overhead
|
||||
struct SocksHandshakeLogger<S> {
|
||||
inner: S,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl<S> SocksHandshakeLogger<S> {
|
||||
fn new(inner: S, label: String) -> Self {
|
||||
Self { inner, label }
|
||||
}
|
||||
|
||||
fn into_inner(self) -> S {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncRead + Unpin> AsyncRead for SocksHandshakeLogger<S> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let before = buf.filled().len();
|
||||
let result = Pin::new(&mut self.inner).poll_read(cx, buf);
|
||||
if let Poll::Ready(Ok(())) = &result {
|
||||
let after = buf.filled().len();
|
||||
if after > before {
|
||||
let bytes = &buf.filled()[before..after];
|
||||
log::trace!(
|
||||
"[socks-handshake:{}] <- {} byte(s): {:02x?}",
|
||||
self.label,
|
||||
bytes.len(),
|
||||
bytes
|
||||
);
|
||||
} else {
|
||||
log::trace!("[socks-handshake:{}] <- EOF (peer closed)", self.label);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncWrite + Unpin> AsyncWrite for SocksHandshakeLogger<S> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
let result = Pin::new(&mut self.inner).poll_write(cx, buf);
|
||||
if let Poll::Ready(Ok(n)) = &result {
|
||||
log::trace!(
|
||||
"[socks-handshake:{}] -> {} byte(s): {:02x?}",
|
||||
self.label,
|
||||
n,
|
||||
&buf[..*n]
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_via_socks(
|
||||
socks_addr: &str,
|
||||
target_host: &str,
|
||||
@@ -416,7 +501,7 @@ async fn connect_via_socks(
|
||||
is_socks5: bool,
|
||||
auth: Option<(&str, &str)>,
|
||||
) -> Result<TcpStream, Box<dyn std::error::Error>> {
|
||||
let mut stream = TcpStream::connect(socks_addr).await?;
|
||||
let stream = TcpStream::connect(socks_addr).await?;
|
||||
|
||||
if is_socks5 {
|
||||
// SOCKS5 connection using async_socks5
|
||||
@@ -433,9 +518,44 @@ async fn connect_via_socks(
|
||||
password: pass.to_string(),
|
||||
});
|
||||
|
||||
connect(&mut stream, target, auth_info).await?;
|
||||
Ok(stream)
|
||||
let has_auth = auth_info.is_some();
|
||||
log::trace!(
|
||||
"[socks-handshake] dialing {} (target={}:{}, has_auth={})",
|
||||
socks_addr,
|
||||
target_host,
|
||||
target_port,
|
||||
has_auth
|
||||
);
|
||||
|
||||
// Disable Nagle so the kernel doesn't further delay/coalesce the
|
||||
// syscalls issued when BufStream flushes
|
||||
let _ = stream.set_nodelay(true);
|
||||
|
||||
// BufStream wrapping is required: async_socks5 calls write_u8 for every
|
||||
// single-byte SOCKS5 / RFC1929 field, and on a raw TcpStream each call
|
||||
// becomes its own TCP segment. Some upstream SOCKS5 implementations
|
||||
// treat such a "fragmented auth submission" as a misbehaving client
|
||||
// and silently FIN instead of returning an RFC1929 status. BufStream
|
||||
// coalesces those small writes into one syscall on flush — this is
|
||||
// the usage pattern shown in the async_socks5 README
|
||||
let label = format!("{socks_addr}->{target_host}:{target_port}");
|
||||
let logged = SocksHandshakeLogger::new(stream, label);
|
||||
let mut buffered = tokio::io::BufStream::new(logged);
|
||||
let handshake = connect(&mut buffered, target, auth_info).await;
|
||||
// Unwrap the layered stream: BufStream → SocksHandshakeLogger → TcpStream
|
||||
let stream = buffered.into_inner().into_inner();
|
||||
match handshake {
|
||||
Ok(_) => {
|
||||
log::trace!("[socks-handshake] handshake completed ok");
|
||||
Ok(stream)
|
||||
}
|
||||
Err(e) => {
|
||||
log::trace!("[socks-handshake] handshake failed: {:?}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut stream = stream;
|
||||
// SOCKS4 - simplified implementation
|
||||
let ip: std::net::IpAddr = target_host.parse()?;
|
||||
|
||||
@@ -1529,10 +1649,9 @@ pub(crate) async fn connect_to_target_via_upstream(
|
||||
target_host, target_port, target_host, target_port
|
||||
);
|
||||
|
||||
if !upstream.username().is_empty() {
|
||||
let (username, password) = upstream_userpass(&upstream);
|
||||
if !username.is_empty() {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let username = upstream.username();
|
||||
let password = upstream.password().unwrap_or("");
|
||||
let auth = general_purpose::STANDARD.encode(format!("{}:{}", username, password));
|
||||
connect_req.push_str(&format!("Proxy-Authorization: Basic {}\r\n", auth));
|
||||
}
|
||||
@@ -1590,19 +1709,15 @@ pub(crate) async fn connect_to_target_via_upstream(
|
||||
let socks_port = upstream.port().unwrap_or(1080);
|
||||
let socks_addr = format!("{}:{}", socks_host, socks_port);
|
||||
|
||||
let username = upstream.username();
|
||||
let password = upstream.password().unwrap_or("");
|
||||
let (username, password) = upstream_userpass(&upstream);
|
||||
let auth = (!username.is_empty()).then_some((username.as_str(), password.as_str()));
|
||||
|
||||
let stream = connect_via_socks(
|
||||
&socks_addr,
|
||||
target_host,
|
||||
target_port,
|
||||
scheme == "socks5",
|
||||
if !username.is_empty() {
|
||||
Some((username, password))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
auth,
|
||||
)
|
||||
.await?;
|
||||
Box::new(stream)
|
||||
@@ -1743,6 +1858,61 @@ mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
/// Build an upstream URL with `urlencoding::encode`-d user/pass,
|
||||
/// mirroring what `proxy_manager::build_proxy_url` actually emits
|
||||
fn parse_encoded_upstream(scheme: &str, user: &str, pass: &str) -> Url {
|
||||
let s = format!(
|
||||
"{}://{}:{}@127.0.0.1:1080",
|
||||
scheme,
|
||||
urlencoding::encode(user),
|
||||
urlencoding::encode(pass),
|
||||
);
|
||||
Url::parse(&s).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upstream_userpass_handles_plain_ascii() {
|
||||
let u = parse_encoded_upstream("socks5", "alice", "secret123");
|
||||
assert_eq!(upstream_userpass(&u), ("alice".into(), "secret123".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upstream_userpass_decodes_special_chars() {
|
||||
// These characters all get percent-encoded by build_proxy_url before
|
||||
// landing in the URL, and must be decoded back to the original literal
|
||||
// before being handed off to the upstream
|
||||
let cases = [
|
||||
("alice", "p@ssw0rd"),
|
||||
("alice", "p:assw0rd"),
|
||||
("alice", "p ass word"),
|
||||
("alice", "abc/d+e=f"),
|
||||
("alice", "100%off!"),
|
||||
("alice", "测试密码"),
|
||||
("u@name", "v@lue"),
|
||||
];
|
||||
for (user, pass) in cases {
|
||||
let u = parse_encoded_upstream("socks5", user, pass);
|
||||
assert_eq!(
|
||||
upstream_userpass(&u),
|
||||
(user.to_string(), pass.to_string()),
|
||||
"decode failed: user={user:?} pass={pass:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upstream_userpass_empty_when_no_credentials() {
|
||||
let u = Url::parse("socks5://127.0.0.1:1080").unwrap();
|
||||
assert_eq!(upstream_userpass(&u), (String::new(), String::new()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upstream_userpass_handles_username_only() {
|
||||
let s = format!("socks5://{}@127.0.0.1:1080", urlencoding::encode("u@name"));
|
||||
let u = Url::parse(&s).unwrap();
|
||||
assert_eq!(upstream_userpass(&u), ("u@name".into(), String::new()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_exact_match() {
|
||||
let mut matcher = BlocklistMatcher::new();
|
||||
|
||||
Reference in New Issue
Block a user