Aaron Manning 5ac9cdb103 fix duplicate csrf value bug
letterboxd appears to now send multiple csrf set-cookie headers
one of which is empty. this means that we are now required to
check both of these to make sure we grab the correct one.
2024-11-17 15:55:18 +11:00

130 lines
3.6 KiB
Rust

use std::fs;
use std::path;
use std::io::Read;
use anyhow::Context;
#[derive(clap::Parser)]
struct Args {
#[clap(long, short)]
username : Option<String>,
#[clap(long, short)]
password : Option<String>,
#[clap(long, short)]
output : path::PathBuf,
}
#[derive(serde::Serialize)]
struct Login<'a> {
#[serde(rename = "__csrf")]
csrf : &'a str,
#[serde(rename = "authenticationCode")]
authentication_code : &'a str,
username : &'a str,
password : &'a str,
}
#[derive(serde::Deserialize)]
#[allow(dead_code)]
struct LoginResult {
result : String,
messages : Vec<String>,
csrf : String,
}
fn main() -> anyhow::Result<()> {
let args : Args = clap::Parser::parse();
let client = reqwest::blocking::ClientBuilder::new()
// CSRF cookies and login cookies must be passed between requests for
// authentication to succeed
.cookie_store(true)
.build()?;
let home_response = client.get("https://letterboxd.com")
.header("user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0")
.header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.send()?;
let home_cookies = home_response.headers()
.get_all("set-cookie");
let mut csrf = None;
for cookie in home_cookies {
if let Ok(cookie) = cookie.to_str() {
let cookie = cookie::Cookie::parse(cookie).context("failed to parse cookie value")?;
if cookie.name() == "com.xk72.webparts.csrf" && cookie.value() != "" {
csrf = Some(cookie.value().to_owned());
}
}
}
let login = Login {
username : &match args.username {
Some(username) => username,
None => {
dialoguer::Input::new()
.with_prompt("Username")
.interact_text()
.unwrap()
}
},
password : &match args.password {
Some(password) => password,
None => {
dialoguer::Password::new()
.with_prompt("Password")
.interact()
.context("failed to read password")?
}
},
authentication_code : "",
csrf: &csrf.context("csrf cookie was not found when fetching homepage")?,
};
let login = serde_urlencoded::to_string(&login)
.context("issue with login request serialization")?;
let mut login_response = client.post("https://letterboxd.com/user/login.do")
.body(login)
.header("Content-Type", "application/x-www-form-urlencoded")
.send()?;
{
let mut buffer = String::new();
login_response.read_to_string(&mut buffer)?;
let login_response = serde_json::from_str::<LoginResult>(buffer.as_str())?;
if login_response.result != "success" {
anyhow::bail!("login failed")
}
}
let mut export_response = client.get("https://letterboxd.com/data/export/")
.send()?;
if export_response.status() != reqwest::StatusCode::OK {
anyhow::bail!("non 200 status code returned from data export")
}
let mut zip = export_response.headers().get("Content-Length")
.and_then(|val| {
val.to_str().ok()
})
.and_then(|val| {
str::parse::<usize>(val).ok()
})
.map(|len| {
Vec::with_capacity(len)
}).unwrap_or(Vec::new());
export_response.read_to_end(&mut zip)?;
fs::write(args.output, zip)?;
Ok(())
}