From 2776fe4e5f6258cadff52c3ffb8b8b246949b92d Mon Sep 17 00:00:00 2001 From: Piotr Czajka Date: Sun, 17 Sep 2023 12:35:35 +0200 Subject: [PATCH] async and sqlite cookie support --- .gitignore | 2 + Cargo.toml | 5 +- README.rst | 15 ++- src/bin.rs | 114 ++++++++++-------- src/firefox.rs | 100 ++++++++++++--- src/lib.rs | 114 ++++++++++++------ .../Profiles/1qbuu7ux.default/cookies.sqlite | Bin 0 -> 1572864 bytes 7 files changed, 238 insertions(+), 112 deletions(-) create mode 100644 tests/resources/Profiles/1qbuu7ux.default/cookies.sqlite diff --git a/.gitignore b/.gitignore index 3d69c04..6a145ed 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ **/*.rs.bk Cargo.lock tags +cookies.sqlite-shm +cookies.sqlite-wal diff --git a/Cargo.toml b/Cargo.toml index 45d7487..a99466f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "browsercookie-rs" -version = "0.1.1" +version = "0.2.0" authors = ["Bharadwaj Machiraju ", "Piotr Czajka `_. diff --git a/src/bin.rs b/src/bin.rs index 92dff51..de80647 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -1,70 +1,88 @@ +use browsercookie::{Attribute, Browser, CookieFinder}; +use clap::{App, Arg}; use regex::Regex; -use clap::{Arg, App}; -use browsercookie::{Browser, Browsercookies}; #[macro_use] extern crate clap; -fn curl_output(bc: &Browsercookies, domain_regex: &Regex) { - print!("Cookie: {}", bc.to_header(domain_regex).unwrap()); +async fn curl_output(cookie_finder: &CookieFinder) { + let cookie_jar = cookie_finder.find().await; + let cookie = cookie_jar.iter().last().expect("Cookie not found"); + print!("Cookie: {}", cookie); } -fn python_output(bc: &Browsercookies, domain_regex: &Regex) { - print!("{{'Cookie': '{}'}}", bc.to_header(domain_regex).unwrap()); +async fn python_output(cookie_finder: &CookieFinder) { + let cookie_jar = cookie_finder.find().await; + let cookie = cookie_jar.iter().last().expect("Cookie not found"); + print!("{{'Cookie': '{}'}}", cookie); } -fn main() { +#[tokio::main] +async fn main() { let matches = App::new("browsercookies") - .version(crate_version!()) - .author(crate_authors!()) - .about(crate_description!()) - .arg(Arg::with_name("domain") - .short("d") - .long("domain") - .value_name("DOMAIN_REGEX") - .required(true) - .help("Sets a domain filter for cookies") - .takes_value(true)) - .arg(Arg::with_name("browser") - .short("b") - .long("browser") - .value_name("BROWSER") - .multiple(true) - .default_value("firefox") - .help("Accepted values: firefox (only one can be provided)") - .takes_value(true)) - .arg(Arg::with_name("name") - .short("n") - .long("name") - .conflicts_with("output") - .value_name("COOKIE_NAME") - .help("Specify a cookie name to output only that value") - .takes_value(true)) - .arg(Arg::with_name("output") - .short("o") - .long("output") - .value_name("OUTPUT_FORMAT") - .help("Accepted values: curl,python (only one can be provided)") - .default_value("curl") - .takes_value(true)) - .get_matches(); + .version(crate_version!()) + .author(crate_authors!()) + .about(crate_description!()) + .arg( + Arg::with_name("domain") + .short("d") + .long("domain") + .value_name("DOMAIN_REGEX") + .required(true) + .help("Sets a domain filter for cookies") + .takes_value(true), + ) + .arg( + Arg::with_name("browser") + .short("b") + .long("browser") + .value_name("BROWSER") + .multiple(true) + .default_value("firefox") + .help("Accepted values: firefox (only one can be provided)") + .takes_value(true), + ) + .arg( + Arg::with_name("name") + .short("n") + .long("name") + .conflicts_with("output") + .value_name("COOKIE_NAME") + .help("Specify a cookie name to output only that value") + .takes_value(true), + ) + .arg( + Arg::with_name("output") + .short("o") + .long("output") + .value_name("OUTPUT_FORMAT") + .help("Accepted values: curl,python (only one can be provided)") + .default_value("curl") + .takes_value(true), + ) + .get_matches(); - let mut bc = Browsercookies::new(); let domain_regex = Regex::new(matches.value_of("domain").unwrap()).unwrap(); - for b in matches.values_of("browser").unwrap() { - if b == "firefox" { - bc.from_browser(Browser::Firefox, &domain_regex).expect("Failed to get cookies from firefox"); + let mut builder = CookieFinder::builder().with_regexp(domain_regex, Attribute::Domain); + + for b in matches.values_of("browser").unwrap() { + if b == "firefox" { + builder = builder.with_browser(Browser::Firefox); } } if let Some(cookie_name) = matches.value_of("name") { - print!("{}", bc.cj.get(cookie_name).expect("Cookie not present").value()); + builder.build().find().await.iter().for_each(|c| { + if c.name() == cookie_name { + println!("{}", c.value()); + } + }); } else { match matches.value_of("output").unwrap() { - "curl" => curl_output(&bc, &domain_regex), - "python" => python_output(&bc, &domain_regex), - _ => () + "curl" => curl_output(&builder.build()).await, + "python" => python_output(&builder.build()).await, + _ => (), } } } diff --git a/src/firefox.rs b/src/firefox.rs index e27f731..86fca6c 100644 --- a/src/firefox.rs +++ b/src/firefox.rs @@ -2,17 +2,22 @@ use byteorder::{LittleEndian, ReadBytesExt}; use cookie::{Cookie, CookieJar}; #[allow(unused_imports)] use dirs::home_dir; +use futures::TryStreamExt; use ini::Ini; use lz4::block::decompress; use memmap::MmapOptions; use regex::Regex; use serde_json::Value; +use sqlx::prelude::*; +use sqlx::sqlite::SqliteConnectOptions; +use sqlx::SqliteConnection; use std::error::Error; use std::fs::File; use std::io::Cursor; use std::path::{Path, PathBuf}; use crate::errors::BrowsercookieError; +use crate::Attribute; #[allow(non_snake_case)] #[derive(Deserialize, Debug)] @@ -80,7 +85,7 @@ fn get_default_profile_path(master_profile: &Path) -> Result { @@ -93,11 +98,44 @@ fn get_default_profile_path(master_profile: &Path) -> Result Result<(), Box> { + let options = SqliteConnectOptions::new() + .filename(sqlite_path) + .read_only(true) + .immutable(true); + let mut conn = SqliteConnection::connect_with(&options) + .await + .expect("Could not connect to cookies.sqlite"); + let mut query = sqlx::query("SELECT name, value, host from moz_cookies").fetch(&mut conn); + + while let Some(row) = query.try_next().await? { + let name: String = row.get(0); + let value: String = row.get(1); + let host: String = row.get(2); + + if domain_regex.0.is_match(&host) { + cookie_jar.add( + Cookie::build(name, value) + .domain(host) + .path("/") + .secure(false) + .http_only(false) + .finish(), + ); + } + } + Ok(()) +} + +async fn load_from_recovery( recovery_path: &Path, - bcj: &mut Box, - domain_regex: &Regex, -) -> Result> { + cookie_jar: &mut CookieJar, + regex_and_attribute: &(Regex, Attribute), +) -> Result<(), Box> { let recovery_file = File::open(recovery_path)?; let recovery_mmap = unsafe { MmapOptions::new().map(&recovery_file)? }; @@ -126,8 +164,8 @@ fn load_from_recovery( serde_json::from_value(c.clone()) as Result { // println!("Loading for {}: {}={}", cookie.host, cookie.name, cookie.value); - if domain_regex.is_match(&cookie.host) { - bcj.add( + if regex_and_attribute.0.is_match(&cookie.host) { + cookie_jar.add( Cookie::build(cookie.name, cookie.value) .domain(cookie.host) .path(cookie.path) @@ -138,11 +176,14 @@ fn load_from_recovery( } } } - Ok(true) + Ok(()) } -pub(crate) fn load(bcj: &mut Box, domain_regex: &Regex) -> Result<(), Box> { - // Returns a CookieJar on heap if following steps go right +pub(crate) async fn load( + cookie_jar: &mut CookieJar, + regex_and_attribute: &(Regex, Attribute), +) -> Result<(), Box> { + // Returns a CookieJar if following steps go right // // 1. Get default profile path for firefox from master ini profiles config. // 2. Load cookies from recovery json (sessionstore-backups/recovery.jsonlz4) @@ -156,16 +197,19 @@ pub(crate) fn load(bcj: &mut Box, domain_regex: &Regex) -> Result<(), let profile_path = get_default_profile_path(&master_profile_path)?; - let mut recovery_path = profile_path; + let mut recovery_path = profile_path.clone(); recovery_path.push("sessionstore-backups/recovery.jsonlz4"); - if !recovery_path.exists() { - return Err(Box::new(BrowsercookieError::InvalidCookieStore( - String::from("Firefox invalid cookie store"), - ))); + if recovery_path.exists() { + load_from_recovery(&recovery_path, cookie_jar, regex_and_attribute).await?; } - load_from_recovery(&recovery_path, bcj, domain_regex)?; + let mut sqlite_path = profile_path.clone(); + + if sqlite_path.exists() { + sqlite_path.push("cookies.sqlite"); + load_from_sqlite(&sqlite_path, cookie_jar, regex_and_attribute).await?; + } Ok(()) } @@ -174,14 +218,15 @@ pub(crate) fn load(bcj: &mut Box, domain_regex: &Regex) -> Result<(), mod tests { use super::*; - #[test] - fn test_recovery_load() { + #[tokio::test] + async fn test_recovery_load() { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push("tests/resources/recovery.jsonlz4"); let mut bcj = Box::new(CookieJar::new()); let domain_re = Regex::new(".*").unwrap(); - load_from_recovery(&path, &mut bcj, &domain_re) + load_from_recovery(&path, &mut bcj, &(domain_re, Attribute::Domain)) + .await .expect("Failed to load from firefox recovery json"); let c = bcj @@ -195,6 +240,23 @@ mod tests { assert_eq!(c.domain(), Some("addons.mozilla.org")); } + #[tokio::test] + async fn test_sqlite_load() { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/resources/Profiles/1qbuu7ux.default/cookies.sqlite"); + let domain_re = Regex::new(".*").unwrap(); + let mut bcj = Box::new(CookieJar::new()); + load_from_sqlite(&path, &mut bcj, &(domain_re, Attribute::Domain)) + .await + .unwrap(); + + let cookie = bcj.get("somename").unwrap(); + + assert_eq!(cookie.value(), "somevalue"); + assert_eq!(cookie.path(), Some("/")); + assert_eq!(cookie.domain(), Some("somehost")); + } + #[test] fn test_master_profile() { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); diff --git a/src/lib.rs b/src/lib.rs index ce776d8..37d4be4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,18 +5,19 @@ //! used with other http libraries like Hyper etc.. //! //! ```rust,ignore -//! use Browsercookie::{Browser, Browsercookies}; +//! use Browsercookie::{Browser, Attribute, CookieFinder}; +//! +//! let mut cookie_jar = CookieFinder::builder() +//! .with_regexp(Regex::new(".*").unwrap(), Attribute::Domain) +//! .with_browser(Browser::Firefox) +//! .build().find().await.unwrap(); +//! +//! println!("{}", cookie_jar.get("searched_cookie_name").unwrap()); //! -//! let mut bc = Browsercookies::new(); -//! let domain_regex = Regex::new(".*"); -//! bc.from_browser(Browser::Firefox, &domain_regex).expect("Failed to get firefox browser cookies"); -//! if let Ok(cookie_header) = bc.to_header(&domain_regex) as Result> { -//! println!("Cookies extracted"); -//! } //! ``` //! //! Using above `to_header` returns a string to be used with http clients as a header -//! directly. +//! directlytrue. //! //! ```rust,ignore //! use reqwest::header; @@ -38,7 +39,7 @@ //! ``` use cookie::CookieJar; use regex::Regex; -use std::error::Error; +use std::collections::HashSet; #[macro_use] extern crate serde; @@ -47,42 +48,66 @@ pub mod errors; mod firefox; /// All supported browsers +#[derive(PartialEq, Eq, Hash)] pub enum Browser { Firefox, } -/// Main struct facilitating operations like collection & parsing of cookies from browsers -pub struct Browsercookies { - pub cj: Box, +pub enum Attribute { + Name, + Value, + Domain, + Path, } -impl Default for Browsercookies { - fn default() -> Self { - Self::new() - } +#[derive(Default)] +pub struct CookieFinder { + regex_and_attribute_pairs: Vec<(Regex, Attribute)>, + browsers: HashSet, } -impl Browsercookies { - pub fn new() -> Browsercookies { - Browsercookies { - cj: Box::new(CookieJar::new()), - } +#[derive(Default)] +pub struct CookieFinderBuilder { + cookie_finder: CookieFinder, +} + +impl CookieFinderBuilder { + pub fn with_regexp(mut self, regex: Regex, attribute: Attribute) -> Self { + self.cookie_finder + .regex_and_attribute_pairs + .push((regex, attribute)); + self } - pub fn from_browser(&mut self, b: Browser, domain_regex: &Regex) -> Result<(), Box> { - match b { - Browser::Firefox => return firefox::load(&mut self.cj, domain_regex), - } + pub fn with_browser(mut self, browser: Browser) -> Self { + self.cookie_finder.browsers.insert(browser); + self + } + + pub fn build(self) -> CookieFinder { + self.cookie_finder + } +} + +impl CookieFinder { + pub fn builder() -> CookieFinderBuilder { + CookieFinderBuilder::default() } - pub fn to_header(&self, domain_regex: &Regex) -> Result> { - let mut header = String::from(""); - for cookie in self.cj.iter() { - if domain_regex.is_match(cookie.domain().unwrap()) { - header.push_str(&format!("{}={}; ", cookie.name(), cookie.value())); + pub async fn find(&self) -> CookieJar { + let mut cookie_jar = CookieJar::new(); + for regex_and_attribute in &self.regex_and_attribute_pairs { + for browser in &self.browsers { + match browser { + Browser::Firefox => { + firefox::load(&mut cookie_jar, regex_and_attribute) + .await + .expect("Something went wrong loading the cookies from Firefox"); + } + } } } - Ok(header) + cookie_jar } } @@ -90,14 +115,25 @@ impl Browsercookies { mod tests { use super::*; - #[test] - fn test_firefox() { - let mut bc = Browsercookies::new(); + #[tokio::test] + async fn test_firefox() { let domain_regex = Regex::new(".*").unwrap(); - bc.from_browser(Browser::Firefox, &domain_regex) - .expect("Failed to get firefox browser cookies"); - if let Ok(cookie_header) = bc.to_header(&domain_regex) as Result> { - assert_eq!(cookie_header, "name=value; "); - } + let cookies = CookieFinder::builder() + .with_regexp(domain_regex, Attribute::Domain) + .with_browser(Browser::Firefox) + .build() + .find() + .await; + assert_eq!(cookies.iter().count(), 2); + let recovery_cookie = cookies.get("name").unwrap(); + assert_eq!(recovery_cookie.value(), "value"); + assert_eq!(recovery_cookie.domain(), Some("httpbin.org")); + assert_eq!(recovery_cookie.path(), Some("/")); + + let sqlite_cookie = cookies.get("somename").unwrap(); + + assert_eq!(sqlite_cookie.value(), "somevalue"); + assert_eq!(sqlite_cookie.path(), Some("/")); + assert_eq!(sqlite_cookie.domain(), Some("somehost")); } } diff --git a/tests/resources/Profiles/1qbuu7ux.default/cookies.sqlite b/tests/resources/Profiles/1qbuu7ux.default/cookies.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..49b8d7e7bc2bc698f86f10c9573b3b93eda8bace GIT binary patch literal 1572864 zcmeIzU2has9Khk(1xt$*@U@~=Mk@lc5si9jbiq{{ODpX*k*j7Xqn+5@DLb=(CRh^_ z{Rqa7;uSG`78CC9(s<>6o0ill8g4Y9&rE(f^R{QtKD)^-U7RcGJpHU3^s+jAGx;WI zHk0?#G)ajOq-!3!`OwG(RzRT z%P)$&+I84Gzu0MaJ88FlZmyH=x{;nLuB5Z`-Oh)d#q^WK*$eH(OX@l2*Dp$g{dA``sd@-dljI`^D8^9>mM@vlo{;>8TM_D;>$S(hByE!&BJJU#t{+0Yj zb#*3GmlhenikL({Co0u4%7;jW%FYjl) zd^BFm)`wdMYh_iRz5c_Gza(SLec!+H>+PMfc`(2ln|G4UJ8yk`X3hRk3LHCQ-%|pw z{=V1uJ@!A~;}Rf1fB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXFJfOhT*!1*~$%(4$ z<^8OekH)pKs?R>)G#)2FfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+z`qH+91k3e9*s^!