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