Files
chnrouter-rsc/main.go
2025-12-30 03:51:30 +08:00

672 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bufio"
"fmt"
"io"
"log"
"net/http"
"strings"
"sync"
"time"
)
const (
// 数据源 URL
urlChnrouteIPv4 = "https://raw.githubusercontent.com/mayaxcn/china-ip-list/master/chnroute.txt"
urlChnrouteIPv6 = "https://raw.githubusercontent.com/mayaxcn/china-ip-list/master/chnroute_v6.txt"
// 缓存时间1小时
cacheDuration = 1 * time.Hour
)
// 缓存结构
type Cache struct {
data string
timestamp time.Time
mu sync.RWMutex
}
var (
cacheIPv4 = &Cache{}
cacheIPv6 = &Cache{}
)
// 从 URL 获取数据
func fetchData(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("fetch error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read error: %w", err)
}
return string(body), nil
}
// 转换为 RouterOS RSC 格式
func convertToRSC(cidrList string, listName string, ipVersion string) string {
var sb strings.Builder
scanner := bufio.NewScanner(strings.NewReader(cidrList))
// 添加头部注释
sb.WriteString(fmt.Sprintf("# China IP List - RouterOS Script\n"))
sb.WriteString(fmt.Sprintf("# Generated: %s\n", time.Now().Format("2006-01-02 15:04:05")))
sb.WriteString(fmt.Sprintf("# Source: https://github.com/mayaxcn/china-ip-list\n\n"))
// 根据 IP 版本选择命令前缀
prefix := "/ip firewall address-list"
if ipVersion == "6" {
prefix = "/ipv6 firewall address-list"
}
count := 0
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
sb.WriteString(fmt.Sprintf("%s add list=%s address=%s\n", prefix, listName, line))
count++
}
sb.WriteString(fmt.Sprintf("\n# Total: %d entries\n", count))
return sb.String()
}
// 获取或更新缓存
func getOrUpdateCache(cache *Cache, url string, listName string, ipVersion string) (string, error) {
cache.mu.RLock()
if time.Since(cache.timestamp) < cacheDuration && cache.data != "" {
defer cache.mu.RUnlock()
return cache.data, nil
}
cache.mu.RUnlock()
// 需要更新缓存
cache.mu.Lock()
defer cache.mu.Unlock()
// 双重检查
if time.Since(cache.timestamp) < cacheDuration && cache.data != "" {
return cache.data, nil
}
log.Printf("Fetching data from %s...", url)
cidrData, err := fetchData(url)
if err != nil {
return "", err
}
rscData := convertToRSC(cidrData, listName, ipVersion)
cache.data = rscData
cache.timestamp = time.Now()
log.Printf("Cache updated: %d bytes", len(rscData))
return rscData, nil
}
// HTTP 处理器
func handleIPv4(w http.ResponseWriter, r *http.Request) {
data, err := getOrUpdateCache(cacheIPv4, urlChnrouteIPv4, "chnroute", "4")
if err != nil {
http.Error(w, fmt.Sprintf("Error: %v", err), http.StatusInternalServerError)
log.Printf("IPv4 error: %v", err)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=chnroute.rsc")
w.Write([]byte(data))
log.Printf("IPv4 request served: %s", r.RemoteAddr)
}
func handleIPv6(w http.ResponseWriter, r *http.Request) {
data, err := getOrUpdateCache(cacheIPv6, urlChnrouteIPv6, "chnroute6", "6")
if err != nil {
http.Error(w, fmt.Sprintf("Error: %v", err), http.StatusInternalServerError)
log.Printf("IPv6 error: %v", err)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=chnroute_v6.rsc")
w.Write([]byte(data))
log.Printf("IPv6 request served: %s", r.RemoteAddr)
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status":"ok","cache_age_ipv4":"%s","cache_age_ipv6":"%s"}`,
time.Since(cacheIPv4.timestamp).Round(time.Second),
time.Since(cacheIPv6.timestamp).Round(time.Second))
}
func handleRoot(w http.ResponseWriter, r *http.Request) {
// 获取缓存状态
cacheIPv4.mu.RLock()
cacheAgeIPv4 := time.Since(cacheIPv4.timestamp).Round(time.Second)
cacheIPv4.mu.RUnlock()
cacheIPv6.mu.RLock()
cacheAgeIPv6 := time.Since(cacheIPv6.timestamp).Round(time.Second)
cacheIPv6.mu.RUnlock()
// 判断状态
statusIPv4 := "🟢"
if cacheAgeIPv4 > cacheDuration {
statusIPv4 = "🔴"
} else if cacheAgeIPv4 > cacheDuration/2 {
statusIPv4 = "🟡"
}
statusIPv6 := "🟢"
if cacheAgeIPv6 > cacheDuration {
statusIPv6 = "🔴"
} else if cacheAgeIPv6 > cacheDuration/2 {
statusIPv6 = "🟡"
}
// 自动检测访问协议和地址
protocol := "http"
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
protocol = "https"
}
host := r.Host
if host == "" {
host = "localhost:8080"
}
baseURL := fmt.Sprintf("%s://%s", protocol, host)
fetchMode := protocol
// 根据协议生成不同的提示和脚本
var protocolNote string
var protocolBadge string
if protocol == "https" {
protocolNote = "您正在使用 <strong>HTTPS 安全连接</strong>,适合外网环境"
protocolBadge = `<span class="badge badge-success">🔒 HTTPS</span>`
} else {
protocolNote = "您正在使用 <strong>HTTP 连接</strong>,适合内网环境"
protocolBadge = `<span class="badge badge-info">🌐 HTTP</span>`
}
html := fmt.Sprintf(`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CHNRoute RSC 服务</title>
<style>
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-code: #f4f4f4;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #e1e4e8;
--link-color: #0366d6;
--shadow: rgba(0, 0, 0, 0.1);
--accent-color: #667eea;
--accent-secondary: #764ba2;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-code: #1f2937;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--border-color: #30363d;
--link-color: #58a6ff;
--shadow: rgba(0, 0, 0, 0.4);
--accent-color: #818cf8;
--accent-secondary: #a78bfa;
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
padding: 20px;
transition: background 0.3s, color 0.3s;
}
.container {
max-width: 900px;
margin: 0 auto;
}
header {
text-align: center;
padding: 40px 0;
border-bottom: 2px solid var(--border-color);
margin-bottom: 40px;
}
h1 {
font-size: 2.5em;
margin-bottom: 10px;
position: relative;
color: #667eea;
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
h1 {
background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
@media (prefers-color-scheme: dark) {
h1 {
color: #818cf8;
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
h1 {
background: linear-gradient(135deg, #818cf8 0%%, #a78bfa 100%%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1em;
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 4px 6px var(--shadow);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px var(--shadow);
}
h2 {
font-size: 1.8em;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
color: var(--text-primary);
}
h3 {
color: var(--text-primary);
margin-bottom: 15px;
font-size: 1.2em;
}
.download-link {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: var(--bg-code);
border-radius: 8px;
margin-bottom: 15px;
text-decoration: none;
color: var(--text-primary);
border: 2px solid var(--border-color);
transition: all 0.3s;
}
.download-link:hover {
border-color: var(--link-color);
transform: translateX(5px);
}
.download-link strong {
font-size: 1.1em;
}
.download-link a {
color: var(--link-color);
text-decoration: none;
font-family: 'Courier New', monospace;
font-weight: bold;
}
.download-link a:hover {
text-decoration: underline;
}
pre {
background: var(--bg-code);
padding: 20px;
border-radius: 8px;
overflow-x: auto;
border-left: 4px solid var(--link-color);
font-family: 'Courier New', Consolas, monospace;
font-size: 0.95em;
line-height: 1.5;
color: var(--text-primary);
}
code {
background: var(--bg-code);
padding: 3px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: var(--text-primary);
}
ul {
list-style: none;
padding-left: 0;
}
ul li {
padding: 10px 0;
padding-left: 30px;
position: relative;
}
ul li:before {
content: "✓";
position: absolute;
left: 0;
color: #10b981;
font-weight: bold;
font-size: 1.2em;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.status-item {
background: var(--bg-code);
padding: 20px;
border-radius: 8px;
text-align: center;
border: 2px solid var(--border-color);
}
.status-icon {
font-size: 3em;
margin-bottom: 10px;
}
.status-label {
font-size: 1.1em;
font-weight: bold;
margin-bottom: 5px;
color: var(--text-primary);
}
.status-time {
color: var(--text-secondary);
font-size: 0.95em;
}
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: bold;
margin-left: 10px;
}
.badge-success {
background: #10b981;
color: white;
}
.badge-info {
background: #3b82f6;
color: white;
}
.protocol-note {
background: var(--bg-code);
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid var(--link-color);
}
.copy-btn {
position: absolute;
top: 10px;
right: 10px;
padding: 6px 12px;
background: var(--link-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
transition: all 0.2s;
}
.copy-btn:hover {
opacity: 0.8;
}
.code-container {
position: relative;
}
footer {
text-align: center;
padding: 30px 0;
color: var(--text-secondary);
border-top: 2px solid var(--border-color);
margin-top: 50px;
}
footer a {
color: var(--link-color);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
h1 { font-size: 2em; }
.card { padding: 20px; }
.download-link { flex-direction: column; align-items: flex-start; gap: 10px; }
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🇨🇳 CHNRoute RSC 服务</h1>
<p class="subtitle">自动转换中国 IP 列表为 RouterOS 可导入格式</p>
</header>
<!-- 下载区域 -->
<div class="card">
<h2>📥 下载地址</h2>
<div class="download-link">
<strong>IPv4 路由列表</strong>
<a href="%s/chnroute.rsc" download>%s/chnroute.rsc</a>
</div>
<div class="download-link">
<strong>IPv6 路由列表</strong>
<a href="%s/chnroute_v6.rsc" download>%s/chnroute_v6.rsc</a>
</div>
</div>
<!-- 使用方法 -->
<div class="card">
<h2>🔧 RouterOS 使用方法%s</h2>
<div class="protocol-note">
%s
</div>
<h3>一键导入脚本</h3>
<div class="code-container">
<button class="copy-btn" onclick="copyCode('code1')">📋 复制</button>
<pre id="code1"># SSH 登录 RouterOS 后执行
/tool fetch url="%s/chnroute.rsc" mode=%s
/import chnroute.rsc
# 验证导入
/ip firewall address-list print count-only where list=chnroute</pre>
</div>
<h3 style="margin-top: 30px;">自动更新脚本(推荐)</h3>
<div class="code-container">
<button class="copy-btn" onclick="copyCode('code2')">📋 复制</button>
<pre id="code2"># 创建更新脚本
/system script add name=update-chnroute source={
:log info "开始更新 CHNRoute"
/file remove [find name="chnroute.rsc"]
/tool fetch url="%s/chnroute.rsc" mode=%s
:delay 3s
/ip firewall address-list remove [find list=chnroute]
/import chnroute.rsc
:local count [/ip firewall address-list print count-only where list=chnroute]
:log info ("CHNRoute 更新完成,共 " . $count . " 条")
}
# 设置每周自动执行每周日凌晨3点
/system scheduler add name=auto-update-chnroute \\
interval=7d \\
on-event="/system script run update-chnroute" \\
start-time=03:00:00</pre>
</div>
<h3 style="margin-top: 30px;">IPv6 导入脚本</h3>
<div class="code-container">
<button class="copy-btn" onclick="copyCode('code3')">📋 复制</button>
<pre id="code3"># IPv6 路由列表导入
/tool fetch url="%s/chnroute_v6.rsc" mode=%s
/import chnroute_v6.rsc
# 验证导入
/ipv6 firewall address-list print count-only where list=chnroute6</pre>
</div>
</div>
<!-- 服务状态 -->
<div class="card">
<h2>📊 服务状态<span class="badge badge-success">运行中</span></h2>
<div class="status-grid">
<div class="status-item">
<div class="status-icon">%s</div>
<div class="status-label">IPv4 缓存</div>
<div class="status-time">已缓存 %v</div>
</div>
<div class="status-item">
<div class="status-icon">%s</div>
<div class="status-label">IPv6 缓存</div>
<div class="status-time">已缓存 %v</div>
</div>
</div>
</div>
<!-- 更新策略 -->
<div class="card">
<h2>♻️ 更新策略</h2>
<ul>
<li><strong>缓存时间:</strong>1 小时(访问时自动刷新过期缓存)</li>
<li><strong>上游数据:</strong>每小时自动更新(来自 APNIC 官方)</li>
<li><strong>数据源:</strong><a href="https://github.com/mayaxcn/china-ip-list" target="_blank">mayaxcn/china-ip-list</a></li>
<li><strong>IP 条目:</strong>IPv4 约 8700+IPv6 约 3500+</li>
</ul>
</div>
<footer>
<p>数据来源:<a href="https://github.com/mayaxcn/china-ip-list" target="_blank">mayaxcn/china-ip-list</a></p>
<p style="margin-top: 10px;">基于 Go 构建 | 支持自动缓存 | 支持 Docker 部署</p>
</footer>
</div>
<script>
function copyCode(elementId) {
const codeElement = document.getElementById(elementId);
const text = codeElement.textContent;
navigator.clipboard.writeText(text).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = '✅ 已复制';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}).catch(() => {
alert('复制失败,请手动复制');
});
}
</script>
</body>
</html>`,
baseURL, baseURL, // 1-2: 下载地址 IPv4 (href + 显示文本)
baseURL, baseURL, // 3-4: 下载地址 IPv6 (href + 显示文本)
protocolBadge, // 5: 协议徽章
protocolNote, // 6: 协议提示
baseURL, fetchMode, // 7-8: 一键导入 (url + mode)
baseURL, fetchMode, // 9-10: 自动更新 (url + mode)
baseURL, fetchMode, // 11-12: IPv6 导入 (url + mode)
statusIPv4, cacheAgeIPv4, // 13-14: IPv4 状态图标 + 缓存时间
statusIPv6, cacheAgeIPv6) // 15-16: IPv6 状态图标 + 缓存时间
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
func main() {
// 启动时预热缓存
go func() {
log.Println("Warming up cache...")
getOrUpdateCache(cacheIPv4, urlChnrouteIPv4, "chnroute", "4")
getOrUpdateCache(cacheIPv6, urlChnrouteIPv6, "chnroute6", "6")
log.Println("Cache warmed up")
}()
// 路由
http.HandleFunc("/", handleRoot)
http.HandleFunc("/chnroute.rsc", handleIPv4)
http.HandleFunc("/chnroute_v6.rsc", handleIPv6)
http.HandleFunc("/health", handleHealth)
port := ":8080"
log.Printf("🚀 Server starting on http://0.0.0.0%s", port)
if err := http.ListenAndServe(port, nil); err != nil {
log.Fatal(err)
}
}