Compare commits

...

14 Commits

Author SHA1 Message Date
Alexandr Bogomyakov
f5a4fc91e5 Update win-service-installer.ps1 2025-12-01 02:10:53 +02:00
AB
e9ceb58b80 Applied clippy fixes for linux 2025-12-01 01:55:00 +02:00
AB
67fe482a7c Applied clippy fixes 2025-12-01 01:52:06 +02:00
AB
7c741c8e62 Merge branch 'main' of github.com:house-of-vanity/v2-uri-parser 2025-12-01 01:48:58 +02:00
AB
1a4a296fae Applied clippy fixes 2025-12-01 01:48:36 +02:00
AB
1249d15aad Cargo format 2025-12-01 01:44:40 +02:00
Alexandr Bogomyakov
6e4a5da563 Silently continue on Remove-Item errors 2025-12-01 01:43:03 +02:00
Alexandr Bogomyakov
e1aca3b282 Update README.md 2025-12-01 01:41:47 +02:00
AB
113c8254ab Added win install script 2025-12-01 01:37:53 +02:00
AB
4fcffd57ca Added win install script 2025-12-01 01:31:56 +02:00
AB
aadbb61a90 Merge branch 'main' of github.com:house-of-vanity/v2-uri-parser 2025-12-01 01:27:30 +02:00
AB
32746af0a0 Added win install script 2025-12-01 01:26:13 +02:00
AB
d3ab227836 Bump libs. added CI 2025-11-29 03:33:25 +02:00
AB
63961624b6 Bump libs. added CI 2025-11-29 01:47:15 +02:00
22 changed files with 407 additions and 129 deletions

44
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: CI
on:
push:
branches: [main, master, develop]
pull_request:
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --locked
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --all-targets --all-features -- -D warnings
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all -- --check

134
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,134 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
use_cross: false
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
use_cross: false
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
use_cross: true
- os: macos-latest
target: x86_64-apple-darwin
use_cross: false
- os: macos-latest
target: aarch64-apple-darwin
use_cross: false
- os: windows-latest
target: x86_64-pc-windows-msvc
use_cross: false
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install musl tools
if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Install cross
if: matrix.use_cross
run: cargo install cross --git https://github.com/cross-rs/cross
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Build
run: |
if [ "${{ matrix.use_cross }}" == "true" ]; then
cross build --release --locked --target ${{ matrix.target }}
else
cargo build --release --locked --target ${{ matrix.target }}
fi
shell: bash
- name: Get binary name
id: binary
run: |
name=$(grep '^name = ' Cargo.toml | head -1 | cut -d'"' -f2)
if [ "${{ runner.os }}" == "Windows" ]; then
echo "name=${name}.exe" >> $GITHUB_OUTPUT
else
echo "name=${name}" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Strip binary
if: runner.os != 'Windows' && !matrix.use_cross
run: strip target/${{ matrix.target }}/release/${{ steps.binary.outputs.name }}
- name: Package
id: package
run: |
name=$(grep '^name = ' Cargo.toml | head -1 | cut -d'"' -f2)
target="${{ matrix.target }}"
binary="${{ steps.binary.outputs.name }}"
cd target/${target}/release
if [ "${{ runner.os }}" == "Windows" ]; then
archive="${name}-${target}.zip"
7z a ../../../${archive} ${binary}
echo "archive=${archive}" >> $GITHUB_OUTPUT
else
archive="${name}-${target}.tar.gz"
tar czf ../../../${archive} ${binary}
echo "archive=${archive}" >> $GITHUB_OUTPUT
fi
shell: bash
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: ${{ steps.package.outputs.archive }}
release:
name: Release
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Generate changelog
run: |
if [ -n "$(git tag --sort=-creatordate | head -n 2 | tail -n 1)" ]; then
prev_tag=$(git tag --sort=-creatordate | head -n 2 | tail -n 1)
git log ${prev_tag}..HEAD --pretty=format:"- %s" > CHANGELOG.md
else
echo "Initial release" > CHANGELOG.md
fi
- uses: softprops/action-gh-release@v2
with:
body_path: CHANGELOG.md
files: artifacts/**/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
Cargo.lock generated
View File

