diff --git a/Cargo.lock b/Cargo.lock index d19f738..fb0efbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,11 @@ dependencies = [ "regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "dtoa" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "error-chain" version = "0.11.0" @@ -201,6 +206,11 @@ dependencies = [ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "itoa" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -251,6 +261,16 @@ name = "matches" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "maxminddb" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "memchr" version = "1.0.2" @@ -285,6 +305,11 @@ name = "nodrop" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "num-traits" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "num_cpus" version = "1.8.0" @@ -317,11 +342,27 @@ name = "percent-encoding" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "proc-macro2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quote" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "quote" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "r2d2" version = "0.8.2" @@ -488,6 +529,37 @@ name = "serde" version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "serde_derive" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive_internals 0.22.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive_internals" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "sha1" version = "0.2.0" @@ -513,6 +585,16 @@ dependencies = [ "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "syn" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "synom" version = "0.11.3" @@ -599,6 +681,11 @@ name = "unicode-xid" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unreachable" version = "1.0.0" @@ -666,6 +753,15 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "woothee" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "yansi" version = "0.3.4" @@ -673,9 +769,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "zlnk" -version = "0.3.3" +version = "0.4.0" dependencies = [ "dotenv 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "maxminddb 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "r2d2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "r2d2_redis 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -683,6 +780,9 @@ dependencies = [ "regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "rocket 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "rocket_codegen 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "woothee 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [metadata] @@ -702,6 +802,7 @@ dependencies = [ "checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9" "checksum derive-error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "92183014af72c63aea490e66526c712bf1066ac50f66c9f34824f02483ec1d98" "checksum dotenv 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a70de3c590ce18df70743cace1cf12565637a0b26fd8b04ef10c7d33fdc66cdc" +"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" @@ -710,6 +811,7 @@ dependencies = [ "checksum hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)" = "368cb56b2740ebf4230520e2b90ebb0461e69034d85d1945febd9b3971426db2" "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" "checksum isatty 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8f2a233726c7bb76995cec749d59582e5664823b7245d4970354408f1d79a7a2" +"checksum itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" @@ -718,17 +820,21 @@ dependencies = [ "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2" "checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376" +"checksum maxminddb 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6c8ab8ed3f67ca5e60533968945748411de7ffc6bc340c7f5d7305749452e417" "checksum memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" "checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" "checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3" "checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" "checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" +"checksum num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dee092fcdf725aee04dd7da1d21debff559237d49ef1cb3e69bcb8ece44c7364" "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" "checksum ordermap 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "b81cf3b8cb96aa0e73bbedfcdc9708d09fec2854ba8d474be4e6f666d7379e8b" "checksum pear 0.0.12 (registry+https://github.com/rust-lang/crates.io-index)" = "b5c2dabd6c1650d9bfac8e46be7b518b31c3885ab4412de1aca330938616c5bd" "checksum pear_codegen 0.0.12 (registry+https://github.com/rust-lang/crates.io-index)" = "df863bb78b3ee6b049278324eea8df6b2553a8db9a3504c0e32cfcc17bc8d18c" "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" +"checksum proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cd07deb3c6d1d9ff827999c7f9b04cdfd66b1b17ae508e14fe47b620f2282ae0" "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" +"checksum quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1eca14c727ad12702eb4b6bfb5a232287dcf8385cb8ca83a3eeaf6519c44c408" "checksum r2d2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f9078ca6a8a5568ed142083bb2f7dc9295b69d16f867ddcc9849e51b17d8db46" "checksum r2d2_redis 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3b00842df5fabfd571dfeb6173584f55bf32909dfc95c1874f8d1f8b01a7f5c4" "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" @@ -747,10 +853,14 @@ dependencies = [ "checksum scheduled-thread-pool 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a2ff3fc5223829be817806c6441279c676e454cc7da608faf03b0ccc09d3889" "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" "checksum serde 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)" = "0e100d00fb985a5bf16b857a436450e404fa613de3321b2e383947a93cbd75df" +"checksum serde_derive 1.0.36 (registry+https://github.com/rust-lang/crates.io-index)" = "6fffe22d41dbddcead5b2c380c4714d44f2eb39292f7e7a0d966d2d45bf56408" +"checksum serde_derive_internals 0.22.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d2f04ed291686ce195a5c8f554aaf36e50a721fbf829ee3b6151e6f85eccf945" +"checksum serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "5c508584d9913df116b91505eec55610a2f5b16e9ed793c46e4d0152872b3e74" "checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" "checksum smallvec 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ee4f357e8cd37bf8822e1b964e96fd39e2cb5a0424f8aaa284ccaccc2162411c" "checksum state 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e2fe297055568778ddc83eb1d4292bcdab36bf9e5e7adf4d0ce4ee59caf778d9" "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +"checksum syn 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)" = "c97c05b8ebc34ddd6b967994d5c6e9852fa92f8b82b3858c39451f97346dcce5" "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" "checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" @@ -763,6 +873,7 @@ dependencies = [ "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" "checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f" "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" "checksum untrusted 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f392d7819dbe58833e26872f5f6f0d68b7bbbe90fc3667e98731c4a15ad9a7ae" "checksum url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f808aadd8cfec6ef90e4a14eb46f24511824d1ac596b9682703c87056c8678b7" @@ -774,4 +885,5 @@ dependencies = [ "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum woothee 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f66c1a3efe1a227c65b485bfed3d5c0282edbaa31e5a05b4bf8b808a7dc43d85" "checksum yansi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a503e4eea629f145a693c8ed1eddba88b3b9de5171c6ebd0e2820cf82d38f934" diff --git a/Cargo.toml b/Cargo.toml index 360d68f..bcc7234 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zlnk" -version = "0.3.3" +version = "0.4.0" authors = ["Jonas Kuche "] [dependencies] @@ -12,3 +12,8 @@ r2d2_redis = "0.7.0" dotenv = "0.11.0" regex = "0.2.10" rand = "0.4.2" +url = "1.7.0" +woothee = "0.8.0" +maxminddb = "0.9.0" +serde_json = "1.0.13" + diff --git a/src/env_loader.rs b/src/env_loader.rs index 4a93387..f9f6d79 100644 --- a/src/env_loader.rs +++ b/src/env_loader.rs @@ -8,7 +8,9 @@ pub struct Env { pub url_regex: Regex, pub short_length: usize, pub short_alphabet: String, - pub bad_request_message: String + pub bad_request_message: String, + pub mmdb_path: String, + pub trust_proxy: bool } const URL_REGEX: &'static str = r"^(https?://)?([\da-z\.-]+)\.([a-z\.]{2,6})([/\w \.-]*)*/?$"; @@ -20,6 +22,8 @@ pub fn init() -> Env { url_regex: Regex::new(&env::var("URL_REGEX").unwrap_or(URL_REGEX.to_string())).unwrap(), short_length: usize::from_str(&env::var("SHORT_LENGTH").unwrap_or("5".to_string())).unwrap(), short_alphabet: env::var("SHORT_ALPHABET").unwrap_or("hex".to_string()), - bad_request_message: env::var("BAD_REQUEST_MESSAGE").unwrap_or("Ungültige URL".to_string()) + bad_request_message: env::var("BAD_REQUEST_MESSAGE").unwrap_or("Ungültige URL".to_string()), + mmdb_path: env::var("MMDB_PATH").unwrap_or("./GeoLite2-Country.mmdb".to_string()), + trust_proxy: env::var("TRUST_PROXY").is_ok() } } diff --git a/src/geo_locate_ip.rs b/src/geo_locate_ip.rs new file mode 100644 index 0000000..36c1342 --- /dev/null +++ b/src/geo_locate_ip.rs @@ -0,0 +1,27 @@ +use maxminddb::{geoip2, Reader}; +use std::net::IpAddr; +use std::str::FromStr; + +pub struct GeoLocateIP { + reader: Reader +} + +impl GeoLocateIP { + pub fn new(path: String) -> Self { + let reader = Reader::open(&path).unwrap(); + GeoLocateIP {reader: reader} + } + pub fn locate(&self, ip_string: String) -> Option { + let ip: IpAddr = FromStr::from_str(&ip_string).unwrap(); + let country = self.reader.lookup(ip); + match country { + Ok(result) => { + let country: geoip2::Country = result; + Some(country.country.unwrap().iso_code.unwrap()) + } + Err(_err) => { + None + } + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d73391d..3090237 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,9 +7,16 @@ extern crate rand; extern crate r2d2; extern crate r2d2_redis; extern crate redis; +extern crate url; +extern crate woothee; +extern crate maxminddb; +#[macro_use] +extern crate serde_json; mod env_loader; mod shortener; +mod geo_locate_ip; +mod stats; #[cfg(test)] mod tests; @@ -23,6 +30,8 @@ use rocket::State; use r2d2::Pool; use r2d2_redis::RedisConnectionManager; use env_loader::Env; +use geo_locate_ip::GeoLocateIP; +use stats::Stats; use shortener::{short, long}; macro_rules! static_file { @@ -54,9 +63,9 @@ fn shorten(long_url: String, env: State, pool: State")] -fn longen(short: String, pool: State>) -> Option { +fn longen(short: String, pool: State>, stats: Stats) -> Option { let connection = &pool.get().unwrap(); - let long = long(short, connection); + let long = long(short, connection, stats); match long { Some(long) => { Some(Redirect::to(&long)) @@ -67,6 +76,20 @@ fn longen(short: String, pool: State>) -> Option/stats")] +fn stats(short: String, pool: State>) -> Option> { + let connection = &pool.get().unwrap(); + let stats = Stats::stats_as_json(short, connection); + match stats { + Ok(stats) => { + Some(Content(ContentType::JSON, stats.to_string())) + } + Err(_stats) => { + None + } + } +} + #[error(404)] fn not_found() -> Content> { Content(ContentType::HTML, include_bytes!("../static/404.html").to_vec()) @@ -80,12 +103,13 @@ fn bad_request(request: &Request) -> String { fn main() { let env = env_loader::init(); + let geo_locate_ip = GeoLocateIP::new(env.mmdb_path.clone()); let manager = RedisConnectionManager::new(env.redis_url.as_str()).unwrap(); let pool = r2d2::Pool::builder() .build(manager) .unwrap(); rocket::ignite() - .mount("/", routes![index, js, jquery, bootstrap_css, bootstrap_js, shorten, longen]) - .catch(errors![not_found, bad_request]).manage(env).manage(pool) + .mount("/", routes![index, js, jquery, bootstrap_css, bootstrap_js, shorten, longen, stats]) + .catch(errors![not_found, bad_request]).manage(env).manage(geo_locate_ip).manage(pool) .launch(); } diff --git a/src/shortener.rs b/src/shortener.rs index 1f79e1d..94f8327 100644 --- a/src/shortener.rs +++ b/src/shortener.rs @@ -1,4 +1,5 @@ use env_loader::Env; +use stats::Stats; use rand::{thread_rng, Rng}; use r2d2; use r2d2_redis; @@ -61,15 +62,15 @@ pub fn short(long_url: String, env: &Env, connection: &r2d2::PooledConnection) -> Option { - let long = connection.get(format!("short_{}", short_url)); +pub fn long(short_url: String, connection: &r2d2::PooledConnection, stats: Stats) -> Option { + let long = connection.get(format!("short_{}", &short_url)); match long { Ok(long) => { + stats.save(short_url, connection).unwrap(); Some(long) } Err(_err) => { None } } - } \ No newline at end of file diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..df97385 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,158 @@ +use rocket::{request, Request, Outcome, State}; +use rocket::request::FromRequest; +use r2d2; +use r2d2_redis; +use redis::{Commands, RedisError}; +use url::Url; +use woothee::parser::Parser; +use env_loader::Env; +use geo_locate_ip::GeoLocateIP; +use serde_json::Value; + +pub struct Stats { + pub refer: String, + pub browser: String, + pub os: String, + pub country: String +} + +struct IP { + ip: String +} + +impl<'a, 'r> FromRequest<'a, 'r> for IP { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let env = request.guard::>().unwrap(); + let trust_proxy = env.trust_proxy; + let forwarded_for_vec: Vec<_> = request.headers().get("X-Forwarded-For").collect(); + if trust_proxy { + let last = forwarded_for_vec.len() - 1; + let forwarded_for = forwarded_for_vec[last]; + return Outcome::Success(IP {ip: forwarded_for.to_string()}); + } else { + let ip = request.remote().unwrap().ip(); + return Outcome::Success(IP {ip: ip.to_string()}); + } + } +} + +impl<'a, 'r> FromRequest<'a, 'r> for Stats { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let ip = request.guard::().unwrap(); + let geo_locate_ip = request.guard::>().unwrap(); + let refers: Vec<_> = request.headers().get("Referer").collect(); + let refer; + if refers.len() == 1 { + let refer_domain = Url::parse(refers[0]).unwrap().host_str().unwrap().to_owned(); + refer = Some(refer_domain.to_string()); + } else { + refer = None; + } + let user_agents: Vec<_> = request.headers().get("User-Agent").collect(); + let browser; + let os; + if user_agents.len() == 1 { + let parser = Parser::new(); + let result = parser.parse(user_agents[0]).unwrap(); + browser = Some(result.name); + os = Some(result.os); + } else { + browser = None; + os = None; + } + let country = geo_locate_ip.locate(ip.ip); + + return Outcome::Success(Stats { + refer: refer.unwrap_or("Unknown".to_string()), + browser: browser.unwrap_or("Unknown".to_string()), + os: os.unwrap_or("Unknown".to_string()), + country: country.unwrap_or("Unknown".to_string()) + }); + } +} + +impl Stats { + pub fn save(&self, short: String, connection: &r2d2::PooledConnection) -> Result<(), RedisError> { + let stats_key = &format!("stats_{}", short); + let refer_key = &format!("refer_{}", self.refer); + let browser_key = &format!("browser_{}", self.browser); + let os_key = &format!("os_{}", self.os); + let country_key = &format!("country_{}", self.country); + let clicks_exists: bool = connection.hexists(stats_key, "clicks")?; + let refer_exists: bool = connection.hexists(stats_key, refer_key)?; + let browser_exists: bool = connection.hexists(stats_key, browser_key)?; + let os_exists: bool = connection.hexists(stats_key, os_key)?; + let country_exists: bool = connection.hexists(stats_key, country_key)?; + if clicks_exists { + let _: () = connection.hincr(stats_key, "clicks", 1)?; + } else { + let _: () = connection.hset(stats_key, "clicks", 1)?; + } + if refer_exists { + let _: () = connection.hincr(stats_key, refer_key, 1)?; + } else { + let _: () = connection.hset(stats_key, refer_key, 1)?; + } + if browser_exists { + let _: () = connection.hincr(stats_key, browser_key, 1)?; + } else { + let _: () = connection.hset(stats_key, browser_key, 1)?; + } + if os_exists { + let _: () = connection.hincr(stats_key, os_key, 1)?; + } else { + let _: () = connection.hset(stats_key, os_key, 1)?; + } + if country_exists { + let _: () = connection.hincr(stats_key, country_key, 1)?; + } else { + let _: () = connection.hset(stats_key, country_key, 1)?; + } + Ok(()) + } + pub fn stats_as_json(short: String, connection: &r2d2::PooledConnection) -> Result { + let _exists: String = connection.get(format!("short_{}", &short))?; + let stats_key = &format!("stats_{}", short); + let mut clicks: u64 = 0; + let mut refers: Vec = Vec::new(); + let mut browsers: Vec = Vec::new(); + let mut oss: Vec = Vec::new(); + let mut countries: Vec = Vec::new(); + let keys: Vec = connection.hkeys(stats_key)?; + for key in keys.iter() { + let value = connection.hget(stats_key, key)?; + if key == "clicks" { + clicks = value; + } else { + let split: Vec<&str> = key.split('_').collect(); + let typ = split[0]; + let name = split[1]; + let val = json!({ + name: value + }); + if typ == "refer" { + refers.push(val); + } else if typ == "browser" { + browsers.push(val); + } else if typ == "os" { + oss.push(val); + } else if typ == "country" { + countries.push(val); + } else { + panic!("Invalid typ: {}!", typ); + } + } + } + Ok(json!({ + "clicks": clicks, + "refers": refers, + "browsers": browsers, + "oss": oss, + "countries": countries + })) + } +} \ No newline at end of file diff --git a/src/tests.rs b/src/tests.rs index eeeed0f..fa8b2b7 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4,11 +4,22 @@ use r2d2; use r2d2_redis::RedisConnectionManager; use redis; use env_loader; +use stats::Stats; use shortener::{short, long}; +fn create_fake_stats() -> Stats { + Stats { + refer: "zlnk.de".to_string(), + browser: "Firefox".to_string(), + os: "Linux".to_string(), + country: "Unknown".to_string() + } +} + #[test] fn shortener_test() { let env = &env_loader::init(); + let stats = create_fake_stats(); let manager = RedisConnectionManager::new(env.redis_url.as_str()).unwrap(); let pool = r2d2::Pool::builder() .build(manager) @@ -17,7 +28,7 @@ fn shortener_test() { let _:() = redis::cmd("FLUSHDB").query(connection.deref()).unwrap(); let long_url = "https://zlnk.de".to_string(); let shorted = short(long_url.clone(), env, connection, None).unwrap(); - let longed = long(shorted, connection).unwrap(); + let longed = long(shorted, connection, stats).unwrap(); assert_eq!(longed, long_url); }