Initial commit
This commit is contained in:
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 1. 安装系统编译依赖(qqwry-py3 需要编译)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libffi-dev \
|
||||||
|
libssl-dev \
|
||||||
|
python3-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 2. 复制并安装 Python 依赖
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# 3. 复制源码 & 纯真库
|
||||||
|
COPY qqwry.dat app.py ./
|
||||||
|
|
||||||
|
# 4. 默认端口 5000,可 docker run -e PORT=8080 覆盖
|
||||||
|
ENV PORT=5000
|
||||||
|
EXPOSE ${PORT}
|
||||||
|
|
||||||
|
# 5. 直接 python 启动
|
||||||
|
CMD ["python", "app.py"]
|
||||||
134
app.py
Normal file
134
app.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
#!/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)
|
||||||
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: czip-api
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
environment:
|
||||||
|
- PORT=5000
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
flask==3.0.0
|
||||||
|
qqwry-py3==1.2.1
|
||||||
Reference in New Issue
Block a user