init
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
venv/
|
23055
mac_list.txt
Normal file
23055
mac_list.txt
Normal file
File diff suppressed because it is too large
Load Diff
145
macvendorapi.py
Normal file
145
macvendorapi.py
Normal file
@@ -0,0 +1,145 @@
|
||||
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)
|
16
requirements.txt
Normal file
16
requirements.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.9.0
|
||||
certifi==2025.4.26
|
||||
click==8.2.1
|
||||
fastapi==0.115.12
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
pydantic==2.11.5
|
||||
pydantic_core==2.33.2
|
||||
sniffio==1.3.1
|
||||
starlette==0.46.2
|
||||
typing-inspection==0.4.1
|
||||
typing_extensions==4.13.2
|
||||
uvicorn==0.34.2
|
Reference in New Issue
Block a user