最近我在抱抱脸空间(Hugging Face Space)搭建了一个 IP 查询接口,使用 Docker 来部署。以下是项目的主要文件和使用案例。
API 地址
你可以通过以下链接访问我搭建的 API:
一键复制抱抱脸空间
如果你需要私有化部署,请复制以下地址以便快速使用:
功能说明
这个接口可以查询 IP 地址的相关信息,并可以实现 Cloudflare Worker 的安全反向代理,防止被 Netcraft 检测为钓鱼网站导致封号。
接口特点
- 高精度查询:IPv6 地址可以精确到区县级别,移动的大部分IPv4地址也可以。
- 实时更新:使用开源的 MaxMind 数据库,数据每天检查更新,确保数据的准确性。
当然,以下是接口返回结果的格式示例,单独列出:
接口返回结果格式
接口返回的结果为 JSON 格式,示例:
{"ip":"2409:8a00:1:0:0:0:0:1a2b","as":{"number":56048,"name":"China Mobile Communicaitons Corporation","info":"中国移动"},"addr":"2409:8a00::/37","location":{"latitude":39.911,"longitude":116.395},"country":{"code":"CN","name":"中国"},"registered_country":{"code":"CN","name":"中国"},"regions":["北京市","东城区"],"regions_short":["北京","东城区"],"type":"宽带"}
字段 | 示例 |
---|---|
ip | 2409:8a00:1:0:0:0:0:1a2b |
addr | 2409:8a00::/37 |
as.number | 56048 |
as.name | China Mobile Communications Corporation |
as.info | 中国移动 |
location.latitude | 39.911 |
location.longitude | 116.395 |
country.code | CN |
country.name | 中国 |
registered_country.code | CN |
registered_country.name | 中国 |
regions | 北京市, 东城区 |
regions_short | 北京, 东城区 |
type | 宽带 |
你可以把这个接口拿去开发你的应用程序。
使用方法
直接访问接口可以获取到你当前的 IPv4 地址。请注意,由于抱抱脸空间的限制,接口只能获取到 IPv4 地址。如果需要获取 IPv6 地址,请自行寻找其他方式。查询时使用格式为:
https://ipgeo-api.hf.space/{ip}
使用案例:用于Cloudflare workers反向代理鉴权
以下是一个示例脚本,能够调用该接口实现根据来访ip归属地进行鉴权,实现优雅且安全的反代GitHub,防止被 Netcraft 检测为钓鱼网站导致封号。
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const clientIP = request.headers.get('cf-connecting-ip');
const ipInfoResponse = await fetch(`https://ipgeo-api.hf.space/${clientIP}`); //
const ipInfo = await ipInfoResponse.json();
const regions = ipInfo.regions || [];
if (!(regions.includes('北京市') || regions.includes('上海市') || regions.includes('陕西省'))) {
return new Response('404', { status: 404 });
return new Response(htmlContent, { status: 404, headers: { 'Content-Type': 'text/html' } });
}
const url = new URL(request.url)
url.hostname = 'github.com'
const modifiedRequest = new Request(url, request)
return fetch(modifiedRequest)
}
示例中是只允许北京市、上海市和陕西省的IP访问。你需要把自己访问的归属地添加进去,不是这个归属地的直接返回403,注意你可以根据自己的使用情况来更改代码中允许访问的IP属地,对于IPV4精确度大部分只能精确到省,但是移动的IPV4能精确到区县,而IPV6精确度可以精确到区县,使用时你要根据自己的实际情况来进行修改。
下列是部署这个api接口需要添加的几个主要文件,如果不想复制就可以自己手动创建以下文件来进行创建。
1. Dockerfile
FROM python:3.13 as py-builder
WORKDIR /code
COPY ./requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.13-slim
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get update && apt-get install -y curl procps && rm -rf /var/lib/apt/lists/*
WORKDIR /code
COPY --from=py-builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY --from=py-builder /usr/local/bin /usr/local/bin
COPY ./main.py .
COPY ./update_and_restart.sh .
RUN touch /code/ip_query.log && chmod 777 /code/ip_query.log
RUN chmod -R 777 /code
RUN chmod +x /code/update_and_restart.sh
CMD ["sh", "/code/update_and_restart.sh"]
2. README.md
---
title: API
emoji: 📊
colorFrom: purple
colorTo: gray
sdk: docker
app_port: 8080
---
3. main.py
import ipaddress
import maxminddb
from fastapi import FastAPI, Request
import json
import datetime
import logging
import sys
import os
from logging.handlers import RotatingFileHandler
LOG_FILE = os.path.join('/code', 'ip_query.log')
try:
formatter = logging.Formatter('%(message)s')
log_handler = RotatingFileHandler(
LOG_FILE,
maxBytes=10*1024*1024,
backupCount=5,
encoding='utf-8'
)
log_handler.setFormatter(formatter)
logger = logging.getLogger('ip_query')
logger.setLevel(logging.INFO)
logger.addHandler(log_handler)
startup_log = {
"时间": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"事件": "系统启动",
"状态": "成功"
}
logger.info(json.dumps(startup_log, ensure_ascii=False))
except Exception as e:
print(f"日志初始化失败: {e}")
sys.exit(1)
city_reader = maxminddb.open_database('GeoLite2-City.mmdb')
asn_reader = maxminddb.open_database('GeoLite2-ASN.mmdb')
cn_reader = maxminddb.open_database('GeoCN.mmdb')
lang = ["zh-CN", "en"]
asn_map = {
9812: "东方有线",
9389: "中国长城",
17962: "天威视讯",
17429: "歌华有线",
7497: "科技网",
24139: "华数",
9801: "中关村",
4538: "教育网",
24151: "CNNIC",
38019: "中国移动", 139080: "中国移动", 9808: "中国移动", 24400: "中国移动", 134810: "中国移动", 24547: "中国移动",
56040: "中国移动", 56041: "中国移动", 56042: "中国移动", 56044: "中国移动", 132525: "中国移动", 56046: "中国移动",
56047: "中国移动", 56048: "中国移动", 59257: "中国移动", 24444: "中国移动",
24445: "中国移动", 137872: "中国移动", 9231: "中国移动", 58453: "中国移动",
4134: "中国电信", 4812: "中国电信", 23724: "中国电信", 136188: "中国电信", 137693: "中国电信", 17638: "中国电信",
140553: "中国电信", 4847: "中国电信", 140061: "中国电信", 136195: "中国电信", 17799: "中国电信", 139018: "中国电信",
134764: "中国电信", 4837: "中国联通", 4808: "中国联通", 134542: "中国联通", 134543: "中国联通",
59019: "金山云",
135377: "优刻云",
45062: "网易云",
37963: "阿里云", 45102: "阿里云国际",
45090: "腾讯云", 132203: "腾讯云国际",
55967: "百度云", 38365: "百度云",
58519: "华为云", 55990: "华为云", 136907: "华为云",
4609: "澳門電訊",
13335: "Cloudflare",
55960: "亚马逊云", 14618: "亚马逊云", 16509: "亚马逊云",
15169: "谷歌云", 396982: "谷歌云", 36492: "谷歌云",
}
def get_as_info(number):
r = asn_map.get(number)
if r:
return r
def get_des(d):
for i in lang:
if i in d['names']:
return d['names'][i]
return d['names']['en']
def get_country(d):
r = get_des(d)
if r in ["香港", "澳门", "台湾"]:
return "中国" + r
return r
def province_match(s):
arr = ['内蒙古', '黑龙江', '河北', '山西', '吉林', '辽宁', '江苏', '浙江', '安徽', '福建', '江西', '山东', '河南', '湖北', '湖南', '广东', '海南', '四川', '贵州', '云南', '陕西', '甘肃', '青海', '广西', '西藏', '宁夏', '新疆', '北京', '天津', '上海', '重庆']
for i in arr:
if i in s:
return i
return ''
def de_duplicate(regions):
regions = filter(bool, regions)
ret = []
[ret.append(i) for i in regions if i not in ret]
return ret
def get_addr(ip, mask):
network = ipaddress.ip_network(f"{ip}/{mask}", strict=False)
first_ip = network.network_address
return f"{first_ip}/{mask}"
def get_maxmind(ip: str):
ret = {"ip": ip}
asn_info = asn_reader.get(ip)
if asn_info:
as_ = {"number": asn_info["autonomous_system_number"], "name": asn_info["autonomous_system_organization"]}
info = get_as_info(as_["number"])
if info:
as_["info"] = info
ret["as"] = as_
city_info, prefix = city_reader.get_with_prefix_len(ip)
ret["addr"] = get_addr(ip, prefix)
if not city_info:
return ret
if "location" in city_info:
location = city_info["location"]
ret["location"] = {
"latitude": location.get("latitude"),
"longitude": location.get("longitude")
}
if "country" in city_info:
country_code = city_info["country"]["iso_code"]
country_name = get_country(city_info["country"])
ret["country"] = {"code": country_code, "name": country_name}
if "registered_country" in city_info:
registered_country_code = city_info["registered_country"]["iso_code"]
ret["registered_country"] = {"code": registered_country_code, "name": get_country(city_info["registered_country"])}
regions = [get_des(i) for i in city_info.get('subdivisions', [])]
if "city" in city_info:
c = get_des(city_info["city"])
if (not regions or c not in regions[-1]) and c not in country_name:
regions.append(c)
regions = de_duplicate(regions)
if regions:
ret["regions"] = regions
return ret
def get_cn(ip: str, info={}):
ret, prefix = cn_reader.get_with_prefix_len(ip)
if not ret:
return
info["addr"] = get_addr(ip, prefix)
regions = de_duplicate([ret["province"], ret["city"], ret["districts"]])
if regions:
info["regions"] = regions
info["regions_short"] = de_duplicate([province_match(ret["province"]), ret["city"].replace('市', ''), ret["districts"]])
if "as" not in info:
info["as"] = {}
info["as"]["info"] = ret['isp']
if ret['net']:
info["type"] = ret['net']
return ret
def get_ip_info(ip):
info = get_maxmind(ip)
if "country" in info and info["country"]["code"] == "CN" and ("registered_country" not in info or info["registered_country"]["code"] == "CN"):
get_cn(ip, info)
return info
def query():
while True:
try:
ip = input('IP: \t').strip()
info = get_ip_info(ip)
print(f"网段:\t{info['addr']}")
if "location" in info:
print(f"经纬度:\t{info['location']['latitude']}, {info['location']['longitude']}")
if "as" in info:
print(f"ISP:\t", end=' ')
if "info" in info["as"]:
print(info["as"]["info"], end=' ')
else:
print(info["as"]["name"], end=' ')
if "type" in info:
print(f"({info['type']})", end=' ')
print(f"ASN{info['as']['number']}", end=' ')
print(info['as']["name"])
if "registered_country" in info and ("country" not in info or info["country"]["code"] != info["registered_country"]["code"]):
print(f"注册地:\t{info['registered_country']['name']}")
if "country" in info:
print(f"使用地:\t{info['country']['name']}")
if "regions" in info:
print(f"位置: \t{' '.join(info['regions'])}")
except Exception as e:
print(e)
raise e
finally:
print("\n")
app = FastAPI()
@app.get("/")
async def api(request: Request, ip: str = None):
client_ip = request.headers.get("x-forwarded-for") or request.headers.get("x-real-ip") or request.client.host
query_ip = ip.strip() if ip else client_ip
result = get_ip_info(query_ip)
log_data = {
"时间": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"访问IP": client_ip,
"查询IP": query_ip,
"请求头": dict(request.headers),
"查询结果": result
}
logger.info(json.dumps(log_data, ensure_ascii=False))
return result
@app.get("/{ip}")
async def path_api(request: Request, ip: str):
client_ip = request.headers.get("x-forwarded-for") or request.headers.get("x-real-ip") or request.client.host
result = get_ip_info(ip)
log_data = {
"时间": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"访问IP": client_ip,
"查询IP": ip,
"请求头": dict(request.headers),
"查询结果": result
}
logger.info(json.dumps(log_data, ensure_ascii=False))
return result
if __name__ == '__main__':
query()
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080, server_header=False, proxy_headers=True)
4. requirements.txt
maxminddb
fastapi
uvicorn
5. update_and_restart.sh
#!/bin/sh
while true; do
date
echo "Updating GeoLite2-City.mmdb..."
curl -L -o "GeoLite2-City.mmdb" "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb" || { echo "Failed to update GeoLite2-City.mmdb"; continue; }
echo "Updating GeoLite2-ASN.mmdb..."
curl -L -o "GeoLite2-ASN.mmdb" "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-ASN.mmdb" || { echo "Failed to update GeoLite2-ASN.mmdb"; continue; }
echo "Updating GeoCN.mmdb..."
curl -L -o "GeoCN.mmdb" "http://github.com/ljxi/GeoCN/releases/download/Latest/GeoCN.mmdb" || { echo "Failed to update GeoCN.mmdb"; continue; }
echo "Attempting to restart uvicorn..."
pkill -f "uvicorn"
nohup uvicorn main:app --host 0.0.0.0 --port 8080 --no-server-header --proxy-headers &
sleep 5
if pgrep -f "uvicorn" > /dev/null; then
echo "uvicorn restarted successfully."
else
echo "Failed to restart uvicorn, retrying..."
continue
fi
sleep 86400
done
附上一个API状态监控页面
https://ipgeo-status.hf.space/status/page
欢迎大家提出建议和反馈!也可以在评论区分享你自己部署的API哦!