Skip to content

Commit d228a7a

Browse files
code-rootclaude
andcommitted
feat: add Samsung firmware fetcher + template profile enhancements
Backend: - New module samsung_fota.py with async fetch from Samsung FOTA servers - GET /api/meta/samsung-firmware endpoint with CSC region selector - Caches firmware lookups to avoid rate limiting Frontend: - ExtendDeviceProfileMeta type with board, hardware, security_patch, soc_model, etc. - Added fetchSamsungFirmware API call - Fixed applyProfileModel() to apply ap_version, csc_version from profile template - Added CSC selector dropdown (XEU, BTU, KSA, UAE, EGY, XSP, etc.) - Added "Fetch Firmware" button - fetches real latest firmware from Samsung and auto-fills ap_version, csc_version - Download icon for Fetch button Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 2a66463 commit d228a7a

5 files changed

Lines changed: 203 additions & 2 deletions

File tree

backend/api/routes/meta_route.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from pathlib import Path
22
from typing import Any, Dict, List
33

4-
from fastapi import APIRouter, Depends
4+
from fastapi import APIRouter, Depends, HTTPException
55

66
from api.deps import get_current_user
77
from config import settings
88
from core.fingerprint.generator import DEVICE_PROFILES, DEVICE_CREATION_PRESETS
99
from core.fingerprint.geo_database import COUNTRY_DB, list_countries
1010
from core.firmware.scan import iter_firmware_packages
11+
from core.firmware.samsung_fota import fetch_latest_firmware
1112
from db.models import User
1213

