diff --git a/Cargo.lock b/Cargo.lock index f644bd0..df82d64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index d5fe97a..241bfbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/README.md b/README.md index b941aa1..7666087 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..e13a238 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,82 @@ +use std::collections::HashMap; + +use eyre::eyre; +use reqwest::StatusCode; + +pub struct QbitClient { + url: String, + username: Option, + password: Option, + client: reqwest::Client, +} + +impl QbitClient { + /// Create a new client and authenticate with qbittorrent + pub fn new(full_url: String, username: Option, password: Option) -> Self { + Self { + url: full_url, + username, + password, + client: reqwest::Client::new(), + } + } + + pub async fn login(&self) -> eyre::Result { + 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 { + 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 = body.json().await?; + let port = json["listen_port"].as_u64() + .ok_or(eyre!("listen_port preference missing"))?; + + Ok(port as _) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 2941378..4516cbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, @@ -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 = 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| { - 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) -> 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, config: &Arc) -> 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::()?; + + 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(()) } \ No newline at end of file