Compare commits

...

13 Commits

25 changed files with 1114 additions and 296 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
/result
/build
/config.yml
/bRAC

346
Cargo.lock generated
View File

@ -94,9 +94,11 @@ version = "0.1.4+2.0"
dependencies = [
"chrono",
"clap",
"gdk-pixbuf 0.3.0",
"gtk4",
"homedir",
"lazy_static",
"libnotify",
"native-tls",
"rand",
"regex",
@ -104,14 +106,32 @@ dependencies = [
"serde_default",
"serde_yml",
"socks",
"tungstenite",
"winapi",
"winresource",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.17.0"
@ -124,15 +144,21 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cairo-rs"
version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae50b5510d86cf96ac2370e66d8dc960882f3df179d6a5a1e52bd94a1416c0f7"
dependencies = [
"bitflags",
"bitflags 2.9.0",
"cairo-sys-rs",
"glib",
"glib 0.20.9",
"libc",
]
@ -142,7 +168,7 @@ version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f18b6bb8e43c7eb0f2aac7976afe0c61b6f5fc2ab7bc4c139537ea56c92290df"
dependencies = [
"glib-sys",
"glib-sys 0.20.9",
"libc",
"system-deps",
]
@ -254,6 +280,25 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "darling"
version = "0.20.11"
@ -289,6 +334,22 @@ dependencies = [
"syn",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@ -405,27 +466,54 @@ dependencies = [
"slab",
]
[[package]]
name = "gdk-pixbuf"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16160d212ae91abe9f3324c3fb233929ba322dde63585d15cda3336f8c529ed1"
dependencies = [
"gdk-pixbuf-sys 0.5.0",
"glib 0.4.1",
"glib-sys 0.5.0",
"gobject-sys 0.5.0",
"libc",
]
[[package]]
name = "gdk-pixbuf"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7563afd6ff0a221edfbb70a78add5075b8d9cb48e637a40a24c3ece3fea414d0"
dependencies = [
"gdk-pixbuf-sys",
"gdk-pixbuf-sys 0.20.7",
"gio",
"glib",
"glib 0.20.9",
"libc",
]
[[package]]
name = "gdk-pixbuf-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "798f97101eea8180da363d0e80e07ec7ec6d1809306601c0100c1de5bc8b4f52"
dependencies = [
"bitflags 1.3.2",
"gio-sys 0.5.0",
"glib-sys 0.5.0",
"gobject-sys 0.5.0",
"libc",
"pkg-config",
]
[[package]]
name = "gdk-pixbuf-sys"
version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67f2587c9202bf997476bbba6aaed4f78a11538a2567df002a5f57f5331d0b5c"
dependencies = [
"gio-sys",
"glib-sys",
"gobject-sys",
"gio-sys 0.20.9",
"glib-sys 0.20.9",
"gobject-sys 0.20.9",
"libc",
"system-deps",
]
@ -437,10 +525,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60"
dependencies = [
"cairo-rs",
"gdk-pixbuf",
"gdk-pixbuf 0.20.9",
"gdk4-sys",
"gio",
"glib",
"glib 0.20.9",
"libc",
"pango",
]
@ -452,16 +540,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"gdk-pixbuf-sys 0.20.7",
"gio-sys 0.20.9",
"glib-sys 0.20.9",
"gobject-sys 0.20.9",
"libc",
"pango-sys",
"pkg-config",
"system-deps",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.3.1"
@ -484,42 +582,68 @@ dependencies = [
"futures-core",
"futures-io",
"futures-util",
"gio-sys",
"glib",
"gio-sys 0.20.9",
"glib 0.20.9",
"libc",
"pin-project-lite",
"smallvec",
]
[[package]]
name = "gio-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a303bbf7a5e75ab3b627117ff10e495d1b9e97e1d68966285ac2b1f6270091bc"
dependencies = [
"bitflags 1.3.2",
"glib-sys 0.5.0",
"gobject-sys 0.5.0",
"libc",
"pkg-config",
]
[[package]]
name = "gio-sys"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "160eb5250a26998c3e1b54e6a3d4ea15c6c7762a6062a19a7b63eff6e2b33f9e"
dependencies = [
"glib-sys",
"gobject-sys",
"glib-sys 0.20.9",
"gobject-sys 0.20.9",
"libc",
"system-deps",
"windows-sys",
]
[[package]]
name = "glib"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9b0452824cc63066940f01adc721804919f0b76cdba3cfab977b00b87f16d4a"
dependencies = [
"bitflags 1.3.2",
"glib-sys 0.5.0",
"gobject-sys 0.5.0",
"lazy_static",
"libc",
]
[[package]]
name = "glib"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707b819af8059ee5395a2de9f2317d87a53dbad8846a2f089f0bb44703f37686"
dependencies = [
"bitflags",
"bitflags 2.9.0",
"futures-channel",
"futures-core",
"futures-executor",
"futures-task",
"futures-util",
"gio-sys",
"gio-sys 0.20.9",
"glib-macros",
"glib-sys",
"gobject-sys",
"glib-sys 0.20.9",
"gobject-sys 0.20.9",
"libc",
"memchr",
"smallvec",
@ -538,6 +662,17 @@ dependencies = [
"syn",
]
[[package]]
name = "glib-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9693049613ff52b93013cc3d2590366d8e530366d288438724b73f6c7dc4be8"
dependencies = [
"bitflags 1.3.2",
"libc",
"pkg-config",
]
[[package]]
name = "glib-sys"
version = "0.20.9"
@ -548,13 +683,25 @@ dependencies = [
"system-deps",
]
[[package]]
name = "gobject-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60d507c87a71b1143c66ed21a969be9b99a76df234b342d733e787e6c9c7d7c2"
dependencies = [
"bitflags 1.3.2",
"glib-sys 0.5.0",
"libc",
"pkg-config",
]
[[package]]
name = "gobject-sys"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c773a3cb38a419ad9c26c81d177d96b4b08980e8bdbbf32dace883e96e96e7e3"
dependencies = [
"glib-sys",
"glib-sys 0.20.9",
"libc",
"system-deps",
]
@ -565,7 +712,7 @@ version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cbc5911bfb32d68dcfa92c9510c462696c2f715548fcd7f3f1be424c739de19"
dependencies = [
"glib",
"glib 0.20.9",
"graphene-sys",
"libc",
]
@ -576,7 +723,7 @@ version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11a68d39515bf340e879b72cecd4a25c1332557757ada6e8aba8654b4b81d23a"
dependencies = [
"glib-sys",
"glib-sys 0.20.9",
"libc",
"pkg-config",
"system-deps",
@ -590,7 +737,7 @@ checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855"
dependencies = [
"cairo-rs",
"gdk4",
"glib",
"glib 0.20.9",
"graphene-rs",
"gsk4-sys",
"libc",
@ -605,8 +752,8 @@ checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc"
dependencies = [
"cairo-sys-rs",
"gdk4-sys",
"glib-sys",
"gobject-sys",
"glib-sys 0.20.9",
"gobject-sys 0.20.9",
"graphene-sys",
"libc",
"pango-sys",
@ -622,10 +769,10 @@ dependencies = [
"cairo-rs",
"field-offset",
"futures-channel",
"gdk-pixbuf",
"gdk-pixbuf 0.20.9",
"gdk4",
"gio",
"glib",
"glib 0.20.9",
"graphene-rs",
"gsk4",
"gtk4-macros",
@ -653,11 +800,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gdk-pixbuf-sys 0.20.7",
"gdk4-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"gio-sys 0.20.9",
"glib-sys 0.20.9",
"gobject-sys 0.20.9",
"graphene-sys",
"gsk4-sys",
"libc",
@ -689,6 +836,23 @@ dependencies = [
"windows",
]
[[package]]
name = "http"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
@ -763,6 +927,34 @@ version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libnotify"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10506a4f8bc6f8f7ccc6fde3a8290378d7aed3d1a26dca606a73e2ffe140cc2d"
dependencies = [
"gdk-pixbuf 0.3.0",
"gdk-pixbuf-sys 0.5.0",
"glib 0.4.1",
"glib-sys 0.5.0",
"gobject-sys 0.5.0",
"libnotify-sys",
]
[[package]]
name = "libnotify-sys"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0a716b9b7d24ed10f1eb431e1527fa13c9a4bf2d4fa68bb3e54da1d0747383c"
dependencies = [
"bitflags 1.3.2",
"gdk-pixbuf-sys 0.5.0",
"glib-sys 0.5.0",
"gobject-sys 0.5.0",
"libc",
"pkg-config",
]
[[package]]
name = "libyml"
version = "0.0.5"
@ -823,7 +1015,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases",
"libc",
@ -850,7 +1042,7 @@ version = "0.10.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
dependencies = [
"bitflags",
"bitflags 2.9.0",
"cfg-if",
"foreign-types",
"libc",
@ -895,7 +1087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1f5dc1b8cf9bc08bfc0843a04ee0fa2e78f1e1fa4b126844a383af4f25f0ec"
dependencies = [
"gio",
"glib",
"glib 0.20.9",
"libc",
"pango-sys",
]
@ -906,8 +1098,8 @@ version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dbb9b751673bd8fe49eb78620547973a1e719ed431372122b20abd12445bab5"
dependencies = [
"glib-sys",
"gobject-sys",
"glib-sys 0.20.9",
"gobject-sys 0.20.9",
"libc",
"system-deps",
]
@ -1040,7 +1232,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"bitflags 2.9.0",
"errno",
"libc",
"linux-raw-sys",
@ -1074,7 +1266,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"bitflags 2.9.0",
"core-foundation",
"core-foundation-sys",
"libc",
@ -1153,6 +1345,17 @@ dependencies = [
"version_check",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -1235,6 +1438,26 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.8.20"
@ -1269,12 +1492,41 @@ dependencies = [
"winnow",
]
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicode-ident"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -1535,13 +1787,23 @@ dependencies = [
"memchr",
]
[[package]]
name = "winresource"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4a67c78ee5782c0c1cb41bebc7e12c6e79644daa1650ebbc1de5d5b08593f7"
dependencies = [
"toml",
"version_check",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags",
"bitflags 2.9.0",
]
[[package]]

View File

@ -16,3 +16,15 @@ gtk4 = { version = "0.9.6", features = [ "v4_10" ] }
chrono = "0.4.40"
serde_default = "0.2.0"
socks = "0.3.4"
libnotify = { version = "1.0.3", optional = true }
gdk-pixbuf = { version = "0.3.0", optional = true }
winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] }
tungstenite = "0.26.2"
[features]
default = []
libnotify = ["dep:libnotify", "dep:gdk-pixbuf"]
winapi = ["dep:winapi"]
[build-dependencies]
winresource = "0.1.20"

View File

@ -1,11 +1,13 @@
.PHONY: clean build
.PHONY: clean build windows build
build: build/windows-x86_64 build/linux-x86_64
windows: build/windows-x86_64
linux: build/linux-x86_64
build/windows-x86_64:
mkdir -p build
mkdir -p $@
cargo build -r --target x86_64-pc-windows-gnu
cargo build -r -F winapi --target x86_64-pc-windows-gnu
curl -s https://api.github.com/repos/wingtk/gvsbuild/releases/latest \
| grep -o ".*browser_download_url.*GTK4_Gvsbuild.*_x64.zip.*" \
| cut -d : -f 2,3 \
@ -16,12 +18,19 @@ build/windows-x86_64:
mv $@/bin/* $@/
cp target/x86_64-pc-windows-gnu/release/bRAC.exe $@
rm -r $@/bin
cp install.bat $@
cp uninstall.bat $@
build/linux-x86_64:
mkdir -p build
mkdir -p $@
cargo build -r --target x86_64-unknown-linux-gnu
# patchbin target/x86_64-unknown-linux-gnu/release/bRAC
cp target/x86_64-unknown-linux-gnu/release/bRAC $@
cp ru.themixray.bRAC.png $@
cp ru.themixray.bRAC.desktop $@
cp install.sh $@
cp uninstall.sh $@
clean:
rm -r build

16
build.rs Normal file
View File

@ -0,0 +1,16 @@
use {
std::{
env,
io,
},
winresource::WindowsResource,
};
fn main() -> io::Result<()> {
if env::var_os("CARGO_CFG_WINDOWS").is_some() {
WindowsResource::new()
.set_icon("icon.ico")
.compile()?;
}
Ok(())
}

33
flake.lock generated
View File

@ -18,6 +18,23 @@
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1744463964,
@ -68,6 +85,7 @@
"root": {
"inputs": {
"flake-parts": "flake-parts",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
@ -89,6 +107,21 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@ -7,16 +7,15 @@
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" ];
perSystem = { config, self', pkgs, lib, system, ... }:
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
devDeps = with pkgs; [ pkg-config openssl gtk4 pango ];
devDeps = with pkgs; [ pkg-config openssl gtk4 pango libnotify ];
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
msrv = cargoToml.package.rust-version;
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
mkDevShell = rustc:
pkgs.mkShell {
shellHook = ''
@ -26,34 +25,19 @@
nativeBuildInputs = devDeps ++ [ rustc ];
};
in {
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ (import inputs.rust-overlay) ];
};
devShells.nightly = (mkDevShell (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)));
devShells.default = (mkDevShell pkgs.rust-bin.stable.latest.default);
devShells.default = self'.devShells.stable;
packages.default = let
deps = with pkgs; [
pkg-config
openssl
gtk4
pango
];
in (pkgs.makeRustPlatform {
packages.default = (pkgs.makeRustPlatform {
cargo = pkgs.rust-bin.stable.latest.minimal;
rustc = pkgs.rust-bin.stable.latest.minimal;
}).buildRustPackage {
inherit (cargoToml.package) name version;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
buildInputs = deps;
nativeBuildInputs = deps;
};
devShells.nightly = (mkDevShell (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)));
devShells.stable = (mkDevShell pkgs.rust-bin.stable.latest.default);
devShells.msrv = (mkDevShell pkgs.rust-bin.stable.${msrv}.default);
};
buildInputs = devDeps;
nativeBuildInputs = devDeps ++ [ pkgs.rustc ];
};
}
);
}

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

30
install.bat Normal file
View File

@ -0,0 +1,30 @@
@echo off
net session >nul 2>&1 || (
echo This script requires administrator privileges.
pause
exit /b
)
set "DEST=C:\Program Files\bRAC"
mkdir "%DEST%" 2>nul
xcopy "." "%DEST%\" /E /I /H /Y >nul
for /d %%u in ("C:\Users\*") do (
if exist "%%u\AppData\Roaming\Microsoft\Windows\Desktop" (
call :s "%%u\AppData\Roaming\Microsoft\Windows\Desktop\bRAC.lnk" "%DEST%\bRAC.exe"
) else if exist "%%u\Desktop" (
call :s "%%u\Desktop\bRAC.lnk" "%DEST%\bRAC.exe"
)
)
exit /b
:s
set "v=%TEMP%\_s.vbs"
> "%v%" echo Set o=CreateObject("WScript.Shell")
>>"%v%" echo Set l=o.CreateShortcut("%~1")
>>"%v%" echo l.TargetPath="%~2"
>>"%v%" echo l.WorkingDirectory="%~dp2"
>>"%v%" echo l.Save
wscript "%v%" >nul
del "%v%" >nul
exit /b

11
install.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
cp bRAC /bin/bRAC
chmod +x /bin/bRAC
cp ru.themixray.bRAC.png /usr/share/pixmaps
cp ru.themixray.bRAC.desktop /usr/share/applications

12
ru.themixray.bRAC.desktop Normal file
View File

@ -0,0 +1,12 @@
[Desktop Entry]
Name=bRAC
Version=0.1.4
Type=Application
Comment=better RAC client
Icon=ru.themixray.bRAC.png
Exec=bRAC
Categories=Network;
StartupNotify=true
DBusActivatable=true
Terminal=false
X-GNOME-UsesNotifications=true

BIN
ru.themixray.bRAC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -26,7 +26,9 @@ pub struct Config {
#[serde(default = "default_true")] pub chunked_enabled: bool,
#[serde(default = "default_true")] pub formatting_enabled: bool,
#[serde(default = "default_true")] pub commands_enabled: bool,
#[serde(default)] pub wrac_enabled: bool,
#[serde(default)] pub proxy: Option<String>,
#[serde(default = "default_true")] pub notifications_enabled: bool,
}
pub fn get_config_path() -> PathBuf {
@ -115,6 +117,8 @@ pub struct Args {
#[arg(long)] pub chunked_enabled: Option<bool>,
#[arg(long)] pub formatting_enabled: Option<bool>,
#[arg(long)] pub commands_enabled: Option<bool>,
#[arg(long)] pub notifications_enabled: Option<bool>,
#[arg(long)] pub wrac_enabled: Option<bool>,
#[arg(long)] pub proxy: Option<String>,
}
@ -124,14 +128,16 @@ impl Args {
if let Some(v) = self.name.clone() { config.name = Some(v) }
if let Some(v) = self.proxy.clone() { config.proxy = Some(v) }
if let Some(v) = self.message_format.clone() { config.message_format = v }
if let Some(v) = self.update_time.clone() { config.update_time = v }
if let Some(v) = self.max_messages.clone() { config.max_messages = v }
if let Some(v) = self.hide_my_ip.clone() { config.hide_my_ip = v }
if let Some(v) = self.show_other_ip.clone() { config.show_other_ip = v }
if let Some(v) = self.auth_enabled.clone() { config.auth_enabled = v }
if let Some(v) = self.ssl_enabled.clone() { config.ssl_enabled = v }
if let Some(v) = self.chunked_enabled.clone() { config.chunked_enabled = v }
if let Some(v) = self.formatting_enabled.clone() { config.formatting_enabled = v }
if let Some(v) = self.commands_enabled.clone() { config.commands_enabled = v }
if let Some(v) = self.update_time { config.update_time = v }
if let Some(v) = self.max_messages { config.max_messages = v }
if let Some(v) = self.hide_my_ip { config.hide_my_ip = v }
if let Some(v) = self.show_other_ip { config.show_other_ip = v }
if let Some(v) = self.auth_enabled { config.auth_enabled = v }
if let Some(v) = self.ssl_enabled { config.ssl_enabled = v }
if let Some(v) = self.chunked_enabled { config.chunked_enabled = v }
if let Some(v) = self.formatting_enabled { config.formatting_enabled = v }
if let Some(v) = self.commands_enabled { config.commands_enabled = v }
if let Some(v) = self.notifications_enabled { config.notifications_enabled = v }
if let Some(v) = self.wrac_enabled { config.wrac_enabled = v }
}
}

View File

@ -77,7 +77,8 @@ macro_rules! connect_rac {
&mut connect(
&$ctx.config(|o| o.host.clone()),
$ctx.config(|o| o.ssl_enabled),
$ctx.config(|o| o.proxy.clone())
$ctx.config(|o| o.proxy.clone()),
$ctx.config(|o| o.wrac_enabled)
)?
};
}

View File

@ -1,22 +1,23 @@
use std::sync::{mpsc::{channel, Receiver}, Arc};
use std::sync::{mpsc::{channel, Receiver}, Arc, RwLock};
use std::cell::RefCell;
use std::time::{Duration, SystemTime};
use std::thread;
use chrono::Local;
use gtk4::{self as gtk, glib::timeout_add_once};
use gtk4 as gtk;
use gtk::gdk_pixbuf::{Pixbuf, PixbufAnimation, PixbufLoader};
use gtk::prelude::*;
use gtk::gdk::{Cursor, Display, Texture};
use gtk::gdk_pixbuf::{Pixbuf, PixbufAnimation, PixbufLoader};
use gtk::gio::{self, ActionEntry, ApplicationFlags, MemoryInputStream, Menu};
use gtk::glib::clone;
use gtk::glib::{
self, clone::Downgrade,
timeout_add_local,
source::timeout_add_local_once,
ControlFlow
ControlFlow,
timeout_add_once
};
use gtk::pango::WrapMode;
use gtk::{
@ -30,7 +31,13 @@ ctx::Context, on_send_message, parse_message, print_message, recv_tick, sanitize
struct UiModel {
chat_box: GtkBox,
chat_scrolled: ScrolledWindow
chat_scrolled: ScrolledWindow,
app: Application,
window: ApplicationWindow,
#[cfg(feature = "libnotify")]
notifications: Arc<RwLock<Vec<libnotify::Notification>>>,
#[cfg(not(feature = "libnotify"))]
notifications: Arc<RwLock<Vec<String>>>
}
thread_local!(
@ -158,9 +165,11 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
let show_other_ip_entry = gui_checkbox_setting!("Show Other IP", show_other_ip, ctx, vbox);
let auth_enabled_entry = gui_checkbox_setting!("Fake Auth Enabled", auth_enabled, ctx, vbox);
let ssl_enabled_entry = gui_checkbox_setting!("SSL Enabled", ssl_enabled, ctx, vbox);
let wrac_enabled_entry = gui_checkbox_setting!("WRAC Enabled", wrac_enabled, ctx, vbox);
let chunked_enabled_entry = gui_checkbox_setting!("Chunked Enabled", chunked_enabled, ctx, vbox);
let formatting_enabled_entry = gui_checkbox_setting!("Formatting Enabled", formatting_enabled, ctx, vbox);
let commands_enabled_entry = gui_checkbox_setting!("Commands Enabled", commands_enabled, ctx, vbox);
let notifications_enabled_entry = gui_checkbox_setting!("Notifications Enabled", notifications_enabled, ctx, vbox);
let save_button = Button::builder()
.label("Save")
@ -182,6 +191,8 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
#[weak] chunked_enabled_entry,
#[weak] formatting_enabled_entry,
#[weak] commands_enabled_entry,
#[weak] notifications_enabled_entry,
#[weak] wrac_enabled_entry,
#[weak] proxy_entry,
move |_| {
let config = Config {
@ -222,9 +233,11 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
show_other_ip: show_other_ip_entry.is_active(),
auth_enabled: auth_enabled_entry.is_active(),
ssl_enabled: ssl_enabled_entry.is_active(),
wrac_enabled: wrac_enabled_entry.is_active(),
chunked_enabled: chunked_enabled_entry.is_active(),
formatting_enabled: formatting_enabled_entry.is_active(),
commands_enabled: commands_enabled_entry.is_active(),
notifications_enabled: notifications_enabled_entry.is_active(),
proxy: {
let proxy = proxy_entry.text().to_string();
@ -257,9 +270,11 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
#[weak] show_other_ip_entry,
#[weak] auth_enabled_entry,
#[weak] ssl_enabled_entry,
#[weak] wrac_enabled_entry,
#[weak] chunked_enabled_entry,
#[weak] formatting_enabled_entry,
#[weak] commands_enabled_entry,
#[weak] notifications_enabled_entry,
#[weak] proxy_entry,
move |_| {
let config = Config::default();
@ -275,9 +290,11 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
show_other_ip_entry.set_active(config.show_other_ip);
auth_enabled_entry.set_active(config.auth_enabled);
ssl_enabled_entry.set_active(config.ssl_enabled);
wrac_enabled_entry.set_active(config.wrac_enabled);
chunked_enabled_entry.set_active(config.chunked_enabled);
formatting_enabled_entry.set_active(config.formatting_enabled);
commands_enabled_entry.set_active(config.commands_enabled);
notifications_enabled_entry.set_active(config.notifications_enabled);
}
));
@ -290,6 +307,22 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
.child(&vbox)
.build();
let controller = gtk::EventControllerKey::new();
controller.connect_key_pressed({
let window = window.clone();
move |_, key, _, _| {
if key == gtk::gdk::Key::Escape {
window.close();
gtk::glib::Propagation::Proceed
} else {
gtk::glib::Propagation::Stop
}
}
});
window.add_controller(controller);
window.present();
}
@ -375,7 +408,7 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
let server_list = ListBox::new();
for url in ["meex.lol:42666", "meex.lol:11234", "91.192.22.20:42666"] {
for url in ["rac://meex.lol", "rac://meex.lol:11234", "rac://91.192.22.20"] {
let url = url.to_string();
let label = Label::builder()
@ -583,11 +616,17 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
UiModel {
chat_scrolled,
chat_box
chat_box,
app: app.clone(),
window: window.clone(),
#[cfg(feature = "libnotify")]
notifications: Arc::new(RwLock::new(Vec::<libnotify::Notification>::new())),
#[cfg(not(feature = "libnotify"))]
notifications: Arc::new(RwLock::new(Vec::<String>::new())),
}
}
fn setup(ctx: Arc<Context>, ui: UiModel) {
fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
let (sender, receiver) = channel();
*ctx.sender.write().unwrap() = Some(Arc::new(sender));
@ -596,6 +635,32 @@ fn setup(ctx: Arc<Context>, ui: UiModel) {
let (tx, rx) = channel();
#[cfg(feature = "libnotify")]
ui.window.connect_notify(Some("is-active"), move |a, _| {
if a.is_active() {
GLOBAL.with(|global| {
if let Some((ui, _)) = &*global.borrow() {
for i in ui.notifications.read().unwrap().clone() {
i.close().expect("libnotify close error");
}
}
});
}
});
#[cfg(not(feature = "libnotify"))]
ui.window.connect_notify(Some("is-active"), move |a, _| {
if a.is_active() {
GLOBAL.with(|global| {
if let Some((ui, _)) = &*global.borrow() {
for i in ui.notifications.read().unwrap().clone() {
ui.app.withdraw_notification(&i);
}
}
});
}
});
GLOBAL.with(|global| {
*global.borrow_mut() = Some((ui, rx));
});
@ -651,6 +716,40 @@ fn load_css() {
);
}
#[cfg(feature = "libnotify")]
fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str) {
use libnotify::Notification;
let notification = Notification::new(title, message, None);
notification.set_app_name("bRAC");
let pixbuf_loader = gdk_pixbuf::PixbufLoader::new();
pixbuf_loader.loader_write(include_bytes!("images/icon.png")).unwrap();
pixbuf_loader.close().unwrap();
notification.set_image_from_pixbuf(&pixbuf_loader.get_pixbuf().unwrap());
notification.show().expect("libnotify send error");
ui.notifications.write().unwrap().push(notification);
}
#[cfg(not(feature = "libnotify"))]
fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str) {
use std::{hash::{DefaultHasher, Hasher}, time::UNIX_EPOCH};
use gtk4::gio::Notification;
let mut hash = DefaultHasher::new();
hash.write(title.as_bytes());
hash.write(message.as_bytes());
let id = format!("bRAC-{}-{}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(), hash.finish());
let notif = Notification::new(title);
notif.set_body(Some(&message));
ui.app.send_notification(Some(&id), &notif);
ui.notifications.write().unwrap().push(id);
}
fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String) {
let Some(message) = sanitize_message(message) else { return; };
@ -663,8 +762,8 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String) {
if let Some((date, ip, content, nick)) = parse_message(message.clone()) {
if let Some(ip) = ip {
if ctx.config(|o| o.show_other_ip) {
let ip = Label::builder()
.label(ip)
let ip_label = Label::builder()
.label(&ip)
.margin_end(10)
.halign(Align::Start)
.valign(Align::Start)
@ -672,11 +771,11 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String) {
.selectable(true)
.build();
hbox.append(&ip);
hbox.append(&ip_label);
}
}
let date = Label::builder()
let date_label = Label::builder()
.label(format!("[{date}]"))
.halign(Align::Start)
.valign(Align::Start)
@ -684,10 +783,10 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String) {
.selectable(true)
.build();
hbox.append(&date);
hbox.append(&date_label);
if let Some((name, color)) = nick {
let name = Label::builder()
let name_label = Label::builder()
.label(format!("<{name}>"))
.halign(Align::Start)
.valign(Align::Start)
@ -695,11 +794,29 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String) {
.selectable(true)
.build();
hbox.append(&name);
hbox.append(&name_label);
if !ui.window.is_active() {
if ctx.config(|o| o.chunked_enabled) {
send_notification(ctx.clone(), ui, &format!("{}'s Message", &name), &content);
// let notif = Notification::new(&format!("{}'s Message", &name));
// notif.set_body(Some(&content));
// app.send_notification(Some("user-message"), &notif);
}
}
} else {
if !ui.window.is_active() {
if ctx.config(|o| o.chunked_enabled) {
send_notification(ctx.clone(), ui, "System Message", &content);
// let notif = Notification::new("System Message");
// notif.set_body(Some(&content));
// app.send_notification(Some("system-message"), &notif);
}
}
}
let content = Label::builder()
.label(content)
let content_label = Label::builder()
.label(&content)
.halign(Align::Start)
.valign(Align::Start)
.css_classes(["message-content"])
@ -708,10 +825,11 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String) {
.wrap_mode(WrapMode::Char)
.build();
hbox.append(&content);
hbox.append(&content_label);
} else {
let content = Label::builder()
.label(message)
let content_label = Label::builder()
.label(&message)
.halign(Align::Start)
.valign(Align::Start)
.css_classes(["message-content"])
@ -720,7 +838,16 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String) {
.wrap_mode(WrapMode::Char)
.build();
hbox.append(&content);
hbox.append(&content_label);
if !ui.window.is_active() {
if ctx.config(|o| o.chunked_enabled) {
send_notification(ctx.clone(), ui, "Chat Message", &message);
// let notif = Notification::new("Chat Message");
// notif.set_body(Some(&message));
// app.send_notification(Some("chat-message"), &notif);
}
}
}
ui.chat_box.append(&hbox);
@ -747,6 +874,11 @@ fn run_recv_loop(ctx: Arc<Context>) {
}
pub fn run_main_loop(ctx: Arc<Context>) {
#[cfg(feature = "libnotify")]
{
libnotify::init("ru.themixray.bRAC").expect("libnotify init error");
}
let application = Application::builder()
.application_id("ru.themixray.bRAC")
.flags(ApplicationFlags::FLAGS_NONE)
@ -757,7 +889,7 @@ pub fn run_main_loop(ctx: Arc<Context>) {
move |app| {
let ui = build_ui(ctx.clone(), app);
setup(ctx.clone(), ui);
setup(app, ctx.clone(), ui);
load_css();
}
});
@ -771,4 +903,9 @@ pub fn run_main_loop(ctx: Arc<Context>) {
});
application.run_with_args::<&str>(&[]);
#[cfg(feature = "libnotify")]
{
libnotify::uninit();
}
}

View File

@ -2,12 +2,9 @@ use std::{
error::Error, sync::Arc, thread, time::{Duration, SystemTime, UNIX_EPOCH}
};
use crate::{connect_rac, proto::{register_user, send_message_auth}};
use crate::connect_rac;
use super::{
proto::{connect, read_messages, send_message, send_message_spoof_auth},
util::sanitize_text
};
use super::proto::{connect, read_messages, send_message, send_message_spoof_auth, register_user, send_message_auth};
use gui::{add_chat_message, clear_chat_messages};
use lazy_static::lazy_static;
@ -19,6 +16,9 @@ pub use gui::run_main_loop;
lazy_static! {
static ref ANSI_REGEX: Regex = Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap();
static ref CONTROL_CHARS_REGEX: Regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap();
pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap();
pub static ref IP_REGEX: Regex = Regex::new(r"\{(.*?)\} (.*)").unwrap();
@ -44,6 +44,11 @@ const HELP_MESSAGE: &str = "Help message:
/spam n text - send message with text n times
/ping - check server ping";
pub fn sanitize_text(input: &str) -> String {
let without_ansi = ANSI_REGEX.replace_all(input, "");
let cleaned_text = CONTROL_CHARS_REGEX.replace_all(&without_ansi, "");
cleaned_text.into_owned()
}
pub fn add_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
for i in message.split("\n")
@ -205,9 +210,7 @@ pub fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
}
},
Err(e) => {
let msg = format!("Read messages error: {}", e.to_string()).to_string();
ctx.add_message(ctx.config(|o| o.max_messages), vec![msg.clone()]);
add_chat_message(ctx.clone(), msg.clone());
println!("Read messages error: {}", e.to_string())
}
_ => {}
}

View File

@ -18,19 +18,7 @@
.message-name { font-weight: bold; }
.message-name-black { color: #2E2E2E; }
.message-name-bright-black { color: #555555; }
.message-name-red { color: #8B0000; }
.message-name-bright-red { color: #FF0000; }
.message-name-green { color: #006400; }
.message-name-bright-green { color: #00FF00; }
.message-name-yellow { color: #8B8B00; }
.message-name-bright-yellow { color: #FFFF00; }
.message-name-blue { color: #00008B; }
.message-name-bright-blue { color: #0000FF; }
.message-name-bright-magenta { color: #FF00FF; }
.message-name-magenta { color: #8B008B; }
.message-name-cyan { color: #008B8B; }
.message-name-bright-cyan { color: #00FFFF; }
.message-name-white { color: #A9A9A9; }
.message-name-bright-white { color: #FFFFFF; }
.message-name-green { color: #70fa7a; }
.message-name-red { color: #fa7070; }
.message-name-magenta { color: #da70fa; }
.message-name-cyan { color: #70fadc; }

View File

@ -1,5 +1,4 @@
#![allow(non_snake_case)]
pub mod chat;
pub mod util;
pub mod proto;

View File

@ -4,8 +4,10 @@ use bRAC::proto::{connect, read_messages, send_message};
use bRAC::chat::{config::{get_config_path, load_config, Args}, ctx::Context, run_main_loop};
use clap::Parser;
fn main() {
#[cfg(feature = "winapi")]
unsafe { winapi::um::wincon::FreeConsole() };
let args = Args::parse();
let config_path = get_config_path();
@ -18,7 +20,7 @@ fn main() {
let mut config = load_config(config_path);
if args.read_messages {
let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone()).expect("Error reading message");
let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error reading message");
print!("{}", read_messages(
&mut stream,
@ -33,7 +35,7 @@ fn main() {
}
if let Some(message) = &args.send_message {
let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone()).expect("Error sending message");
let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error sending message");
send_message(
&mut stream,

267
src/proto/mod.rs Normal file
View File

@ -0,0 +1,267 @@
use std::{error::Error, fmt::Debug, io::{Read, Write}, net::{TcpStream, ToSocketAddrs}, time::Duration};
use native_tls::{TlsConnector, TlsStream};
use socks::Socks5Stream;
use tungstenite::WebSocket;
pub mod rac;
pub mod wrac;
pub trait Stream: Read + Write + Unpin + Send + Sync + Debug {
fn set_read_timeout(&self, timeout: Duration);
fn set_write_timeout(&self, timeout: Duration);
}
impl Stream for TcpStream {
fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(&self, Some(timeout)); }
fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(&self, Some(timeout)); }
}
impl Stream for Socks5Stream {
fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(self.get_ref(), Some(timeout)); }
fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(self.get_ref(), Some(timeout)); }
}
impl<T: Stream> Stream for TlsStream<T> {
fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); }
fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); }
}
impl Stream for TlsStream<Box<dyn Stream>> {
fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); }
fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); }
}
pub enum RacStream {
WRAC(WebSocket<Box<dyn Stream>>),
RAC(Box<dyn Stream>)
}
/// `socks5://user:pass@127.0.0.1:12345/path -> ("127.0.0.1:12345", ("user", "pass"))` \
/// `socks5://127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \
/// `https://127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \
/// `127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \
/// `user:pass@127.0.0.1:12345 -> ("127.0.0.1:12345", ("user", "pass"))`
pub fn parse_socks5_url(url: &str) -> Option<(String, Option<(String, String)>)> {
let (_, url) = url.split_once("://").unwrap_or(("", url));
let (url, _) = url.split_once("/").unwrap_or((url, ""));
if let Some((auth, url)) = url.split_once("@") {
let (user, pass) = auth.split_once(":")?;
Some((url.to_string(), Some((user.to_string(), pass.to_string()))))
} else {
Some((url.to_string(), None))
}
}
/// url -> (host, ssl, wrac) \
/// `127.0.0.1` -> `("127.0.0.1:42666", false, false)` \
/// `127.0.0.1:12345` -> `("127.0.0.1:12345", false, false)` \
/// `rac://127.0.0.1/` -> `("127.0.0.1:42666", false, false)` \
/// `racs://127.0.0.1/` -> `("127.0.0.1:42667", true, false)` \
/// `wrac://127.0.0.1/` -> `("127.0.0.1:52666", false, true)` \
/// `wracs://127.0.0.1/` -> `(127.0.0.1:52667, true, true)` \
pub fn parse_rac_url(url: &str) -> Option<(String, bool, bool)> {
let (scheme, url) = url.split_once("://").unwrap_or(("rac", url));
let (host, _) = url.split_once("/").unwrap_or((url, ""));
match scheme.to_lowercase().as_str() {
"rac" => {
Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:42666")
},
false, false
))
},
"racs" => {
Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:42667")
},
true, false
))
},
"wrac" => {
Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:52666")
},
false, true
))
},
"wracs" => {
Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:52667")
},
true, true
))
},
_ => None,
}
}
/// Create RAC connection (also you can just TcpStream::connect)
///
/// host - host string, example: "example.com:12345", "example.com" (default port is 42666)
/// ssl - wrap with ssl client, write false if you dont know what it is
/// proxy - socks5 proxy (host, (user, pass))
/// wrac - to use wrac protocol
pub fn connect(host: &str, ssl: bool, proxy: Option<String>, wrac: bool) -> Result<RacStream, Box<dyn Error>> {
let (host, ssl_, wrac_) = parse_rac_url(host).ok_or::<Box<dyn Error>>("url parse error".into())?;
let (ssl, wrac) = (ssl_ || ssl, wrac_ || wrac);
let stream: Box<dyn Stream> = if let Some(proxy) = proxy {
if let Some((proxy, auth)) = parse_socks5_url(&proxy) {
if let Some((user, pass)) = auth {
Box::new(Socks5Stream::connect_with_password(&proxy, host.as_str(), &user, &pass)?)
} else {
Box::new(Socks5Stream::connect(&proxy, host.as_str())?)
}
} else {
return Err("proxy parse error".into());
}
} else {
let addr = host.to_socket_addrs()?.next().ok_or::<Box<dyn Error>>("addr parse error".into())?;
Box::new(TcpStream::connect(&addr)?)
};
let stream = if ssl {
let ip: String = host.split_once(":")
.map(|o| o.0.to_string())
.unwrap_or(host.clone());
Box::new(TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()?
.connect(&ip, stream)?)
} else {
stream
};
stream.set_read_timeout(Duration::from_secs(3));
stream.set_write_timeout(Duration::from_secs(3));
if wrac {
let (client, _) = tungstenite::client(
&format!("ws{}://{host}", if ssl { "s" } else { "" }),
stream
)?;
Ok(RacStream::WRAC(client))
} else {
Ok(RacStream::RAC(stream))
}
}
/// Send message with fake auth
///
/// Explaination:
///
/// let (name, message) = message.split("> ") else { return send_message(stream, message) }
/// if send_message_auth(name, name, message) != 0 {
/// let name = "\x1f" + name
/// register_user(stream, name, name)
/// send_message_spoof_auth(stream, name + "> " + message)
/// }
pub fn send_message_spoof_auth(stream: &mut RacStream, message: &str, remove_null: bool) -> Result<(), Box<dyn Error>> {
let Some((name, message)) = message.split_once("> ") else { return send_message(stream, message) };
if let Ok(f) = send_message_auth(stream, &name, &message, &message, remove_null) {
if f != 0 {
let name = format!("\x1f{name}");
register_user(stream, &name, &name, remove_null)?;
send_message_spoof_auth(stream, &format!("{name}> {message}"), remove_null)?;
}
}
Ok(())
}
/// Send message
///
/// stream - any stream that can be written to
/// message - message text
pub fn send_message(
stream: &mut RacStream,
message: &str
) -> Result<(), Box<dyn Error>> {
match stream {
RacStream::WRAC(websocket) => wrac::send_message(websocket, message),
RacStream::RAC(stream) => rac::send_message(stream, message)
}
}
/// Register user
///
/// stream - any stream that can be written to
/// name - user name
/// password - user password
/// remove_null - remove null bytes on reading
///
/// returns whether the user was registered
pub fn register_user(
stream: &mut RacStream,
name: &str,
password: &str,
remove_null: bool
) -> Result<bool, Box<dyn Error>> {
match stream {
RacStream::WRAC(websocket) => wrac::register_user(websocket, name, password),
RacStream::RAC(stream) => rac::register_user(stream, name, password, remove_null)
}
}
/// Send message with auth
///
/// stream - any stream that can be written to
/// message - message text
/// name - user name
/// password - user password
/// remove_null - remove null bytes on reading
///
/// returns 0 if the message was sent successfully
/// returns 1 if the user does not exist
/// returns 2 if the password is incorrect
pub fn send_message_auth(
stream: &mut RacStream,
name: &str,
password: &str,
message: &str,
remove_null: bool
) -> Result<u8, Box<dyn Error>> {
match stream {
RacStream::WRAC(websocket) => wrac::send_message_auth(websocket, name, password, message),
RacStream::RAC(stream) => rac::send_message_auth(stream, name, password, message, remove_null)
}
}
/// Read messages
///
/// max_messages - max messages in list
/// last_size - last returned packet size
/// remove_null - start with skipping null bytes
/// chunked - is chunked reading enabled
///
/// returns (messages, packet size)
pub fn read_messages(
stream: &mut RacStream,
max_messages: usize,
last_size: usize,
remove_null: bool,
chunked: bool
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
match stream {
RacStream::WRAC(websocket) => wrac::read_messages(websocket, max_messages, last_size, chunked),
RacStream::RAC(stream) => rac::read_messages(stream, max_messages, last_size, remove_null, chunked)
}
}

View File

@ -1,83 +1,4 @@
#![allow(unused)]
use std::{error::Error, fmt::Debug, io::{Read, Write}, net::{SocketAddr, TcpStream, ToSocketAddrs}, str::FromStr, time::Duration};
use native_tls::{TlsConnector, TlsStream};
use socks::Socks5Stream;
use crate::util::parse_socks5_url;
pub trait RacStream: Read + Write + Unpin + Send + Sync + Debug {
fn set_read_timeout(&self, timeout: Duration);
fn set_write_timeout(&self, timeout: Duration);
}
impl RacStream for TcpStream {
fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(&self, Some(timeout)); }
fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(&self, Some(timeout)); }
}
impl RacStream for Socks5Stream {
fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(self.get_ref(), Some(timeout)); }
fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(self.get_ref(), Some(timeout)); }
}
impl<T: RacStream> RacStream for TlsStream<T> {
fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); }
fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); }
}
impl RacStream for TlsStream<Box<dyn RacStream>> {
fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); }
fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); }
}
/// Create RAC connection (also you can just TcpStream::connect)
///
/// host - host string, example: "example.com:12345", "example.com" (default port is 42666)
/// ssl - wrap with ssl client, write false if you dont know what it is
/// proxy - socks5 proxy (host, (user, pass))
pub fn connect(host: &str, ssl: bool, proxy: Option<String>) -> Result<Box<dyn RacStream>, Box<dyn Error>> {
let host = if host.contains(":") {
host.to_string()
} else {
format!("{host}:42666")
};
let stream: Box<dyn RacStream> = if let Some(proxy) = proxy {
if let Some((proxy, auth)) = parse_socks5_url(&proxy) {
if let Some((user, pass)) = auth {
Box::new(Socks5Stream::connect_with_password(&proxy, host.as_str(), &user, &pass)?)
} else {
Box::new(Socks5Stream::connect(&proxy, host.as_str())?)
}
} else {
return Err("proxy parse error".into());
}
} else {
let addr = host.to_socket_addrs()?.next().ok_or::<Box<dyn Error>>("addr parse error".into())?;
Box::new(TcpStream::connect(&addr)?)
};
let stream = if ssl {
let ip: String = host.split_once(":")
.map(|o| o.0.to_string())
.unwrap_or(host.clone());
Box::new(TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()?
.connect(&ip, stream)?)
} else {
stream
};
stream.set_read_timeout(Duration::from_secs(3));
stream.set_write_timeout(Duration::from_secs(3));
Ok(stream)
}
use std::{error::Error, io::{Read, Write}};
/// Send message
///
@ -155,30 +76,6 @@ pub fn send_message_auth(
}
}
/// Send message with fake auth
///
/// Explaination:
///
/// let (name, message) = message.split("> ") else { return send_message(stream, message) }
/// if send_message_auth(name, name, message) != 0 {
/// let name = "\x1f" + name
/// register_user(stream, name, name)
/// send_message_spoof_auth(stream, name + "> " + message)
/// }
pub fn send_message_spoof_auth(stream: &mut (impl Write + Read), message: &str, remove_null: bool) -> Result<(), Box<dyn Error>> {
let Some((name, message)) = message.split_once("> ") else { return send_message(stream, message) };
if let Ok(f) = send_message_auth(stream, &name, &message, &message, remove_null) {
if f != 0 {
let name = format!("\x1f{name}");
register_user(stream, &name, &name, remove_null);
send_message_spoof_auth(stream, &format!("{name}> {message}"), remove_null);
}
}
Ok(())
}
/// Skip null bytes and return first non-null byte
pub fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
loop {

128
src/proto/wrac.rs Normal file
View File

@ -0,0 +1,128 @@
use std::{error::Error, io::{Read, Write}};
use tungstenite::{WebSocket, Message};
/// Send message
///
/// stream - any stream that can be written to
/// message - message text
pub fn send_message(
stream: &mut WebSocket<impl Write + Read>,
message: &str
) -> Result<(), Box<dyn Error>> {
stream.write(Message::Binary(format!("\x01{message}").as_bytes().to_vec().into()))?;
stream.flush()?;
Ok(())
}
/// Register user
///
/// stream - any stream that can be written to
/// name - user name
/// password - user password
///
/// returns whether the user was registered
pub fn register_user(
stream: &mut WebSocket<impl Write + Read>,
name: &str,
password: &str
) -> Result<bool, Box<dyn Error>> {
stream.write(Message::Binary(format!("\x03{name}\n{password}").as_bytes().to_vec().into()))?;
stream.flush()?;
if let Ok(msg) = stream.read() {
Ok(!msg.is_binary() || msg.into_data().get(0).unwrap_or(&0) == &0)
} else {
Ok(true)
}
}
/// Send message with auth
///
/// stream - any stream that can be written to
/// message - message text
/// name - user name
/// password - user password
///
/// returns 0 if the message was sent successfully
/// returns 1 if the user does not exist
/// returns 2 if the password is incorrect
pub fn send_message_auth(
stream: &mut WebSocket<impl Write + Read>,
name: &str,
password: &str,
message: &str
) -> Result<u8, Box<dyn Error>> {
stream.write(Message::Binary(format!("\x02{name}\n{password}\n{message}").as_bytes().to_vec().into()))?;
stream.flush()?;
if let Ok(msg) = stream.read() {
if msg.is_binary() {
Ok(0)
} else {
Ok(*msg.into_data().get(0).unwrap_or(&0))
}
} else {
Ok(0)
}
}
/// Read messages
///
/// max_messages - max messages in list
/// last_size - last returned packet size
/// chunked - is chunked reading enabled
///
/// returns (messages, packet size)
pub fn read_messages(
stream: &mut WebSocket<impl Write + Read>,
max_messages: usize,
last_size: usize,
chunked: bool
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
stream.write(Message::Binary(vec![0x00].into()))?;
stream.flush()?;
let packet_size = {
let msg = stream.read()?;
if !msg.is_binary() {
return Err("msg is not binary".into());
}
let len = msg.into_data().to_vec();
String::from_utf8(len)?
.trim_matches(char::from(0))
.parse()?
};
if last_size == packet_size {
return Ok(None);
}
let to_read = if !chunked || last_size == 0 {
stream.write(Message::Binary(vec![0x00, 0x01].into()))?;
packet_size
} else {
stream.write(Message::Binary(format!("\x00\x02{}", last_size).as_bytes().to_vec().into()))?;
packet_size - last_size
};
stream.flush()?;
let msg = stream.read()?;
if !msg.is_binary() {
return Err("msg is not binary".into());
}
let packet_data = msg.into_data().to_vec();
if packet_data.len() > to_read {
return Err("too big msg".into());
}
let packet_data = String::from_utf8_lossy(&packet_data).to_string();
let lines: Vec<&str> = packet_data.split("\n").collect();
let lines: Vec<String> = lines.clone().into_iter()
.skip(if lines.len() >= max_messages { lines.len() - max_messages } else { 0 })
.map(|o| o.to_string())
.collect();
Ok(Some((lines, packet_size)))
}

View File

@ -1,29 +0,0 @@
use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
static ref ANSI_REGEX: Regex = Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap();
static ref CONTROL_CHARS_REGEX: Regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap();
}
pub fn sanitize_text(input: &str) -> String {
let without_ansi = ANSI_REGEX.replace_all(input, "");
let cleaned_text = CONTROL_CHARS_REGEX.replace_all(&without_ansi, "");
cleaned_text.into_owned()
}
/// `socks5://user:pass@127.0.0.1:12345/path -> ("127.0.0.1:12345", ("user", "pass"))` \
/// `socks5://127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \
/// `https://127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \
/// `127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \
/// `user:pass@127.0.0.1:12345 -> ("127.0.0.1:12345", ("user", "pass"))`
pub fn parse_socks5_url(url: &str) -> Option<(String, Option<(String, String)>)> {
let (_, url) = url.split_once("://").unwrap_or(("", url));
let (url, _) = url.split_once("/").unwrap_or((url, ""));
if let Some((auth, url)) = url.split_once("@") {
let (user, pass) = auth.split_once(":")?;
Some((url.to_string(), Some((user.to_string(), pass.to_string()))))
} else {
Some((url.to_string(), None))
}
}

35
uninstall.bat Normal file
View File

@ -0,0 +1,35 @@
@echo off
net session >nul 2>&1 || (
echo This script requires administrator privileges.
pause
exit /b
)
set "TARGET=C:\Program Files\bRAC\bRAC.exe"
for /d %%u in ("C:\Users\*") do (
call :d "%%u\AppData\Roaming\Microsoft\Windows\Desktop"
call :d "%%u\Desktop"
)
cd /d "%TEMP%"
rmdir /s /q "C:\Program Files\bRAC"
exit /b
:d
if not exist "%~1" exit /b
for %%f in ("%~1\*.lnk") do (
call :c "%%~f"
)
exit /b
:c
set "v=%TEMP%\_c.vbs"
> "%v%" echo Set o=CreateObject("WScript.Shell")
>>"%v%" echo Set l=o.CreateShortcut("%~1")
>>"%v%" echo WScript.Echo l.TargetPath
for /f "usebackq delims=" %%t in (`wscript //nologo "%v%"`) do (
if /I "%%t"=="%TARGET%" del /f /q "%~1"
)
del "%v%" >nul
exit /b

14
uninstall.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
getent passwd | while IFS=: read -r name password uid gid gecos home shell; do
rm -rf $home/.config/bRAC;
done
rm -f /bin/bRAC
rm -f ru.themixray.bRAC.png /usr/share/pixmaps
rm -f ru.themixray.bRAC.desktop /usr/share/applications