| 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()) |
| } |
|
|