diff --git a/.gitignore b/.gitignore index ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba..b0d62291848d1dd3ec174f7e470ee7216915ca88 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +*.sqlite +*.db diff --git a/Cargo.lock b/Cargo.lock index ff528f3d167396d7eec94fb425658e5c31e871c6..09c13291d19d44b0a2edc753607d9006781c7d1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + [[package]] name = "cookie" version = "0.11.3" @@ -159,7 +172,7 @@ dependencies = [ "hkdf", "hmac", "percent-encoding 2.1.0", - "rand", + "rand 0.7.3", "sha2", "time", ] @@ -221,11 +234,26 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "ffddns-web" version = "0.1.0" dependencies = [ + "chrono", + "rand 0.8.3", "rocket", + "rusqlite", ] [[package]] @@ -248,6 +276,17 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] + [[package]] name = "ghash" version = "0.2.3" @@ -344,6 +383,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + [[package]] name = "language-tags" version = "0.2.2" @@ -356,6 +401,22 @@ version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" +[[package]] +name = "libsqlite3-sys" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d90181c2904c287e5390186be820e5ef311a3c62edebb7d6ca3d6a48ce041d" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "log" version = "0.3.9" @@ -374,6 +435,15 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "matches" version = "0.1.8" @@ -395,6 +465,25 @@ dependencies = [ "log 0.3.9", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -445,6 +534,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + [[package]] name = "polyval" version = "0.3.3" @@ -485,11 +580,23 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom", + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.3.0", + "rand_core 0.6.1", + "rand_hc 0.3.0", ] [[package]] @@ -499,7 +606,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.1", ] [[package]] @@ -508,7 +625,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom", + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5" +dependencies = [ + "getrandom 0.2.2", ] [[package]] @@ -517,7 +643,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core 0.6.1", ] [[package]] @@ -573,6 +708,29 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "rusqlite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a656821bb6317a84b257737b7934f79c0dbb7eb694710475908280ebad3e64" +dependencies = [ + "bitflags", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "libsqlite3-sys", + "lru-cache", + "memchr", + "serde_json", + "time", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + [[package]] name = "safemem" version = "0.3.3" @@ -585,6 +743,17 @@ version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +[[package]] +name = "serde_json" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.8.2" @@ -739,6 +908,12 @@ dependencies = [ "percent-encoding 1.0.1", ] +[[package]] +name = "vcpkg" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" + [[package]] name = "version_check" version = "0.1.5" diff --git a/README.md b/README.md index b80f793ca07630f71e1b94662a1a0bc4361b7d0d..605bc9c9366159025977bb2006153719e9a3fae4 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ Daten speichern müssen. Ein paar Stichpunkte: - Token wird gelöscht - Domain kann wieder registriert werden +Um Spam zu vermeiden sollten wir ein captcha in irgendeiner Form nutzen. + + Über das Token könnte man auch zusätzliche Dinge realisieren, die Authentifizierung benötigen. Z.B. ein simples Webinterface um einzusehen, wann die Domain zuletzt geupdatet wurde oder um sie manuell freizugeben. diff --git a/ffddns-web/Cargo.toml b/ffddns-web/Cargo.toml index d4d708ce9d9a938ecad2b2f4eb39a9ceb4c63632..50459fd7211b8006638b6ad0595009385d9bc2e7 100644 --- a/ffddns-web/Cargo.toml +++ b/ffddns-web/Cargo.toml @@ -8,3 +8,6 @@ edition = "2018" [dependencies] rocket = "0.4.7" +rusqlite = {version = "0.21", features = ["chrono", "serde_json"]} +chrono = "0.4.19" +rand = "0.8.3" diff --git a/ffddns-web/src/db.rs b/ffddns-web/src/db.rs new file mode 100644 index 0000000000000000000000000000000000000000..27c7f17b57eb5b135e650b73f341faf119e6b22d --- /dev/null +++ b/ffddns-web/src/db.rs @@ -0,0 +1,104 @@ +use rusqlite as sqlite; +use sqlite::params; +use std::path::PathBuf; +use chrono::{Utc, DateTime}; +use std::net::{Ipv4Addr, Ipv6Addr}; + + +pub struct Database { + conn: sqlite::Connection, +} + + +unsafe impl Send for Database {} +unsafe impl Sync for Database {} + + +impl Database { + pub fn new(path: PathBuf) -> Self { + let conn: sqlite::Connection = sqlite::Connection::open(&path).unwrap(); + conn.execute_batch(include_str!("init.sql")).unwrap(); + Database { conn } + } + + pub fn get_all_domains(&self) -> Vec<Domain> { + let mut stmt: sqlite::Statement = self.conn.prepare("SELECT * FROM domains").unwrap(); + + stmt.query_map( + params![], + |row| Ok(Domain::from_row(row)) + ).unwrap().map(|x| x.unwrap()).collect() + } + + pub fn insert_new_domain(&self, d: &Domain) { + self.conn.execute( + "INSERT INTO domains VALUES ($1, $2, $3, $4, $5)", + params![d.domainname, d.token, d.lastupdate, d.ipv4.map(|x| x.to_string()), d.ipv6.map(|x| x.to_string())] + ).unwrap(); + } + + + pub fn get_domain(&self, domain: &String) -> Option<Domain> { + let r: sqlite::Result<_> = self.conn.query_row_and_then( + "SELECT * FROM domains WHERE domainname=$1", + params![domain], + |row| Ok(Domain::from_row(row)) + ); + + match r { + Err(_) => None, + Ok(o) => Some(o) + } + } + + pub fn remove_domain(&self, d: String) { + self.conn.execute( + "DELETE FROM domains WHERE domainname=$1", + params![d] + ).unwrap(); + } + + + pub fn update_lastupdate(&self, d: &String, lastupdate: DateTime<Utc>) { + self.conn.execute( + "UPDATE domains SET lastupdate=$2 WHERE domainname=$1", + params![d, lastupdate] + ).unwrap(); + } + + pub fn update_ipv4(&self, d: &String, addr: Ipv4Addr) { + self.conn.execute( + "UPDATE domains SET ipv4=$2 WHERE domainname=$1", + params![d, addr.to_string()] + ).unwrap(); + } + + pub fn update_ipv6(&self, d: &String, addr: Ipv6Addr) { + self.conn.execute( + "UPDATE domains SET ipv6=$2 WHERE domainname=$1", + params![d, addr.to_string()] + ).unwrap(); + } +} + + +#[derive(Debug, Clone)] +pub struct Domain { + pub domainname: String, + pub token: String, + pub lastupdate: Option<DateTime<Utc>>, + pub ipv4: Option<Ipv4Addr>, + pub ipv6: Option<Ipv6Addr>, +} + +impl Domain { + fn from_row(row: &sqlite::Row) -> Self { + Self { + domainname: row.get("domainname").unwrap(), + token: row.get("token").unwrap(), + lastupdate: row.get("lastupdate").unwrap(), + ipv4: row.get::<_, Option<String>>("ipv4").unwrap().map(|x| x.parse().unwrap()), + ipv6: row.get::<_, Option<String>>("ipv6").unwrap().map(|x| x.parse().unwrap()), + } + } +} diff --git a/ffddns-web/src/init.sql b/ffddns-web/src/init.sql new file mode 100644 index 0000000000000000000000000000000000000000..f02ddb0444552da0441a1e66e38cb249bed51a4a --- /dev/null +++ b/ffddns-web/src/init.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS domains ( + domainname TEXT PRIMARY KEY, + token TEXT NOT NULL, + lastupdate TEXT, + ipv4 TEXT, + ipv6 TEXT +); diff --git a/ffddns-web/src/main.rs b/ffddns-web/src/main.rs index aac2ed8d209d2e8e5df6e8587cdcc6f9985584d6..b7f3c6966078edfb78c7f02ace9337ff5ba0f803 100644 --- a/ffddns-web/src/main.rs +++ b/ffddns-web/src/main.rs @@ -1,17 +1,145 @@ #![feature(proc_macro_hygiene, decl_macro)] -use rocket; -use rocket::get; -use rocket::routes; +mod db; +use chrono::{DateTime, Utc}; +use db::{ + Database, + Domain, +}; +use rocket::{ + self, + get, + post, + routes, + State, +}; +use rocket::request::{ + Request, + FromRequest, + Outcome, +}; +use std::fmt::{self, Display}; +use std::net::IpAddr; +use rand; -#[get("/update?<key>&<address>")] -fn index(key: String, address: Option<String>) -> String { - format!("{}: updating ip address to {:?}", key, address) + +pub struct ClientIp(IpAddr); + + +impl ClientIp { + pub fn inner(&self) -> &IpAddr { + let ClientIp(ip) = self; + ip + } + pub fn into_inner(self) -> IpAddr { + let ClientIp(ip) = self; + ip + } +} + + +impl Display for ClientIp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.inner().to_string()) + } +} + + +impl<'a, 'r> FromRequest<'a, 'r> for ClientIp { + type Error = String; + + fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> { + let ip = request.client_ip().unwrap(); + Outcome::Success(ClientIp(ip)) + } +} + + +#[derive(Debug, Clone)] +pub struct DomainUpdate { + domain: String, + ip: IpAddr +} + + +#[get("/update?<token>&<domain>&<ip>")] +fn update(db: State<Database>, clientip: ClientIp, token: String, domain: String, ip: Option<String>) -> String { + let new_ip: IpAddr = { + if let Some(iip) = ip { + iip.parse::<IpAddr>().unwrap() + } + else { + clientip.into_inner() + } + }; + + let d = db.get_domain(&domain).unwrap(); + + if d.token != token { + return "not a valid token".to_string(); + } + + match new_ip { + IpAddr::V4(addr) => db.update_ipv4(&domain, addr), + IpAddr::V6(addr) => db.update_ipv6(&domain, addr), + } + + db.update_lastupdate(&domain, Utc::now()); + + format!("{} updated to {:?}", domain, new_ip) +} + + +#[get("/create?<domain>")] +fn create(db: State<Database>, domain: String) -> String { + let token = generate_token(); + let d = Domain { + domainname: domain.clone(), + token: token.clone(), + lastupdate: None, + ipv4: None, + ipv6: None + }; + + db.insert_new_domain(&d); + + format!("your token for {}: {}", domain, token) } +#[get("/status?<domain>")] +fn status(db: State<Database>, domain: String) -> String { + let domaininfo = match db.get_domain(&domain) { + None => return "domain not found".to_string(), + Some(r) => r, + }; + + format!("{:#?}", domaininfo) +} + + + fn main() { + let db = db::Database::new("./ffddns.sqlite".into()); + rocket::ignite() - .mount("/", routes![index]) + .mount("/", routes![ + update, + create, + status + ]) + .manage(db) .launch(); } + + + +fn generate_token() -> String { + let mut token = String::new(); + + for _ in 0..8 { + token.push_str(&format!("{:02x}", rand::random::<u8>())); + } + + token +}