from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse import re import os import httpx from urllib.parse import unquote app = FastAPI(title="MAC Address Vendor Lookup API") def normalize_mac(mac_address: str) -> str: mac_clean = unquote(mac_address) mac_clean = re.sub(r'[^a-fA-F0-9]', '', mac_clean) if len(mac_clean) < 6: return None return '-'.join([mac_clean[i:i+2].upper() for i in range(0, len(mac_clean), 2)]) def is_valid_mac(mac_address: str) -> bool: mac_clean = unquote(mac_address) mac_clean = re.sub(r'[^a-fA-F0-9]', '', mac_clean) return len(mac_clean) >= 6 and len(mac_clean) <= 12 and all(c in '0123456789abcdefABCDEF' for c in mac_clean) def get_mac_prefix(mac_address: str) -> str: mac_clean = re.sub(r'[^a-fA-F0-9]', '', mac_address) return mac_clean[:6].upper() def lookup_local_file(mac_prefix: str) -> str: if not os.path.exists('mac_list.txt'): return None try: with open('mac_list.txt', 'r') as f: for line in f: line = line.strip() if line and ' ' in line: prefix, vendor = line.split(' ', 1) if prefix.upper() == mac_prefix: return vendor except Exception: pass return None async def lookup_remote_gist(mac_prefix: str) -> str: url = "https://gist.githubusercontent.com/aallan/b4bb86db86079509e6159810ae9bd3e4/raw/846ae1b646ab0f4d646af9115e47365f4118e5f6/mac-vendor.txt" try: async with httpx.AsyncClient() as client: response = await client.get(url, timeout=5.0) if response.status_code == 200: for line in response.text.split('\n'): line = line.strip() if line and ' ' in line: prefix, vendor = line.split(' ', 1) if prefix.upper() == mac_prefix: return vendor except Exception: pass return None async def lookup_remote_api(normalized_mac: str) -> str: mac_for_api = normalized_mac if len(mac_for_api.replace('-', '')) == 6: mac_for_api = mac_for_api + '-00-00-00' url = f"https://api.macvendors.com/{mac_for_api}" try: async with httpx.AsyncClient() as client: response = await client.get(url, timeout=5.0) if response.status_code == 200: return response.text.strip() except Exception: pass return None @app.get("/lookup/{mac_address}") async def lookup_mac_vendor(mac_address: str): if not is_valid_mac(mac_address): return JSONResponse( status_code=400, content={ "mac_address": mac_address, "vendor": None, "detail": "Invalid MAC address format" } ) normalized_mac = normalize_mac(mac_address) mac_prefix = get_mac_prefix(mac_address) vendor = lookup_local_file(mac_prefix) if not vendor: vendor = await lookup_remote_gist(mac_prefix) if not vendor: vendor = await lookup_remote_api(normalized_mac) if vendor: return { "mac_address": normalized_mac, "vendor": vendor, "detail": None } else: return JSONResponse( status_code=404, content={ "mac_address": normalized_mac, "vendor": None, "detail": "Vendor not found" } ) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)