135 lines
4.8 KiB
Python
135 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
RESTful API + 响应式 WebUI(手机优先)
|
||
自动把用户公网 IP 填到输入框
|
||
依赖:flask>=3.0.0 qqwry-py3>=1.2.1
|
||
"""
|
||
import os
|
||
from flask import Flask, jsonify, request
|
||
|
||
app = Flask(__name__)
|
||
|
||
# 1. 加载纯真库
|
||
from qqwry import QQwry
|
||
q = QQwry()
|
||
dat_path = os.path.join(os.path.dirname(__file__), "qqwry.dat")
|
||
q.load_file(dat_path)
|
||
|
||
# 2. API:查询 IP
|
||
@app.route("/ip/<ip>", methods=["GET"])
|
||
def query_ip(ip: str):
|
||
country, area = q.lookup(ip)
|
||
return jsonify(ip=ip, country=country or None, area=area or None)
|
||
|
||
# 3. API:健康检查
|
||
@app.route("/health", methods=["GET"])
|
||
def health():
|
||
return "ok"
|
||
|
||
# 4. 响应式 WebUI(手机优先)+ 自动获取客户端公网 IP
|
||
UI_HTML = """<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||
<title>纯真 IP 查询</title>
|
||
<style>
|
||
*{box-sizing:border-box}
|
||
html,body{margin:0;padding:0;height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#f2f4f7}
|
||
.container{max-width:600px;margin:0 auto;padding:16px}
|
||
.card{background:#fff;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,.08);padding:24px}
|
||
h1{font-size:22px;margin:0 0 20px;text-align:center;color:#303133}
|
||
.input-group{position:relative;margin-bottom:16px}
|
||
input{width:100%;padding:12px 44px 12px 12px;font-size:16px;border:1px solid #dcdfe6;border-radius:8px;outline:none;transition:border .2s}
|
||
input:focus{border-color:#409eff}
|
||
.btn-clear{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;font-size:14px;color:#909399;cursor:pointer;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s}
|
||
.btn-clear:hover{color:#409eff;background:#ecf5ff}
|
||
button{width:100%;padding:12px 0;font-size:17px;color:#fff;background:#409eff;border:none;border-radius:8px;cursor:pointer;transition:background .2s}
|
||
button:hover{background:#66b1ff}
|
||
.result{margin-top:20px;line-height:1.7;font-size:15px;color:#606266}
|
||
.error{color:#f56c6c}
|
||
footer{text-align:center;margin-top:16px;font-size:12px;color:#909399}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="card">
|
||
<h1>纯真 IP 查询</h1>
|
||
<div class="input-group">
|
||
<input id="ipIn" placeholder="正在获取本机 IP…" autocomplete="off">
|
||
<button class="btn-clear" onclick="ipIn.value=''" title="清空">✕</button>
|
||
</div>
|
||
<button onclick="query()">查 询</button>
|
||
<div id="res" class="result"></div>
|
||
</div>
|
||
<footer>数据提供:纯真 IP 库</footer>
|
||
</div>
|
||
|
||
<script>
|
||
async function query(){
|
||
const ip = ipIn.value.trim();
|
||
if(!ip){show('请输入 IP 地址','error'); return}
|
||
show('查询中…');
|
||
try{
|
||
const r = await fetch(`/ip/${ip}`);
|
||
const j = await r.json();
|
||
if(!r.ok) throw new Error(j.message||'网络异常');
|
||
show(`
|
||
<b>IP:</b>${j.ip}<br>
|
||
<b>国家/地区:</b>${j.country||'-'}<br>
|
||
<b>详细:</b>${j.area||'-'}`, 'success');
|
||
}catch(e){
|
||
show(e.message, 'error');
|
||
}
|
||
}
|
||
function show(msg, type=''){
|
||
res.innerHTML = type==='error' ? `<span class="error">${msg}</span>` : msg;
|
||
}
|
||
|
||
// 自动获取当前设备公网 IP 并填入输入框
|
||
(async function fillMyIP(){
|
||
const services = [
|
||
'https://1.1.1.1/cdn-cgi/trace',
|
||
'https://checkip.amazonaws.com',
|
||
'https://ifconfig.me/ip',
|
||
'https://api.ipify.org',
|
||
'https://icanhazip.com',
|
||
'https://ipinfo.io/ip'
|
||
];
|
||
const timeout = ms => new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms));
|
||
for (const svc of services) {
|
||
try {
|
||
const r = await Promise.race([fetch(svc), timeout(1500)]);
|
||
if (!r.ok) throw new Error('bad response');
|
||
const t = await r.text();
|
||
const ip = svc.includes('cdn-cgi/trace') ? (t.match(/^ip=(.+)$/m)?.[1] || '') : t.trim();
|
||
if (ip && /^[\d.]+$/.test(ip) || /^[\da-f:]+$/i.test(ip)) {
|
||
ipIn.value = ip;
|
||
ipIn.placeholder = '请输入 IP 地址';
|
||
return;
|
||
}
|
||
}catch{}
|
||
}
|
||
ipIn.placeholder = '请输入 IP 地址';
|
||
})();
|
||
|
||
// 回车快捷查询
|
||
ipIn.addEventListener('keyup', e => e.key==='Enter' && query());
|
||
</script>
|
||
</body>
|
||
</html>"""
|
||
|
||
@app.route("/ui")
|
||
def ui():
|
||
return UI_HTML
|
||
|
||
@app.route("/")
|
||
def index():
|
||
return """<script>location="/ui"</script>"""
|
||
|
||
# 5. 启动
|
||
if __name__ == "__main__":
|
||
app.run(host="0.0.0.0",
|
||
port=int(os.getenv("PORT", 5000)),
|
||
debug=False)
|