Verified Commit 60d73ca7 authored by Ole Martin Ruud's avatar Ole Martin Ruud
Browse files

Change to using unhtml for authentication process

parent 250f4b95
......@@ -445,6 +445,7 @@ dependencies = [
"fern 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)",
"structopt 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unhtml 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
......
......@@ -14,3 +14,4 @@ log = "0.4.8"
fern = "0.5.8"
chrono = "0.4.7"
unhtml = { version = "0.7.2", features = ["derive"] }
serde = { version = "1.0.99", features = ["derive"] }
use log::*;
use reqwest::{Client, Request, Response};
use std::io::Write;
use std::string::ToString;
use unhtml::scraper::{Html, Selector};
use unhtml::FromHtml;
use crate::{fmt_dbg, Config, Result, ROM_URL};
use crate::{Config, Result};
fn fmt_dbg<D: std::fmt::Debug>(d: D) -> String {
format!("{:?}", d)
}
pub struct App {
pub config: Config,
......@@ -11,121 +18,136 @@ pub struct App {
impl App {
fn request(&mut self, req: Request) -> Result<Response> {
debug!("Sending request:\n{:#?}\n{:#?}", req, req.body());
trace!("Sending request:\n{:#?}", req);
let res = self.client.execute(req)?;
debug!("Recived response:\n{:#?}", res);
trace!("Recived response:\n{:#?}", res);
Ok(res)
}
/// Run authentication through FEIDE to login to tp.uio.no
pub fn authenticate(&mut self) -> Result<()> {
/// Run authentication through FEIDE to login to tp.uio.no and load the page for ordering rooms
///
/// On success returns a string with the content of the HTML page.
pub fn authenticate(&mut self) -> Result<String> {
info!("Fetching organization selection page");
let req = self.client.get(ROM_URL).build()?;
// TODO ensure success by inspecting response
let req = self.client.get(&self.config.base_url).build()?;
let mut res = self.request(req)?;
info!("Parsing organization selection page");
// TODO reduce boilerplate from html scraping
let html = Html::parse_document(&res.text()?);
let main_form = html
.select(&Selector::parse("form[name=f]").map_err(fmt_dbg)?)
.next()
.ok_or("Did not find main form")?;
// Fetch URL for SAML authentication
let saml_url = {
let mut url = res.url().clone();
// Clear query parameters as we will fill them through `OrgSelectForm`
url.set_query(None);
url
};
let auth_state = main_form
.select(&Selector::parse("input[name=AuthState]").map_err(fmt_dbg)?)
.next()
.ok_or("Did not find auth state")
.and_then(|e| {
e.value()
.attr("value")
.ok_or("Did not find 'value' attr in auth state")
})?;
let auth_len = main_form
.select(&Selector::parse("input[name=asLen]").map_err(fmt_dbg)?)
.next()
.ok_or("Did not find auth state len")
.and_then(|e| {
e.value()
.attr("value")
.ok_or("Did not find 'value' attr in auth state len")
})?;
info!("Parsing organization selection page");
let content = &res.text()?;
let mut org_form = OrgSelectForm::from_html(content).map_err(|e| e.to_string())?;
if org_form.org.is_none() {
// TODO This should probably be specified through CLI instead of default value.
org_form.org = Some("ntnu.no".into());
}
let req = self
.client
.get(res.url().clone())
.query(&[
// TODO turn into proper struct
("AuthState", &*auth_state),
("asLen", &*auth_len),
("org", "ntnu.no"),
])
.build()?;
#[derive(FromHtml, serde::Serialize, Debug)]
#[html(selector = "form[name=f]")]
struct OrgSelectForm {
#[html(selector = "[name=AuthState]", attr = "value")]
#[serde(rename = "AuthState")]
auth_state: String,
#[html(selector = "[name=asLen]", attr = "value")]
#[serde(rename = "asLen")]
as_len: String,
#[html(selector = "[name=org]", attr = "value")]
org: Option<String>,
};
info!("Sending organization selection form");
// TODO inspect result to ensure success
let res = self.request(req)?;
let req = self.client.get(saml_url.clone()).query(&org_form).build()?;
let mut res = self.request(req)?;
info!("Parsing login form");
let content = res.text()?;
let mut login_form = LoginForm::from_html(&content).map_err(|e| e.to_string())?;
login_form.feidename = Some(self.config.username.clone());
login_form.password = Some(self.config.password.clone());
#[derive(FromHtml, serde::Serialize, Debug)]
#[html(selector = "form[name=f]")]
struct LoginForm {
#[html(selector = "[name=AuthState]", attr = "value")]
#[serde(rename = "AuthState")]
auth_state: String,
#[html(selector = "[name=asLen]", attr = "value")]
#[serde(rename = "asLen")]
as_len: String,
#[html(selector = "[name=org]", attr = "value")]
org: String,
#[html(selector = "[name=has_js]", attr = "value")]
has_js: String,
#[html(selector = "[name=inside_iframe]", attr = "value")]
inside_iframe: String,
#[html(selector = "[name=feidename]", attr = "value")]
feidename: Option<String>,
#[html(selector = "[name=password]", attr = "value")]
password: Option<String>,
};
info!("Sending login form");
let req = self
.client
.post(res.url().clone())
// TODO turn into proper struct
.form(&[
("asLen", &*auth_len),
("AuthState", &*auth_state),
("org", "ntnu.no"),
("has_js", "0"),
("inside_iframe", "0"),
("feidename", &self.config.username),
("password", &self.config.password),
])
.post(saml_url.clone())
.form(&login_form)
.build()?;
info!("Sending login form with credentials");
let mut res = self.request(req)?;
info!("Parsing result of login form (SAMLResponse and RelayState)");
// TODO reduce boilerplate from html scraping
let html = Html::parse_document(&res.text()?);
let main_form = html
.select(&Selector::parse("form[method=post]").map_err(fmt_dbg)?)
.next()
.ok_or("Did not find main form")?;
let form_url = main_form
.value()
.attr("action")
.ok_or("Did not find 'action' in main form")?;
let saml_response = main_form
.select(&Selector::parse("input[name=SAMLResponse]").map_err(fmt_dbg)?)
.next()
.ok_or("Did not find SAMLResponse")
.and_then(|n| {
n.value()
.attr("value")
.ok_or("Did not find 'value' attr in SAMLResponse")
})?;
let relay_state = main_form
.select(&Selector::parse("input[name=RelayState]").map_err(fmt_dbg)?)
.next()
.ok_or("Did not find RelayState")
.and_then(|n| {
n.value()
.attr("value")
.ok_or("Did not find 'value' attr in RelayState")
})?;
info!("Parsing login verification form");
let content = res.text()?;
let login_verification_form =
LoginVerificationForm::from_html(&content).map_err(|e| e.to_string())?;
let login_verification_url =
LoginVertificationUrl::from_html(&content).map_err(|e| e.to_string())?;
#[derive(FromHtml, serde::Serialize, Debug)]
#[html(selector = "form")]
struct LoginVerificationForm {
#[html(selector = "[name=SAMLResponse]", attr = "value")]
#[serde(rename = "SAMLResponse")]
saml_response: String,
#[html(selector = "[name=RelayState]", attr = "value")]
#[serde(rename = "RelayState")]
relay_state: String,
};
#[derive(FromHtml, Debug)]
#[html(selector = "form")]
struct LoginVertificationUrl(#[html(attr = "action")] String);
info!("Sending login verification form");
let req = self
.client
.post(form_url)
.form(&[("SAMLResponse", saml_response), ("RelayState", relay_state)])
.post(&login_verification_url.0)
.form(&login_verification_form)
.build()?;
let mut res = self.request(req)?;
info!("Sending SAMLResponse and RelayState form to vertify authentication");
// TODO inspect return to ensure success
let _ = self.request(req)?;
info!("Parsing result of login verification form");
let content = res.text()?;
let auth_header = AuthHeader::from_html(&content).map_err(|e| e.to_string())?;
info!("Successfully authenticated");
#[derive(FromHtml, Debug)]
#[html(selector = "#head-login")]
struct AuthHeader {
#[html(selector = "[title]", attr = "inner")]
org: String,
#[html(selector = "#head-login-user-fullname", attr = "inner")]
fullname: String,
};
Ok(())
info!(
"Successfully authenticated as '{}' at '{}'",
auth_header.fullname, auth_header.org
);
Ok(content)
}
/// Query for avaliable rooms
......@@ -135,7 +157,7 @@ impl App {
let form_params = [
("start", "08:00"), // HH:MM
("duration", "01:00"), // HH:MM
("preset_date", "31.08.2019"), // DD.MM.YYYY
("preset_date", "02.09.2019"), // DD.MM.YYYY
("area", "50000"), // ID
("building", "501"), // ID
("roomtype", ""), // ID
......@@ -145,7 +167,11 @@ impl App {
("preformsubmit", "1"), // 1
];
let req = self.client.post(ROM_URL).form(&form_params).build()?;
let req = self
.client
.post(&self.config.base_url)
.form(&form_params)
.build()?;
info!("Sending room query");
let mut res = self.request(req)?;
......@@ -172,7 +198,11 @@ impl App {
("submitall", "Bestill ⇨"),
];
let req = self.client.post(ROM_URL).form(&form_params).build()?;
let req = self
.client
.post(&self.config.base_url)
.form(&form_params)
.build()?;
info!("Sending room order");
let mut res = self.request(req)?;
......@@ -213,7 +243,11 @@ impl App {
("tokenrb", token), // TOKEN
];
let req = self.client.post(ROM_URL).form(&form_params).build()?;
let req = self
.client
.post(&self.config.base_url)
.form(&form_params)
.build()?;
info!("Sending room order confirmation");
let mut res = self.request(req)?;
......
......@@ -33,10 +33,13 @@ pub struct Config {
verbosity: usize,
#[structopt(short, long, parse(try_from_str = fern::log_file))]
log_file: Option<File>,
}
fn fmt_dbg<D: std::fmt::Debug>(d: D) -> String {
format!("{:?}", d)
#[structopt(
short,
long,
help = "Base URL for tp.uio.no service",
default_value = "https://tp.uio.no/ntnu/rombestilling"
)]
base_url: String,
}
fn main() {
......@@ -53,18 +56,15 @@ fn main() {
}
}
const ROM_URL: &str = "https://tp.uio.no/ntnu/rombestilling/";
fn run() -> Result<()> {
let mut config = Config::from_args();
logger::setup_logging(config.verbosity, config.log_file.take())?;
info!("Creating reqwest client");
let client = Client::builder().cookie_store(true).build()?;
let mut app = App { config, client };
// TODO uncomment when implemented properly
// app.authenticate()?;
app.authenticate()?;
// app.order_room()?;
Ok(())
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment