use crate::error::{NetworkError, PixivError, Result};
use reqwest::{Client, Response};
use serde::Serialize;
use std::collections::HashMap;
use std::net::IpAddr;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct BypassSniClient {
pub(crate) client: Client,
access_token: Option<String>,
refresh_token: Option<String>,
base_url: String,
pub ip: IpAddr,
}
impl BypassSniClient {
pub fn new(ip: &str) -> Result<Self> {
let ip = ip
.parse::<std::net::IpAddr>()
.map_err(|_| PixivError::NetworkError(NetworkError::InvalidUrl(format!(
"Invalid IP address: {}",
ip
))))?;
tracing::info!(ip = %ip, "Using SNI bypass with IP address");
let mut builder = reqwest::Client::builder();
let socket_addr = std::net::SocketAddr::new(ip, 443);
builder = builder
.danger_accept_invalid_certs(true)
.resolve("app-api.pixiv.net", socket_addr);
let client = builder
.build()
.map_err(|e| PixivError::NetworkError(NetworkError::RequestError(e)))?;
Ok(Self {
client,
access_token: None,
refresh_token: None,
base_url: format!("https://{}", ip),
ip,
})
}
pub fn set_access_token(&mut self, token: String) {
self.access_token = Some(token);
}
pub fn access_token(&self) -> Option<&str> {
self.access_token.as_deref()
}
pub fn set_refresh_token(&mut self, token: String) {
self.refresh_token = Some(token);
}
pub fn refresh_token(&self) -> Option<&str> {
self.refresh_token.as_deref()
}
pub async fn get(&self, url: &str) -> Result<Response> {
self.send_request(reqwest::Method::GET, url, None::<&()>).await
}
pub async fn post<T: Serialize + ?Sized>(&self, url: &str, body: &T) -> Result<Response> {
self.send_request(reqwest::Method::POST, url, Some(body)).await
}
pub async fn send_request<T: Serialize + ?Sized>(
&self,
method: reqwest::Method,
url: &str,
body: Option<&T>,
) -> Result<Response> {
debug!(method = %method, url = %url, "Sending API request with SNI bypass");
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::HOST,
reqwest::header::HeaderValue::from_static("app-api.pixiv.net"),
);
let mut request = self.client.request(method.clone(), url).headers(headers);
if let Some(token) = &self.access_token {
request = request.header("Authorization", format!("Bearer {}", token));
}
if let Some(body) = body {
request = request.json(body);
}
let response = request.send().await?;
if !response.status().is_success() {
return Err(PixivError::ApiError(format!(
"API request failed: {} - {}",
response.status(),
response.text().await.unwrap_or_else(|_| "Failed to get error information".to_string())
)));
}
debug!(status = %response.status(), "API request completed successfully");
Ok(response)
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn set_base_url(&mut self, base_url: String) {
self.base_url = base_url;
}
pub fn generate_security_headers(&self) -> HashMap<String, String> {
use chrono::Utc;
use md5::compute;
let mut headers = HashMap::new();
let local_time = Utc::now().format("%Y-%m-%dT%H:%M:%S+00:00").to_string();
let hash_input = format!("{}{}", local_time, "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c");
let hash = format!("{:x}", compute(hash_input));
headers.insert("x-client-time".to_string(), local_time);
headers.insert("x-client-hash".to_string(), hash);
headers
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bypass_sni_client_creation() {
let result = BypassSniClient::new("210.140.131.145");
assert!(result.is_ok());
}
#[test]
fn test_invalid_ip() {
let result = BypassSniClient::new("invalid_ip");
assert!(result.is_err());
}
}