feat: init mirror-proxy project
Build and Push to GHCR / build-and-push (push) Has been cancelled

- 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:
Agent
2026-05-24 15:27:45 +08:00
commit 9223e35164
16 changed files with 2063 additions and 0 deletions
+274
View File
@@ -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)
}