commit 5ef7757fe472f7dfe9c98717249f392f38ab5a62 Author: XOF Date: Sat Dec 27 17:46:13 2025 +0800 添加 main.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..8e93492 --- /dev/null +++ b/main.go @@ -0,0 +1,544 @@ +// main.go +package main + +import ( + "context" + "flag" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" + + "github.com/sirupsen/logrus" +) + +// Version 用于嵌入构建版本号 +var Version = "dev" + +// Config 定义配置结构体 +type Config struct { + ListenAddress string + Port int + LogLevel string + DisguiseURL string +} + +var config Config + +var client = &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + for key, val := range via[0].Header { + if _, ok := req.Header[key]; !ok { + req.Header[key] = val + } + } + return nil + }, + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DisableKeepAlives: false, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, +} + +type CustomFormatter struct { + logrus.TextFormatter +} + +func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) { + timestamp := entry.Time.Format("2006-01-02 15:04:05.000") + + var levelColor string + switch entry.Level { + case logrus.DebugLevel: + levelColor = "\033[36m" + case logrus.InfoLevel: + levelColor = "\033[32m" + case logrus.WarnLevel: + levelColor = "\033[33m" + case logrus.ErrorLevel: + levelColor = "\033[31m" + case logrus.FatalLevel, logrus.PanicLevel: + levelColor = "\033[35m" + } + + resetColor := "\033[0m" + logMessage := fmt.Sprintf("%s %s[%s]%s %s\n", + timestamp, + levelColor, + strings.ToUpper(entry.Level.String()), + resetColor, + entry.Message) + + return []byte(logMessage), nil +} + +func init() { + logrus.SetFormatter(&CustomFormatter{ + TextFormatter: logrus.TextFormatter{ + DisableColors: false, + FullTimestamp: true, + TimestampFormat: "2006-01-02 15:04:05.000", + }, + }) +} + +func preprocessArgs() { + alias := map[string]string{ + "--listen": "-l", + "--port": "-p", + "--log-level": "-ll", + "--disguise": "-w", + } + + newArgs := make([]string, 0, len(os.Args)) + newArgs = append(newArgs, os.Args[0]) + + for _, arg := range os.Args[1:] { + if strings.HasPrefix(arg, "--") && strings.Contains(arg, "=") { + parts := strings.SplitN(arg, "=", 2) + if short, ok := alias[parts[0]]; ok { + arg = short + "=" + parts[1] + } + } else if short, ok := alias[arg]; ok { + arg = short + } + newArgs = append(newArgs, arg) + } + + if len(newArgs) > 0 { + os.Args = newArgs + } else { + logrus.Warn("命令行参数为空,使用原始参数") + } +} + +func usage() { + const helpText = `HubP - Docker Hub 代理服务器 + +参数说明: + -l, --listen 监听地址 (默认: 0.0.0.0) + -p, --port 监听端口 (默认: 18184) + -ll, --log-level 日志级别: debug/info/warn/error (默认: info) + -w, --disguise 伪装网站 URL (默认: onlinealarmkur.com) + +示例: + ./HubP -l 0.0.0.0 -p 18184 -ll debug -w www.bing.com + ./HubP --listen=0.0.0.0 --port=18184 --log-level=debug --disguise=www.bing.com` + + fmt.Fprintf(os.Stderr, "%s\n", helpText) +} + +func validateConfig() error { + if config.Port < 1 || config.Port > 65535 { + return fmt.Errorf("无效的端口号: %d", config.Port) + } + if config.DisguiseURL == "" { + return fmt.Errorf("伪装网站 URL 不能为空") + } + return nil +} + +func main() { + preprocessArgs() + flag.Usage = usage + + defaultListenAddress := getEnv("HUBP_LISTEN", "0.0.0.0") + defaultPort := getEnvAsInt("HUBP_PORT", 18184) + defaultLogLevel := getEnv("HUBP_LOG_LEVEL", "debug") + defaultDisguiseURL := getEnv("HUBP_DISGUISE", "onlinealarmkur.com") + + flag.StringVar(&config.ListenAddress, "l", defaultListenAddress, "监听地址") + flag.IntVar(&config.Port, "p", defaultPort, "监听端口") + flag.StringVar(&config.LogLevel, "ll", defaultLogLevel, "日志级别") + flag.StringVar(&config.DisguiseURL, "w", defaultDisguiseURL, "伪装网站 URL") + + if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { + logrus.Fatal("解析命令行参数失败:", err) + } + + level, err := logrus.ParseLevel(config.LogLevel) + if err != nil { + logrus.Warnf("无效的日志级别 '%s',使用默认级别 'info'", config.LogLevel) + level = logrus.InfoLevel + } + logrus.SetLevel(level) + + if err := validateConfig(); err != nil { + logrus.Fatal("配置验证失败: ", err) + } + + printStartupInfo() + + addr := fmt.Sprintf("%s:%d", config.ListenAddress, config.Port) + http.HandleFunc("/", handleRequest) + + server := &http.Server{ + Addr: addr, + Handler: http.DefaultServeMux, + } + + go func() { + logrus.Info("服务启动成功") + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logrus.Fatal("服务启动失败: ", err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + logrus.Info("正在关闭服务器...") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + logrus.Error("服务器强制关闭: ", err) + } + logrus.Info("服务器已关闭") +} + +func printStartupInfo() { + const blue = "\033[34m" + const green = "\033[32m" + const reset = "\033[0m" + + fmt.Println(blue + "\n╔════════════════════════════════════════════════════════════╗" + reset) + fmt.Println(blue + "║" + green + " HubP Docker Hub 代理服务器 " + blue + "║" + reset) + fmt.Printf(blue+"║"+green+" 版本: %-33s"+blue+"║\n"+reset, Version) + fmt.Println(blue + "╠════════════════════════════════════════════════════════════╣" + reset) + fmt.Printf(blue+"║"+reset+" 监听地址: %-43s"+blue+"║\n"+reset, config.ListenAddress) + fmt.Printf(blue+"║"+reset+" 监听端口: %-43d"+blue+"║\n"+reset, config.Port) + fmt.Printf(blue+"║"+reset+" 日志级别: %-43s"+blue+"║\n"+reset, config.LogLevel) + fmt.Printf(blue+"║"+reset+" 伪装网站: %-43s"+blue+"║\n"+reset, config.DisguiseURL) + fmt.Println(blue + "╚════════════════════════════════════════════════════════════╝" + reset) + fmt.Println() +} + +func handleRequest(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + // 健康检查 + if path == "/health" || path == "/healthz" { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + return + } + + if logrus.IsLevelEnabled(logrus.DebugLevel) { + var routeTag string + if strings.HasPrefix(path, "/v2/") { + routeTag = "[Docker]" + } else if strings.HasPrefix(path, "/auth/") { + routeTag = "[认证]" + } else if strings.HasPrefix(path, "/production-cloudflare/") { + routeTag = "[CF]" + } else { + routeTag = "[伪装]" + } + + logrus.Debugf("%s 请求: [%s %s] 来自 %s", + routeTag, r.Method, r.URL.String(), r.RemoteAddr) + } + + if strings.HasPrefix(path, "/v2/") { + handleRegistryRequest(w, r) + } else if strings.HasPrefix(path, "/auth/") { + handleAuthRequest(w, r) + } else if strings.HasPrefix(path, "/production-cloudflare/") { + handleCloudflareRequest(w, r) + } else { + handleDisguise(w, r) + } +} + +func handleRegistryRequest(w http.ResponseWriter, r *http.Request) { + const targetHost = "registry-1.docker.io" + + ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) + defer cancel() + + pathParts := strings.Split(r.URL.Path, "/") + v2PathParts := pathParts[2:] + pathString := strings.Join(v2PathParts, "/") + + url := &url.URL{ + Scheme: "https", + Host: targetHost, + Path: "/v2/" + pathString, + RawQuery: r.URL.RawQuery, + } + + headers := copyHeaders(r.Header) + headers.Set("Host", targetHost) + + logrus.Debugf("Docker镜像: 转发请求至 %s", url.String()) + + resp, err := sendRequestWithContext(ctx, r.Method, url.String(), headers, r.Body) + if err != nil { + logrus.Errorf("Docker镜像: 请求失败 - %v", err) + if logrus.IsLevelEnabled(logrus.DebugLevel) { + http.Error(w, fmt.Sprintf("代理错误: %v", err), http.StatusBadGateway) + } else { + http.Error(w, "服务暂时不可用", http.StatusBadGateway) + } + return + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusUnauthorized { + handleAuthChallenge(w, r, resp) + return + } + + respHeaders := copyHeaders(resp.Header) + + if respHeaders.Get("WWW-Authenticate") != "" { + currentDomain := r.Host + respHeaders.Set("WWW-Authenticate", + fmt.Sprintf(`Bearer realm="https://%s/auth/token", service="registry.docker.io"`, currentDomain)) + } + + for k, v := range respHeaders { + for _, val := range v { + w.Header().Add(k, val) + } + } + w.WriteHeader(resp.StatusCode) + + written, err := io.Copy(w, resp.Body) + if err != nil { + logrus.Errorf("Docker镜像: 传输响应失败 - %v", err) + return + } + + if logrus.IsLevelEnabled(logrus.DebugLevel) { + logrus.Debugf("Docker镜像: 响应完成 [状态: %d] [大小: %.2f KB]", + resp.StatusCode, float64(written)/1024) + } +} + +func handleAuthRequest(w http.ResponseWriter, r *http.Request) { + const targetHost = "auth.docker.io" + + ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) + defer cancel() + + pathParts := strings.Split(r.URL.Path, "/") + authPathParts := pathParts[2:] + pathString := strings.Join(authPathParts, "/") + + url := &url.URL{ + Scheme: "https", + Host: targetHost, + Path: "/" + pathString, + RawQuery: r.URL.RawQuery, + } + + headers := copyHeaders(r.Header) + headers.Set("Host", targetHost) + + logrus.Debugf("认证服务: 转发请求至 %s", url.String()) + + resp, err := sendRequestWithContext(ctx, r.Method, url.String(), headers, r.Body) + if err != nil { + logrus.Errorf("认证服务: 请求失败 - %v", err) + if logrus.IsLevelEnabled(logrus.DebugLevel) { + http.Error(w, fmt.Sprintf("代理错误: %v", err), http.StatusBadGateway) + } else { + http.Error(w, "服务暂时不可用", http.StatusBadGateway) + } + return + } + defer resp.Body.Close() + + for k, v := range resp.Header { + for _, val := range v { + w.Header().Add(k, val) + } + } + w.WriteHeader(resp.StatusCode) + + written, err := io.Copy(w, resp.Body) + if err != nil { + logrus.Errorf("认证服务: 传输响应失败 - %v", err) + return + } + + if logrus.IsLevelEnabled(logrus.DebugLevel) { + logrus.Debugf("认证服务: 响应完成 [状态: %d] [大小: %.2f KB]", + resp.StatusCode, float64(written)/1024) + } +} + +func handleCloudflareRequest(w http.ResponseWriter, r *http.Request) { + const targetHost = "production.cloudflare.docker.com" + + ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) + defer cancel() + + pathParts := strings.Split(r.URL.Path, "/") + cfPathParts := pathParts[2:] + pathString := strings.Join(cfPathParts, "/") + + url := &url.URL{ + Scheme: "https", + Host: targetHost, + Path: "/" + pathString, + RawQuery: r.URL.RawQuery, + } + + headers := copyHeaders(r.Header) + headers.Set("Host", targetHost) + + logrus.Debugf("Cloudflare: 转发请求至 %s", url.String()) + + resp, err := sendRequestWithContext(ctx, r.Method, url.String(), headers, r.Body) + if err != nil { + logrus.Errorf("Cloudflare: 请求失败 - %v", err) + if logrus.IsLevelEnabled(logrus.DebugLevel) { + http.Error(w, fmt.Sprintf("代理错误: %v", err), http.StatusBadGateway) + } else { + http.Error(w, "服务暂时不可用", http.StatusBadGateway) + } + return + } + defer resp.Body.Close() + + for k, v := range resp.Header { + for _, val := range v { + w.Header().Add(k, val) + } + } + w.WriteHeader(resp.StatusCode) + + written, err := io.Copy(w, resp.Body) + if err != nil { + logrus.Errorf("Cloudflare: 传输响应失败 - %v", err) + return + } + + if logrus.IsLevelEnabled(logrus.DebugLevel) { + logrus.Debugf("Cloudflare: 响应完成 [状态: %d] [大小: %.2f KB]", + resp.StatusCode, float64(written)/1024) + } +} + +func handleAuthChallenge(w http.ResponseWriter, r *http.Request, resp *http.Response) { + for k, v := range resp.Header { + for _, val := range v { + w.Header().Add(k, val) + } + } + + if authHeader := w.Header().Get("WWW-Authenticate"); authHeader != "" { + currentDomain := r.Host + w.Header().Set("WWW-Authenticate", + fmt.Sprintf(`Bearer realm="https://%s/auth/token", service="registry.docker.io"`, currentDomain)) + } + + w.WriteHeader(resp.StatusCode) + + _, err := io.Copy(w, resp.Body) + if err != nil { + logrus.Errorf("认证响应传输失败: %v", err) + } +} + +func handleDisguise(w http.ResponseWriter, r *http.Request) { + targetURL := &url.URL{ + Scheme: "https", + Host: config.DisguiseURL, + Path: r.URL.Path, + RawQuery: r.URL.RawQuery, + } + + if logrus.IsLevelEnabled(logrus.DebugLevel) { + logrus.Debugf("伪装页面: 转发请求至 %s", targetURL.String()) + } + + ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) + defer cancel() + + headers := copyHeaders(r.Header) + headers.Del("Accept-Encoding") + + resp, err := sendRequestWithContext(ctx, r.Method, targetURL.String(), headers, r.Body) + if err != nil { + logrus.Errorf("伪装页面: 请求失败 - %v", err) + http.Error(w, "服务器错误", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + for k, v := range resp.Header { + for _, val := range v { + w.Header().Add(k, val) + } + } + w.WriteHeader(resp.StatusCode) + + written, err := io.Copy(w, resp.Body) + if err != nil { + logrus.Errorf("伪装页面: 传输响应失败 - %v", err) + return + } + + if logrus.IsLevelEnabled(logrus.DebugLevel) { + logrus.Debugf("伪装页面: 响应完成 [状态: %d] [大小: %.2f KB]", + resp.StatusCode, float64(written)/1024) + } +} + +func sendRequestWithContext(ctx context.Context, method, url string, headers http.Header, body io.ReadCloser) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, method, url, body) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %v", err) + } + + req.Header = headers + + startTime := time.Now() + resp, err := client.Do(req) + + if err == nil && logrus.IsLevelEnabled(logrus.DebugLevel) { + duration := time.Since(startTime) + logrus.Debugf("请求耗时: %.2f 秒 (%s)", duration.Seconds(), url) + } + + return resp, err +} + +func copyHeaders(src http.Header) http.Header { + dst := make(http.Header) + for key, values := range src { + dst[key] = append([]string(nil), values...) + } + return dst +} + +func getEnv(key, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultValue +} + +func getEnvAsInt(key string, defaultValue int) int { + if valueStr, exists := os.LookupEnv(key); exists { + if value, err := strconv.Atoi(valueStr); err == nil { + return value + } + } + return defaultValue +}