Browse Source

[WIP]

master
Erik Zscheile 7 months ago
parent
commit
0cf898f303
4 changed files with 185 additions and 86 deletions
  1. +27
    -27
      src/bin/dsbfnr-wlog.rs
  2. +5
    -27
      src/bin/dsbfnr.rs
  3. +25
    -0
      src/lib.rs
  4. +128
    -32
      src/query_dsb.rs

+ 27
- 27
src/bin/dsbfnr-wlog.rs View File

@@ -1,7 +1,5 @@
fn main() {
use clap::Arg;
use dsbfnr::fetch;
use std::sync::Arc;

let matches = clap::App::new("dsbfnr-wlog")
.version(clap::crate_version!())
@@ -14,6 +12,12 @@ fn main() {
.takes_value(true)
.help("sets the destination dir (defaults to '.')"),
)
.arg(
Arg::with_name("nested")
.short("n")
.long("nested")
.help("create subdirectory in destdir based on dsbpath and week"),
)
.arg(
Arg::with_name("user")
.short("u")
@@ -58,34 +62,30 @@ fn main() {
)
.expect("unable to query DSB for dsbpath");
println!("using dsbpath = {}", dsbpath);
let week = matches.value_of("week").unwrap();

let progbar_master = indicatif::MultiProgress::new();
let mut destination = matches.value_of("destdir").unwrap_or(".").to_string();
if matches.is_present("nested") {
destination.reserve(destination.len() + dsbpath.len() + week.len() + 3);
destination += "/";
destination += &dsbpath;
destination += "/";
destination += week;
if std::path::Path::new(&destination).is_dir() {
println!("dsbfnr-wlog: destination already exists!");
return;
}
}
let destination = std::path::PathBuf::from(destination + "/");
std::fs::create_dir_all(&destination).expect("unable to create destination directory");

let shared = Arc::new(fetch::SharedWorkerData {
destination: std::path::PathBuf::from(
matches.value_of("destdir").unwrap_or(".").to_string() + "/",
),
urlbase: url::Url::parse(&format!(
dsbfnr::fetch_all(
destination,
url::Url::parse(&format!(
"https://app.dsbcontrol.de/data/{}/{}/",
dsbpath, matches.value_of("week").unwrap()
dsbpath, week
))
.expect("invalid dsbpath"),
progbar_sum: progbar_master.add(indicatif::ProgressBar::new(0)),
cooldown_dur: std::time::Duration::from_micros(200),
});

shared.progbar_sum.set_style(dsbfnr::PROGBAR_STYLE.clone());

let ths: Vec<_> = categories
.iter()
.map(|cat| fetch::spawn_fetch_folder(shared.clone(), cat, &progbar_master))
.collect();

std::mem::drop(shared);

progbar_master.join().unwrap();

for i in ths.into_iter() {
i.join().unwrap();
}
&categories[..],
);
}

+ 5
- 27
src/bin/dsbfnr.rs View File

@@ -1,8 +1,5 @@
use dsbfnr::fetch;

fn main() {
use clap::Arg;
use std::sync::Arc;

let matches = clap::App::new("dsbfnr")
.version(clap::crate_version!())
@@ -36,33 +33,14 @@ fn main() {
let categories: Vec<_> = matches.values_of("categories").unwrap().collect();

curl::init();
let progbar_master = indicatif::MultiProgress::new();

let shared = Arc::new(fetch::SharedWorkerData {
destination: std::path::PathBuf::from(
matches.value_of("destdir").unwrap_or(".").to_string() + "/",
),
urlbase: url::Url::parse(&format!(
dsbfnr::fetch_all(
std::path::PathBuf::from(matches.value_of("destdir").unwrap_or(".").to_string() + "/"),
url::Url::parse(&format!(
"https://app.dsbcontrol.de/data/{}/",
matches.value_of("dsbpath").unwrap()
))
.expect("invalid dsbpath"),
progbar_sum: progbar_master.add(indicatif::ProgressBar::new(0)),
cooldown_dur: std::time::Duration::from_micros(200),
});

shared.progbar_sum.set_style(dsbfnr::PROGBAR_STYLE.clone());

let ths: Vec<_> = categories
.iter()
.map(|cat| fetch::spawn_fetch_folder(shared.clone(), cat, &progbar_master))
.collect();

std::mem::drop(shared);

progbar_master.join().unwrap();

for i in ths.into_iter() {
i.join().unwrap();
}
&categories[..],
);
}

+ 25
- 0
src/lib.rs View File

@@ -8,3 +8,28 @@ lazy_static::lazy_static! {
.template("{bar:80.bold.green/blue} {pos:>4}/{len:4} [{elapsed} | ETA:{eta}]")
.progress_chars("═╸┄");
}

pub fn fetch_all(destination: std::path::PathBuf, urlbase: url::Url, categories: &[&str]) {
let progbar_master = indicatif::MultiProgress::new();

let shared = std::sync::Arc::new(fetch::SharedWorkerData {
destination,
urlbase,
progbar_sum: progbar_master.add(indicatif::ProgressBar::new(0)),
cooldown_dur: std::time::Duration::from_micros(200),
});

shared.progbar_sum.set_style(PROGBAR_STYLE.clone());

let ths: Vec<_> = categories
.iter()
.map(|cat| fetch::spawn_fetch_folder(shared.clone(), cat, &progbar_master))
.collect();

std::mem::drop(shared);
progbar_master.join().unwrap();

for i in ths.into_iter() {
i.join().unwrap();
}
}

+ 128
- 32
src/query_dsb.rs View File

@@ -1,18 +1,9 @@
use std::collections::HashMap;
use std::io::{Read, Write};

static BUNDLE_ID: &str = "de.ytrizja.dsbfnr";

static LOGIN_DATA_APITAG: &[u8] = b"__LASTFOCUS=&__VIEWSTATE=%2FwEPDwULLTEyMzYwN\
jY5NTAPZBYCAgMPFgIeBXN0eWxlBS1iYWNrZ3JvdW5kLWltYWdlOnVybCgnaW1nL2JnX21vcm5pb\
mcuanBnJyk7KTsWAgIBD2QWAgIJDxYCHgRUZXh0BQRXRUIxZGSN%2BjtYE9gRM9l4lHibtfPe4LZ\
eEk7Ux%2Fcq1E43S%2B08Lg%3D%3D&__VIEWSTATEGENERATOR=C2EE9ABB&__EVENTTARGET=&\
__EVENTARGUMENT=&__EVENTVALIDATION=%2FwEdAATQ7TzOYK6kE0lH%2Bc499SptDFTzKcXJq\
Lg%2BOeJ6QAEa2kPTPkdPWl%2B8YN2NtDCtxic2kbNk7CH1EMXf%2B0xXlqLzXXr8CpBQh17r6W3\
v5y0TGLi7T6d707LeFt4rJdxONTI%3D&ctl03=Anmelden";

// we should try "https://app.dsbcontrol.de/JsonHandler.ashx/GetData"...
static BUNDLE_ID: &str = "de.heinekingmedia.inhouse.dsbmobile.web";
static API_URL_GET_DATA: &str =
"https://www.dsbmobile.de/jhw-ecd92528-a4b9-425f-89ee-c7038c72b9a6.ashx/GetData";
"https://app.dsbcontrol.de/JsonHandler.ashx/GetData";
static API_URL_LOGIN: &str = "https://www.dsbmobile.de/Login.aspx";
static API_URL_DEFAULT: &str = "https://www.dsbmobile.de/default.aspx";

@@ -69,6 +60,48 @@ fn curl_postrd_helper<'a>(
move |into| Ok(data.read(into).unwrap())
}

fn curl_wr_helper<'a>(
buf: &'a mut Vec<u8>,
) -> impl FnMut(&[u8]) -> Result<usize, curl::easy::WriteError> + 'a {
move |data| {
buf.extend_from_slice(data);
Ok(data.len())
}
}

fn parse_html_attrs(mut inp: &str) -> (HashMap<&str, String>, &str) {
fn parse_single_attr(inp: &str) -> Option<(&str, String, &str)> {
inp.find('=').map(|x| {
let (key, mut rest) = inp.split_at(x);
rest = &rest[1..];
let (mut is_escaped, mut is_quoted) = (false, false);
let mut value = String::with_capacity(rest.len());
let mut rit = rest.chars();
while let Some(i) = rit.next() {
match i {
_ if is_escaped => {
value.push(i);
is_escaped = false;
}
'"' => is_quoted = !is_quoted,
'\\' => is_escaped = true,
_ if i.is_whitespace() && !is_quoted => break,
_ => value.push(i),
}
}
value.shrink_to_fit();
(key, value, rit.as_str())
})
}
let mut ret = HashMap::new();
inp = inp.trim();
while let Some((key, value, rest)) = parse_single_attr(inp) {
ret.insert(key, value);
inp = rest.trim_start();
}
(ret, inp)
}

fn jhw_marshal(data: &json::JsonValue) -> Result<String, ApiProtocolError> {
use flate2::Compression;
let mut e = flate2::write::GzEncoder::new(Vec::new(), Compression::default());
@@ -112,6 +145,7 @@ fn jhw_unmarshal(data: &[u8]) -> Result<json::JsonValue, ApiProtocolError> {
d.read_to_string(&mut s).map_err(map_err_marshalled)?;
s
} else {
eprintln!("\njson_r := {}\n", json_r.dump());
return Err(ApiProtocolError {
marshalled: true,
direction: ApiDirection::Response,
@@ -127,42 +161,86 @@ fn jhw_unmarshal(data: &[u8]) -> Result<json::JsonValue, ApiProtocolError> {
})
}

fn query_jhw(progbar: &indicatif::ProgressBar, user: &str, pass: &str) -> Result<json::JsonValue, ApiError> {
fn query_jhw(
progbar: &indicatif::ProgressBar,
user: &str,
pass: &str,
) -> Result<json::JsonValue, ApiError> {
use curl::easy::Easy;
let mut h = Easy::new();
h.cookie_file("")?;
// 0. get login params
let apitag = {
h.url(API_URL_LOGIN)?;
h.get(true)?;
let mut buf = Vec::new();
{
let mut htr = h.transfer();
htr.write_function(curl_wr_helper(&mut buf))?;
htr.perform()?;
}
let mut apitag_ser = url::form_urlencoded::Serializer::new(String::new());
apitag_ser.append_pair("__LASTFOCUS", "");
apitag_ser.append_pair("__EVENTTARGET", "");
apitag_ser.append_pair("__EVENTARGUMENT", "");
for i in std::str::from_utf8(&buf[..])
.map_err(|e| ApiProtocolError {
marshalled: false,
direction: ApiDirection::Response,
kind: e.into(),
})?
.lines()
{
let i_ = i.trim_start_matches("<input type=\"hidden\" ");
if i == i_ {
continue;
}
let i = i_.trim_end_matches(" />");
if i == i_ {
continue;
}
let (attrs, rest) = parse_html_attrs(i);
if let Some((name, value)) = attrs
.get("name")
.and_then(|name| attrs.get("value").map(|value| (name, value)))
{
assert!(rest.is_empty());
let name: String = url::form_urlencoded::byte_serialize(name.as_bytes()).collect();
let value: String =
url::form_urlencoded::byte_serialize(value.as_bytes()).collect();
apitag_ser.append_pair(&name, &value);
}
}
apitag_ser.finish()
};
h.reset();

// 1. login
{
let logdat = apitag + &format!("&txtUser={}&txtPass={}&ctl03=Anmelden", user, pass);
h.url(API_URL_LOGIN)?;
h.post(true)?;
let mut logdat = Vec::new();
logdat.extend(LOGIN_DATA_APITAG.iter());
logdat.extend(format!("&txtUser={}&txtPass={}", user, pass).as_bytes());
let mut hdrs = curl::easy::List::new();
hdrs.append(&format!("Content-Length: {}", logdat.len()))?;
h.http_headers(hdrs)?;
let mut htr = h.transfer();
htr.read_function(curl_postrd_helper(&logdat[..]))?;
htr.read_function(curl_postrd_helper(logdat.as_bytes()))?;
htr.perform()?;
}
progbar.inc(1);
h.reset();
// 2. JHW API call
let dat_res = {
use chrono::prelude::*;
h.url(API_URL_GET_DATA)?;
h.post(true)?;
let mut hdrs = curl::easy::List::new();
hdrs.append("Accept: application/json, text/javascript, */*; q=0.01")?;
hdrs.append("Accept-Encoding: gzip, deflate, br")?;
hdrs.append(&("Referer: ".to_string() + API_URL_DEFAULT))?;
hdrs.append("Content-Type: application/json;charset=utf-8")?;
hdrs.append(&("Bundle_ID: ".to_string() + BUNDLE_ID))?;
h.http_headers(hdrs)?;
let curdate = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let ua = "Mozilla/5.0 (X11; Linux i686; rv:68.0) Gecko/20100101 Firefox/68.0";
let dat_req = json::object!(
"UserId" => "",
"UserPw" => "",
"Abos" => json::array![],
"AppVersion" => "2.3",
"Language" => "de",
"OsVersion" => curl::Version::num(),
"OsVersion" => ua,
"AppId" => "",
"Device" => "WebApp",
"PushId" => "",
@@ -170,18 +248,32 @@ fn query_jhw(progbar: &indicatif::ProgressBar, user: &str, pass: &str) -> Result
"Date" => curdate.clone(),
"LastUpdate" => curdate.clone(),
);
println!("\ndat_req = {}\n", &dat_req);
let dat_req = jhw_marshal(&dat_req)?;
println!("\ndat_req = {}\n", &dat_req);
progbar.inc(1);

h.url(API_URL_GET_DATA)?;
h.post(true)?;
let mut hdrs = curl::easy::List::new();
hdrs.append(&("User-Agent: ".to_string() + ua))?;
hdrs.append("Accept: application/json, text/javascript, */*; q=0.01")?;
hdrs.append("Accept-Language: de,en-US;q=0.7,en;q=0.3")?;
hdrs.append("Accept-Encoding: gzip, deflate, br")?;
hdrs.append("Content-Type: application/json;charset=utf-8")?;
hdrs.append(&format!("Content-Length: {}", dat_req.len()))?;
hdrs.append(&("Referer: ".to_string() + API_URL_DEFAULT))?;
hdrs.append(&("Bundle_ID: ".to_string() + BUNDLE_ID))?;
hdrs.append("X-Requested-With: XMLHttpRequest")?;
h.http_headers(hdrs)?;
let mut buf = Vec::new();
{
let mut htr = h.transfer();
htr.read_function(curl_postrd_helper(dat_req.as_bytes()))?;
htr.write_function(|data| {
buf.extend_from_slice(data);
Ok(data.len())
})?;
htr.write_function(curl_wr_helper(&mut buf))?;
htr.perform()?;
}
println!("\nresponse_code = {}\n", h.response_code()?);
progbar.inc(1);
jhw_unmarshal(&buf[..])?
};
@@ -209,7 +301,7 @@ fn map_select_title<'a>(
}

pub fn query_dsbpath(user: &str, pass: &str) -> Result<String, ApiError> {
let progbar = indicatif::ProgressBar::new(6).with_style(crate::PROGBAR_STYLE.clone());
let progbar = indicatif::ProgressBar::new(7).with_style(crate::PROGBAR_STYLE.clone());

let res = query_jhw(&progbar, user, pass)?;

@@ -217,6 +309,10 @@ pub fn query_dsbpath(user: &str, pass: &str) -> Result<String, ApiError> {
// | map(select(.Title == "Pläne")) | .[].Root.Childs
// | map(select(.Title == "Schüler Stundenplan")) | .[].Id

if let Some(x) = res["ResultStatusInfo"].as_str() {
progbar.println(x);
}

if let Some(mut x) = res["ResultMenuItems"]
.members()
.filter_map(|i| map_select_title("Inhalte", i))


Loading…
Cancel
Save