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

220 lines
6.0 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) {
html := `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>CHNRoute RSC 服务</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
h1 { color: #333; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
.link { margin: 10px 0; }
</style>
</head>
<body>
<h1>🇨🇳 CHNRoute RSC 转换服务</h1>
<p>自动从 <a href="https://github.com/mayaxcn/china-ip-list">mayaxcn/china-ip-list</a> 获取最新数据并转换为 RouterOS 格式</p>
<h2>📥 下载地址</h2>
<div class="link">
<strong>IPv4:</strong> <a href="/chnroute.rsc">/chnroute.rsc</a>
</div>
<div class="link">
<strong>IPv6:</strong> <a href="/chnroute_v6.rsc">/chnroute_v6.rsc</a>
</div>
<h2>🔧 RouterOS 使用方法</h2>
<pre>/tool fetch url="http://你的服务器IP:8080/chnroute.rsc" mode=http
/import chnroute.rsc
/ip firewall address-list print count-only where list=chnroute</pre>
<h2>♻️ 更新策略</h2>
<ul>
<li>缓存时间1 小时</li>
<li>上游更新:每小时</li>
<li>自动刷新缓存</li>
</ul>
<h2>📊 健康检查</h2>
<div class="link"><a href="/health">/health</a></div>
</body>
</html>`
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)
}
}