添加 main.go
This commit is contained in:
219
main.go
Normal file
219
main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user