1314
router = APIRouter(prefix="/meta", tags=["meta"])
@@ -88,3 +89,29 @@ async def list_device_presets(_user: User = Depends(get_current_user)) -> List[D
8889
}
8990
for key, cfg in DEVICE_CREATION_PRESETS.items()
9091
]
92+
93+
94+
@router.get("/samsung-firmware")
95+
async def get_samsung_firmware(
96+
model: str,
97+
csc: str = "XEU",
98+
_user: User = Depends(get_current_user),
99+
) -> Dict[str, Any]:
100+
"""
101+
Fetch latest Samsung firmware versions from FOTA servers.
102+
103+
Query params:
104+
- model: Samsung model code (e.g. "SM-S921B", "SM-A556B")
105+
- csc: Customer Service Center / region code (default: "XEU"). Examples: "XEU", "BTU", "KSA", "UAE", "EGY", "XSP"
106+
107+
Returns: {"model", "csc", "ap_version", "csc_version", "full_version"}
108+
109+
Note: This is a public API call to Samsung's FOTA server. Firmware versions are verified real from the live server.
110+
"""
111+
try:
112+
result = await fetch_latest_firmware(model, csc)
113+
return result
114+
except ValueError as e:
115+
raise HTTPException(404, detail=str(e))
116+
except Exception as e:
117+
raise HTTPException(500, detail=f"Firmware fetch failed: {e}")
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""
2+
Samsung FOTA firmware version fetcher.
3+
4+
Public Samsung FOTA server API for fetching latest firmware versions:
5+
https://fota-cloud-dn.ospserver.net/firmware/{csc}/{model}/version.xml
6+
7+
Response format:
8+
<versioninfo>
9+
<firmware>
10+
<version>
11+
<latest>AP_VERSION/CSC_VERSION/CP_VERSION/PDA_VERSION</latest>
12+
</version>
13+
</firmware>
14+
</versioninfo>
15+
16+
Example: S921BXXU3CXJ1/S921BOXM3CXJ1/S921BXXU3CXJ1/S921BXXU3CXJ1
17+
Index: [0]AP [1]CSC [2]CP [3]PDA
18+
"""
19+
20+
import httpx
21+
import xml.etree.ElementTree as ET
22+
from functools import lru_cache
23+
from typing import Optional, Dict, Any
24+
import logging
25+
26+
logger = logging.getLogger(__name__)
27+
28+
FOTA_URL = "https://fota-cloud-dn.ospserver.net/firmware/{csc}/{model}/version.xml"
29+
HTTP_TIMEOUT = 10.0
30+
31+
32+
@lru_cache(maxsize=128)
33+
def _fetch_cached(model: str, csc: str) -> Optional[str]:
34+
"""Cached HTTP fetch from Samsung FOTA server."""
35+
url = FOTA_URL.format(csc=csc.upper(), model=model.upper())
36+
try:
37+
response = httpx.get(url, timeout=HTTP_TIMEOUT)
38+
response.raise_for_status()
39+
return response.text
40+
except Exception as e:
41+
logger.warning(f"Samsung FOTA fetch failed for {model}/{csc}: {e}")
42+
return None
43+
44+
45+
async def fetch_latest_firmware(model: str, csc: str) -> Dict[str, Any]:
46+
"""
47+
Fetch latest Samsung firmware versions for a device + region.
48+
49+
Args:
50+
model: Samsung model code (e.g. "SM-S921B")
51+
csc: Customer Service Center code / region (e.g. "XEU", "KSA", "UAE")
52+
53+
Returns:
54+
{
55+
"model": "SM-S921B",
56+
"csc": "XEU",
57+
"ap_version": "S921BXXU3CXJ1",
58+
"csc_version": "S921BXEF3CXJ1",
59+
"full_version": "S921BXXU3CXJ1/S921BXEF3CXJ1/S921BXXU3CXJ1/S921BXXU3CXJ1"
60+
}
61+
62+
Raises:
63+
ValueError: If firmware not found or parsing fails
64+
httpx.HTTPError: If Samsung server returns error
65+
"""
66+
xml_text = _fetch_cached(model, csc)
67+
if not xml_text:
68+
raise ValueError(f"No firmware found for {model}/{csc}")
69+
70+
try:
71+
root = ET.fromstring(xml_text)
72+
latest_elem = root.find(".//latest")
73+
if latest_elem is None or not latest_elem.text:
74+
raise ValueError(f"No <latest> tag found in response")
75+
76+
latest = latest_elem.text.strip()
77+
parts = latest.split("/")
78+
79+
ap_version = parts[0] if len(parts) > 0 else None
80+
csc_version = parts[1] if len(parts) > 1 else None
81+
82+
return {
83+
"model": model.upper(),
84+
"csc": csc.upper(),
85+
"ap_version": ap_version,
86+
"csc_version": csc_version,
87+
"full_version": latest,
88+
}
89+
except ET.ParseError as e:
90+
logger.error(f"Failed to parse Samsung FOTA XML for {model}/{csc}: {e}")
91+
raise ValueError(f"Invalid XML response from Samsung FOTA server: {e}")
92+
93+
94+
def clear_cache():
95+
"""Clear the firmware fetch cache (useful for testing)."""
96+
_fetch_cached.cache_clear()

frontend/src/api/client.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,21 @@ export async function listCountries(): Promise<{ countries: Record<string, Count
300300
return res.data
301301
}
302302

303+
export interface SamsungFirmwareInfo {
304+
model: string
305+
csc: string
306+
ap_version: string | null
307+
csc_version: string | null
308+
full_version: string
309+
}
310+
311+
export async function fetchSamsungFirmware(model: string, csc: string = 'XEU'): Promise<SamsungFirmwareInfo> {
312+
const res = await apiClient.get<SamsungFirmwareInfo>('/api/meta/samsung-firmware', {
313+
params: { model, csc },
314+
})
315+
return res.data
316+
}
317+
303318
// ─── Fingerprint Randomization ────────────────────────────────────────────
304319

305320
export async function randomizeFingerprintWithCountry(

frontend/src/components/FingerprintEditor.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useMemo, useState } from 'react'
22
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
3-
import { Shuffle, Save, Send, Loader2, AlertCircle } from 'lucide-react'
3+
import { Shuffle, Save, Send, Loader2, AlertCircle, Download } from 'lucide-react'
44
import {
55
getFingerprint,
66
updateFingerprint,
@@ -9,6 +9,7 @@ import {
99
getDeviceProfiles,
1010
listCountries,
1111
randomizeFingerprintWithCountry,
12+
fetchSamsungFirmware,
1213
} from '../api/client'
1314
import type { DeviceFingerprint } from '../types'
1415
import clsx from 'clsx'
@@ -31,6 +32,20 @@ function validate(fp: Partial<DeviceFingerprint>): string | null {
3132
return null
3233
}
3334

35+
const CSC_CODES = ['XEU', 'BTU', 'XSP', 'XEF', 'KSA', 'UAE', 'EGY', 'XSA', 'DBT', 'GEN']
36+
const CSC_LABELS: Record<string, string> = {
37+
'XEU': 'Europe (Germany)',
38+
'BTU': 'UK',
39+
'XSP': 'Spain',
40+
'XEF': 'France',
41+
'KSA': 'Saudi Arabia',
42+
'UAE': 'UAE',
43+
'EGY': 'Egypt',
44+
'XSA': 'Australia',
45+
'DBT': 'Germany (T-Mobile)',
46+
'GEN': 'Generic/Unlocked',
47+
}
48+
3449
export default function FingerprintEditor({ deviceId, isRunning }: Props) {
3550
const queryClient = useQueryClient()
3651
const [form, setForm] = useState<Partial<DeviceFingerprint>>({})
@@ -40,6 +55,7 @@ export default function FingerprintEditor({ deviceId, isRunning }: Props) {
4055
const [selectedCountry, setSelectedCountry] = useState<string>('')
4156
const [countrySearch, setCountrySearch] = useState<string>('')
4257
const [showCountryDropdown, setShowCountryDropdown] = useState(false)
58+
const [selectedCsc, setSelectedCsc] = useState<string>('XEU')
4359

4460
const { data: fp, isLoading } = useQuery({
4561
queryKey: ['fingerprint', deviceId],
@@ -77,6 +93,8 @@ export default function FingerprintEditor({ deviceId, isRunning }: Props) {
7793
build_fingerprint: p.build_fingerprint,
7894
sdk_version: p.sdk_version,
7995
android_version: p.android_version,
96+
ap_version: p.ap_version ?? prev.ap_version,
97+
csc_version: p.csc_version ?? prev.csc_version,
8098
}))
8199
}
82100

@@ -137,6 +155,23 @@ export default function FingerprintEditor({ deviceId, isRunning }: Props) {
137155
},
138156
})
139157

158+
const fetchFirmwareMutation = useMutation({
159+
mutationFn: () => fetchSamsungFirmware(form.device_model || 'SM-G996B', selectedCsc),
160+
onSuccess: (data) => {
161+
setForm((prev) => ({
162+
...prev,
163+
ap_version: data.ap_version || prev.ap_version,
164+
csc_version: data.csc_version || prev.csc_version,
165+
}))
166+
setStatusMsg(`Firmware fetched: AP ${data.ap_version}, CSC ${data.csc_version}`)
167+
setError(null)
168+
},
169+
onError: (e: Error) => {
170+
setError(`Failed to fetch firmware: ${e.message}`)
171+
setStatusMsg(null)
172+
},
173+
})
174+
140175
function set<K extends keyof DeviceFingerprint>(key: K, value: DeviceFingerprint[K]) {
141176
setForm((prev) => ({ ...prev, [key]: value }))
142177
}
@@ -231,6 +266,28 @@ export default function FingerprintEditor({ deviceId, isRunning }: Props) {
231266
>
232267
Randomize + Apply
233268
</button>
269+
<select
270+
value={selectedCsc}
271+
onChange={(e) => setSelectedCsc(e.target.value)}
272+
className="input text-sm px-2 py-1"
273+
disabled={fetchFirmwareMutation.isPending}
274+
>
275+
{CSC_CODES.map((code) => (
276+
<option key={code} value={code}>
277+
{code} - {CSC_LABELS[code]}
278+
</option>
279+
))}
280+
</select>
281+
<button
282+
type="button"
283+
className="btn-secondary btn-sm"
284+
onClick={() => fetchFirmwareMutation.mutate()}
285+
disabled={fetchFirmwareMutation.isPending || !form.device_model}
286+
title={!form.device_model ? 'Select a device model first' : undefined}
287+
>
288+
{fetchFirmwareMutation.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Download className="w-4 h-4" />}
289+
Fetch Firmware
290+
</button>
234291
<button
235292
type="button"
236293
className="btn-primary btn-sm"

frontend/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ export interface DeviceProfileMeta {
158158
android_version: string
159159
ap_version?: string | null
160160
csc_version?: string | null
161+
board?: string | null
162+
hardware?: string | null
163+
security_patch?: string | null
164+
first_api_level?: number | null
165+
soc_model?: string | null
166+
soc_manufacturer?: string | null
161167
}
162168

163169
export interface WSMessage {

0 commit comments

Comments
 (0)