添加 main.go

This commit is contained in:
XOF
2025-12-30 02:51:16 +08:00
commit d738347227

219
main.go Normal file
View File

@@ -0,0 +1,219 @@
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)
}
}