178 lines
6.5 KiB
Python
178 lines
6.5 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
MT Detail Probe — Find the XHR endpoint that returns ownership data with website/address.
|
||
|
|
Intercepts all network calls on vessel detail page and saves them.
|
||
|
|
"""
|
||
|
|
import asyncio, json, sys, os, time, struct, hmac, hashlib, base64
|
||
|
|
|
||
|
|
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
if hasattr(sys.stdout, 'reconfigure'):
|
||
|
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace', line_buffering=True)
|
||
|
|
|
||
|
|
EMAIL = "operation@mrlogisticcorp.com"
|
||
|
|
PASSWORD = "NKh9i8Z!7fU9jfi"
|
||
|
|
TOTP_SECRET = "MNWTEPTFJZBUC32GJFEWY6LVKQ2GGYKH"
|
||
|
|
|
||
|
|
SHIP_IDS = ['437855', '756199', '733924'] # CALUMET, ANTONIA S, GOLDEN JOY
|
||
|
|
|
||
|
|
|
||
|
|
def totp(secret):
|
||
|
|
s = secret.upper().replace(' ', '')
|
||
|
|
pad = (-len(s)) % 8
|
||
|
|
key = base64.b32decode(s + '=' * pad)
|
||
|
|
counter = int(time.time()) // 30
|
||
|
|
msg = struct.pack('>Q', counter)
|
||
|
|
h = hmac.new(key, msg, hashlib.sha1).digest()
|
||
|
|
offset = h[-1] & 0x0f
|
||
|
|
code = struct.unpack('>I', h[offset:offset+4])[0] & 0x7fffffff
|
||
|
|
return str(code % 1000000).zfill(6)
|
||
|
|
|
||
|
|
|
||
|
|
async def do_login(page):
|
||
|
|
print("LOGIN...")
|
||
|
|
await page.goto('https://www.marinetraffic.com/en/users/login',
|
||
|
|
wait_until='domcontentloaded', timeout=30000)
|
||
|
|
await asyncio.sleep(3)
|
||
|
|
await page.fill('input[name="username"]', EMAIL)
|
||
|
|
await page.click('button[type="submit"]')
|
||
|
|
await asyncio.sleep(3)
|
||
|
|
await page.fill('input[type="password"]', PASSWORD)
|
||
|
|
await page.click('button[type="submit"]')
|
||
|
|
await asyncio.sleep(4)
|
||
|
|
if 'mfa' in page.url.lower() or 'auth.kpler' in page.url:
|
||
|
|
try:
|
||
|
|
await page.click('button:has-text("Google Authenticator")', timeout=3000)
|
||
|
|
await asyncio.sleep(2)
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
otp = totp(TOTP_SECRET)
|
||
|
|
print(f" TOTP: {otp}")
|
||
|
|
try:
|
||
|
|
await page.fill('input[name="code"]', otp)
|
||
|
|
await page.click('button[type="submit"]')
|
||
|
|
except Exception:
|
||
|
|
try:
|
||
|
|
await page.fill('input[type="text"]', otp)
|
||
|
|
await page.click('button[type="submit"]')
|
||
|
|
except Exception as e:
|
||
|
|
print(f" 2FA error: {e}")
|
||
|
|
await asyncio.sleep(8)
|
||
|
|
ok = 'marinetraffic.com' in page.url and 'auth.kpler' not in page.url
|
||
|
|
print(f" Login: {'OK' if ok else 'FAILED'} | {page.url[:80]}")
|
||
|
|
return ok
|
||
|
|
|
||
|
|
|
||
|
|
async def probe_vessel(page, ship_id):
|
||
|
|
"""Navigate to vessel detail page, intercept ALL XHR responses."""
|
||
|
|
print(f"\n--- Probing ship_id={ship_id} ---")
|
||
|
|
captured = []
|
||
|
|
|
||
|
|
async def on_response(response):
|
||
|
|
url = response.url
|
||
|
|
# Only care about API/JSON calls
|
||
|
|
if any(x in url for x in ['/api/', '/vessels/', '/ships/', '/ownership', '/details',
|
||
|
|
'/summary', '/company', '/management', 'json']):
|
||
|
|
try:
|
||
|
|
ct = response.headers.get('content-type', '')
|
||
|
|
if 'json' in ct or 'javascript' in ct:
|
||
|
|
body = await response.body()
|
||
|
|
text = body.decode('utf-8', errors='replace')
|
||
|
|
if len(text) > 50 and ('owner' in text.lower() or 'address' in text.lower()
|
||
|
|
or 'contact' in text.lower() or 'website' in text.lower()):
|
||
|
|
captured.append({'url': url, 'body': text[:2000]})
|
||
|
|
print(f" [XHR] {url[:100]}")
|
||
|
|
print(f" {text[:200]}")
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
page.on('response', on_response)
|
||
|
|
|
||
|
|
url = f'https://www.marinetraffic.com/en/ais/details/ships/shipid:{ship_id}'
|
||
|
|
await page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
||
|
|
await asyncio.sleep(8)
|
||
|
|
|
||
|
|
page.remove_listener('response', on_response)
|
||
|
|
|
||
|
|
# Also try direct JS fetch for known endpoints
|
||
|
|
print(f"\n Trying direct JS fetches...")
|
||
|
|
endpoints = [
|
||
|
|
f'/en/vessels/summary-details/shipid:{ship_id}',
|
||
|
|
f'/en/vessels/vesselDetails/shipid:{ship_id}',
|
||
|
|
f'/en/vessels/ownership/shipid:{ship_id}',
|
||
|
|
f'/en/ais/details/ships/shipid:{ship_id}/tab:ownership',
|
||
|
|
f'/en/vessels/{ship_id}/ownership',
|
||
|
|
f'/en/vessels/{ship_id}/details',
|
||
|
|
f'/en/api/vessels/{ship_id}',
|
||
|
|
]
|
||
|
|
for ep in endpoints:
|
||
|
|
url_full = f'https://www.marinetraffic.com{ep}'
|
||
|
|
js = f"""
|
||
|
|
async () => {{
|
||
|
|
try {{
|
||
|
|
const r = await fetch({json.dumps(url_full)}, {{
|
||
|
|
credentials: 'include',
|
||
|
|
headers: {{
|
||
|
|
'X-Requested-With': 'XMLHttpRequest',
|
||
|
|
'Accept': 'application/json',
|
||
|
|
}}
|
||
|
|
}});
|
||
|
|
const text = await r.text();
|
||
|
|
return {{status: r.status, body: text.slice(0,500)}};
|
||
|
|
}} catch(e) {{ return {{error: e.message}}; }}
|
||
|
|
}}
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
result = await page.evaluate(js)
|
||
|
|
status = result.get('status', '?')
|
||
|
|
body = result.get('body', result.get('error', ''))[:150]
|
||
|
|
if status == 200 and len(body) > 20:
|
||
|
|
print(f" [200] {ep}")
|
||
|
|
print(f" {body}")
|
||
|
|
captured.append({'url': url_full, 'status': status, 'body': body})
|
||
|
|
else:
|
||
|
|
print(f" [{status}] {ep}")
|
||
|
|
except Exception as e:
|
||
|
|
print(f" [ERR] {ep}: {e}")
|
||
|
|
await asyncio.sleep(0.5)
|
||
|
|
|
||
|
|
return captured
|
||
|
|
|
||
|
|
|
||
|
|
async def main():
|
||
|
|
from playwright.async_api import async_playwright
|
||
|
|
|
||
|
|
all_captured = []
|
||
|
|
|
||
|
|
async with async_playwright() as p:
|
||
|
|
browser = await p.chromium.launch(
|
||
|
|
headless=False,
|
||
|
|
args=['--no-sandbox', '--disable-blink-features=AutomationControlled']
|
||
|
|
)
|
||
|
|
context = await browser.new_context(
|
||
|
|
viewport={'width': 1440, 'height': 900},
|
||
|
|
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
||
|
|
'(KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
||
|
|
)
|
||
|
|
page = await context.new_page()
|
||
|
|
|
||
|
|
if not await do_login(page):
|
||
|
|
await browser.close()
|
||
|
|
return
|
||
|
|
|
||
|
|
await asyncio.sleep(3)
|
||
|
|
|
||
|
|
for ship_id in SHIP_IDS:
|
||
|
|
captured = await probe_vessel(page, ship_id)
|
||
|
|
all_captured.extend(captured)
|
||
|
|
await asyncio.sleep(3)
|
||
|
|
|
||
|
|
# Save all captured data
|
||
|
|
with open('mt_detail_probe_results.json', 'w', encoding='utf-8') as f:
|
||
|
|
json.dump(all_captured, f, ensure_ascii=False, indent=2)
|
||
|
|
print(f"\n\nSaved {len(all_captured)} captured responses to mt_detail_probe_results.json")
|
||
|
|
|
||
|
|
await browser.close()
|
||
|
|
|
||
|
|
|
||
|
|
asyncio.run(main())
|