201 lines
6.7 KiB
Python
201 lines
6.7 KiB
Python
"""
|
|
URA GLS API — Reverse-Engineered from JavaScript source
|
|
========================================================
|
|
Endpoints and auth flow discovered by auto-reverse from:
|
|
https://eservice.ura.gov.sg/maps/?service=GLSRELEASE&site=1
|
|
|
|
Auth flow (from OnemapAPI.js + Env.js):
|
|
1. GET eservice.ura.gov.sg/sharedServicesWeb/onemapService/getOnemapToken (JSONP)
|
|
→ returns {token: {token: "...", expiry: ...}}
|
|
2. GET eservice.ura.gov.sg/sharedServicesWeb/onemapService/getOnemapLandLotToken (JSONP)
|
|
→ returns {token: {token: "...", expiry: ...}}
|
|
3. OneMap APIs use: Authorization: <token> header
|
|
|
|
ArcGIS APIs (maps.ura.gov.sg) — NO auth needed, just geo-blocked to SG IPs.
|
|
|
|
Run from a Singapore IP.
|
|
"""
|
|
|
|
import json
|
|
import requests
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Config (from Env.js)
|
|
# ---------------------------------------------------------------------------
|
|
ESERVICE = "https://eservice.ura.gov.sg"
|
|
MAP_HOST = "https://maps.ura.gov.sg"
|
|
ONEMAP = "https://www.onemap.gov.sg"
|
|
|
|
session = requests.Session()
|
|
session.headers.update({
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
"Referer": f"{ESERVICE}/maps/",
|
|
"Origin": ESERVICE,
|
|
})
|
|
|
|
|
|
def dump(label, resp):
|
|
print(f"\n{'='*70}")
|
|
print(f" {label}")
|
|
print(f" {resp.url}")
|
|
print(f" Status: {resp.status_code}")
|
|
print(f"{'='*70}")
|
|
try:
|
|
data = resp.json()
|
|
print(json.dumps(data, indent=2, ensure_ascii=False)[:4000])
|
|
return data
|
|
except Exception:
|
|
print(resp.text[:2000])
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 1: Get OneMap tokens (JSONP endpoints from OnemapAPI.js)
|
|
# ---------------------------------------------------------------------------
|
|
print("=" * 70)
|
|
print(" AUTH: Fetching OneMap tokens")
|
|
print("=" * 70)
|
|
|
|
# These are JSONP endpoints — they wrap the response in a callback function
|
|
# We need to strip the JSONP wrapper to get the JSON
|
|
def fetch_jsonp_token(url):
|
|
"""Fetch a JSONP token endpoint and extract the JSON payload."""
|
|
resp = session.get(url, timeout=15)
|
|
text = resp.text.strip()
|
|
# JSONP format: callbackName({"key":"value"})
|
|
# Strip the callback wrapper
|
|
if text.startswith("("):
|
|
text = text[1:-1]
|
|
elif "(" in text:
|
|
start = text.index("(") + 1
|
|
end = text.rindex(")")
|
|
text = text[start:end]
|
|
return json.loads(text)
|
|
|
|
try:
|
|
token_data = fetch_jsonp_token(f"{ESERVICE}/sharedServicesWeb/onemapService/getOnemapToken")
|
|
onemap_token = token_data.get("token", {}).get("token", "")
|
|
print(f" OneMap Token: {onemap_token[:50]}...")
|
|
print(f" Expiry: {token_data.get('token', {}).get('expiry', 'N/A')}")
|
|
except Exception as e:
|
|
print(f" Failed to get OneMap token: {e}")
|
|
onemap_token = ""
|
|
|
|
try:
|
|
landlot_data = fetch_jsonp_token(f"{ESERVICE}/sharedServicesWeb/onemapService/getOnemapLandLotToken")
|
|
landlot_token = landlot_data.get("token", {}).get("token", "")
|
|
print(f" LandLot Token: {landlot_token[:50]}...")
|
|
except Exception as e:
|
|
print(f" Failed to get LandLot token: {e}")
|
|
landlot_token = ""
|
|
|
|
# Set auth header for OneMap requests
|
|
if onemap_token:
|
|
session.headers["Authorization"] = onemap_token
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 2: ArcGIS — URA GLS Sale Sites (the main target)
|
|
# ---------------------------------------------------------------------------
|
|
print("\n" + "=" * 70)
|
|
print(" ARCGIS ENDPOINTS (maps.ura.gov.sg)")
|
|
print("=" * 70)
|
|
|
|
# Layer metadata — field names, types, etc.
|
|
dump(
|
|
"URA Sale Sites — Layer Metadata",
|
|
session.get(f"{MAP_HOST}/ArcGis/rest/services/lsag/ura_sale_sites/MapServer/0", params={"f": "json"}, timeout=15),
|
|
)
|
|
|
|
# Query — get first 5 features with all fields
|
|
dump(
|
|
"URA Sale Sites — Sample Data",
|
|
session.get(
|
|
f"{MAP_HOST}/ArcGis/rest/services/lsag/ura_sale_sites/MapServer/0/query",
|
|
params={"where": "1=1", "outFields": "*", "f": "json", "resultRecordCount": "5"},
|
|
timeout=15,
|
|
),
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 3: ArcGIS — HDB Sale Sites
|
|
# ---------------------------------------------------------------------------
|
|
dump(
|
|
"HDB Sale Sites — Sample Data",
|
|
session.get(
|
|
f"{MAP_HOST}/ArcGis/rest/services/lsag/hdb_sale_sites2/MapServer/0/query",
|
|
params={"where": "1=1", "outFields": "*", "f": "json", "resultRecordCount": "3"},
|
|
timeout=15,
|
|
),
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 4: ArcGIS — JTC Sale Sites
|
|
# ---------------------------------------------------------------------------
|
|
dump(
|
|
"JTC Sale Sites — Sample Data",
|
|
session.get(
|
|
f"{MAP_HOST}/arcgis/rest/services/lsag/jtc_sale_sites/MapServer/0/query",
|
|
params={"where": "1=1", "outFields": "*", "f": "json", "resultRecordCount": "3"},
|
|
timeout=15,
|
|
),
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 5: OneMap APIs (with auth token)
|
|
# ---------------------------------------------------------------------------
|
|
print("\n" + "=" * 70)
|
|
print(" ONEMAP APIs (with Authorization header)")
|
|
print("=" * 70)
|
|
|
|
# Reverse Geocode (Orchard Road)
|
|
dump(
|
|
"OneMap Reverse Geocode",
|
|
session.get(
|
|
f"{ONEMAP}/api/public/revgeocode",
|
|
params={"location": "1.3004,103.8460", "addressType": "all", "otherFeatures": "Y"},
|
|
timeout=15,
|
|
),
|
|
)
|
|
|
|
# Land Ownership query
|
|
if landlot_token:
|
|
session.headers["Authorization"] = landlot_token
|
|
dump(
|
|
"OneMap Land Ownership",
|
|
session.get(
|
|
f"{ONEMAP}/api/public/landlotAPI/retrieveLandOwnership",
|
|
params={"latitude": "1.3004", "longtitude": "103.8460"},
|
|
timeout=15,
|
|
),
|
|
)
|
|
|
|
# Address search
|
|
if onemap_token:
|
|
session.headers["Authorization"] = onemap_token
|
|
dump(
|
|
"OneMap Address Search",
|
|
session.get(
|
|
f"{ONEMAP}/api/common/elastic/search",
|
|
params={"searchVal": "Orchard Road", "returnGeom": "Y", "getAddrDetails": "Y", "pageNum": "1"},
|
|
timeout=15,
|
|
),
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 6: URA eService APIs (parking, session-based)
|
|
# ---------------------------------------------------------------------------
|
|
print("\n" + "=" * 70)
|
|
print(" URA eSERVICE APIs")
|
|
print("=" * 70)
|
|
|
|
dump(
|
|
"Parking Place List",
|
|
session.get(f"{ESERVICE}/ecasService/ppInfo/getParkingPlaceList.do", timeout=15),
|
|
)
|
|
|
|
dump(
|
|
"Parking Rate Availability",
|
|
session.get(f"{ESERVICE}/ecasPPService/ppInfo/getPPCurrentRateAvailability.do", timeout=15),
|
|
)
|