191 lines
6.8 KiB
Python
191 lines
6.8 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Probe MT's actual API endpoints by logging in via Playwright and intercepting network calls.
|
||
|
|
"""
|
||
|
|
import os, sys, json, time, math, struct, hmac, hashlib, base64
|
||
|
|
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
|
||
|
|
EMAIL = "operation@mrlogisticcorp.com"
|
||
|
|
PASSWORD = "NKh9i8Z!7fU9jfi"
|
||
|
|
TOTP_SECRET = "MNWTEPTFJZBUC32GJFEWY6LVKQ2GGYKH"
|
||
|
|
|
||
|
|
|
||
|
|
def totp(secret):
|
||
|
|
s = secret.upper().replace(' ', '')
|
||
|
|
pad = (-len(s)) % 8 # correct padding: 0, 2, 4, or 6 chars
|
||
|
|
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)
|
||
|
|
|
||
|
|
|
||
|
|
from playwright.sync_api import sync_playwright
|
||
|
|
|
||
|
|
api_responses = []
|
||
|
|
|
||
|
|
with sync_playwright() as p:
|
||
|
|
browser = p.chromium.launch(
|
||
|
|
headless=True,
|
||
|
|
args=['--no-sandbox', '--disable-blink-features=AutomationControlled']
|
||
|
|
)
|
||
|
|
context = browser.new_context(
|
||
|
|
viewport={'width': 1280, '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 = context.new_page()
|
||
|
|
|
||
|
|
# Use page.route() to intercept tile API calls and capture their bodies
|
||
|
|
tile_responses = []
|
||
|
|
|
||
|
|
def handle_tile_route(route):
|
||
|
|
response = route.fetch()
|
||
|
|
try:
|
||
|
|
body = response.body()
|
||
|
|
body_text = body.decode('utf-8', errors='replace')
|
||
|
|
tile_responses.append({
|
||
|
|
'url': route.request.url,
|
||
|
|
'status': response.status,
|
||
|
|
'size': len(body),
|
||
|
|
'body': body_text[:1000],
|
||
|
|
})
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
route.fulfill(response=response)
|
||
|
|
|
||
|
|
# Intercept tile data requests
|
||
|
|
page.route('**/getData/get_data_json_4/**', handle_tile_route)
|
||
|
|
page.route('**/getData/**', handle_tile_route)
|
||
|
|
|
||
|
|
# Also store all response URLs for inspection
|
||
|
|
intercepted_urls = []
|
||
|
|
def on_resp(resp):
|
||
|
|
url = resp.url
|
||
|
|
skip = ['.js', '.css', '.png', '.jpg', '.gif', '.svg', '.woff', '.ttf',
|
||
|
|
'google-analytics', 'hotjar', 'facebook', 'segment.io', 'amplitude']
|
||
|
|
if any(s in url for s in skip):
|
||
|
|
return
|
||
|
|
intercepted_urls.append({'url': url, 'status': resp.status})
|
||
|
|
page.on('response', on_resp)
|
||
|
|
|
||
|
|
# === STEP 1: LOGIN ===
|
||
|
|
print("Step 1: Login to MT...")
|
||
|
|
page.goto('https://www.marinetraffic.com/en/users/login',
|
||
|
|
wait_until='domcontentloaded', timeout=30000)
|
||
|
|
time.sleep(3)
|
||
|
|
print(f" Redirected to: {page.url}")
|
||
|
|
|
||
|
|
# Email step
|
||
|
|
page.fill('input[name="username"]', EMAIL)
|
||
|
|
page.click('button[type="submit"]')
|
||
|
|
time.sleep(3)
|
||
|
|
print(f" After email: {page.url}")
|
||
|
|
|
||
|
|
# Password step
|
||
|
|
page.fill('input[type="password"]', PASSWORD)
|
||
|
|
page.click('button[type="submit"]')
|
||
|
|
time.sleep(4)
|
||
|
|
print(f" After password: {page.url}")
|
||
|
|
|
||
|
|
# 2FA step
|
||
|
|
if 'mfa-login-options' in page.url:
|
||
|
|
print(" 2FA options screen — clicking Google Authenticator...")
|
||
|
|
page.click('button:has-text("Google Authenticator")')
|
||
|
|
time.sleep(3)
|
||
|
|
otp = totp(TOTP_SECRET)
|
||
|
|
print(f" TOTP code: {otp}")
|
||
|
|
page.fill('input[name="code"]', otp)
|
||
|
|
page.click('button[type="submit"]')
|
||
|
|
time.sleep(5)
|
||
|
|
print(f" After 2FA: {page.url}")
|
||
|
|
|
||
|
|
# Verify login
|
||
|
|
logged_in = 'marinetraffic.com' in page.url and 'auth.kpler' not in page.url
|
||
|
|
print(f" Logged in: {logged_in} | URL: {page.url}")
|
||
|
|
page.screenshot(path='mt_after_login.png')
|
||
|
|
|
||
|
|
if not logged_in:
|
||
|
|
print(" ERROR: Not logged in. Stopping.")
|
||
|
|
browser.close()
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
# === STEP 2: LOAD ROTTERDAM MAP ===
|
||
|
|
print("\nStep 2: Loading Rotterdam map (zoom 10)...")
|
||
|
|
intercepted_urls.clear()
|
||
|
|
tile_responses.clear()
|
||
|
|
page.goto('https://www.marinetraffic.com/en/ais/home/centerx:4.5/centery:51.9/zoom:10',
|
||
|
|
wait_until='load', timeout=30000)
|
||
|
|
time.sleep(10)
|
||
|
|
page.screenshot(path='mt_rotterdam_map.png')
|
||
|
|
print(f" Page title: {page.title()}")
|
||
|
|
print(f" Intercepted URLs: {len(intercepted_urls)}")
|
||
|
|
print(f" Tile responses captured: {len(tile_responses)}")
|
||
|
|
|
||
|
|
# Show captured tile data
|
||
|
|
if tile_responses:
|
||
|
|
print("\n=== CAPTURED TILE DATA ===")
|
||
|
|
for tr in tile_responses[:5]:
|
||
|
|
print(f" {tr['status']} ({tr['size']}b): {tr['url'][:90]}")
|
||
|
|
print(f" Body: {tr['body'][:300]}")
|
||
|
|
with open('mt_tile_data.json', 'w') as f:
|
||
|
|
json.dump(tile_responses, f, indent=2)
|
||
|
|
print("Saved to mt_tile_data.json")
|
||
|
|
else:
|
||
|
|
print("\nNO tile responses captured via route interceptor.")
|
||
|
|
|
||
|
|
# === STEP 3: FETCH BODIES OF INTERESTING URLS ===
|
||
|
|
print(f"\nStep 3: Fetching bodies of non-static responses...")
|
||
|
|
api_responses = []
|
||
|
|
for item in intercepted_urls:
|
||
|
|
url = item['url']
|
||
|
|
status = item['status']
|
||
|
|
if status != 200:
|
||
|
|
continue
|
||
|
|
try:
|
||
|
|
# Use page.request (same session/cookies) to re-fetch interesting URLs
|
||
|
|
r = page.request.get(url, timeout=10000)
|
||
|
|
body = r.body()
|
||
|
|
size = len(body)
|
||
|
|
if size < 100:
|
||
|
|
continue
|
||
|
|
body_preview = body[:500].decode('utf-8', errors='replace')
|
||
|
|
api_responses.append({'status': status, 'size': size, 'url': url, 'body_preview': body_preview})
|
||
|
|
body_lower = body_preview.lower()
|
||
|
|
has_vessel = any(k in body_lower for k in ['mmsi', 'shipname', 'shiptype', '"lat"', 'latitude'])
|
||
|
|
mark = '*** VESSEL ***' if has_vessel else ''
|
||
|
|
print(f" ({size:7}b) {mark} {url[:110]}")
|
||
|
|
if has_vessel:
|
||
|
|
print(f" {body_preview[:200]}")
|
||
|
|
except Exception as e:
|
||
|
|
pass
|
||
|
|
|
||
|
|
with open('mt_all_responses.json', 'w') as f:
|
||
|
|
json.dump(api_responses, f, indent=2)
|
||
|
|
|
||
|
|
# Also try tile API directly via authenticated session
|
||
|
|
print("\nStep 4: Direct tile API test (Rotterdam z10)...")
|
||
|
|
lat, lon, zoom = 51.9, 4.5, 10
|
||
|
|
import math
|
||
|
|
lat_rad = math.radians(lat)
|
||
|
|
n = 2.0 ** zoom
|
||
|
|
x = int((lon + 180.0) / 360.0 * n)
|
||
|
|
y = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
|
||
|
|
tile_url = f'https://www.marinetraffic.com/getData/get_data_json_4/z:{zoom}/X:{x}/Y:{y}/station:0'
|
||
|
|
print(f" Tile URL: {tile_url}")
|
||
|
|
try:
|
||
|
|
r = page.request.get(tile_url, headers={
|
||
|
|
'Referer': f'https://www.marinetraffic.com/en/ais/home/centerx:{lon}/centery:{lat}/zoom:{zoom}',
|
||
|
|
'X-Requested-With': 'XMLHttpRequest',
|
||
|
|
})
|
||
|
|
body = r.body().decode('utf-8', errors='replace')
|
||
|
|
print(f" Status: {r.status}, Size: {len(body)}")
|
||
|
|
print(f" Response: {body[:300]}")
|
||
|
|
except Exception as e:
|
||
|
|
print(f" Error: {e}")
|
||
|
|
|
||
|
|
browser.close()
|
||
|
|
print("\nDone!")
|