@@ -609,7 +609,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "v2parser"
version = "0.3.1"
version = "0.4.0"
dependencies = [
"base64",
"clap",

View File

@@ -1,7 +1,7 @@
[package]
name = "v2parser"
version = "0.3.1"
edition = "2021"
version = "0.4.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -15,7 +15,9 @@ serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"
urlencoding = "2"
tokio = { version = "1.0", features = ["full"] }
tempfile = "3"
[target.'cfg(unix)'.dependencies]
signal-hook = "0.3"
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
tempfile = "3"
futures = "0.3"

View File

@@ -21,3 +21,10 @@ Options:
-h, --help Print help
-V, --version Print version
```
### Install as Windows Task to start automatically
Run this command via PowerShell
```
powershell -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/house-of-vanity/v2-uri-parser/main/scripts/win-service-installer.ps1 -OutFile $env:TEMP\v2.ps1; & $env:TEMP\v2.ps1"
```

View File

@@ -0,0 +1,115 @@
# Check for admin rights at start
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Host "Administrator rights required. Restarting..." -ForegroundColor Yellow
$scriptPath = $MyInvocation.MyCommand.Path
if ([string]::IsNullOrEmpty($scriptPath)) {
$scriptPath = "$env:TEMP\v2proxy-installer.ps1"
}
Start-Process powershell -Verb RunAs -ArgumentList "-ExecutionPolicy Bypass -NoExit -File `"$scriptPath`""
exit
}
# Create user binary directory
$binPath = "$env:USERPROFILE\.local\bin"
New-Item -ItemType Directory -Force -Path $binPath
# Download v2parser
$v2repo = "house-of-vanity/v2-uri-parser"
$v2release = Invoke-RestMethod "https://api.github.com/repos/$v2repo/releases/latest"
$v2asset = $v2release.assets | Where-Object { $_.name -eq "v2parser-x86_64-pc-windows-msvc.zip" }
$v2zip = "$env:TEMP\v2parser.zip"
Invoke-WebRequest -Uri $v2asset.browser_download_url -OutFile $v2zip
Expand-Archive -Path $v2zip -DestinationPath $binPath -Force
Remove-Item $v2zip -ErrorAction SilentlyContinue
# Download Xray-core
$xrayRepo = "XTLS/Xray-core"
$xrayRelease = Invoke-RestMethod "https://api.github.com/repos/$xrayRepo/releases/latest"
$xrayAsset = $xrayRelease.assets | Where-Object { $_.name -eq "Xray-windows-64.zip" }
$xrayZip = "$env:TEMP\xray.zip"
Invoke-WebRequest -Uri $xrayAsset.browser_download_url -OutFile $xrayZip
Expand-Archive -Path $xrayZip -DestinationPath $binPath -Force
Remove-Item $xrayZip -ErrorAction SilentlyContinue
# Request server location
$serverLocation = Read-Host "Enter server location (e.g., US-NY, DE-Berlin, JP-Tokyo)"
$serverLocation = $serverLocation -replace '[^a-zA-Z0-9-]', '-'
# Request proxy URI from user
do {
$uri = Read-Host "Enter proxy URI (vless://, vmess://, shadowsocks://, trojan://, or socks://)"
$validPrefix = $uri -match "^(vless|vmess|shadowsocks|trojan|socks)://"
if (-not $validPrefix) {
Write-Host "Invalid URI. Must start with vless://, vmess://, shadowsocks://, trojan://, or socks://" -ForegroundColor Red
}
} while (-not $validPrefix)
# Find available port
$port = Read-Host "Enter HTTP port (default: 1080)"
if ([string]::IsNullOrWhiteSpace($port)) {
$port = 1080
}
$port = [int]$port
while ($true) {
$listener = $null
try {
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $port)
$listener.Start()
$listener.Stop()
Write-Host "Port $port is available" -ForegroundColor Green
break
} catch {
Write-Host "Port $port is in use, trying $($port + 1)" -ForegroundColor Yellow
$port++
} finally {
if ($listener) { $listener.Stop() }
}
}
# Create unique task name
$taskName = "V2ProxyService $serverLocation $port"
$v2parserPath = Join-Path $binPath "v2parser.exe"
$xrayPath = Join-Path $binPath "xray.exe"
# Create batch file wrapper
$batchPath = Join-Path $binPath "v2proxy-$serverLocation.bat"
$batchContent = "@echo off`ncd /d `"$binPath`"`n`"$v2parserPath`" `"$uri`" --httpport $port --run --xray-binary `"$xrayPath`""
Set-Content -Path $batchPath -Value $batchContent -Encoding ASCII
# Remove existing task if exists
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
# Create scheduled task action
$action = New-ScheduledTaskAction -Execute $batchPath -WorkingDirectory $binPath
# Create trigger for system startup
$trigger = New-ScheduledTaskTrigger -AtStartup
# Create principal to run with highest privileges
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
# Create settings
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
# Register scheduled task
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description "V2Ray Proxy Service - $serverLocation"
# Start task immediately
Start-ScheduledTask -TaskName $taskName
Start-Sleep -Seconds 2
# Check if task is running
$task = Get-ScheduledTask -TaskName $taskName
$taskInfo = Get-ScheduledTaskInfo -TaskName $taskName
Write-Host "`nTask '$taskName' created!" -ForegroundColor Green
Write-Host "Status: $($task.State)" -ForegroundColor Cyan
Write-Host "Last Run: $($taskInfo.LastRunTime)" -ForegroundColor Cyan
Write-Host "Last Result: $($taskInfo.LastTaskResult)" -ForegroundColor Cyan
Write-Host "Proxy is running on http://127.0.0.1:$port" -ForegroundColor Cyan
Read-Host "`nPress Enter to exit"

