proper status code, added detail in reposne
This commit is contained in:
186
macvendorapi.py
186
macvendorapi.py
@@ -1,144 +1,120 @@
|
|||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
from contextlib import asynccontextmanager
|
from fastapi.responses import JSONResponse
|
||||||
import httpx
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
from typing import Dict, Optional
|
import httpx
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
local_mac_vendors: Dict[str, str] = {}
|
app = FastAPI(title="MAC Address Vendor Lookup API")
|
||||||
|
|
||||||
@asynccontextmanager
|
def normalize_mac(mac_address: str) -> str:
|
||||||
async def lifespan(app: FastAPI):
|
mac_clean = unquote(mac_address)
|
||||||
load_local_mac_vendors()
|
mac_clean = re.sub(r'[^a-fA-F0-9]', '', mac_clean)
|
||||||
yield
|
|
||||||
|
|
||||||
app = FastAPI(title="MAC Address Vendor Lookup", version="1.0.0", lifespan=lifespan)
|
if len(mac_clean) < 6:
|
||||||
|
return None
|
||||||
|
|
||||||
local_mac_vendors: Dict[str, str] = {}
|
return '-'.join([mac_clean[i:i+2].upper() for i in range(0, len(mac_clean), 2)])
|
||||||
|
|
||||||
def load_local_mac_vendors():
|
def is_valid_mac(mac_address: str) -> bool:
|
||||||
"""Load MAC vendor data from local file"""
|
mac_clean = unquote(mac_address)
|
||||||
global local_mac_vendors
|
mac_clean = re.sub(r'[^a-fA-F0-9]', '', mac_clean)
|
||||||
if local_mac_vendors or not os.path.exists("mac_list.txt"):
|
return len(mac_clean) >= 6 and len(mac_clean) <= 12 and all(c in '0123456789abcdefABCDEF' for c in mac_clean)
|
||||||
return
|
|
||||||
|
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:
|
try:
|
||||||
with open("mac_list.txt", "r", encoding="utf-8") as file:
|
with open('mac_list.txt', 'r') as f:
|
||||||
for line in file:
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line:
|
if line and ' ' in line:
|
||||||
parts = line.split('\t', 1)
|
prefix, vendor = line.split(' ', 1)
|
||||||
if len(parts) == 2:
|
if prefix.upper() == mac_prefix:
|
||||||
oui = parts[0].strip().upper()
|
return vendor
|
||||||
vendor = parts[1].strip()
|
|
||||||
local_mac_vendors[oui] = vendor
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def lookup_from_github(oui: str) -> Optional[str]:
|
return None
|
||||||
"""Lookup vendor from GitHub gist"""
|
|
||||||
|
async def lookup_remote_gist(mac_prefix: str) -> str:
|
||||||
url = "https://gist.githubusercontent.com/aallan/b4bb86db86079509e6159810ae9bd3e4/raw/846ae1b646ab0f4d646af9115e47365f4118e5f6/mac-vendor.txt"
|
url = "https://gist.githubusercontent.com/aallan/b4bb86db86079509e6159810ae9bd3e4/raw/846ae1b646ab0f4d646af9115e47365f4118e5f6/mac-vendor.txt"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.get(url, timeout=10.0)
|
response = await client.get(url, timeout=5.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:
|
if response.status_code == 200:
|
||||||
content_type = response.headers.get("content-type", "")
|
for line in response.text.split('\n'):
|
||||||
if "application/json" in content_type:
|
line = line.strip()
|
||||||
return None
|
if line and ' ' in line:
|
||||||
else:
|
prefix, vendor = line.split(' ', 1)
|
||||||
return response.text.strip()
|
if prefix.upper() == mac_prefix:
|
||||||
|
return vendor
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def normalize_mac_for_api(mac_address: str) -> str:
|
async def lookup_remote_api(normalized_mac: str) -> str:
|
||||||
"""Normalize MAC address for external API (XX-XX-XX format)"""
|
mac_for_api = normalized_mac
|
||||||
clean_mac = re.sub(r'[^a-fA-F0-9]', '', mac_address.upper())
|
if len(mac_for_api.replace('-', '')) == 6:
|
||||||
if len(clean_mac) >= 6:
|
mac_for_api = mac_for_api + '-00-00-00'
|
||||||
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:
|
url = f"https://api.macvendors.com/{mac_for_api}"
|
||||||
"""Normalize MAC address for response format"""
|
|
||||||
clean_mac = re.sub(r'[^a-fA-F0-9]', '', mac_address.upper())
|
|
||||||
|
|
||||||
if len(clean_mac) == 6:
|
try:
|
||||||
return f"{clean_mac[0:2]}-{clean_mac[2:4]}-{clean_mac[4:6]}"
|
async with httpx.AsyncClient() as client:
|
||||||
elif len(clean_mac) == 12:
|
response = await client.get(url, timeout=5.0)
|
||||||
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]}"
|
if response.status_code == 200:
|
||||||
else:
|
return response.text.strip()
|
||||||
raise ValueError("Invalid MAC address format")
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def get_oui(mac_address: str) -> str:
|
return None
|
||||||
"""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}")
|
@app.get("/lookup/{mac_address}")
|
||||||
async def lookup_vendor(mac_address: str):
|
async def lookup_mac_vendor(mac_address: str):
|
||||||
try:
|
if not is_valid_mac(mac_address):
|
||||||
oui = get_oui(mac_address)
|
return JSONResponse(
|
||||||
formatted_mac = normalize_mac_response(mac_address)
|
status_code=400,
|
||||||
vendor = None
|
content={
|
||||||
|
"mac_address": mac_address,
|
||||||
|
"vendor": None,
|
||||||
|
"detail": "Invalid MAC address format"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 1. Try local file first
|
normalized_mac = normalize_mac(mac_address)
|
||||||
if oui in local_mac_vendors:
|
mac_prefix = get_mac_prefix(mac_address)
|
||||||
vendor = local_mac_vendors[oui]
|
|
||||||
|
|
||||||
# 2. Try GitHub gist if not found locally
|
vendor = lookup_local_file(mac_prefix)
|
||||||
if not vendor:
|
|
||||||
vendor = await lookup_from_github(oui)
|
|
||||||
|
|
||||||
# 3. Try macvendors.com API if still not found
|
if not vendor:
|
||||||
if not vendor:
|
vendor = await lookup_remote_gist(mac_prefix)
|
||||||
vendor = await lookup_from_macvendors_api(mac_address)
|
|
||||||
|
|
||||||
# Default to Unknown if nothing found
|
if not vendor:
|
||||||
if not vendor:
|
vendor = await lookup_remote_api(normalized_mac)
|
||||||
vendor = "Unknown"
|
|
||||||
|
|
||||||
|
if vendor:
|
||||||
return {
|
return {
|
||||||
"mac_address": formatted_mac,
|
"mac_address": normalized_mac,
|
||||||
"vendor": vendor
|
"vendor": vendor,
|
||||||
|
"detail": None
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
except ValueError as e:
|
return JSONResponse(
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
status_code=404,
|
||||||
except Exception as e:
|
content={
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
"mac_address": normalized_mac,
|
||||||
|
"vendor": None,
|
||||||
|
"detail": "Vendor not found"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
Reference in New Issue
Block a user