Spaces:
Running
Running
Upload main.rs
Browse files- src/main.rs +161 -18
src/main.rs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
};
|
| 9 |
use tower_http::{cors::{CorsLayer, Any}, compression::CompressionLayer, trace::TraceLayer};
|
| 10 |
use serde_json::json;
|
| 11 |
-
use serde::Deserialize;
|
| 12 |
use std::collections::HashMap;
|
| 13 |
use std::sync::Arc;
|
| 14 |
use bytes::Bytes;
|
|
@@ -35,6 +35,7 @@ struct AppState {
|
|
| 35 |
api: Arc<Mutex<APIClient>>,
|
| 36 |
api_by_arl: Arc<RwLock<HashMap<String, Arc<Mutex<APIClient>>>>>,
|
| 37 |
pair: Arc<RwLock<HashMap<String, PairSession>>>,
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
fn json_error(status: StatusCode, message: impl Into<String>) -> axum::response::Response {
|
|
@@ -1019,6 +1020,98 @@ async fn mobile_login(email: &str, password_md5: &str) -> Result<String, String>
|
|
| 1019 |
Ok(arl)
|
| 1020 |
}
|
| 1021 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1022 |
|
| 1023 |
async fn gw_light_call(
|
| 1024 |
client: &reqwest::Client,
|
|
@@ -1223,21 +1316,26 @@ async fn login(Json(req): Json<LoginReq>) -> impl IntoResponse {
|
|
| 1223 |
return json_error(StatusCode::BAD_REQUEST, "Email and password_md5 required");
|
| 1224 |
}
|
| 1225 |
|
| 1226 |
-
// Используем мобильный API — не блокируется Cloudflare в датацентрах
|
| 1227 |
match mobile_login(&email, &pass).await {
|
| 1228 |
-
Ok(arl) => {
|
| 1229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1230 |
"arl": arl,
|
| 1231 |
"token": null,
|
| 1232 |
"uid": "",
|
| 1233 |
"access_token": null
|
| 1234 |
-
}))).into_response()
|
|
|
|
| 1235 |
}
|
| 1236 |
-
Err(e) => json_error(StatusCode::UNAUTHORIZED, format!("login_failed; {e}")),
|
| 1237 |
}
|
| 1238 |
}
|
| 1239 |
|
| 1240 |
-
#[derive(Clone)]
|
| 1241 |
struct PairSession {
|
| 1242 |
created_ms: u128,
|
| 1243 |
access_token: Option<String>,
|
|
@@ -1248,6 +1346,34 @@ struct PairSession {
|
|
| 1248 |
error: Option<String>,
|
| 1249 |
}
|
| 1250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1251 |
fn gen_pair_code() -> String {
|
| 1252 |
let ms = now_ms();
|
| 1253 |
let hex = format!("{:x}", md5::compute(format!("pair:{ms}:{SECRET}").as_bytes()));
|
|
@@ -1315,6 +1441,7 @@ async fn pair_start(State(state): State<AppState>) -> impl IntoResponse {
|
|
| 1315 |
},
|
| 1316 |
);
|
| 1317 |
}
|
|
|
|
| 1318 |
(StatusCode::OK, Json(json!({ "code": code }))).into_response()
|
| 1319 |
}
|
| 1320 |
|
|
@@ -1332,6 +1459,8 @@ async fn pair_status(State(state): State<AppState>, Query(q): Query<PairStatusQu
|
|
| 1332 |
};
|
| 1333 |
if now.saturating_sub(sess.created_ms) > ttl_ms {
|
| 1334 |
map.remove(&code);
|
|
|
|
|
|
|
| 1335 |
return json_error(StatusCode::NOT_FOUND, "code expired");
|
| 1336 |
}
|
| 1337 |
|
|
@@ -1399,6 +1528,8 @@ async fn pair_oauth(State(state): State<AppState>, Json(req): Json<PairOauthReq>
|
|
| 1399 |
sess.user_id = Some(user_id);
|
| 1400 |
}
|
| 1401 |
sess.error = None;
|
|
|
|
|
|
|
| 1402 |
(StatusCode::OK, Json(json!({ "ok": true }))).into_response()
|
| 1403 |
}
|
| 1404 |
|
|
@@ -1564,26 +1695,34 @@ async fn pair_complete(State(state): State<AppState>, Form(f): Form<PairComplete
|
|
| 1564 |
|
| 1565 |
let pass_md5 = format!("{:x}", md5::compute(password.as_bytes()));
|
| 1566 |
|
| 1567 |
-
|
| 1568 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1569 |
Err(e) => {
|
| 1570 |
let mut map = state.pair.write().await;
|
| 1571 |
if let Some(s) = map.get_mut(&code) {
|
| 1572 |
s.error = Some(e.clone());
|
| 1573 |
}
|
|
|
|
|
|
|
| 1574 |
Html(format!("<h3>Ошибка входа</h3><pre>{}</pre>", htmlesc(&e))).into_response()
|
| 1575 |
}
|
| 1576 |
Ok(arl) => {
|
| 1577 |
-
|
| 1578 |
-
|
| 1579 |
-
|
| 1580 |
-
|
| 1581 |
-
|
| 1582 |
-
s.error = None;
|
| 1583 |
-
}
|
| 1584 |
}
|
| 1585 |
-
|
| 1586 |
-
|
|
|
|
| 1587 |
}
|
| 1588 |
}
|
| 1589 |
}
|
|
@@ -1815,6 +1954,7 @@ async fn gw_page(Json(req): Json<GwPageReq>) -> impl IntoResponse {
|
|
| 1815 |
jar.add_cookie_str(&format!("arl={arl}; Domain=.deezer.com; Path=/"), &url_deezer);
|
| 1816 |
|
| 1817 |
let client = reqwest::Client::builder()
|
|
|
|
| 1818 |
.cookie_provider(jar.clone())
|
| 1819 |
.timeout(std::time::Duration::from_secs(20))
|
| 1820 |
.build()
|
|
@@ -1944,6 +2084,7 @@ async fn gw_flow(Json(req): Json<GwFlowReq>) -> impl IntoResponse {
|
|
| 1944 |
jar.add_cookie_str(&format!("arl={arl}; Domain=.deezer.com; Path=/"), &url_deezer);
|
| 1945 |
|
| 1946 |
let client = reqwest::Client::builder()
|
|
|
|
| 1947 |
.cookie_provider(jar.clone())
|
| 1948 |
.timeout(std::time::Duration::from_secs(20))
|
| 1949 |
.build()
|
|
@@ -2208,7 +2349,9 @@ async fn main() {
|
|
| 2208 |
api: Arc::new(Mutex::new(APIClient::new())),
|
| 2209 |
api_by_arl: Arc::new(RwLock::new(HashMap::new())),
|
| 2210 |
pair: Arc::new(RwLock::new(HashMap::new())),
|
|
|
|
| 2211 |
};
|
|
|
|
| 2212 |
|
| 2213 |
let cors = CorsLayer::new()
|
| 2214 |
.allow_origin(Any)
|
|
|
|
| 8 |
};
|
| 9 |
use tower_http::{cors::{CorsLayer, Any}, compression::CompressionLayer, trace::TraceLayer};
|
| 10 |
use serde_json::json;
|
| 11 |
+
use serde::{Deserialize, Serialize};
|
| 12 |
use std::collections::HashMap;
|
| 13 |
use std::sync::Arc;
|
| 14 |
use bytes::Bytes;
|
|
|
|
| 35 |
api: Arc<Mutex<APIClient>>,
|
| 36 |
api_by_arl: Arc<RwLock<HashMap<String, Arc<Mutex<APIClient>>>>>,
|
| 37 |
pair: Arc<RwLock<HashMap<String, PairSession>>>,
|
| 38 |
+
pair_store_path: String,
|
| 39 |
}
|
| 40 |
|
| 41 |
fn json_error(status: StatusCode, message: impl Into<String>) -> axum::response::Response {
|
|
|
|
| 1020 |
Ok(arl)
|
| 1021 |
}
|
| 1022 |
|
| 1023 |
+
async fn web_login(email: &str, password_md5: &str) -> Result<String, String> {
|
| 1024 |
+
let jar = Arc::new(Jar::default());
|
| 1025 |
+
let url_deezer = "https://www.deezer.com".parse::<Url>().map_err(|_| "web:url".to_string())?;
|
| 1026 |
+
jar.add_cookie_str("comeback=1; Domain=.deezer.com; Path=/", &url_deezer);
|
| 1027 |
+
|
| 1028 |
+
let client = reqwest::Client::builder()
|
| 1029 |
+
.no_proxy()
|
| 1030 |
+
.cookie_provider(jar.clone())
|
| 1031 |
+
.timeout(std::time::Duration::from_secs(25))
|
| 1032 |
+
.build()
|
| 1033 |
+
.map_err(|_| "web:client".to_string())?;
|
| 1034 |
+
|
| 1035 |
+
let _ = client
|
| 1036 |
+
.get("https://www.deezer.com/")
|
| 1037 |
+
.header(ACCEPT, "*/*")
|
| 1038 |
+
.header("Accept-Language", "en-US,en;q=0.9")
|
| 1039 |
+
.header(
|
| 1040 |
+
"User-Agent",
|
| 1041 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
| 1042 |
+
)
|
| 1043 |
+
.send()
|
| 1044 |
+
.await;
|
| 1045 |
+
|
| 1046 |
+
let ud = gw_light_call_custom(
|
| 1047 |
+
&client,
|
| 1048 |
+
"deezer.getUserData",
|
| 1049 |
+
"null",
|
| 1050 |
+
None,
|
| 1051 |
+
&json!({}),
|
| 1052 |
+
None,
|
| 1053 |
+
false,
|
| 1054 |
+
)
|
| 1055 |
+
.await
|
| 1056 |
+
.map_err(|e| format!("web:getUserData:{e}"))?;
|
| 1057 |
+
|
| 1058 |
+
let check_form = ud.get("checkForm").and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
|
| 1059 |
+
if check_form.is_empty() {
|
| 1060 |
+
return Err("web:no_checkForm".to_string());
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
let _ = gw_light_call_custom(
|
| 1064 |
+
&client,
|
| 1065 |
+
"user.checkCredentials",
|
| 1066 |
+
&check_form,
|
| 1067 |
+
None,
|
| 1068 |
+
&json!({
|
| 1069 |
+
"login": email,
|
| 1070 |
+
"password": password_md5,
|
| 1071 |
+
"checkFormLogin": check_form
|
| 1072 |
+
}),
|
| 1073 |
+
None,
|
| 1074 |
+
false,
|
| 1075 |
+
)
|
| 1076 |
+
.await
|
| 1077 |
+
.map_err(|e| format!("web:checkCredentials:{e}"))?;
|
| 1078 |
+
|
| 1079 |
+
let ud2 = gw_light_call_custom(
|
| 1080 |
+
&client,
|
| 1081 |
+
"deezer.getUserData",
|
| 1082 |
+
"null",
|
| 1083 |
+
None,
|
| 1084 |
+
&json!({}),
|
| 1085 |
+
None,
|
| 1086 |
+
false,
|
| 1087 |
+
)
|
| 1088 |
+
.await
|
| 1089 |
+
.map_err(|e| format!("web:getUserData2:{e}"))?;
|
| 1090 |
+
|
| 1091 |
+
let check_form2 = ud2.get("checkForm").and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
|
| 1092 |
+
if check_form2.is_empty() {
|
| 1093 |
+
return Err("web:no_checkForm2".to_string());
|
| 1094 |
+
}
|
| 1095 |
+
|
| 1096 |
+
let arl_v = gw_light_call_custom(
|
| 1097 |
+
&client,
|
| 1098 |
+
"user.getArl",
|
| 1099 |
+
&check_form2,
|
| 1100 |
+
None,
|
| 1101 |
+
&json!({}),
|
| 1102 |
+
None,
|
| 1103 |
+
true,
|
| 1104 |
+
)
|
| 1105 |
+
.await
|
| 1106 |
+
.map_err(|e| format!("web:getArl:{e}"))?;
|
| 1107 |
+
|
| 1108 |
+
let arl = arl_v.as_str().unwrap_or("").to_string();
|
| 1109 |
+
if arl.len() < 100 {
|
| 1110 |
+
return Err(format!("web:arl_too_short:{}", arl.len()));
|
| 1111 |
+
}
|
| 1112 |
+
Ok(arl)
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
|
| 1116 |
async fn gw_light_call(
|
| 1117 |
client: &reqwest::Client,
|
|
|
|
| 1316 |
return json_error(StatusCode::BAD_REQUEST, "Email and password_md5 required");
|
| 1317 |
}
|
| 1318 |
|
|
|
|
| 1319 |
match mobile_login(&email, &pass).await {
|
| 1320 |
+
Ok(arl) => (StatusCode::OK, Json(json!({
|
| 1321 |
+
"arl": arl,
|
| 1322 |
+
"token": null,
|
| 1323 |
+
"uid": "",
|
| 1324 |
+
"access_token": null
|
| 1325 |
+
}))).into_response(),
|
| 1326 |
+
Err(e1) => match web_login(&email, &pass).await {
|
| 1327 |
+
Ok(arl) => (StatusCode::OK, Json(json!({
|
| 1328 |
"arl": arl,
|
| 1329 |
"token": null,
|
| 1330 |
"uid": "",
|
| 1331 |
"access_token": null
|
| 1332 |
+
}))).into_response(),
|
| 1333 |
+
Err(e2) => json_error(StatusCode::UNAUTHORIZED, format!("login_failed; mobile:{e1}; web:{e2}")),
|
| 1334 |
}
|
|
|
|
| 1335 |
}
|
| 1336 |
}
|
| 1337 |
|
| 1338 |
+
#[derive(Clone, Serialize, Deserialize)]
|
| 1339 |
struct PairSession {
|
| 1340 |
created_ms: u128,
|
| 1341 |
access_token: Option<String>,
|
|
|
|
| 1346 |
error: Option<String>,
|
| 1347 |
}
|
| 1348 |
|
| 1349 |
+
async fn pair_store_load(state: &AppState) {
|
| 1350 |
+
let path = state.pair_store_path.trim();
|
| 1351 |
+
if path.is_empty() {
|
| 1352 |
+
return;
|
| 1353 |
+
}
|
| 1354 |
+
if let Ok(txt) = tokio::fs::read_to_string(path).await {
|
| 1355 |
+
if let Ok(map) = serde_json::from_str::<HashMap<String, PairSession>>(&txt) {
|
| 1356 |
+
*state.pair.write().await = map;
|
| 1357 |
+
}
|
| 1358 |
+
}
|
| 1359 |
+
}
|
| 1360 |
+
|
| 1361 |
+
async fn pair_store_save(state: &AppState) {
|
| 1362 |
+
let path = state.pair_store_path.trim();
|
| 1363 |
+
if path.is_empty() {
|
| 1364 |
+
return;
|
| 1365 |
+
}
|
| 1366 |
+
let map = state.pair.read().await.clone();
|
| 1367 |
+
let Ok(txt) = serde_json::to_string(&map) else { return };
|
| 1368 |
+
let tmp = format!("{path}.tmp");
|
| 1369 |
+
if tokio::fs::write(&tmp, txt).await.is_ok() {
|
| 1370 |
+
if tokio::fs::rename(&tmp, path).await.is_err() {
|
| 1371 |
+
let _ = tokio::fs::remove_file(path).await;
|
| 1372 |
+
let _ = tokio::fs::rename(&tmp, path).await;
|
| 1373 |
+
}
|
| 1374 |
+
}
|
| 1375 |
+
}
|
| 1376 |
+
|
| 1377 |
fn gen_pair_code() -> String {
|
| 1378 |
let ms = now_ms();
|
| 1379 |
let hex = format!("{:x}", md5::compute(format!("pair:{ms}:{SECRET}").as_bytes()));
|
|
|
|
| 1441 |
},
|
| 1442 |
);
|
| 1443 |
}
|
| 1444 |
+
pair_store_save(&state).await;
|
| 1445 |
(StatusCode::OK, Json(json!({ "code": code }))).into_response()
|
| 1446 |
}
|
| 1447 |
|
|
|
|
| 1459 |
};
|
| 1460 |
if now.saturating_sub(sess.created_ms) > ttl_ms {
|
| 1461 |
map.remove(&code);
|
| 1462 |
+
drop(map);
|
| 1463 |
+
pair_store_save(&state).await;
|
| 1464 |
return json_error(StatusCode::NOT_FOUND, "code expired");
|
| 1465 |
}
|
| 1466 |
|
|
|
|
| 1528 |
sess.user_id = Some(user_id);
|
| 1529 |
}
|
| 1530 |
sess.error = None;
|
| 1531 |
+
drop(map);
|
| 1532 |
+
pair_store_save(&state).await;
|
| 1533 |
(StatusCode::OK, Json(json!({ "ok": true }))).into_response()
|
| 1534 |
}
|
| 1535 |
|
|
|
|
| 1695 |
|
| 1696 |
let pass_md5 = format!("{:x}", md5::compute(password.as_bytes()));
|
| 1697 |
|
| 1698 |
+
let res = match mobile_login(&email, &pass_md5).await {
|
| 1699 |
+
Ok(arl) => Ok(arl),
|
| 1700 |
+
Err(e1) => match web_login(&email, &pass_md5).await {
|
| 1701 |
+
Ok(arl) => Ok(arl),
|
| 1702 |
+
Err(e2) => Err(format!("mobile:{e1}; web:{e2}")),
|
| 1703 |
+
},
|
| 1704 |
+
};
|
| 1705 |
+
|
| 1706 |
+
match res {
|
| 1707 |
Err(e) => {
|
| 1708 |
let mut map = state.pair.write().await;
|
| 1709 |
if let Some(s) = map.get_mut(&code) {
|
| 1710 |
s.error = Some(e.clone());
|
| 1711 |
}
|
| 1712 |
+
drop(map);
|
| 1713 |
+
pair_store_save(&state).await;
|
| 1714 |
Html(format!("<h3>Ошибка входа</h3><pre>{}</pre>", htmlesc(&e))).into_response()
|
| 1715 |
}
|
| 1716 |
Ok(arl) => {
|
| 1717 |
+
let mut map = state.pair.write().await;
|
| 1718 |
+
if let Some(s) = map.get_mut(&code) {
|
| 1719 |
+
s.arl = Some(arl);
|
| 1720 |
+
s.email = Some(email);
|
| 1721 |
+
s.error = None;
|
|
|
|
|
|
|
| 1722 |
}
|
| 1723 |
+
drop(map);
|
| 1724 |
+
pair_store_save(&state).await;
|
| 1725 |
+
Html("<h3>Готово</h3><p>Вернись на ТВ — плагин подключится автоматически.</p>".to_string()).into_response()
|
| 1726 |
}
|
| 1727 |
}
|
| 1728 |
}
|
|
|
|
| 1954 |
jar.add_cookie_str(&format!("arl={arl}; Domain=.deezer.com; Path=/"), &url_deezer);
|
| 1955 |
|
| 1956 |
let client = reqwest::Client::builder()
|
| 1957 |
+
.no_proxy()
|
| 1958 |
.cookie_provider(jar.clone())
|
| 1959 |
.timeout(std::time::Duration::from_secs(20))
|
| 1960 |
.build()
|
|
|
|
| 2084 |
jar.add_cookie_str(&format!("arl={arl}; Domain=.deezer.com; Path=/"), &url_deezer);
|
| 2085 |
|
| 2086 |
let client = reqwest::Client::builder()
|
| 2087 |
+
.no_proxy()
|
| 2088 |
.cookie_provider(jar.clone())
|
| 2089 |
.timeout(std::time::Duration::from_secs(20))
|
| 2090 |
.build()
|
|
|
|
| 2349 |
api: Arc::new(Mutex::new(APIClient::new())),
|
| 2350 |
api_by_arl: Arc::new(RwLock::new(HashMap::new())),
|
| 2351 |
pair: Arc::new(RwLock::new(HashMap::new())),
|
| 2352 |
+
pair_store_path: std::env::var("PAIR_STORE_PATH").ok().unwrap_or_else(|| "pair_store.json".to_string()),
|
| 2353 |
};
|
| 2354 |
+
pair_store_load(&state).await;
|
| 2355 |
|
| 2356 |
let cors = CorsLayer::new()
|
| 2357 |
.allow_origin(Any)
|