View File

@@ -1,4 +1,4 @@
use clap::{value_parser, Arg, Command};
use clap::{Arg, Command, value_parser};
pub mod config_models;
mod parser;
pub mod utils;
@@ -54,7 +54,10 @@ async fn main() {
let httpport = matches.get_one::<u16>("httpport").copied();
let get_metadata = matches.get_flag("get_metadata");
let run_mode = matches.get_flag("run");
let xray_binary = matches.get_one::<String>("xray_binary").map(|s| s.as_str()).unwrap_or("xray-core");
let xray_binary = matches
.get_one::<String>("xray_binary")
.map(|s| s.as_str())
.unwrap_or("xray-core");
if get_metadata {
print!("{}", parser::get_metadata(uri));

View File

@@ -18,17 +18,17 @@ pub fn get_metadata(uri: &str) -> String {
name: data.remarks,
host: data.host.clone(),
address: data.address.clone(),
port: data.port.clone(),
port: data.port,
protocol,
};
let serialized = serde_json::to_string(&meta_data).unwrap();
return serialized;
serde_json::to_string(&meta_data).unwrap()
}
pub fn create_json_config(uri: &str, socks_port: Option<u16>, http_port: Option<u16>) -> String {
let config = create_config(uri, socks_port, http_port);
let serialized = serde_json::to_string(&config).unwrap();
return serialized;
serde_json::to_string(&config).unwrap()
}
pub fn create_config(
@@ -42,11 +42,11 @@ pub fn create_config(
socks_port,
http_port,
});
let config = config_models::Config {
config_models::Config {
outbounds: vec![outbound_object],
inbounds: inbound_config,
};
return config;
}
}
pub fn create_outbound_object(uri: &str) -> config_models::Outbound {
@@ -56,7 +56,7 @@ pub fn create_outbound_object(uri: &str) -> config_models::Outbound {
let allow_insecure = data.allowInsecure == Some(String::from("true"))
|| data.allowInsecure == Some(String::from("1"));
let outbound = Outbound {
Outbound {
protocol: name,
tag: String::from("proxy"),
streamSettings: StreamSettings {
@@ -79,7 +79,7 @@ pub fn create_outbound_object(uri: &str) -> config_models::Outbound {
} else {
None
},
wsSettings: if network_type == String::from("ws") {
wsSettings: if network_type == "ws" {
Some(WsSettings {
Host: data.host.clone(),
path: data.path.clone(),
@@ -88,7 +88,7 @@ pub fn create_outbound_object(uri: &str) -> config_models::Outbound {
} else {
None
},
tcpSettings: if network_type == String::from("tcp") {
tcpSettings: if network_type == "tcp" {
Some(TCPSettings {
header: Some(TCPHeader {
r#type: Some(data.header_type.unwrap_or(String::from("none"))),
@@ -109,7 +109,7 @@ pub fn create_outbound_object(uri: &str) -> config_models::Outbound {
} else {
None
},
grpcSettings: if network_type == String::from("grpc") {
grpcSettings: if network_type == "grpc" {
Some(GRPCSettings {
authority: data.authority,
multiMode: Some(false),
@@ -118,7 +118,7 @@ pub fn create_outbound_object(uri: &str) -> config_models::Outbound {
} else {
None
},
quicSettings: if network_type == String::from("quic") {
quicSettings: if network_type == "quic" {
Some(QuicSettings {
header: Some(NonHeaderObject {
r#type: Some(String::from("none")),
@@ -129,7 +129,7 @@ pub fn create_outbound_object(uri: &str) -> config_models::Outbound {
} else {
None
},
kcpSettings: if network_type == String::from("kcp") {
kcpSettings: if network_type == "kcp" {
Some(KCPSettings {
mtu: None,
tti: None,
@@ -143,7 +143,7 @@ pub fn create_outbound_object(uri: &str) -> config_models::Outbound {
} else {
None
},
xhttpSettings: if network_type == String::from("xhttp") {
xhttpSettings: if network_type == "xhttp" {
Some(XHTTPSettings {
host: data.host.clone(),
path: data.path.clone(),
@@ -155,14 +155,12 @@ pub fn create_outbound_object(uri: &str) -> config_models::Outbound {
},
},
settings: outbound_settings,
};
return outbound;
}
}
fn get_uri_data(uri: &str) -> (String, RawData, OutboundSettings) {
let protocol = uri_identifier::get_uri_protocol(uri);
return match protocol {
match protocol {
Some(uri_identifier::Protocols::Vless) => {
let d = vless::data::get_data(uri);
let s = vless::create_outbound_settings(&d);
@@ -194,5 +192,5 @@ fn get_uri_data(uri: &str) -> (String, RawData, OutboundSettings) {
None => {
panic!("The protocol is not supported");
}
};
}
}

View File

@@ -5,14 +5,14 @@ use crate::{
parser::shadow_socks::models,
utils::{url_decode, url_decode_str},
};
use base64::{engine::general_purpose, Engine};
use base64::{Engine, engine::general_purpose};
pub fn get_data(uri: &str) -> RawData {
let data = uri.split_once("ss://").unwrap().1;
let (raw_data, name) = data.split_once("#").unwrap_or((data, ""));
let (raw_uri, _) = raw_data.split_once("?").unwrap_or((raw_data, ""));
let parsed_address = parse_ss_address(raw_uri);
return RawData {
RawData {
remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")),
server_method: url_decode(Some(parsed_address.method)),
address: Some(parsed_address.address),
@@ -42,7 +42,7 @@ pub fn get_data(uri: &str) -> RawData {
allowInsecure: None,
vnext_security: None,
username: None,
};
}
}
fn parse_ss_address(raw_data: &str) -> models::ShadowSocksAddress {
@@ -65,10 +65,10 @@ fn parse_ss_address(raw_data: &str) -> models::ShadowSocksAddress {
.split_once(":")
.expect("No `:` found in the decoded base64");
return models::ShadowSocksAddress {
models::ShadowSocksAddress {
method: String::from(method),
password: String::from(password),
address: parsed.host().unwrap().to_string(),
port: parsed.port().unwrap().as_u16(),
};
}
}

View File

@@ -3,7 +3,7 @@ mod models;
use crate::config_models::*;
pub fn create_outbound_settings(data: &RawData) -> OutboundSettings {
return OutboundSettings::ShadowSocks(ShadowSocksOutboundSettings {
OutboundSettings::ShadowSocks(ShadowSocksOutboundSettings {
servers: vec![ShadowSocksServerObject {
address: data.address.clone(),
port: data.port,
@@ -11,5 +11,5 @@ pub fn create_outbound_settings(data: &RawData) -> OutboundSettings {
level: Some(0),
method: data.server_method.clone(),
}],
});
})
}

View File

@@ -5,14 +5,14 @@ use crate::{
parser::socks::models,
utils::{url_decode, url_decode_str},
};
use base64::{engine::general_purpose, Engine};
use base64::{Engine, engine::general_purpose};
pub fn get_data(uri: &str) -> RawData {
let data = uri.split_once("://").unwrap().1;
let (raw_data, name) = data.split_once("#").unwrap_or((data, ""));
let (raw_uri, _) = raw_data.split_once("?").unwrap_or((raw_data, ""));
let parsed_address = parse_socks_address(raw_uri);
return RawData {
RawData {
remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")),
username: url_decode(parsed_address.username),
address: Some(parsed_address.address),
@@ -42,7 +42,7 @@ pub fn get_data(uri: &str) -> RawData {
quic_security: None,
allowInsecure: None,
vnext_security: None,
};
}
}
fn parse_socks_address(raw_data: &str) -> models::SocksAddress {
@@ -54,7 +54,7 @@ fn parse_socks_address(raw_data: &str) -> models::SocksAddress {
let parsed = address_wo_slash.parse::<Uri>().unwrap();
return match maybe_userinfo {
match maybe_userinfo {
Some(userinfo) => {
let url_decoded = url_decode_str(&userinfo).unwrap_or(userinfo);
let username_and_password = general_purpose::STANDARD
@@ -64,7 +64,7 @@ fn parse_socks_address(raw_data: &str) -> models::SocksAddress {
std::str::from_utf8(&a).expect("Base64 did not yield a valid utf-8 string"),
)
})
.unwrap_or(String::from(url_decoded.clone()));
.unwrap_or(url_decoded.clone());
let (username, password) = username_and_password
.split_once(":")
@@ -83,5 +83,5 @@ fn parse_socks_address(raw_data: &str) -> models::SocksAddress {
address: parsed.host().unwrap().to_string(),
port: parsed.port().unwrap().as_u16(),
},
};
}
}

View File

@@ -3,7 +3,7 @@ mod models;
use crate::config_models::*;
pub fn create_outbound_settings(data: &RawData) -> OutboundSettings {
return OutboundSettings::Socks(SocksOutboundSettings {
OutboundSettings::Socks(SocksOutboundSettings {
servers: vec![SocksServerObject {
users: match (&data.username, &data.uuid) {
(Some(username), Some(uuid)) => Some(vec![SocksUser {
@@ -16,5 +16,5 @@ pub fn create_outbound_settings(data: &RawData) -> OutboundSettings {
port: data.port,
level: Some(0),
}],
});
})
}

View File

@@ -12,7 +12,7 @@ pub fn get_data(uri: &str) -> RawData {
let parsed_address = parse_trojan_address(data.split_once("?").unwrap().0);
let query: Vec<(&str, &str)> = querystring::querify(raw_query);
return RawData {
RawData {
remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")),
uuid: Some(parsed_address.uuid),
port: Some(parsed_address.port),
@@ -42,7 +42,7 @@ pub fn get_data(uri: &str) -> RawData {
allowInsecure: get_parameter_value(&query, "allowInsecure"),
server_method: None,
username: None,
};
}
}
fn parse_trojan_address(raw_data: &str) -> models::TrojanAddress {
@@ -56,9 +56,9 @@ fn parse_trojan_address(raw_data: &str) -> models::TrojanAddress {
let parsed = address_wo_slash.parse::<Uri>().unwrap();
return models::TrojanAddress {
models::TrojanAddress {
uuid: url_decode(Some(uuid)).unwrap(),
address: parsed.host().unwrap().to_string(),
port: parsed.port().unwrap().as_u16(),
};
}
}

View File

@@ -3,12 +3,12 @@ mod models;
use crate::config_models::*;
pub fn create_outbound_settings(data: &RawData) -> OutboundSettings {
return OutboundSettings::Trojan(TrojanOutboundSettings {
OutboundSettings::Trojan(TrojanOutboundSettings {
servers: vec![TrojanServerObject {
address: data.address.clone(),
port: data.port,
password: data.uuid.clone(),
level: Some(0),
}],
});
})
}

View File

@@ -26,7 +26,7 @@ pub fn get_uri_protocol(uri: &str) -> Option<Protocols> {
if uri.starts_with("trojan://") {
return Some(Protocols::Trojan);
}
return None;
None
}
#[cfg(test)]
@@ -34,8 +34,10 @@ mod tests {
use super::*;
#[test]
fn return_none_for_invalid_uri() {
let protocol = get_uri_protocol("123-vless://3d1c3f04-729d-59d3-bdb6-3f3f4352e173@root.ii.one:2083?security=reality&sni=www.spamhaus.org&fp=safari&pbk=7xhH4b_VkliBxGulljcyPOH-bYUA2dl-XAdZAsfhk04&sid=6ba85179e30d4fc2&type=tcp&flow=xtls-rprx-vision#Ha-ac");
assert!(matches!(protocol, None));
let protocol = get_uri_protocol(
"123-vless://3d1c3f04-729d-59d3-bdb6-3f3f4352e173@root.ii.one:2083?security=reality&sni=www.spamhaus.org&fp=safari&pbk=7xhH4b_VkliBxGulljcyPOH-bYUA2dl-XAdZAsfhk04&sid=6ba85179e30d4fc2&type=tcp&flow=xtls-rprx-vision#Ha-ac",
);
assert!(protocol.is_none());
}
#[test]
fn recognize_vless_protocol() {

View File

@@ -12,7 +12,7 @@ pub fn get_data(uri: &str) -> RawData {
let parsed_address = parse_vless_address(data.split_once("?").unwrap().0);
let query: Vec<(&str, &str)> = querystring::querify(raw_query);
return RawData {
RawData {
remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")),
uuid: Some(parsed_address.uuid),
port: Some(parsed_address.port),
@@ -41,8 +41,8 @@ pub fn get_data(uri: &str) -> RawData {
extra: url_decode(get_parameter_value(&query, "extra")),
allowInsecure: get_parameter_value(&query, "allowInsecure"),
server_method: None,
username:None,
};
username: None,
}
}
fn parse_vless_address(raw_data: &str) -> models::VlessAddress {
@@ -56,9 +56,9 @@ fn parse_vless_address(raw_data: &str) -> models::VlessAddress {
let parsed = address_wo_slash.parse::<Uri>().unwrap();
return models::VlessAddress {
models::VlessAddress {
uuid: url_decode(Some(uuid)).unwrap(),
address: parsed.host().unwrap().to_string(),
port: parsed.port().unwrap().as_u16(),
};
}
}

View File

@@ -3,7 +3,7 @@ mod models;
use crate::config_models::*;
pub fn create_outbound_settings(data: &RawData) -> OutboundSettings {
return OutboundSettings::Vless(VlessOutboundSettings {
OutboundSettings::Vless(VlessOutboundSettings {
vnext: vec![VnextServerObject {
port: data.port,
address: data.address.clone(),
@@ -15,5 +15,5 @@ pub fn create_outbound_settings(data: &RawData) -> OutboundSettings {
security: None,
}]),
}],
});
})
}

View File

@@ -1,30 +1,27 @@
use crate::config_models::RawData;
use crate::parser::vmess::models::VmessAddress;
use crate::utils::{get_parameter_value, url_decode, url_decode_str};
use base64::{engine::general_purpose, Engine};
use base64::{Engine, engine::general_purpose};
use http::Uri;
use serde_json::Value;
pub fn get_data(uri: &str) -> RawData {
let data = uri.split_once("vmess://").unwrap().1;
return match general_purpose::STANDARD
.decode(url_decode_str(data).unwrap_or(String::from(data)))
{
match general_purpose::STANDARD.decode(url_decode_str(data).unwrap_or(String::from(data))) {
Ok(decoded) => get_raw_data_from_base64(&decoded),
Err(_) => get_raw_data_from_uri(data),
};
}
}
fn get_raw_data_from_base64(decoded_base64: &Vec<u8>) -> RawData {
fn get_raw_data_from_base64(decoded_base64: &[u8]) -> RawData {
let json_str = std::str::from_utf8(decoded_base64).unwrap();
let json = serde_json::from_str::<Value>(json_str).unwrap();
return RawData {
RawData {
remarks: url_decode(get_str_field(&json, "ps")).unwrap_or(String::from("")),
uuid: get_str_field(&json, "id"),
port: get_str_field(&json, "port")
.and_then(|s| Some(s.parse::<u16>().expect("port is not a number"))),
port: get_str_field(&json, "port").map(|s| s.parse::<u16>().expect("port is not a number")),
address: get_str_field(&json, "add"),
alpn: url_decode(get_str_field(&json, "alpn")),
path: url_decode(get_str_field(&json, "path")),
@@ -59,11 +56,11 @@ fn get_raw_data_from_base64(decoded_base64: &Vec<u8>) -> RawData {
allowInsecure: None,
server_method: None,
username: None,
};
}
}
fn get_str_field(json: &Value, field: &str) -> Option<String> {
return json.get(field).and_then(|v| v.as_str()).map(String::from);
json.get(field).and_then(|v| v.as_str()).map(String::from)
}
fn get_raw_data_from_uri(data: &str) -> RawData {
@@ -75,7 +72,7 @@ fn get_raw_data_from_uri(data: &str) -> RawData {
let parsed_address = parse_vmess_address(data.split_once("?").unwrap().0);
let query: Vec<(&str, &str)> = querystring::querify(raw_query);
return RawData {
RawData {
remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")),
uuid: Some(parsed_address.uuid),
port: Some(parsed_address.port),
@@ -105,7 +102,7 @@ fn get_raw_data_from_uri(data: &str) -> RawData {
allowInsecure: get_parameter_value(&query, "allowInsecure"),
server_method: None,
username: None,
};
}
}
fn parse_vmess_address(raw_data: &str) -> VmessAddress {
@@ -119,9 +116,9 @@ fn parse_vmess_address(raw_data: &str) -> VmessAddress {
let parsed = address_wo_slash.parse::<Uri>().unwrap();
return VmessAddress {
VmessAddress {
uuid: url_decode(Some(uuid)).unwrap(),
address: parsed.host().unwrap().to_string(),
port: parsed.port().unwrap().as_u16(),
};
}
}

View File

@@ -3,7 +3,7 @@ mod models;
use crate::config_models::*;
pub fn create_outbound_settings(data: &RawData) -> OutboundSettings {
return OutboundSettings::Vmess(VmessOutboundSettings {
OutboundSettings::Vmess(VmessOutboundSettings {
vnext: vec![VnextServerObject {
port: data.port,
address: data.address.clone(),
@@ -15,5 +15,5 @@ pub fn create_outbound_settings(data: &RawData) -> OutboundSettings {
security: data.vnext_security.clone(),
}]),
}],
});
})
}

View File

@@ -7,25 +7,19 @@ pub struct InboundGenerationOptions {
pub fn generate_inbound_config(options: InboundGenerationOptions) -> Vec<config_models::Inbound> {
let mut inbounds: Vec<config_models::Inbound> = vec![];
match options.socks_port {
Some(port) => {
inbounds.push(generate_socks_inbound(port));
}
None => {}
if let Some(port) = options.socks_port {
inbounds.push(generate_socks_inbound(port));
}
match options.http_port {
Some(port) => {
inbounds.push(generate_http_inbound(port));
}
None => {}
if let Some(port) = options.http_port {
inbounds.push(generate_http_inbound(port));
}
return inbounds;
inbounds
}
pub fn generate_http_inbound(http_port: u16) -> config_models::Inbound {
return config_models::Inbound {
config_models::Inbound {
protocol: String::from("http"),
port: http_port,
tag: String::from("http-in"),
@@ -42,11 +36,11 @@ pub fn generate_http_inbound(http_port: u16) -> config_models::Inbound {
String::from("quic"),
]),
}),
};
}
}
pub fn generate_socks_inbound(socks_port: u16) -> config_models::Inbound {
return config_models::Inbound {
config_models::Inbound {
protocol: String::from("socks"),
port: socks_port,
tag: String::from("socks-in"),
@@ -63,5 +57,5 @@ pub fn generate_socks_inbound(socks_port: u16) -> config_models::Inbound {
String::from("quic"),
]),
}),
};
}
}

View File

@@ -1,17 +1,17 @@
pub mod inbound_generator;
pub fn url_decode_str(value: &str) -> Option<String> {
return urlencoding::decode(value)
urlencoding::decode(value)
.ok()
.map(|decoded| decoded.into_owned());
.map(|decoded| decoded.into_owned())
}
pub fn url_decode(value: Option<String>) -> Option<String> {
return value.and_then(|s| {
value.and_then(|s| {
urlencoding::decode(&s)
.ok()
.map(|decoded| decoded.into_owned())
});
})
}
pub fn parse_raw_json(input: &str) -> Option<serde_json::Value> {
@@ -24,8 +24,5 @@ pub fn parse_raw_json(input: &str) -> Option<serde_json::Value> {
}
pub fn get_parameter_value(query: &Vec<(&str, &str)>, param: &str) -> Option<String> {
return query
.iter()
.find(|q| String::from(q.0) == String::from(param))
.map(|q| q.1.to_string());
query.iter().find(|q| q.0 == param).map(|q| q.1.to_string())
}

View File

@@ -1,7 +1,7 @@
use std::process::Stdio;
use tokio::process::{Child, Command};
use tempfile::NamedTempFile;
use std::io::Write;
use std::process::Stdio;
use tempfile::NamedTempFile;
use tokio::process::{Child, Command};
pub struct XrayRunner {
process: Option<Child>,
@@ -16,7 +16,11 @@ impl XrayRunner {
}
}
pub async fn start(&mut self, config_json: &str, xray_binary: &str) -> Result<(), Box<dyn std::error::Error>> {
pub async fn start(
&mut self,
config_json: &str,
xray_binary: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Create temporary config file with .json extension
let mut temp_file = NamedTempFile::with_suffix(".json")?;
temp_file.write_all(config_json.as_bytes())?;
@@ -27,10 +31,10 @@ impl XrayRunner {
// Start xray-core process
let mut cmd = Command::new(xray_binary);
cmd.arg("-config")
.arg(&config_path)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
.arg(&config_path)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let child = cmd.spawn()?;
@@ -46,7 +50,7 @@ impl XrayRunner {
println!("Stopping xray-core process...");
// Try graceful shutdown first
if let Err(_) = process.kill().await {
if (process.kill().await).is_err() {
eprintln!("Failed to kill xray-core process gracefully");
}
@@ -58,36 +62,17 @@ impl XrayRunner {
}
// Cleanup config file
if let Some(_) = self.config_file.take() {
if self.config_file.take().is_some() {
println!("Cleaned up temporary config file");
}
Ok(())
}
pub fn is_running(&mut self) -> bool {
if let Some(process) = &mut self.process {
match process.try_wait() {
Ok(Some(_)) => {
// Process has exited
self.process = None;
false
}
Ok(None) => true, // Process is still running
Err(_) => {
// Error checking status, assume not running
self.process = None;
false
}
}
} else {
false
}
}
}
impl Drop for XrayRunner {
fn drop(&mut self) {
#[allow(unused_mut)]
if let Some(mut process) = self.process.take() {
#[cfg(unix)]
{
@@ -109,11 +94,11 @@ impl Drop for XrayRunner {
pub async fn wait_for_shutdown_signal() {
#[cfg(unix)]
{
use futures::stream::StreamExt;
use signal_hook::consts::signal::*;
use signal_hook_tokio::Signals;
use futures::stream::StreamExt;
let mut signals = Signals::new(&[SIGINT, SIGTERM]).expect("Failed to create signals");
let mut signals = Signals::new([SIGINT, SIGTERM]).expect("Failed to create signals");
while let Some(signal) = signals.next().await {
match signal {