from fastapi import FastAPI, HTTPException from contextlib import asynccontextmanager import httpx import re import os from typing import Dict, Optional local_mac_vendors: Dict[str, str] = {} @asynccontextmanager async def lifespan(app: FastAPI): load_local_mac_vendors() yield app = FastAPI(title="MAC Address Vendor Lookup", version="1.0.0", lifespan=lifespan) local_mac_vendors: Dict[str, str] = {} def load_local_mac_vendors(): """Load MAC vendor data from local file""" global local_mac_vendors if local_mac_vendors or not os.path.exists("mac_list.txt"): return try: with open("mac_list.txt", "r", encoding="utf-8") as file: for line in file: line = line.strip() if line: parts = line.split('\t', 1) if len(parts) == 2: oui = parts[0].strip().upper() vendor = parts[1].strip() local_mac_vendors[oui] = vendor except Exception: pass async def lookup_from_github(oui: str) -> Optional[str]: """Lookup vendor from GitHub gist""" 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=10.0) response.raise_for_status() for line in response.text.strip().split('\n'): if line.strip(): parts = line.split('\t', 1) if len(parts) == 2 and parts[0].strip().upper() == oui: return parts[1].strip() except Exception: pass return None async def lookup_from_macvendors_api(mac_address: str) -> Optional[str]: """Lookup vendor from macvendors.com API""" try: formatted_mac = normalize_mac_for_api(mac_address) url = f"https://api.macvendors.com/{formatted_mac}" async with httpx.AsyncClient() as client: response = await client.get(url, timeout=10.0) if response.status_code == 200: content_type = response.headers.get("content-type", "") if "application/json" in content_type: return None else: return response.text.strip() except Exception: pass return None def normalize_mac_for_api(mac_address: str) -> str: """Normalize MAC address for external API (XX-XX-XX format)""" clean_mac = re.sub(r'[^a-fA-F0-9]', '', mac_address.upper()) if len(clean_mac) >= 6: return f"{clean_mac[0:2]}-{clean_mac[2:4]}-{clean_mac[4:6]}" else: raise ValueError("Invalid MAC address format") def normalize_mac_response(mac_address: str) -> str: """Normalize MAC address for response format""" clean_mac = re.sub(r'[^a-fA-F0-9]', '', mac_address.upper()) if len(clean_mac) == 6: return f"{clean_mac[0:2]}-{clean_mac[2:4]}-{clean_mac[4:6]}" elif len(clean_mac) == 12: return f"{clean_mac[0:2]}-{clean_mac[2:4]}-{clean_mac[4:6]}-{clean_mac[6:8]}-{clean_mac[8:10]}-{clean_mac[10:12]}" else: raise ValueError("Invalid MAC address format") def get_oui(mac_address: str) -> str: """Extract OUI (first 6 hex digits) from MAC address""" clean_mac = re.sub(r'[^a-fA-F0-9]', '', mac_address.upper()) if len(clean_mac) >= 6: return clean_mac[:6] else: raise ValueError("MAC address too short") @app.get("/") async def root(): return {"message": "MAC Address Vendor Lookup API"} @app.get("/lookup/{mac_address}") async def lookup_vendor(mac_address: str): try: oui = get_oui(mac_address) formatted_mac = normalize_mac_response(mac_address) vendor = None # 1. Try local file first if oui in local_mac_vendors: vendor = local_mac_vendors[oui] # 2. Try GitHub gist if not found locally if not vendor: vendor = await lookup_from_github(oui) # 3. Try macvendors.com API if still not found if not vendor: vendor = await lookup_from_macvendors_api(mac_address) # Default to Unknown if nothing found if not vendor: vendor = "Unknown" return { "mac_address": formatted_mac, "vendor": vendor } except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail="Internal server error") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)