|
use super::generate_checksum_with_repair; |
|
use crate::app::{ |
|
constant::{COMMA, EMPTY_STRING}, |
|
lazy::TOKEN_LIST_FILE, |
|
model::TokenInfo, |
|
}; |
|
use crate::common::model::token::TokenPayload; |
|
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; |
|
use chrono::{DateTime, Local, TimeZone}; |
|
|
|
|
|
fn normalize_and_write(content: &str, file_path: &str) -> String { |
|
let normalized = content.replace("\r\n", "\n"); |
|
if normalized != content { |
|
if let Err(e) = std::fs::write(file_path, &normalized) { |
|
eprintln!("警告: 无法更新规范化的文件: {}", e); |
|
} |
|
} |
|
normalized |
|
} |
|
|
|
|
|
pub fn parse_token(token_part: &str) -> String { |
|
|
|
let colon_pos = token_part.rfind(':'); |
|
let encoded_colon_pos = token_part.rfind("%3A"); |
|
|
|
match (colon_pos, encoded_colon_pos) { |
|
(None, None) => token_part.to_string(), |
|
(Some(pos1), None) => token_part[(pos1 + 1)..].to_string(), |
|
(None, Some(pos2)) => token_part[(pos2 + 3)..].to_string(), |
|
(Some(pos1), Some(pos2)) => { |
|
|
|
let pos = pos1.max(pos2); |
|
let start = if pos == pos2 { pos + 3 } else { pos + 1 }; |
|
token_part[start..].to_string() |
|
} |
|
} |
|
} |
|
|
|
|
|
pub fn load_tokens() -> Vec<TokenInfo> { |
|
let token_list_file = TOKEN_LIST_FILE.as_str(); |
|
|
|
|
|
if !std::path::Path::new(&token_list_file).exists() { |
|
if let Err(e) = std::fs::write(&token_list_file, EMPTY_STRING) { |
|
eprintln!("警告: 无法创建文件 '{}': {}", &token_list_file, e); |
|
} |
|
} |
|
|
|
|
|
let token_map: std::collections::HashMap<String, String> = |
|
match std::fs::read_to_string(&token_list_file) { |
|
Ok(content) => { |
|
let normalized = normalize_and_write(&content, &token_list_file); |
|
normalized |
|
.lines() |
|
.filter_map(|line| { |
|
let line = line.trim(); |
|
if line.is_empty() || line.starts_with('#') { |
|
return None; |
|
} |
|
|
|
let parts: Vec<&str> = line.split(COMMA).collect(); |
|
match parts[..] { |
|
[token_part, checksum] => { |
|
let token = parse_token(token_part); |
|
Some((token, generate_checksum_with_repair(checksum))) |
|
} |
|
_ => { |
|
eprintln!("警告: 忽略无效的token-list行: {}", line); |
|
None |
|
} |
|
} |
|
}) |
|
.collect() |
|
} |
|
Err(e) => { |
|
eprintln!("警告: 无法读取token-list文件: {}", e); |
|
std::collections::HashMap::new() |
|
} |
|
}; |
|
|
|
|
|
let token_list_content = token_map |
|
.iter() |
|
.map(|(token, checksum)| format!("{},{}", token, checksum)) |
|
.collect::<Vec<_>>() |
|
.join("\n"); |
|
|
|
if let Err(e) = std::fs::write(&token_list_file, token_list_content) { |
|
eprintln!("警告: 无法更新token-list文件: {}", e); |
|
} |
|
|
|
|
|
token_map |
|
.into_iter() |
|
.map(|(token, checksum)| TokenInfo { |
|
token: token.clone(), |
|
checksum, |
|
profile: None, |
|
}) |
|
.collect() |
|
} |
|
|
|
pub fn write_tokens(token_infos: &[TokenInfo], file_path: &str) -> std::io::Result<()> { |
|
let content = token_infos |
|
.iter() |
|
.map(|info| format!("{},{}", info.token, info.checksum)) |
|
.collect::<Vec<String>>() |
|
.join("\n"); |
|
|
|
std::fs::write(file_path, content) |
|
} |
|
|
|
pub(super) const HEADER_B64: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; |
|
pub(super) const ISSUER: &str = "https://authentication.cursor.sh"; |
|
pub(super) const SCOPE: &str = "openid profile email offline_access"; |
|
pub(super) const AUDIENCE: &str = "https://cursor.com"; |
|
|
|
|
|
pub fn validate_token(token: &str) -> bool { |
|
|
|
let parts: Vec<&str> = token.split('.').collect(); |
|
if parts.len() != 3 { |
|
return false; |
|
} |
|
|
|
if parts[0] != HEADER_B64 { |
|
return false; |
|
} |
|
|
|
|
|
let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { |
|
Ok(decoded) => decoded, |
|
Err(_) => return false, |
|
}; |
|
|
|
|
|
let payload_str = match String::from_utf8(payload) { |
|
Ok(s) => s, |
|
Err(_) => return false, |
|
}; |
|
|
|
|
|
let payload: TokenPayload = match serde_json::from_str(&payload_str) { |
|
Ok(p) => p, |
|
Err(_) => return false, |
|
}; |
|
|
|
|
|
if let Ok(time_value) = payload.time.parse::<i64>() { |
|
let current_time = chrono::Utc::now().timestamp(); |
|
if time_value > current_time { |
|
return false; |
|
} |
|
} else { |
|
return false; |
|
} |
|
|
|
|
|
let bytes = payload.randomness.as_bytes(); |
|
if bytes.len() != 18 { |
|
return false; |
|
} |
|
|
|
|
|
for (i, &b) in bytes.iter().enumerate() { |
|
let valid = match i { |
|
|
|
0..=7 | 9..=12 | 14..=17 => b.is_ascii_hexdigit(), |
|
|
|
8 | 13 => b == b'-', |
|
_ => unreachable!(), |
|
}; |
|
|
|
if !valid { |
|
return false; |
|
} |
|
} |
|
|
|
|
|
let current_time = chrono::Utc::now().timestamp(); |
|
if current_time > payload.exp { |
|
return false; |
|
} |
|
|
|
|
|
if payload.iss != ISSUER { |
|
return false; |
|
} |
|
|
|
|
|
if payload.scope != SCOPE { |
|
return false; |
|
} |
|
|
|
|
|
if payload.aud != AUDIENCE { |
|
return false; |
|
} |
|
|
|
true |
|
} |
|
|
|
|
|
pub fn extract_user_id(token: &str) -> Option<String> { |
|
|
|
let parts: Vec<&str> = token.split('.').collect(); |
|
if parts.len() != 3 { |
|
return None; |
|
} |
|
|
|
|
|
let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { |
|
Ok(decoded) => decoded, |
|
Err(_) => return None, |
|
}; |
|
|
|
|
|
let payload_str = match String::from_utf8(payload) { |
|
Ok(s) => s, |
|
Err(_) => return None, |
|
}; |
|
|
|
|
|
let payload: TokenPayload = match serde_json::from_str(&payload_str) { |
|
Ok(p) => p, |
|
Err(_) => return None, |
|
}; |
|
|
|
|
|
Some( |
|
payload |
|
.sub |
|
.split('|') |
|
.nth(1) |
|
.unwrap_or(&payload.sub) |
|
.to_string(), |
|
) |
|
} |
|
|
|
|
|
pub fn extract_time(token: &str) -> Option<DateTime<Local>> { |
|
|
|
let parts: Vec<&str> = token.split('.').collect(); |
|
if parts.len() != 3 { |
|
return None; |
|
} |
|
|
|
|
|
let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { |
|
Ok(decoded) => decoded, |
|
Err(_) => return None, |
|
}; |
|
|
|
|
|
let payload_str = match String::from_utf8(payload) { |
|
Ok(s) => s, |
|
Err(_) => return None, |
|
}; |
|
|
|
|
|
let payload: TokenPayload = match serde_json::from_str(&payload_str) { |
|
Ok(p) => p, |
|
Err(_) => return None, |
|
}; |
|
|
|
|
|
payload |
|
.time |
|
.parse::<i64>() |
|
.ok() |
|
.and_then(|timestamp| Local.timestamp_opt(timestamp, 0).single()) |
|
} |
|
|