Assistant
{"content": "1|#!/usr/bin/env python3\n2|\"\"\"Location Service — collect GPS + cellular data from browser devices.\"\"\"\n3|\n4|import json\n5|import sqlite3\n6|import os\n7|import base64\n8|from datetime import datetime, timezone\n9|from pathlib import Path\n10|from flask import Flask, render_template, request, jsonify, Response\n11|\n12|app = Flask(__name__)\n13|\n14|# ── Auth config ──\n15|API_USERNAME = os.environ.get(\"LOCATION_API_USER\", \"admin\")\n16|API_PASSWORD = os.environ.get(\"LOCATION_API_PASS\", \"s3cr3t!\")\n17|PROTECTED_PREFIXES = [\"/api/latest\", \"/api/ips\", \"/api/devices\"]\n18|\n19|\n20|def is_local_request():\n21| \"\"\"Check if request originates from localhost/127.0.0.1.\n22|\n23| Since nginx proxies Flask on localhost, request.remote_addr is always\n24| 127.0.0.1. We rely on X-Forwarded-For: if it's empty or starts with\n25| 127.0.0.1, the request is truly local. Otherwise it's external.\n26| \"\"\"\n27| xff = request.headers.get(\"X-Forwarded-For\", \"\").strip()\n28| if not xff:\n29| return True # No proxy header = direct/local connection\n30| first_ip = xff.split(\",\")[0].strip()\n31| if first_ip in (\"127.0.0.1\", \"::1\", \"localhost\", \"\"):\n32| return True\n33| return False\n34|\n35|\n36|def check_auth():\n37| \"\"\"HTTP Basic Auth for protected API endpoints. Skips localhost.\"\"\"\n38| if not any(request.path.startswith(p) for p in PROTECTED_PREFIXES):\n39| return None\n40| if is_local_request():\n41| return None\n42| auth_header = request.headers.get(\"Authorization\", \"\")\n43| if auth_header.startswith(\"Basic \"):\n44| try:\n45| decoded = base64.b64decode(auth_header[6:]).decode(\"utf-8\")\n46| username, password = decoded.split(\":\", 1)\n47| if username == API_USERNAME and password == API_PASSWORD:\n48| return None\n49| except Exception:\n50| pass\n51| return Response(\n52| \"Authentication required.\",\n53| 401,\n54| {\"WWW-Authenticate\": 'Basic realm=\"Location API\"'},\n55| )\n56|\n57|\n58|@app.before_request\n59|def enforce_auth():\n60| result = check_auth()\n61| if result is not None:\n62| return result\n63|\n64|DB_PATH = Path(__file__).parent / \"locations.db\"\n65|\n66|\n67|def get_db():\n68| conn = str(DB_PATH)\n69| db = sqlite3.connect(conn)\n70| db.row_factory = sqlite3.Row\n71| db.execute(\"\"\"\n72| CREATE TABLE IF NOT EXISTS device_reports (\n73| id INTEGER PRIMARY KEY AUTOINCREMENT,\n74| device_id TEXT,\n75| device_name TEXT,\n76| user_agent TEXT,\n77| ip_addr TEXT,\n78| reported_at TEXT,\n79| gps_lat REAL,\n80| gps_lon REAL,\n81| gps_accuracy REAL,\n82| gps_altitude REAL,\n83| gps_alt_accuracy REAL,\n84| gps_speed REAL,\n85| gps_heading REAL,\n86| gps_timestamp TEXT,\n87| cell_type TEXT,\n88| cell_effective_type TEXT,\n89| cell_downlink REAL,\n90| cell_rtt INTEGER,\n91| cell_downlink_max REAL,\n92| cell_save_data INTEGER,\n93| connection_info_raw TEXT,\n94| extra_json TEXT\n95| )\n96| \"\"\")\n97| db.execute(\"\"\"\n98| CREATE TABLE IF NOT EXISTS wifi_reports (\n99| id INTEGER PRIMARY KEY AUTOINCREMENT,\n100| report_id INTEGER,\n101| ssid TEXT,\n102| bssid TEXT,\n103| frequency REAL,\n104| signal_level INTEGER,\n105| ip_addr TEXT,\n106| timestamp TEXT,\n107| FOREIGN KEY (report_id) REFERENCES device_reports(id)\n108| )\n109| \"\"\")\n110| db.commit()\n111| return db\n112|\n113|\n114|@app.route(\"/\")\n115|def index():\n116| \"\"\"Main page — location collector UI.\"\"\"\n117| response = Response(render_template(\"index.html\"))\n118| response.headers[\"Cache-Control\"] = \"no-store, no-cache, must-revalidate, max-age=0\"\n119| response.headers[\"Pragma\"] = \"no-cache\"\n120| return response\n121|\n122|\n123|def get_client_ip():\n124| \"\"\"Extract the real client IP from request, checking proxy headers first.\"\"\"\n125| # Check common proxy headers (behind nginx reverse proxy)\n126| headers_to_check = [\n127| \"X-Forwarded-For\", # Standard proxy header (may contain chain: client, proxy1, proxy2)\n128| \"X-Real-IP\", # nginx proxy_set_header\n129| \"CF-Connecting-IP\", # Cloudflare\n130| \"X-Client-IP\", # Some proxies\n131| \"X-Cluster-Client-IP\", # Rackspace, etc.\n132| \"Forwarded\", # RFC 7239 standard\n133| \"True-Client-IP\", # Akamai, Cloudflare Enterprise\n134| ]\n135|\n136| for header in headers_to_check:\n137| value = request.headers.get(header, \"\").strip()\n138| if value:\n139| # X-Forwarded-For can contain a chain: \"client, proxy1, proxy2\"\n140| # Take the first (original client) IP\n141| if header == \"X-Forwarded-For\":\n142| ip = value.split(\",\")[0].strip()\n143| elif header == \"Forwarded\":\n144| # Parse \"for=192.0.2.60;proto=http;by=203.0.113.43\"\n145| import re\n146| match = re.search(r'for=\"?([^\";,\\s]+)\"?', value)\n147| ip = match.group(1) if match else None\n148| else:\n149| ip = value\n150|\n151| if ip and ip != \"127.0.0.1\":\n152| return ip\n153|\n154| # Fallback to direct connection IP\n155| return request.remote_addr or \"unknown\"\n156|\n157|\n158|@app.route(\"/api/report\", methods=[\"POST\"])\n159|def report_location():\n160| \"\"\"Receive a location + cell data report from a browser.\"\"\"\n161| db = get_db()\n162| body = request.get_json(force=True, silent=True) or {}\n163|\n164| gps = body.get(\"geolocation\") or {}\n165| conn_info = body.get(\"connection\") or {}\n166| extra = body.get(\"extra\") or {}\n167|\n168| # Collect all IP info\n169| client_ip = get_client_ip()\n170| server_ip = request.remote_addr or \"unknown\"\n171|\n172| # Build IP info object\n173| ip_info = {\n174| \"client_ip\": client_ip,\n175| \"server_ip\": server_ip,\n176| \"x_forwarded_for\": request.headers.get(\"X-Forwarded-For\", \"\"),\n177| \"x_real_ip\": request.headers.get(\"X-Real-IP\", \"\"),\n178| \"cf_connecting_ip\": request.headers.get(\"CF-Connecting-IP\", \"\"),\n179| }\n180|\n181| cursor = db.execute(\n182| \"\"\"INSERT INTO device_reports\n183| (device_id, device_name, user_agent, ip_addr, reported_at,\n184| gps_lat, gps_lon, gps_accuracy, gps_altitude, gps_alt_accuracy,\n185| gps_speed, gps_heading, gps_timestamp,\n186| cell_type, cell_effective_type, cell_downlink, cell_rtt,\n187| cell_downlink_max, cell_save_data, connection_info_raw, extra_json)\n188| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\"\"\",\n189| (\n190| body.get(\"deviceId\", \"\"),\n191| body.get(\"deviceName\", \"\"),\n192| request.headers.get(\"User-Agent\", \"\")[:500],\n193| client_ip,\n194| datetime.now(timezone.utc).isoformat(),\n195| gps.get(\"latitude\"),\n196| gps.get(\"longitude\"),\n197| gps.get(\"accuracy\"),\n198| gps.get(\"altitude\"),\n199| gps.get(\"altitudeAccuracy\"),\n200| gps.get(\"speed\"),\n201| gps.get(\"heading\"),\n202| gps.get(\"timestamp\"),\n203| conn_info.get(\"type\"),\n204| conn_info.get(\"effectiveType\"),\n205| conn_info.get(\"downlink\"),\n206| conn_info.get(\"rtt\"),\n207| conn_info.get(\"downlinkMax\"),\n208| 1 if conn_info.get(\"saveData\") else 0,\n209| json.dumps(conn_info),\n210| json.dumps({**extra, \"ip_info\": ip_info}) if extra else json.dumps({\"ip_info\": ip_info}),\n211| ),\n212| )\n213|\n214| report_id = cursor.lastrowid\n215|\n216| # Save WiFi info if provided\n217| wifi_list = body.get(\"wifi\") or []\n218| for w in wifi_list:\n219| db.execute(\n220| \"INSERT INTO wifi_reports (report_id, ssid, bssid, frequency, signal_level, ip_addr, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)\",\n221| (\n222| report_id,\n223| w.get(\"ssid\"),\n224| w.get(\"bssid\"),\n225| w.get(\"frequency\"),\n226| w.get(\"signalLevel\"),\n227| w.get(\"ipAddr\"),\n228| datetime.now(timezone.utc).isoformat(),\n229| ),\n230| )\n231|\n232| db.commit()\n233| db.close()\n234|\n235| return jsonify({\"status\": \"ok\", \"id\": report_id})\n236|\n237|\n238|@app.route(\"/api/devices\")\n239|def list_devices():\n240| \"\"\"Return all device reports, latest first.\"\"\"\n241| db = get_db()\n242| rows = db.execute(\n243| \"SELECT * FROM device_reports ORDER BY id DESC LIMIT 200\"\n244| ).fetchall()\n245|\n246| devices = []\n247| for r in rows:\n248| d = dict(r)\n249| try:\n250| d[\"connection_info_raw\"] = json.loads(d.get(\"connection_info_raw\") or \"{}\")\n251| except Exception:\n252| d[\"connection_info_raw\"] = {}\n253| try:\n254| d[\"extra_json\"] = json.loads(d.get(\"extra_json\") or \"{}\")\n255| except Exception:\n256| d[\"extra_json\"] = {}\n257|\n258| # WiFi for this report\n259| wifi_rows = db.execute(\n260| \"SELECT * FROM wifi_reports WHERE report_id = ?\", (d[\"id\"],)\n261| ).fetchall()\n262| d[\"wifi\"] = [dict(w) for w in wifi_rows]\n263|\n264| devices.append(d)\n265|\n266| db.close()\n267| return jsonify(devices)\n268|\n269|\n270|@app.route(\"/api/device/\")\n271|def device_detail(report_id):\n272| \"\"\"Return a single device report.\"\"\"\n273| db = get_db()\n274| row = db.execute(\"SELECT * FROM device_reports WHERE id = ?\", (report_id,)).fetchone()\n275| if not row:\n276| db.close()\n277| return jsonify({\"error\": \"not found\"}), 404\n278|\n279| d = dict(row)\n280| try:\n281| d[\"connection_info_raw\"] = json.loads(d.get(\"connection_info_raw\") or \"{}\")\n282| except Exception:\n283| d[\"connection_info_raw\"] = {}\n284|\n285| wifi_rows = db.execute(\n286| \"SELECT * FROM wifi_reports WHERE report_id = ?\", (report_id,)\n287| ).fetchall()\n288| d[\"wifi\"] = [dict(w) for w in wifi_rows]\n289|\n290| db.close()\n291| return jsonify(d)\n292|\n293|\n294|@app.route(\"/api/ips\")\n295|def list_ips():\n296| \"\"\"Return all unique IP addresses seen across reports, with usage counts.\"\"\"\n297| db = get_db()\n298| rows = db.execute(\"\"\"\n299| SELECT ip_addr, COUNT(*) as count, MIN(reported_at) as first_seen, MAX(reported_at) as last_seen\n300| FROM device_reports\n301| WHERE ip_addr != ''\n302| GROUP BY ip_addr\n303| ORDER BY count DESC\n304| \"\"\").fetchall()\n305|\n306| # Also extract IPs from extra_json (ip_info.x_forwarded_for, etc.)\n307| all_ips = {}\n308| for r in rows:\n309| d = dict(r)\n310| ip = d[\"ip_addr\"]\n311| all_ips[ip] = {\n312| \"ip\": ip,\n313| \"count\": d[\"count\"],\n314| \"first_seen\": d[\"first_seen\"],\n315| \"last_seen\": d[\"last_seen\"],\n316| \"source\": \"direct\"\n317| }\n318|\n319| # Scan extra_json for forwarded IPs\n320| extra_rows = db.execute(\"\"\"\n321| SELECT extra_json FROM device_reports WHERE extra_json LIKE '%ip_info%'\n322| \"\"\").fetchall()\n323| for row in extra_rows:\n324| try:\n325| extra = json.loads(row[\"extra_json\"])\n326| ip_info = extra.get(\"ip_info\", {})\n327| for key in [\"x_forwarded_for\", \"x_real_ip\", \"cf_connecting_ip\"]:\n328| raw = ip_info.get(key, \"\")\n329| if raw:\n330| # X-Forwarded-For can have multiple IPs\n331| for ip in raw.split(\",\"):\n332| ip = ip.strip()\n333| if ip and ip not in all_ips:\n334| all_ips[ip] = {\"ip\": ip, \"count\": 0, \"source\": key}\n335| except Exception:\n336| pass\n337|\n338| db.close()\n339| return jsonify(list(all_ips.values()))\n340|\n341|\n342|@app.route(\"/api/ip-info\")\n343|def ip_info():\n344| \"\"\"Return the client's own IP info (useful for testing what the server sees).\"\"\"\n345| return jsonify({\n346| \"client_ip\": get_client_ip(),\n347| \"server_ip\": request.remote_addr or \"unknown\",\n348| \"headers\": {\n349| \"x_forwarded_for\": request.headers.get(\"X-Forwarded-For\", \"\"),\n350| \"x_real_ip\": request.headers.get(\"X-Real-IP\", \"\"),\n351| \"cf_connecting_ip\": request.headers.get(\"CF-Connecting-IP\", \"\"),\n352| \"x_client_ip\": request.headers.get(\"X-Client-IP\", \"\"),\n353| \"forwarded\": request.headers.get(\"Forwarded\", \"\"),\n354| \"true_client_ip\": request.headers.get(\"True-Client-IP\", \"\"),\n355| }\n356| })\n357|\n358|\n359|@app.route(\"/api/latest\")\n360|def latest():\n361| \"\"\"Return the most recent report per device_id.\"\"\"\n362| db = get_db()\n363| rows = db.execute(\"\"\"\n364| SELECT dr.* FROM device_reports dr\n365| INNER JOIN (\n366| SELECT device_id, MAX(id) as max_id\n367| FROM device_reports\n368| WHERE device_id != ''\n369| GROUP BY device_id\n370| ) latest ON dr.device_id = latest.device_id AND dr.id = latest.max_id\n371| ORDER BY dr.reported_at DESC\n372| \"\"\").fetchall()\n373|\n374| devices = []\n375| for r in rows:\n376| d = dict(r)\n377| try:\n378| d[\"connection_info_raw\"] = json.loads(d.get(\"connection_info_raw\") or \"{}\")\n379| except Exception:\n380| d[\"connection_info_raw\"] = {}\n381| wifi_rows = db.execute(\n382| \"SELECT * FROM wifi_reports WHERE report_id = ?\", (d[\"id\"],)\n383| ).fetchall()\n384| d[\"wifi\"] = [dict(w) for w in wifi_rows]\n385| devices.append(d)\n386|\n387| db.close()\n388| return jsonify(devices)\n389|\n390|\n391|@app.route(\"/dashboard\")\n392|def dashboard():\n393| \"\"\"Dashboard view of all collected data.\"\"\"\n394| return render_template(\"dashboard.html\")\n395|\n396|\n397|if __name__ == \"__main__\":\n398| app.run(host=\"127.0.0.1\", port=9118, debug=False)\n399|", "total_lines": 398, "file_size": 12852, "truncated": false, "is_binary": false, "is_image": false}