libellule 0.1.1

Unofficial implementation of the PRONOTE protocol
Documentation
use base64::{Engine, engine::general_purpose::STANDARD};
use rand::Rng;
use serde_json::json;
use url::Url;

use crate::{
    error::Error,
    protocol::{Function, InstanceParameters, Response},
    session::{FunctionContext, Session},
};

const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";

#[derive(Debug)]
pub struct Instance {
    pub(crate) http: reqwest::Client,
    pub(crate) base_url: Url,
    pub(crate) session: Session,
    pub(crate) parameters: InstanceParameters,
}

impl Instance {
    pub async fn new(url: String) -> Result<Self, Error> {
        let http = reqwest::Client::builder().user_agent(USER_AGENT).build()?;

        // TODO: parse url in a more meaningful way
        let base_url = Url::parse(&url)?;

        let response = http
            .get(base_url.join("eleve.html").unwrap())
            .send()
            .await?
            .text()
            .await?;

        let session_id = extract_session_id(&response).ok_or(Error::SessionIdNotFound)?;

        let mut session = Session::new(session_id);

        let mut iv = [0u8; 16];
        rand::rng().fill_bytes(&mut iv);

        let data = json!({
            "Uuid": STANDARD.encode(iv),
            "identifiantNav": null
        });

        let context = FunctionContext::new(&base_url, &http, Function::InstanceParameters, None);

        let response: Response<InstanceParameters> = session.call(context, data).await?;

        session.iv = *md5::compute(iv);

        Ok(Instance {
            http,
            base_url,
            session,
            parameters: response.secured_data.data,
        })
    }

    pub fn label(&self) -> &str {
        self.parameters.general.label.as_str()
    }
}

fn extract_session_id(input: &str) -> Option<u32> {
    let start = input.find("Start")?;
    let input = &input[start..];

    let open = input.find('{')? + 1;
    let close = input.find('}')?;
    let inner = &input[open..close];

    let mut session_id = None;

    for pair in inner.split(',') {
        let (key, value) = pair.split_once(":")?;

        if key.contains("h") {
            session_id = Some(value.parse().ok()?)
        }
    }

    session_id
}