forked from FOSS/Steam-Panel
v3.0.4
This commit is contained in:
+36
-13
@@ -215,7 +215,9 @@ async def import_accounts(file: UploadFile = File(...)):
|
||||
Supported formats (: or | separator):
|
||||
login|password|{mafile_json}
|
||||
login|password|email|email_password|{mafile_json}
|
||||
login|password|email|email_password|{mafile_json}|notes
|
||||
login|password|email|email_password
|
||||
login|password|email|email_password|notes (no mafile, with notes)
|
||||
"""
|
||||
content = (await file.read()).decode("utf-8", errors="ignore")
|
||||
result = BulkImportResult()
|
||||
@@ -226,11 +228,27 @@ async def import_accounts(file: UploadFile = File(...)):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Split off embedded mafile JSON (everything from first '{' onward)
|
||||
notes: str | None = None
|
||||
|
||||
# Split off embedded mafile JSON — find matching braces
|
||||
json_start = line.find("{")
|
||||
if json_start != -1:
|
||||
mafile_str = line[json_start:]
|
||||
depth = 0
|
||||
json_end = json_start
|
||||
for i in range(json_start, len(line)):
|
||||
if line[i] == "{":
|
||||
depth += 1
|
||||
elif line[i] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
json_end = i
|
||||
break
|
||||
|
||||
mafile_str = line[json_start : json_end + 1]
|
||||
prefix = line[:json_start].rstrip("|:")
|
||||
suffix = line[json_end + 1 :].lstrip("|:")
|
||||
if suffix:
|
||||
notes = suffix
|
||||
else:
|
||||
mafile_str = None
|
||||
prefix = line
|
||||
@@ -262,12 +280,15 @@ async def import_accounts(file: UploadFile = File(...)):
|
||||
email = mafile_data.get("mail") or mafile_data.get("email") or None
|
||||
email_password = mafile_data.get("mail_password") or mafile_data.get("email_password") or None
|
||||
else:
|
||||
# No mafile — must be exactly 4 fields
|
||||
if len(parts) != 4:
|
||||
result.errors.append(f"Invalid line (need 4 fields): {line[:50]}")
|
||||
# No mafile — 4 fields, or 5 fields with notes
|
||||
if len(parts) == 5:
|
||||
login, password, email, email_password, notes = parts
|
||||
elif len(parts) == 4:
|
||||
login, password, email, email_password = parts
|
||||
else:
|
||||
result.errors.append(f"Invalid line (need 4-5 fields): {line[:50]}")
|
||||
result.skipped += 1
|
||||
continue
|
||||
login, password, email, email_password = parts
|
||||
mafile_data = None
|
||||
|
||||
try:
|
||||
@@ -281,9 +302,9 @@ async def import_accounts(file: UploadFile = File(...)):
|
||||
continue
|
||||
|
||||
await db.execute(
|
||||
"""INSERT INTO accounts (login, password, email, email_password)
|
||||
VALUES (?, ?, ?, ?)""",
|
||||
(login, password, email or None, email_password or None),
|
||||
"""INSERT INTO accounts (login, password, email, email_password, notes)
|
||||
VALUES (?, ?, ?, ?, ?)""",
|
||||
(login, password, email or None, email_password or None, notes or None),
|
||||
)
|
||||
result.imported += 1
|
||||
except Exception as exc:
|
||||
@@ -298,7 +319,8 @@ async def import_accounts(file: UploadFile = File(...)):
|
||||
steam_id = (mafile_data.get("Session") or {}).get("SteamID") or None
|
||||
|
||||
settings.mafiles_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = settings.mafiles_dir / f"{account_name}.mafile"
|
||||
safe_name = Path(account_name).name
|
||||
dest = settings.mafiles_dir / f"{safe_name}.mafile"
|
||||
dest.write_text(json.dumps(mafile_data, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
await db.execute(
|
||||
@@ -325,12 +347,13 @@ async def upload_mafile(account_id: int, file: UploadFile = File(...)):
|
||||
content = (await file.read()).decode("utf-8")
|
||||
try:
|
||||
mafile = MafileData.model_validate_json(content)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid mafile format")
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid mafile format: {exc}")
|
||||
|
||||
settings.mafiles_dir.mkdir(parents=True, exist_ok=True)
|
||||
steam_id = mafile.Session.SteamID or row["steam_id"] or account_id
|
||||
mafile_path = settings.mafiles_dir / f"{steam_id}.mafile"
|
||||
safe_name = Path(str(steam_id)).name
|
||||
mafile_path = settings.mafiles_dir / f"{safe_name}.mafile"
|
||||
mafile_path.write_text(content, encoding="utf-8")
|
||||
|
||||
await db.execute(
|
||||
|
||||
@@ -84,3 +84,54 @@ async def generate_2fa_by_account(request: Generate2FAByAccountRequest):
|
||||
if not row or not row["shared_secret"]:
|
||||
raise HTTPException(status_code=404, detail="Account not found or has no shared_secret")
|
||||
return {"code": gen_code(row["shared_secret"])}
|
||||
|
||||
|
||||
class ConfirmationRespondRequest(BaseModel):
|
||||
ids: list[str]
|
||||
nonces: list[str]
|
||||
accept: bool
|
||||
|
||||
|
||||
@router.get("/confirmations/{account_id}")
|
||||
async def get_confirmations(account_id: int):
|
||||
"""Fetch pending Steam confirmations for an account."""
|
||||
from app.services.steam_confirmations import fetch_confirmations
|
||||
|
||||
db = await get_db()
|
||||
cursor = await db.execute("SELECT * FROM accounts WHERE id = ?", (account_id,))
|
||||
account = await cursor.fetchone()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
|
||||
try:
|
||||
confirmations = await fetch_confirmations(dict(account))
|
||||
return {"success": True, "confirmations": confirmations}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"[confirmations] Error fetching for account {account_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/confirmations/{account_id}/respond")
|
||||
async def respond_confirmations(account_id: int, request: ConfirmationRespondRequest):
|
||||
"""Accept or deny confirmations for an account."""
|
||||
from app.services.steam_confirmations import respond_to_confirmation
|
||||
|
||||
if len(request.ids) != len(request.nonces):
|
||||
raise HTTPException(status_code=400, detail="ids and nonces must have same length")
|
||||
|
||||
db = await get_db()
|
||||
cursor = await db.execute("SELECT * FROM accounts WHERE id = ?", (account_id,))
|
||||
account = await cursor.fetchone()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
|
||||
try:
|
||||
success = await respond_to_confirmation(dict(account), request.ids, request.nonces, request.accept)
|
||||
return {"success": success}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"[confirmations] Error responding for account {account_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -53,19 +53,23 @@ async def upload_mafiles(files: list[UploadFile] = File(...)):
|
||||
try:
|
||||
mafile = MafileData.model_validate_json(content)
|
||||
steam_id = mafile.Session.SteamID or mafile.account_name or file.filename
|
||||
dest = settings.mafiles_dir / f"{steam_id}.mafile"
|
||||
safe_name = Path(str(steam_id)).name
|
||||
dest = settings.mafiles_dir / f"{safe_name}.mafile"
|
||||
dest.write_text(content, encoding="utf-8")
|
||||
uploaded += 1
|
||||
|
||||
if mafile.account_name:
|
||||
mafile_steam_id = str(mafile.Session.SteamID) if mafile.Session.SteamID else None
|
||||
await db.execute(
|
||||
"""UPDATE accounts
|
||||
SET mafile_path = ?, shared_secret = ?, identity_secret = ?
|
||||
SET mafile_path = ?, shared_secret = ?, identity_secret = ?,
|
||||
steam_id = COALESCE(steam_id, ?)
|
||||
WHERE login = ?""",
|
||||
(
|
||||
str(dest),
|
||||
mafile.shared_secret or "",
|
||||
mafile.identity_secret or "",
|
||||
mafile_steam_id,
|
||||
mafile.account_name,
|
||||
),
|
||||
)
|
||||
|
||||
+9
-5
@@ -2,7 +2,7 @@ import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
class AccountCreate(BaseModel):
|
||||
@@ -90,14 +90,18 @@ class ProxyOut(BaseModel):
|
||||
|
||||
|
||||
class MafileSession(BaseModel):
|
||||
SessionID: str = ""
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
SessionID: str | None = ""
|
||||
AccessToken: str = ""
|
||||
RefreshToken: str = ""
|
||||
SteamID: int = 0
|
||||
SteamLoginSecure: str = ""
|
||||
SteamID: int | None = 0
|
||||
SteamLoginSecure: str | None = ""
|
||||
|
||||
|
||||
class MafileData(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
shared_secret: str
|
||||
serial_number: str = ""
|
||||
revocation_code: str = ""
|
||||
@@ -107,7 +111,7 @@ class MafileData(BaseModel):
|
||||
identity_secret: str = ""
|
||||
secret_1: str = ""
|
||||
device_id: str = ""
|
||||
server_time: str = ""
|
||||
server_time: str | int = ""
|
||||
fully_enrolled: bool = False
|
||||
Session: MafileSession = Field(default_factory=MafileSession)
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ def read_mafile_data(account: dict) -> dict | None:
|
||||
return None
|
||||
try:
|
||||
return json.loads(Path(mafile_path).read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError, ValueError):
|
||||
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
||||
logger.warning(f"Failed to read mafile for {account.get('login', '?')}: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -80,7 +81,8 @@ async def create_steam_session(account: dict):
|
||||
shared_secret = mafile.get("shared_secret", shared_secret)
|
||||
identity_secret = mafile.get("identity_secret", identity_secret)
|
||||
device_id = mafile.get("device_id")
|
||||
steamid = int(mafile.get("Session", {}).get("SteamID", 0)) or None
|
||||
steamid_raw = (mafile.get("Session") or {}).get("SteamID") or 0
|
||||
steamid = int(steamid_raw) or None
|
||||
|
||||
steam = Steam(
|
||||
login=account["login"],
|
||||
@@ -155,6 +157,61 @@ def extract_session_cookies(steam) -> str | None:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_mafile_tokens(steam, account: dict) -> bool:
|
||||
"""Extract fresh tokens from a logged-in steam session and update the .mafile on disk."""
|
||||
mafile_path = account.get("mafile_path")
|
||||
if not mafile_path:
|
||||
return False
|
||||
|
||||
try:
|
||||
mafile = json.loads(Path(mafile_path).read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False
|
||||
|
||||
_req = getattr(steam, "_requests", None)
|
||||
_sess = getattr(_req, "_session", None) if _req else None
|
||||
if not _sess or not hasattr(_sess, "cookie_jar"):
|
||||
return False
|
||||
|
||||
access_token = None
|
||||
session_id = None
|
||||
steam_id = str(getattr(steam, "_steamid", "") or "")
|
||||
|
||||
for c in _sess.cookie_jar:
|
||||
if c.key == "steamLoginSecure" and c.value:
|
||||
raw = __import__("urllib.parse", fromlist=["unquote"]).unquote(c.value)
|
||||
if "||" in raw:
|
||||
parts = raw.split("||", 1)
|
||||
if not steam_id:
|
||||
steam_id = parts[0]
|
||||
access_token = parts[1]
|
||||
elif c.key == "sessionid" and c.value:
|
||||
session_id = c.value
|
||||
|
||||
if not access_token:
|
||||
return False
|
||||
|
||||
session = mafile.setdefault("Session", {})
|
||||
session["AccessToken"] = access_token
|
||||
if session_id:
|
||||
session["SessionID"] = session_id
|
||||
if steam_id:
|
||||
session["SteamID"] = steam_id
|
||||
session["SteamLoginSecure"] = f"{steam_id}%7C%7C{access_token}"
|
||||
|
||||
refresh_token = getattr(steam, "_refresh_token", None)
|
||||
if refresh_token:
|
||||
session["RefreshToken"] = refresh_token
|
||||
|
||||
try:
|
||||
Path(mafile_path).write_text(json.dumps(mafile, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
logger.debug(f"[auth] Updated mafile tokens: {mafile_path}")
|
||||
return True
|
||||
except OSError as e:
|
||||
logger.warning(f"[auth] Failed to update mafile: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def check_cookies_alive(cookies_json: str, proxy: dict | None = None) -> bool:
|
||||
"""Check if Steam session cookies are still valid.
|
||||
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
"""Steam mobile confirmations — fetch, accept, deny trade/market confirmations."""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import struct
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
from yarl import URL
|
||||
from loguru import logger
|
||||
|
||||
|
||||
COMMUNITY_BASE = "https://steamcommunity.com"
|
||||
MOBILE_USER_AGENT = "Dalvik/2.1.0 (Linux; U; Android 9; Valve Steam App Version/3)"
|
||||
|
||||
CONF_TYPES = {
|
||||
0: "Invalid",
|
||||
1: "Test",
|
||||
2: "Trade",
|
||||
3: "MarketListing",
|
||||
4: "FeatureOptOut",
|
||||
5: "PhoneNumberChange",
|
||||
6: "AccountRecovery",
|
||||
}
|
||||
|
||||
|
||||
def _decode_jwt_payload(token: str) -> dict:
|
||||
parts = token.split(".")
|
||||
payload_b64 = parts[1] + "=" * (4 - len(parts[1]) % 4)
|
||||
return json.loads(base64.urlsafe_b64decode(payload_b64))
|
||||
|
||||
|
||||
def _is_token_expired(token: str, margin: int = 60) -> bool:
|
||||
"""Check if a JWT access token is expired (with safety margin in seconds)."""
|
||||
try:
|
||||
payload = _decode_jwt_payload(token)
|
||||
return payload.get("exp", 0) < int(time.time()) + margin
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
async def _resolve_proxy(account: dict):
|
||||
"""Resolve proxy for an account (same logic as steam_auth)."""
|
||||
from app.core.proxy_manager import proxy_manager
|
||||
|
||||
proxy_address = account.get("proxy")
|
||||
if proxy_address:
|
||||
from app.database import get_db
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT protocol FROM proxies WHERE address = ?", (proxy_address,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
protocol = row["protocol"] if row else "http"
|
||||
return {"address": proxy_address, "protocol": protocol}
|
||||
if proxy_manager.count > 0:
|
||||
return proxy_manager.get_next()
|
||||
return None
|
||||
|
||||
|
||||
async def _refresh_access_token(
|
||||
refresh_token: str, steam_id: str, connector: aiohttp.BaseConnector | None = None,
|
||||
) -> str | None:
|
||||
"""Use RefreshToken to get a fresh AccessToken via Steam protobuf API."""
|
||||
try:
|
||||
from pysteamauth.pb2.steammessages_auth.steamclient_pb2 import (
|
||||
CAuthentication_AccessToken_GenerateForApp_Request,
|
||||
CAuthentication_AccessToken_GenerateForApp_Response,
|
||||
)
|
||||
except ImportError:
|
||||
logger.warning("[confirmations] protobuf classes not available for token refresh")
|
||||
return None
|
||||
|
||||
msg = CAuthentication_AccessToken_GenerateForApp_Request()
|
||||
msg.refresh_token = refresh_token
|
||||
msg.steamid = int(steam_id)
|
||||
encoded = base64.b64encode(msg.SerializeToString()).decode()
|
||||
|
||||
kw: dict = {"headers": {"User-Agent": MOBILE_USER_AGENT}, "timeout": aiohttp.ClientTimeout(total=15)}
|
||||
if connector:
|
||||
kw["connector"] = connector
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(**kw) as http:
|
||||
async with http.post(
|
||||
"https://api.steampowered.com/IAuthenticationService/GenerateAccessTokenForApp/v1/",
|
||||
data={"input_protobuf_encoded": encoded},
|
||||
) as resp:
|
||||
eresult = resp.headers.get("x-eresult", "?")
|
||||
raw = await resp.read()
|
||||
if raw:
|
||||
resp_msg = CAuthentication_AccessToken_GenerateForApp_Response()
|
||||
resp_msg.ParseFromString(raw)
|
||||
if resp_msg.access_token:
|
||||
logger.info(f"[confirmations] Refreshed AccessToken for SteamID {steam_id}")
|
||||
return resp_msg.access_token
|
||||
logger.warning(f"[confirmations] Token refresh failed for SteamID {steam_id}: eresult={eresult}")
|
||||
except Exception as e:
|
||||
logger.error(f"[confirmations] Token refresh error for SteamID {steam_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _has_mobile_audience(token: str) -> bool:
|
||||
"""Check if JWT token has 'mobile' in its audience claim."""
|
||||
try:
|
||||
payload = _decode_jwt_payload(token)
|
||||
aud = payload.get("aud", [])
|
||||
return "mobile" in aud
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def _ensure_valid_access_token(account: dict, mafile: dict) -> str:
|
||||
"""Return a valid AccessToken, refreshing if expired. Updates mafile on disk."""
|
||||
session = mafile.get("Session", {})
|
||||
access_token = session.get("AccessToken", "")
|
||||
refresh_token = session.get("RefreshToken", "")
|
||||
steam_id = str(session.get("SteamID", ""))
|
||||
|
||||
needs_refresh = (
|
||||
not access_token
|
||||
or _is_token_expired(access_token)
|
||||
or not _has_mobile_audience(access_token)
|
||||
)
|
||||
|
||||
if not needs_refresh:
|
||||
return access_token
|
||||
|
||||
if not refresh_token:
|
||||
raise ValueError("AccessToken expired and no RefreshToken available")
|
||||
|
||||
if _is_token_expired(refresh_token, margin=0):
|
||||
raise ValueError("Both AccessToken and RefreshToken are expired — re-authentication required")
|
||||
|
||||
from app.core.proxy_manager import proxy_manager
|
||||
proxy = await _resolve_proxy(account)
|
||||
connector = proxy_manager.get_connector(proxy) if proxy else None
|
||||
|
||||
new_token = await _refresh_access_token(refresh_token, steam_id, connector)
|
||||
if not new_token:
|
||||
raise ValueError(
|
||||
"Failed to refresh AccessToken — session may be revoked or proxy IP mismatch"
|
||||
)
|
||||
|
||||
# Update mafile on disk
|
||||
mafile["Session"]["AccessToken"] = new_token
|
||||
mafile["Session"]["SteamLoginSecure"] = f"{steam_id}%7C%7C{new_token}"
|
||||
mafile_path = account.get("mafile_path")
|
||||
if mafile_path:
|
||||
try:
|
||||
Path(mafile_path).write_text(json.dumps(mafile, indent=2), encoding="utf-8")
|
||||
logger.debug(f"[confirmations] Updated mafile on disk: {mafile_path}")
|
||||
except OSError as e:
|
||||
logger.warning(f"[confirmations] Could not update mafile: {e}")
|
||||
|
||||
return new_token
|
||||
|
||||
|
||||
async def _get_steam_server_time() -> int:
|
||||
"""Query Steam server time for HMAC generation."""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
"https://api.steampowered.com/ITwoFactorService/QueryTime/v0001",
|
||||
data={"steamid": "0"},
|
||||
headers={"User-Agent": MOBILE_USER_AGENT},
|
||||
timeout=aiohttp.ClientTimeout(total=10),
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json()
|
||||
return int(data.get("response", {}).get("server_time", time.time()))
|
||||
except Exception:
|
||||
pass
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def _generate_confirmation_hash(identity_secret: str, steam_time: int, tag: str) -> str:
|
||||
"""HMAC-SHA1(identity_secret, time_bytes + tag_bytes) → Base64."""
|
||||
key = base64.b64decode(identity_secret)
|
||||
msg = struct.pack(">Q", steam_time)
|
||||
tag_bytes = tag.encode("ascii")
|
||||
msg += tag_bytes[:32]
|
||||
mac = hmac.new(key, msg, hashlib.sha1).digest()
|
||||
return base64.b64encode(mac).decode()
|
||||
|
||||
|
||||
def _build_query_params(
|
||||
device_id: str, steam_id: str, identity_secret: str, steam_time: int, tag: str
|
||||
) -> dict:
|
||||
"""Build confirmation query parameters."""
|
||||
return {
|
||||
"p": device_id,
|
||||
"a": steam_id,
|
||||
"k": _generate_confirmation_hash(identity_secret, steam_time, tag),
|
||||
"t": str(steam_time),
|
||||
"m": "react",
|
||||
"tag": tag,
|
||||
}
|
||||
|
||||
|
||||
def _build_cookies(steam_id: str, access_token: str, session_id: str) -> dict:
|
||||
"""Build mobile cookies for confirmation requests."""
|
||||
steam_login_secure = f"{steam_id}%7C%7C{access_token}"
|
||||
return {
|
||||
"steamLoginSecure": steam_login_secure,
|
||||
"sessionid": session_id,
|
||||
"mobileClient": "android",
|
||||
"mobileClientVersion": "777777 3.7.2",
|
||||
"Steam_Language": "english",
|
||||
}
|
||||
|
||||
|
||||
async def fetch_confirmations(account: dict) -> list[dict]:
|
||||
"""Fetch pending confirmations for an account.
|
||||
|
||||
Returns list of confirmation dicts with id, nonce, type, headline, summary etc.
|
||||
"""
|
||||
from app.services.steam_auth import read_mafile_data
|
||||
from app.core.proxy_manager import proxy_manager
|
||||
|
||||
mafile = read_mafile_data(account)
|
||||
if not mafile:
|
||||
raise ValueError("No mafile data for this account")
|
||||
|
||||
identity_secret = mafile.get("identity_secret", "")
|
||||
device_id = mafile.get("device_id", "")
|
||||
session = mafile.get("Session", {})
|
||||
steam_id = str(session.get("SteamID", ""))
|
||||
session_id = session.get("SessionID", "")
|
||||
|
||||
if not identity_secret or not steam_id:
|
||||
raise ValueError("Missing identity_secret or SteamID in mafile")
|
||||
|
||||
access_token = await _ensure_valid_access_token(account, mafile)
|
||||
|
||||
if not device_id:
|
||||
device_id = f"android:{steam_id}"
|
||||
|
||||
if not session_id:
|
||||
import secrets
|
||||
session_id = secrets.token_hex(12)
|
||||
|
||||
steam_time = await _get_steam_server_time()
|
||||
params = _build_query_params(device_id, steam_id, identity_secret, steam_time, "conf")
|
||||
cookies = _build_cookies(steam_id, access_token, session_id)
|
||||
|
||||
url = f"{COMMUNITY_BASE}/mobileconf/getlist"
|
||||
|
||||
proxy = await _resolve_proxy(account)
|
||||
connector = proxy_manager.get_connector(proxy) if proxy else aiohttp.TCPConnector()
|
||||
|
||||
async with aiohttp.ClientSession(
|
||||
headers={"User-Agent": MOBILE_USER_AGENT},
|
||||
cookie_jar=aiohttp.CookieJar(unsafe=True),
|
||||
connector=connector,
|
||||
) as http:
|
||||
for domain in ["steamcommunity.com", "store.steampowered.com"]:
|
||||
for k, v in cookies.items():
|
||||
http.cookie_jar.update_cookies({k: v}, URL(f"https://{domain}"))
|
||||
|
||||
async with http.get(url, params=params) as resp:
|
||||
if resp.status != 200:
|
||||
raise ValueError(f"HTTP {resp.status} from Steam confirmations API")
|
||||
data = await resp.json()
|
||||
|
||||
if not data.get("success"):
|
||||
if data.get("needsauth"):
|
||||
raise ValueError("Session expired — needs re-authentication")
|
||||
detail = data.get("detail", "")
|
||||
message = data.get("message", "")
|
||||
login = account.get("login", "?")
|
||||
logger.warning(f"[confirmations] {login}: {message or 'unknown error'}")
|
||||
|
||||
if "authenticator" in message.lower() or "authenticator" in detail.lower():
|
||||
raise ValueError(
|
||||
"Invalid authenticator — mafile secrets may be outdated. "
|
||||
"Re-link Steam Guard or re-import a fresh mafile."
|
||||
)
|
||||
if detail:
|
||||
raise ValueError(f"Steam: {detail}")
|
||||
if message:
|
||||
raise ValueError(f"Steam: {message}")
|
||||
raise ValueError("Steam rejected the request — likely expired session or invalid mafile tokens")
|
||||
|
||||
confirmations = []
|
||||
for conf in data.get("conf", []):
|
||||
confirmations.append({
|
||||
"id": str(conf.get("id", "")),
|
||||
"nonce": str(conf.get("nonce", "")),
|
||||
"type": conf.get("type", 0),
|
||||
"type_name": CONF_TYPES.get(conf.get("type", 0), "Unknown"),
|
||||
"creator_id": str(conf.get("creator_id", "")),
|
||||
"headline": conf.get("headline", ""),
|
||||
"summary": [conf.get("summary", "")] if isinstance(conf.get("summary"), str) else conf.get("summary", []),
|
||||
"accept": conf.get("accept", "Accept"),
|
||||
"cancel": conf.get("cancel", "Cancel"),
|
||||
"icon": conf.get("icon", ""),
|
||||
})
|
||||
|
||||
logger.info(f"[confirmations] {account.get('login', '?')}: fetched {len(confirmations)} confirmations")
|
||||
return confirmations
|
||||
|
||||
|
||||
async def respond_to_confirmation(
|
||||
account: dict, confirmation_ids: list[str], confirmation_nonces: list[str], accept: bool
|
||||
) -> bool:
|
||||
"""Accept or deny one or more confirmations.
|
||||
|
||||
Uses multiajaxop for batch operations, ajaxop for single.
|
||||
"""
|
||||
from app.services.steam_auth import read_mafile_data
|
||||
from app.core.proxy_manager import proxy_manager
|
||||
|
||||
mafile = read_mafile_data(account)
|
||||
if not mafile:
|
||||
raise ValueError("No mafile data")
|
||||
|
||||
identity_secret = mafile.get("identity_secret", "")
|
||||
device_id = mafile.get("device_id", "")
|
||||
session = mafile.get("Session", {})
|
||||
steam_id = str(session.get("SteamID", ""))
|
||||
session_id = session.get("SessionID", "")
|
||||
|
||||
if not identity_secret or not steam_id:
|
||||
raise ValueError("Missing secrets in mafile")
|
||||
|
||||
access_token = await _ensure_valid_access_token(account, mafile)
|
||||
|
||||
if not device_id:
|
||||
device_id = f"android:{steam_id}"
|
||||
if not session_id:
|
||||
import secrets
|
||||
session_id = secrets.token_hex(12)
|
||||
|
||||
op = "allow" if accept else "cancel"
|
||||
tag = "accept" if accept else "reject"
|
||||
steam_time = await _get_steam_server_time()
|
||||
params = _build_query_params(device_id, steam_id, identity_secret, steam_time, tag)
|
||||
cookies = _build_cookies(steam_id, access_token, session_id)
|
||||
|
||||
proxy = await _resolve_proxy(account)
|
||||
connector = proxy_manager.get_connector(proxy) if proxy else aiohttp.TCPConnector()
|
||||
|
||||
async with aiohttp.ClientSession(
|
||||
headers={"User-Agent": MOBILE_USER_AGENT},
|
||||
cookie_jar=aiohttp.CookieJar(unsafe=True),
|
||||
connector=connector,
|
||||
) as http:
|
||||
for domain in ["steamcommunity.com", "store.steampowered.com"]:
|
||||
for k, v in cookies.items():
|
||||
http.cookie_jar.update_cookies({k: v}, URL(f"https://{domain}"))
|
||||
|
||||
if len(confirmation_ids) == 1:
|
||||
url = f"{COMMUNITY_BASE}/mobileconf/ajaxop"
|
||||
query = {
|
||||
"op": op,
|
||||
**params,
|
||||
"cid": confirmation_ids[0],
|
||||
"ck": confirmation_nonces[0],
|
||||
}
|
||||
async with http.get(url, params=query) as resp:
|
||||
data = await resp.json()
|
||||
else:
|
||||
url = f"{COMMUNITY_BASE}/mobileconf/multiajaxop"
|
||||
form_data = {"op": op}
|
||||
form_data.update(params)
|
||||
for cid in confirmation_ids:
|
||||
form_data[f"cid[]"] = cid
|
||||
for ck in confirmation_nonces:
|
||||
form_data[f"ck[]"] = ck
|
||||
|
||||
payload = aiohttp.FormData()
|
||||
payload.add_field("op", op)
|
||||
for k, v in params.items():
|
||||
payload.add_field(k, str(v))
|
||||
for cid in confirmation_ids:
|
||||
payload.add_field("cid[]", cid)
|
||||
for ck in confirmation_nonces:
|
||||
payload.add_field("ck[]", ck)
|
||||
|
||||
async with http.post(url, data=payload) as resp:
|
||||
data = await resp.json()
|
||||
|
||||
success = data.get("success", False)
|
||||
action_word = "accepted" if accept else "denied"
|
||||
logger.info(f"[confirmations] {account.get('login', '?')}: {action_word} {len(confirmation_ids)} confirmations — success={success}")
|
||||
return success
|
||||
@@ -165,6 +165,10 @@ async def validate_account(account: dict, params: dict, task_id: str = "") -> No
|
||||
except Exception as cookie_err:
|
||||
logger.warning(f"[validate] {login}: failed to save cookies — {cookie_err}")
|
||||
|
||||
from app.services.steam_auth import update_mafile_tokens
|
||||
if update_mafile_tokens(steam, account):
|
||||
logger.info(f"[validate] {login}: updated mafile tokens")
|
||||
|
||||
except Exception as exc:
|
||||
status = "invalid"
|
||||
error_msg = str(exc)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SteamPanel</title>
|
||||
<script type="module" crossorigin src="/assets/index-Bxw--tTv.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cm89176E.css">
|
||||
<script type="module" crossorigin src="/assets/index-D5Q9Ekc6.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-aSRiX3qP.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user