- Docker Hub / GHCR / GitHub reverse proxy - Web admin panel with link management - Dynamic admin path, user and password config - Rate limiting per link (dual/single auth mode) - Docker and docker-compose deployment support - GitHub Actions workflow for auto-publish to GHCR
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"mirror-proxy/internal/auth"
|
||||
"mirror-proxy/internal/config"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewHandler(cfg *config.Config) *Handler {
|
||||
return &Handler{cfg: cfg}
|
||||
}
|
||||
|
||||
// GetLinks 获取所有链接
|
||||
func (h *Handler) GetLinks(w http.ResponseWriter, r *http.Request) {
|
||||
links := h.cfg.ListLinks()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(links)
|
||||
}
|
||||
|
||||
// CreateLink 创建新链接
|
||||
func (h *Handler) CreateLink(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
RateLimit int `json:"rate_limit"`
|
||||
AuthMode string `json:"auth_mode"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Type == "" {
|
||||
req.Type = "docker"
|
||||
}
|
||||
if req.RateLimit <= 0 {
|
||||
req.RateLimit = 100
|
||||
}
|
||||
if req.AuthMode == "" {
|
||||
req.AuthMode = "dual"
|
||||
}
|
||||
|
||||
link := &config.Link{
|
||||
ID: generateID(),
|
||||
Name: req.Name,
|
||||
Token: auth.GenerateToken(),
|
||||
Type: req.Type,
|
||||
AuthMode: req.AuthMode,
|
||||
Enabled: true,
|
||||
RateLimit: req.RateLimit,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
h.cfg.SetLink(link)
|
||||
if err := h.cfg.Save("config.json"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(link)
|
||||
}
|
||||
|
||||
// UpdateLink 更新链接
|
||||
func (h *Handler) UpdateLink(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Path[len("/api/links/"):]
|
||||
|
||||
link, ok := h.cfg.GetLink(id)
|
||||
if !ok {
|
||||
http.Error(w, "Link not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
RateLimit int `json:"rate_limit"`
|
||||
AuthMode string `json:"auth_mode"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
link.Name = req.Name
|
||||
}
|
||||
if req.Type != "" {
|
||||
link.Type = req.Type
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
link.Enabled = *req.Enabled
|
||||
}
|
||||
if req.RateLimit > 0 {
|
||||
link.RateLimit = req.RateLimit
|
||||
}
|
||||
if req.AuthMode != "" {
|
||||
link.AuthMode = req.AuthMode
|
||||
}
|
||||
|
||||
h.cfg.SetLink(link)
|
||||
if err := h.cfg.Save("config.json"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(link)
|
||||
}
|
||||
|
||||
// DeleteLink 删除链接
|
||||
func (h *Handler) DeleteLink(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Path[len("/api/links/"):]
|
||||
|
||||
_, ok := h.cfg.GetLink(id)
|
||||
if !ok {
|
||||
http.Error(w, "Link not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
h.cfg.DeleteLink(id)
|
||||
if err := h.cfg.Save("config.json"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// RegenerateToken 重新生成token
|
||||
func (h *Handler) RegenerateToken(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Path[len("/api/links/"):len(r.URL.Path)-len("/token")]
|
||||
|
||||
link, ok := h.cfg.GetLink(id)
|
||||
if !ok {
|
||||
http.Error(w, "Link not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
link.Token = auth.GenerateToken()
|
||||
h.cfg.SetLink(link)
|
||||
if err := h.cfg.Save("config.json"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"token": link.Token})
|
||||
}
|
||||
|
||||
// GetStats 获取统计信息
|
||||
func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) {
|
||||
links := h.cfg.ListLinks()
|
||||
stats := make(map[string]interface{})
|
||||
stats["total_links"] = len(links)
|
||||
stats["links"] = links
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(stats)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (h *Handler) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"listen_addr": h.cfg.ListenAddr,
|
||||
"admin_path": h.cfg.AdminPath,
|
||||
"admin_user": h.cfg.AdminUser,
|
||||
"docker_enabled": true,
|
||||
"ghcr_enabled": true,
|
||||
"github_enabled": true,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateConfig 更新系统配置
|
||||
func (h *Handler) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
ListenAddr string `json:"listen_addr"`
|
||||
AdminPath string `json:"admin_path"`
|
||||
AdminUser string `json:"admin_user"`
|
||||
AdminPass string `json:"admin_pass"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
restartRequired := false
|
||||
|
||||
if req.ListenAddr != "" {
|
||||
h.cfg.ListenAddr = req.ListenAddr
|
||||
restartRequired = true
|
||||
}
|
||||
if req.AdminPath != "" {
|
||||
if !strings.HasPrefix(req.AdminPath, "/") {
|
||||
req.AdminPath = "/" + req.AdminPath
|
||||
}
|
||||
if h.cfg.AdminPath != req.AdminPath {
|
||||
h.cfg.AdminPath = req.AdminPath
|
||||
restartRequired = true
|
||||
}
|
||||
}
|
||||
if req.AdminUser != "" {
|
||||
h.cfg.AdminUser = req.AdminUser
|
||||
}
|
||||
if req.AdminPass != "" {
|
||||
h.cfg.AdminPass = req.AdminPass
|
||||
}
|
||||
|
||||
if err := h.cfg.Save("config.json"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"listen_addr": h.cfg.ListenAddr,
|
||||
"admin_path": h.cfg.AdminPath,
|
||||
"admin_user": h.cfg.AdminUser,
|
||||
"restart_required": restartRequired,
|
||||
})
|
||||
}
|
||||
|
||||
// LinksHandler 路由分发
|
||||
func (h *Handler) LinksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if r.URL.Path == "/api/links" {
|
||||
h.GetLinks(w, r)
|
||||
return
|
||||
}
|
||||
case http.MethodPost:
|
||||
if r.URL.Path == "/api/links" {
|
||||
h.CreateLink(w, r)
|
||||
return
|
||||
}
|
||||
case http.MethodPut:
|
||||
if len(r.URL.Path) > len("/api/links/") {
|
||||
h.UpdateLink(w, r)
|
||||
return
|
||||
}
|
||||
case http.MethodDelete:
|
||||
if len(r.URL.Path) > len("/api/links/") {
|
||||
h.DeleteLink(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
func generateID() string {
|
||||
// 生成8位随机ID
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
b := make([]byte, 8)
|
||||
for i := range b {
|
||||
idx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||||
b[i] = charset[idx.Int64()]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"mirror-proxy/internal/config"
|
||||
)
|
||||
|
||||
type RateLimiter struct {
|
||||
visitors map[string]*visitor
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type visitor struct {
|
||||
count int
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
func NewRateLimiter() *RateLimiter {
|
||||
rl := &RateLimiter{visitors: make(map[string]*visitor)}
|
||||
go rl.cleanup()
|
||||
return rl
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) cleanup() {
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
for range ticker.C {
|
||||
rl.mu.Lock()
|
||||
for ip, v := range rl.visitors {
|
||||
if time.Since(v.lastSeen) > time.Minute {
|
||||
delete(rl.visitors, ip)
|
||||
}
|
||||
}
|
||||
rl.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) Allow(ip string, limit int) bool {
|
||||
if limit <= 0 {
|
||||
return true
|
||||
}
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
v, exists := rl.visitors[ip]
|
||||
if !exists {
|
||||
rl.visitors[ip] = &visitor{count: 1, lastSeen: time.Now()}
|
||||
return true
|
||||
}
|
||||
|
||||
if time.Since(v.lastSeen) > time.Minute {
|
||||
v.count = 1
|
||||
v.lastSeen = time.Now()
|
||||
return true
|
||||
}
|
||||
|
||||
if v.count >= limit {
|
||||
return false
|
||||
}
|
||||
|
||||
v.count++
|
||||
v.lastSeen = time.Now()
|
||||
return true
|
||||
}
|
||||
|
||||
func GenerateToken() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func getAuthMode(link *config.Link) string {
|
||||
if link.AuthMode == "" {
|
||||
return "dual"
|
||||
}
|
||||
return link.AuthMode
|
||||
}
|
||||
|
||||
func ValidateLinkToken(linkID, token string) (*config.Link, bool) {
|
||||
cfg := config.Get()
|
||||
link, ok := cfg.GetLink(linkID)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if !link.Enabled {
|
||||
return nil, false
|
||||
}
|
||||
if getAuthMode(link) != "dual" {
|
||||
return nil, false
|
||||
}
|
||||
if link.Token != token {
|
||||
return nil, false
|
||||
}
|
||||
return link, true
|
||||
}
|
||||
|
||||
func ValidateLinkTokenSingle(token string) (*config.Link, bool) {
|
||||
cfg := config.Get()
|
||||
link, ok := cfg.GetLinkByToken(token)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if !link.Enabled {
|
||||
return nil, false
|
||||
}
|
||||
if getAuthMode(link) != "single" {
|
||||
return nil, false
|
||||
}
|
||||
return link, true
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
|
||||
const LinkContextKey contextKey = "link"
|
||||
|
||||
func ProxyAuthMiddleware(rl *RateLimiter) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/")
|
||||
parts := strings.SplitN(path, "/", 3)
|
||||
if len(parts) < 1 {
|
||||
http.Error(w, "Unauthorized: missing token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var link *config.Link
|
||||
var ok bool
|
||||
var newPath string
|
||||
|
||||
// 优先尝试 dual 模式 (/{linkID}/{token}/...)
|
||||
if len(parts) >= 2 {
|
||||
link, ok = ValidateLinkToken(parts[0], parts[1])
|
||||
if ok {
|
||||
if len(parts) == 2 {
|
||||
newPath = "/"
|
||||
} else {
|
||||
newPath = "/" + parts[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试 single 模式 (/{token}/...)
|
||||
if !ok {
|
||||
link, ok = ValidateLinkTokenSingle(parts[0])
|
||||
if ok {
|
||||
if len(parts) == 1 {
|
||||
newPath = "/"
|
||||
} else {
|
||||
newPath = "/" + parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
http.Error(w, "Unauthorized: invalid link or token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := r.RemoteAddr
|
||||
if xf := r.Header.Get("X-Forwarded-For"); xf != "" {
|
||||
clientIP = strings.Split(xf, ",")[0]
|
||||
}
|
||||
|
||||
if !rl.Allow(clientIP, link.RateLimit) {
|
||||
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
r.URL.Path = newPath
|
||||
|
||||
// 存储link到context
|
||||
ctx := context.WithValue(r.Context(), LinkContextKey, link)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ListenAddr string `json:"listen_addr"`
|
||||
AdminPath string `json:"admin_path"`
|
||||
AdminUser string `json:"admin_user"`
|
||||
AdminPass string `json:"admin_pass"`
|
||||
DockerHubHost string `json:"docker_hub_host"`
|
||||
GHCRCacheEnabled bool `json:"ghcr_cache_enabled"`
|
||||
CacheDir string `json:"cache_dir"`
|
||||
MaxCacheSize int64 `json:"max_cache_size"`
|
||||
Links map[string]*Link `json:"links"`
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
Type string `json:"type"` // docker, ghcr, github
|
||||
AuthMode string `json:"auth_mode"` // single, dual
|
||||
Enabled bool `json:"enabled"`
|
||||
RateLimit int `json:"rate_limit"` // requests per minute
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
AccessCount int64 `json:"access_count"`
|
||||
LastAccess int64 `json:"last_access"`
|
||||
}
|
||||
|
||||
var (
|
||||
cfg *Config
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
cfg = defaultConfig()
|
||||
return cfg, cfg.Save(path)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
cfg = defaultConfig()
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.Links == nil {
|
||||
cfg.Links = make(map[string]*Link)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
ListenAddr: ":8080",
|
||||
AdminPath: "/admin",
|
||||
AdminUser: "admin",
|
||||
AdminPass: "admin123",
|
||||
DockerHubHost: "registry-1.docker.io",
|
||||
GHCRCacheEnabled: true,
|
||||
CacheDir: "./cache",
|
||||
MaxCacheSize: 10 * 1024 * 1024 * 1024, // 10GB
|
||||
Links: make(map[string]*Link),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) Save(path string) error {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
func (c *Config) GetLink(id string) (*Link, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
link, ok := c.Links[id]
|
||||
return link, ok
|
||||
}
|
||||
|
||||
func (c *Config) GetLinkByToken(token string) (*Link, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
for _, link := range c.Links {
|
||||
if link.Token == token {
|
||||
return link, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *Config) SetLink(link *Link) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.Links[link.ID] = link
|
||||
}
|
||||
|
||||
func (c *Config) DeleteLink(id string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.Links, id)
|
||||
}
|
||||
|
||||
func (c *Config) ListLinks() []*Link {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
links := make([]*Link, 0, len(c.Links))
|
||||
for _, l := range c.Links {
|
||||
links = append(links, l)
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
func Get() *Config {
|
||||
return cfg
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CORS 跨域中间件
|
||||
func CORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, Origin, X-Requested-With")
|
||||
w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type, X-Docker-Token")
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Logger 日志中间件
|
||||
func Logger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
||||
next.ServeHTTP(wrapped, r)
|
||||
// 简单日志输出
|
||||
_ = start
|
||||
})
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.statusCode = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// BasicAuth 基础认证中间件
|
||||
func BasicAuth(username, password string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != username || pass != password {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Admin Panel"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// StripPrefix 安全地移除路径前缀
|
||||
func StripPrefix(prefix string, h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := strings.TrimPrefix(r.URL.Path, prefix)
|
||||
if p == r.URL.Path {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
r.URL.Path = p
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type ProxyRequest struct {
|
||||
ID string `json:"id"`
|
||||
LinkID string `json:"link_id"`
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Status int `json:"status"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
Duration int64 `json:"duration_ms"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
TotalRequests int64 `json:"total_requests"`
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
LinkStats map[string]*LinkStat `json:"link_stats"`
|
||||
}
|
||||
|
||||
type LinkStat struct {
|
||||
LinkID string `json:"link_id"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
BytesTransferred int64 `json:"bytes_transferred"`
|
||||
LastAccess int64 `json:"last_access"`
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewDockerHubProxy 创建 Docker Hub 反向代理
|
||||
func NewDockerHubProxy() http.Handler {
|
||||
target, _ := url.Parse("https://registry-1.docker.io")
|
||||
|
||||
p := httputil.NewSingleHostReverseProxy(target)
|
||||
p.Director = func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.Host = target.Host
|
||||
req.Header.Set("Host", target.Host)
|
||||
if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", "Docker-Client/24.0.0")
|
||||
}
|
||||
req.Header.Del("X-Forwarded-For")
|
||||
}
|
||||
|
||||
p.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
fmt.Fprintf(w, `{"errors":[{"code":"PROXY_ERROR","message":"%s"}]}`, err.Error())
|
||||
}
|
||||
|
||||
p.Transport = &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewGHCRProxy 创建 GHCR 反向代理
|
||||
func NewGHCRProxy() http.Handler {
|
||||
target, _ := url.Parse("https://ghcr.io")
|
||||
|
||||
p := httputil.NewSingleHostReverseProxy(target)
|
||||
p.Director = func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.Host = target.Host
|
||||
req.Header.Set("Host", target.Host)
|
||||
if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", "Docker-Client/24.0.0")
|
||||
}
|
||||
req.Header.Del("X-Forwarded-For")
|
||||
}
|
||||
|
||||
p.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
fmt.Fprintf(w, `{"errors":[{"code":"PROXY_ERROR","message":"%s"}]}`, err.Error())
|
||||
}
|
||||
|
||||
p.Transport = &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewGitHubProxy 创建 GitHub 主站反向代理
|
||||
func NewGitHubProxy() http.Handler {
|
||||
target, _ := url.Parse("https://github.com")
|
||||
|
||||
p := httputil.NewSingleHostReverseProxy(target)
|
||||
p.Director = func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.Host = target.Host
|
||||
req.Header.Set("Host", target.Host)
|
||||
if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", "MirrorProxy/1.0")
|
||||
}
|
||||
req.Header.Del("X-Forwarded-For")
|
||||
}
|
||||
|
||||
p.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
fmt.Fprintf(w, "GitHub proxy error: %s", err.Error())
|
||||
}
|
||||
|
||||
p.Transport = &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// NewGitHubRawProxy 创建 GitHub Raw 反向代理
|
||||
func NewGitHubRawProxy() http.Handler {
|
||||
target, _ := url.Parse("https://raw.githubusercontent.com")
|
||||
|
||||
p := httputil.NewSingleHostReverseProxy(target)
|
||||
p.Director = func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.Host = target.Host
|
||||
req.Header.Set("Host", target.Host)
|
||||
if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", "MirrorProxy/1.0")
|
||||
}
|
||||
req.Header.Del("X-Forwarded-For")
|
||||
}
|
||||
|
||||
p.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
fmt.Fprintf(w, "GitHub raw proxy error: %s", err.Error())
|
||||
}
|
||||
|
||||
p.Transport = &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// NewGitHubAPIProxy 创建 GitHub API 反向代理
|
||||
func NewGitHubAPIProxy() http.Handler {
|
||||
target, _ := url.Parse("https://api.github.com")
|
||||
|
||||
p := httputil.NewSingleHostReverseProxy(target)
|
||||
p.Director = func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.Host = target.Host
|
||||
req.Header.Set("Host", target.Host)
|
||||
if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", "MirrorProxy/1.0")
|
||||
}
|
||||
req.Header.Del("X-Forwarded-For")
|
||||
}
|
||||
|
||||
p.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
fmt.Fprintf(w, `{"message":"GitHub API proxy error: %s"}`, err.Error())
|
||||
}
|
||||
|
||||
p.Transport = &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
Reference in New Issue
Block a user