use std::sync::Arc;
use cookie::Cookie;
use futures::{stream, StreamExt, TryFutureExt, TryStream, TryStreamExt};
use hyper::{
body::{self, Bytes},
client::{connect::Connect, ResponseFuture},
header, Body, Client, HeaderMap, Request,
};
use thiserror::Error;
use crate::{
ids::{Course, Semester},
Career,
};
const USER_AGENT: &str = "ubs";
const FAKE1_URL: &str = "https://www.pub.hub.buffalo.edu/psc/csprdpub_1/EMPLOYEE/SA/c/SSR_STUDENT_FL.SSR_CLSRCH_MAIN_FL.GBL?Page=SSR_CLSRCH_MAIN_FL&pslnkid=CS_S201605302223124733554248&ICAJAXTrf=true&ICAJAX=1&ICMDTarget=start&ICPanelControlStyle=%20pst_side1-fixed%20pst_panel-mode%20";
const FAKE2_URL: &str ="https://www.pub.hub.buffalo.edu/psc/csprdpub_1/EMPLOYEE/SA/c/SSR_STUDENT_FL.SSR_CLSRCH_ES_FL.GBL?Page=SSR_CLSRCH_ES_FL&SEARCH_GROUP=SSR_CLASS_SEARCH_LFF&SEARCH_TEXT=gly%20105&ES_INST=UBFLO&ES_STRM=2231&ES_ADV=N&INVOKE_SEARCHAGAIN=PTSF_GBLSRCH_FLUID";
macro_rules! PAGE1_URL {
() => { "https://www.pub.hub.buffalo.edu/psc/csprdpub_3/EMPLOYEE/SA/c/SSR_STUDENT_FL.SSR_CRSE_INFO_FL.GBL?Page=SSR_CRSE_INFO_FL&Page=SSR_CS_WRAP_FL&CRSE_OFFER_NBR=1&INSTITUTION=UBFLO&CRSE_ID={}&STRM={}&ACAD_CAREER={}" };
}
const TOKEN1_URL: &str ="https://www.pub.hub.buffalo.edu/psc/csprdpub/EMPLOYEE/SA/c/NUI_FRAMEWORK.PT_LANDINGPAGE.GBL?tab=DEFAULT";
const TOKEN2_URL: &str ="https://www.pub.hub.buffalo.edu/psc/csprdpub/EMPLOYEE/SA/c/NUI_FRAMEWORK.PT_LANDINGPAGE.GBL?tab=DEFAULT&";
const TOKEN_COOKIE_NAME: &str = "psprd-8083-PORTAL-PSJSESSIONID";
#[derive(Debug, Clone)]
pub struct Query {
course: Course,
semester: Semester,
career: Career,
}
impl Query {
pub fn new(course: Course, semester: Semester, career: Career) -> Self {
Self {
course,
semester,
career,
}
}
}
pub struct Session<T> {
client: Client<T, Body>,
token: Token,
}
impl<T> Session<T> {
pub fn new(client: Client<T, Body>, token: Token) -> Self {
Self { client, token }
}
}
impl<T> Session<T>
where
T: Connect + Clone + Send + Sync + 'static,
{
pub fn schedule_iter(&self, query: Query) -> impl TryStream<Ok = Bytes, Error = SessionError> {
let client = self.client.clone();
let token = self.token.clone();
stream::iter(1..)
.then(move |page_num| {
let client = client.clone();
let token = token.clone();
let query = query.clone();
Box::pin(async move {
Ok(Self::get_page(client, token, query, page_num)
.await?
.await?)
})
})
.and_then(|response| Box::pin(body::to_bytes(response.into_body()).err_into()))
}
async fn get_page(
client: Client<T, Body>,
token: Token,
query: Query,
page_num: u32,
) -> Result<ResponseFuture, SessionError> {
#[allow(clippy::never_loop)] loop {
match page_num {
1 => {
client
.request(
Request::builder()
.uri(FAKE1_URL)
.header(header::COOKIE, token.as_str())
.body(Body::empty())?,
)
.await?;
client
.request(
Request::builder()
.uri(FAKE2_URL)
.header(header::COOKIE, token.as_str())
.body(Body::empty())?,
)
.await?;
let page = client.request(
Request::builder()
.uri(format!(
PAGE1_URL!(),
query.course.id(),
query.semester.id(),
query.career.id()
))
.header(header::USER_AGENT, USER_AGENT)
.header(header::COOKIE, token.as_str())
.header(header::COOKIE, "HttpOnly")
.header(header::COOKIE, "Path=/")
.body(Body::empty())?,
);
return Ok(page);
}
_ => {
let _page_num = page_num + 1;
return Err(SessionError::PagesNotImplemented);
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct Token(Arc<str>);
impl Token {
pub async fn new<T>(client: &Client<T, Body>) -> Result<Self, SessionError>
where
T: Connect + Clone + Send + Sync + 'static,
{
let response = client
.request(
Request::builder()
.uri(TOKEN1_URL)
.header(header::USER_AGENT, USER_AGENT)
.body(Body::empty())?,
)
.await?;
let response = client
.request(
Request::builder()
.uri(TOKEN2_URL)
.header(header::USER_AGENT, USER_AGENT)
.header(
header::COOKIE,
Token::token_cookie(response.headers())
.ok_or(SessionError::TokenCookieNotFound)?
.to_string(),
)
.body(Body::empty())?,
)
.await?;
Ok(Self(Arc::from(
Token::token_cookie(response.headers())
.ok_or(SessionError::TokenCookieNotFound)?
.to_string(),
)))
}
fn as_str(&self) -> &str {
&self.0
}
fn token_cookie(headers: &HeaderMap) -> Option<Cookie<'_>> {
headers
.get_all(header::SET_COOKIE)
.iter()
.filter_map(|string| {
string
.to_str()
.ok()
.and_then(|raw_cookie| Cookie::parse(raw_cookie).ok())
})
.find(|cookie| cookie.name() == TOKEN_COOKIE_NAME)
}
}
#[derive(Debug, Error)]
pub enum SessionError {
#[error("an argument while building an HTTP request was invalid")]
MalformedHttpArgs(#[from] hyper::http::Error),
#[error(transparent)]
HttpRequestFailed(#[from] hyper::Error),
#[error("could not parse cookie with an invalid format")]
MalformedCookie(#[from] cookie::ParseError),
#[error("could not find or parse the token cookie")]
TokenCookieNotFound,
#[error("more than one page has not been implemented")]
PagesNotImplemented,
}