Rewrite to make more reliable

This commit is contained in:
SeanOMik 2024-05-18 23:26:03 -04:00
parent 0e527da269
commit 1ad352967c
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
5 changed files with 173 additions and 240 deletions

181
Cargo.lock generated
View File

@ -17,12 +17,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "atomic"
version = "0.6.0"
@ -120,25 +114,6 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "crossbeam-channel"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c3242926edf34aec4ac3a77108ad4854bffaa2e4ddc1824124ce59231302d5"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f"
dependencies = [
"cfg-if",
]
[[package]]
name = "encoding_rs"
version = "0.8.33"
@ -193,18 +168,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "filetime"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys 0.52.0",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -235,15 +198,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "futures-channel"
version = "0.3.29"
@ -300,14 +254,15 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "gluetun-qbit-port-updater"
version = "0.1.2"
version = "0.2.0"
dependencies = [
"anyhow",
"eyre",
"figment",
"notify",
"reqwest",
"serde",
"serde_json",
"stable-eyre",
"tokio",
]
[[package]]
@ -444,26 +399,6 @@ version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "ipnet"
version = "2.9.0"
@ -485,26 +420,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kqueue"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -557,7 +472,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.48.0",
]
@ -580,25 +494,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.4.1",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"walkdir",
"windows-sys 0.48.0",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
@ -717,9 +612,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "proc-macro2"
version = "1.0.70"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
dependencies = [
"unicode-ident",
]
@ -739,9 +634,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.33"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
@ -818,15 +713,6 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.22"
@ -861,18 +747,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.193"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.193"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
@ -881,9 +767,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.108"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
@ -944,9 +830,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.41"
version = "2.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269"
checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f"
dependencies = [
"proc-macro2",
"quote",
@ -1004,9 +890,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.35.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"bytes",
@ -1015,9 +901,21 @@ dependencies = [
"num_cpus",
"pin-project-lite",
"socket2 0.5.5",
"tokio-macros",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
@ -1126,16 +1024,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
@ -1243,15 +1131,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View File

@ -1,14 +1,15 @@
[package]
name = "gluetun-qbit-port-updater"
version = "0.1.2"
version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
eyre = "0.6.12"
figment = { version = "0.10.12", features = ["env"] }
notify = "6.1.1"
reqwest = { version = "0.11.22", features = ["blocking"] }
reqwest = { version = "0.11.22", features = ["blocking", "json"] }
serde = { version = "1.0.193", features = ["serde_derive"] }
serde_json = "1.0.117"
stable-eyre = "0.2.2"
tokio = { version = "1.37.0", features = ["rt", "macros", "fs"] }

View File

@ -1,14 +1,18 @@
# Gluetun Qbittorrent Port Updater
Automatically updates qBittorrent's listening port to the port forwarded by [Gluetun](https://github.com/qdm12/gluetun/).
It works by checking the port in use by qBittorrent, and reading the port file created by Gluetun. If they do not match, it updates the port that qBittorrent is using.
## Configuration
Currently the only configuration method is through environmental variables:
| Name | Description | Example | Required |
| Name | Description | Example | Required | Default |
|---|---|---|---|
| PORT_UPD_QBITTORRENT_LOGIN | The login information for the webui. | `admin` | false |
| PORT_UPD_QBITTORRENT_PASSWORD | The password for the webui. | `adminadmin` | false |
| PORT_UPD_QBITTORRENT_PORT | The port of the webui. | `8080` | true |
| PORT_UPD_QBITTORRENT_HOST | The host of the webui. | `localhost` | true |
| PORT_UPD_QBITTORRENT_HTTPS | Set to `true` if the connection to the webui is https encrypted. | `false` | true |
| PORT_UPD_PORT_FILE | The path to the file that contains the port. | `/tmp/gluetun/forwarded_port` | true |
| PORT_UPD_QBITTORRENT_LOGIN | The login information for the webui. | `admin` | false | empty string |
| PORT_UPD_QBITTORRENT_PASSWORD | The password for the webui. | `adminadmin` | false | empty string |
| PORT_UPD_QBITTORRENT_PORT | The port of the webui. | `8080` | true | |
| PORT_UPD_QBITTORRENT_HOST | The host of the webui. | `localhost` | true | |
| PORT_UPD_QBITTORRENT_HTTPS | Set to `true` if the connection to the webui is https encrypted. | `false` | true | |
| PORT_UPD_PORT_FILE | The path to the file that contains the port. | `/tmp/gluetun/forwarded_port` | true | |
| PORT_UPD_MAX_FAILURES | The amount of times to recheck the port before exiting. | 10 | false | 10 |
| PORT_UPD_RECHECK_PERIOD | The period, in second, to check the port. | 60 | false | 60 |

82
src/client.rs Normal file
View File

@ -0,0 +1,82 @@
use std::collections::HashMap;
use eyre::eyre;
use reqwest::StatusCode;
pub struct QbitClient {
url: String,
username: Option<String>,
password: Option<String>,
client: reqwest::Client,
}
impl QbitClient {
/// Create a new client and authenticate with qbittorrent
pub fn new(full_url: String, username: Option<String>, password: Option<String>) -> Self {
Self {
url: full_url,
username,
password,
client: reqwest::Client::new(),
}
}
pub async fn login(&self) -> eyre::Result<String> {
let login_url = format!("{}/api/v2/auth/login", self.url);
let mut form = HashMap::new();
if let (Some(username), Some(password)) = (&self.username, &self.password) {
form.insert("username", username);
form.insert("password", password);
}
let res = self.client.post(login_url)
.form(&form)
.send().await?;
let cookie = if let Some(cookie) = res.headers().get(reqwest::header::SET_COOKIE) {
let cookies: Vec<_> = cookie.to_str().unwrap().split(";").collect();
let sid = cookies.iter().find(|c| c.starts_with("SID=")).unwrap();
sid[4..].to_string()
//format!("{};", sid)
} else if res.status() == StatusCode::FORBIDDEN {
return Err(eyre!("Failure to auth, credentials are incorrect!"));
} else {
res.error_for_status()?;
"".to_string()
};
Ok(cookie)
}
pub async fn set_listen_port(&self, token: String, port: u32) -> eyre::Result<()> {
let mut form = HashMap::new();
let json = format!("{{\"listen_port\": \"{}\"}}", port);
form.insert("json", json);
let cookie = format!("SID={};", token);
let url = format!("{}/api/v2/app/setPreferences", self.url);
let _body = self.client.post(url)
.form(&form)
.header("Cookie", cookie)
.send().await?
.error_for_status()?;
Ok(())
}
pub async fn get_listen_port(&self, token: String) -> eyre::Result<u32> {
let cookie = format!("SID={};", token);
let url = format!("{}/api/v2/app/preferences", self.url);
let body = self.client.post(url)
.header("Cookie", cookie)
.send().await?
.error_for_status()?;
let json: HashMap<String, serde_json::Value> = body.json().await?;
let port = json["listen_port"].as_u64()
.ok_or(eyre!("listen_port preference missing"))?;
Ok(port as _)
}
}

View File

@ -1,12 +1,12 @@
use std::{path::Path, time::Duration, sync::Arc, collections::HashMap};
use std::{path::Path, process::exit, sync::Arc, time::Duration};
use anyhow::anyhow;
use figment::{Figment, providers::Env};
use notify::{RecursiveMode, Watcher, Event, EventKind, event::{ModifyKind, DataChange}};
use reqwest::StatusCode;
use serde::Deserialize;
use stable_eyre::eyre::Report;
mod client;
use client::*;
#[derive(Debug, PartialEq, Deserialize)]
struct Config {
qbittorrent_login: Option<String>,
@ -15,6 +15,18 @@ struct Config {
qbittorrent_port: u32,
qbittorrent_host: String,
port_file: String,
#[serde(default="default_max_failures")]
max_failures: u32,
#[serde(default="default_seconds_delay")]
recheck_period: u32,
}
fn default_max_failures() -> u32 {
10
}
fn default_seconds_delay() -> u32 {
60
}
impl Config {
@ -31,110 +43,65 @@ impl Config {
}
}
fn main() -> Result<(), Report> {
#[tokio::main]
async fn main() -> Result<(), Report> {
stable_eyre::install()?;
let config: Arc<Config> = Arc::new(Figment::new()
.merge(Env::prefixed("PORT_UPD_"))
.extract()?);
let client = Arc::new(QbitClient::new(config.qbit_full_url(), config.qbittorrent_login.clone(), config.qbittorrent_password.clone()));
let port_path = Path::new(&config.port_file);
while !port_path.exists() {
println!("Couldn't find {}", config.port_file);
println!("Could not find {}", config.port_file);
println!("Trying again in 10 seconds");
std::thread::sleep(Duration::from_secs(10));
}
/* let url = format!("{}/api/v2/app/version", config.qbit_full_url());
while let Err(e) = reqwest::blocking::get(&url) {//.and_then(|r| r.error_for_status()) {
println!("Couldn't reach qbittorrent at {} ({})", url, e);
println!("Trying again in 10 seconds");
std::thread::sleep(Duration::from_secs(10));
} */
let config_clone = config.clone();
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
match res {
Ok(e) => {
//println!("e: {:?}", e);
if e.kind == EventKind::Modify(ModifyKind::Data(DataChange::Content))
|| e.kind.is_create() || e.kind.is_access() {
while let Err(e) = update_port(config_clone.clone()) {
println!("Could not update qbittorrent port! ({e})");
println!("Trying again in 10 seconds");
std::thread::sleep(Duration::from_secs(10));
}
}
},
Err(e) => println!("watch error: {:?}", e),
}
})?;
// update the port once on start in the case that gluetun got connected before this started
while let Err(e) = update_port(config.clone()) {
while let Err(e) = ensure_port_sync(&client, &config).await {
println!("Could not update qbittorrent port! ({e})");
println!("Trying again in 10 seconds");
std::thread::sleep(Duration::from_secs(10));
}
let mut fails = 0;
loop {
match watcher.watch(port_path, RecursiveMode::Recursive) {
tokio::time::sleep(Duration::from_secs(config.recheck_period as _)).await;
match ensure_port_sync(&client, &config).await {
Ok(()) => {},
Err(e) => {
println!("watch error: {:?}", e);
fails += 1;
if fails >= config.max_failures {
println!("Exceeded max failure count, exiting...");
exit(1);
}
println!("Trying again in 10 seconds");
std::thread::sleep(Duration::from_secs(10));
tokio::time::sleep(Duration::from_secs(10)).await;
}
}
}
}
fn update_port(config: Arc<Config>) -> anyhow::Result<()> {
let qbit_url = config.qbit_full_url();
let client = reqwest::blocking::Client::new();
let login_url = format!("{}/api/v2/auth/login", qbit_url);
let mut form = HashMap::new();
if let (Some(login), Some(pass)) = (config.qbittorrent_login.clone(),
config.qbittorrent_password.clone()) {
form.insert("username", login);
form.insert("password", pass);
}
let res = client.post(login_url)
.form(&form)
.send()?;
let cookie = if let Some(cookie) = res.headers().get(reqwest::header::SET_COOKIE) {
let cookies: Vec<_> = cookie.to_str().unwrap().split(";").collect();
let sid = cookies.iter().find(|c| c.starts_with("SID=")).unwrap();
format!("{};", sid)
} else if res.status() == StatusCode::FORBIDDEN {
return Err(anyhow!("Failure to auth, credentials are incorrect!"));
} else {
res.error_for_status()?;
"".to_string()
};
async fn ensure_port_sync(client: &Arc<QbitClient>, config: &Arc<Config>) -> eyre::Result<()> {
let new_port = std::fs::read_to_string(&config.port_file)?;
let new_port = new_port.trim();
let new_port = new_port.trim().parse::<u32>()?;
let token = client.login().await?;
let mut form = HashMap::new();
let json = format!("{{\"listen_port\": \"{}\"}}", new_port);
form.insert("json", json);
let current_port = client.get_listen_port(token.clone()).await?;
let url = format!("{}/api/v2/app/setPreferences", qbit_url);
let _body = client.post(url)
.form(&form)
.header("Cookie", cookie)
.send()?
.error_for_status()?;
println!("Successfully updated qbittorrent to port {}", new_port);
if new_port != current_port {
println!("Detected port change: {}", new_port);
client.set_listen_port(token.clone(), 0).await?;
client.set_listen_port(token, new_port).await?;
println!("Updated port");
}
Ok(())
}