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