Files
Ghub/main.go
2025-12-28 15:33:39 +08:00

641 lines
17 KiB
Go

// main.go
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/sirupsen/logrus"
)
var Version = "dev"
type Config struct {
ListenAddress string
Port int
LogLevel string
DisguiseURL string
}
var config Config
var client = &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
DisableKeepAlives: false,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
var remoteOptions = []remote.Option{
remote.WithAuth(authn.Anonymous),
remote.WithTransport(client.Transport),
}
// Token 缓存
type TokenCache struct {
mu sync.RWMutex
cache map[string]*CachedToken
}
type CachedToken struct {
Token string
ExpiresAt time.Time
}
var tokenCache = &TokenCache{
cache: make(map[string]*CachedToken),
}
func (tc *TokenCache) Get(key string) (string, bool) {
tc.mu.RLock()
defer tc.mu.RUnlock()
if cached, ok := tc.cache[key]; ok {
if time.Now().Before(cached.ExpiresAt) {
return cached.Token, true
}
delete(tc.cache, key)
}
return "", false
}
func (tc *TokenCache) Set(key, token string, ttl time.Duration) {
tc.mu.Lock()
defer tc.mu.Unlock()
tc.cache[key] = &CachedToken{
Token: token,
ExpiresAt: time.Now().Add(ttl),
}
}
// Manifest 缓存
type ManifestCache struct {
mu sync.RWMutex
cache map[string]*CachedManifest
}
type CachedManifest struct {
Data []byte
MediaType string
Digest string
ExpiresAt time.Time
}
var manifestCache = &ManifestCache{
cache: make(map[string]*CachedManifest),
}
func (mc *ManifestCache) Get(key string) (*CachedManifest, bool) {
mc.mu.RLock()
defer mc.mu.RUnlock()
if cached, ok := mc.cache[key]; ok {
if time.Now().Before(cached.ExpiresAt) {
return cached, true
}
delete(mc.cache, key)
}
return nil, false
}
func (mc *ManifestCache) Set(key string, data []byte, mediaType, digest string, ttl time.Duration) {
mc.mu.Lock()
defer mc.mu.Unlock()
mc.cache[key] = &CachedManifest{
Data: data,
MediaType: mediaType,
Digest: digest,
ExpiresAt: time.Now().Add(ttl),
}
}
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"
return []byte(fmt.Sprintf("%s %s[%s]%s %s\n",
timestamp, levelColor, strings.ToUpper(entry.Level.String()), resetColor, entry.Message)), 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) > 1 {
os.Args = newArgs
}
}
func usage() {
fmt.Fprintf(os.Stderr, `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
`)
}
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
flag.StringVar(&config.ListenAddress, "l", getEnv("HUBP_LISTEN", "0.0.0.0"), "监听地址")
flag.IntVar(&config.Port, "p", getEnvAsInt("HUBP_PORT", 18184), "监听端口")
flag.StringVar(&config.LogLevel, "ll", getEnv("HUBP_LOG_LEVEL", "debug"), "日志级别")
flag.StringVar(&config.DisguiseURL, "w", getEnv("HUBP_DISGUISE", "onlinealarmkur.com"), "伪装网站 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, green, reset = "\033[34m", "\033[32m", "\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 getCleanHost(r *http.Request) string {
host := r.Host
if idx := strings.Index(host, ":"); idx != -1 {
return host[:idx]
}
return host
}
func handleRegistryRequest(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/v2/")
// /v2/ 端点
if path == "" {
w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
w.WriteHeader(http.StatusOK)
w.Write([]byte("{}"))
return
}
imageName, apiType, reference := parseRegistryPath(path)
if imageName == "" || apiType == "" {
http.Error(w, "Invalid path format", http.StatusBadRequest)
return
}
if !strings.Contains(imageName, "/") {
imageName = "library/" + imageName
}
imageRef := fmt.Sprintf("registry-1.docker.io/%s", imageName)
switch apiType {
case "manifests":
handleManifestRequest(w, r, imageRef, reference)
case "blobs":
handleBlobRequest(w, r, imageRef, reference)
default:
http.Error(w, "API endpoint not found", http.StatusNotFound)
}
}
func parseRegistryPath(path string) (imageName, apiType, reference string) {
if idx := strings.Index(path, "/manifests/"); idx != -1 {
imageName = path[:idx]
apiType = "manifests"
reference = path[idx+len("/manifests/"):]
return
}
if idx := strings.Index(path, "/blobs/"); idx != -1 {
imageName = path[:idx]
apiType = "blobs"
reference = path[idx+len("/blobs/"):]
return
}
return "", "", ""
}
func handleManifestRequest(w http.ResponseWriter, r *http.Request, imageRef, reference string) {
cacheKey := fmt.Sprintf("%s@%s", imageRef, reference)
// 检查缓存
if r.Method == http.MethodGet {
if cached, ok := manifestCache.Get(cacheKey); ok {
logrus.Debugf("Docker镜像: 使用缓存的 manifest")
w.Header().Set("Content-Type", cached.MediaType)
w.Header().Set("Docker-Content-Digest", cached.Digest)
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(cached.Data)))
w.WriteHeader(http.StatusOK)
w.Write(cached.Data)
return
}
}
var ref name.Reference
var err error
if strings.HasPrefix(reference, "sha256:") {
ref, err = name.NewDigest(fmt.Sprintf("%s@%s", imageRef, reference))
} else {
ref, err = name.NewTag(fmt.Sprintf("%s:%s", imageRef, reference))
}
if err != nil {
logrus.Errorf("解析镜像引用失败: %v", err)
http.Error(w, "Invalid reference", http.StatusBadRequest)
return
}
if r.Method == http.MethodHead {
desc, err := remote.Head(ref, remoteOptions...)
if err != nil {
logrus.Errorf("HEAD请求失败: %v", err)
http.Error(w, "Manifest not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", string(desc.MediaType))
w.Header().Set("Docker-Content-Digest", desc.Digest.String())
w.Header().Set("Content-Length", fmt.Sprintf("%d", desc.Size))
w.WriteHeader(http.StatusOK)
logrus.Debugf("Docker镜像: HEAD 响应完成")
} else {
desc, err := remote.Get(ref, remoteOptions...)
if err != nil {
logrus.Errorf("GET请求失败: %v", err)
http.Error(w, "Manifest not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", string(desc.MediaType))
w.Header().Set("Docker-Content-Digest", desc.Digest.String())
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(desc.Manifest)))
w.WriteHeader(http.StatusOK)
w.Write(desc.Manifest)
// 缓存 manifest
ttl := 10 * time.Minute
if strings.HasPrefix(reference, "sha256:") {
ttl = 1 * time.Hour
}
manifestCache.Set(cacheKey, desc.Manifest, string(desc.MediaType), desc.Digest.String(), ttl)
logrus.Debugf("Docker镜像: manifest 响应完成 [大小: %.2f KB]", float64(len(desc.Manifest))/1024)
}
}
func handleBlobRequest(w http.ResponseWriter, r *http.Request, imageRef, digest string) {
digestRef, err := name.NewDigest(fmt.Sprintf("%s@%s", imageRef, digest))
if err != nil {
logrus.Errorf("解析digest引用失败: %v", err)
http.Error(w, "Invalid digest reference", http.StatusBadRequest)
return
}
layer, err := remote.Layer(digestRef, remoteOptions...)
if err != nil {
logrus.Errorf("获取layer失败: %v", err)
http.Error(w, "Layer not found", http.StatusNotFound)
return
}
size, err := layer.Size()
if err != nil {
logrus.Errorf("获取layer大小失败: %v", err)
http.Error(w, "Failed to get layer size", http.StatusInternalServerError)
return
}
reader, err := layer.Compressed()
if err != nil {
logrus.Errorf("获取layer内容失败: %v", err)
http.Error(w, "Failed to get layer content", http.StatusInternalServerError)
return
}
defer reader.Close()
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
w.Header().Set("Docker-Content-Digest", digest)
w.WriteHeader(http.StatusOK)
written, err := io.Copy(w, reader)
if err != nil {
logrus.Errorf("传输layer失败: %v", err)
return
}
logrus.Debugf("Docker镜像: blob 传输完成 [大小: %.2f MB]", float64(written)/(1024*1024))
}
func handleAuthRequest(w http.ResponseWriter, r *http.Request) {
cacheKey := r.URL.RawQuery
if token, ok := tokenCache.Get(cacheKey); ok {
logrus.Debugf("认证服务: 使用缓存的 token")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(token))
return
}
const targetHost = "auth.docker.io"
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
path := strings.TrimPrefix(r.URL.Path, "/auth/")
targetURL := &url.URL{
Scheme: "https",
Host: targetHost,
Path: "/" + path,
RawQuery: r.URL.RawQuery,
}
headers := copyHeaders(r.Header)
headers.Set("Host", targetHost)
logrus.Debugf("认证服务: 转发请求至 %s", targetURL.String())
resp, err := sendRequestWithContext(ctx, r.Method, targetURL.String(), headers, r.Body)
if err != nil {
logrus.Errorf("认证服务: 请求失败 - %v", err)
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)
if resp.StatusCode == http.StatusOK {
data, err := io.ReadAll(resp.Body)
if err == nil {
var tokenResp map[string]interface{}
if json.Unmarshal(data, &tokenResp) == nil {
ttl := 5 * time.Minute
if expiresIn, ok := tokenResp["expires_in"].(float64); ok {
ttl = time.Duration(expiresIn*0.9) * time.Second
}
tokenCache.Set(cacheKey, string(data), ttl)
logrus.Debugf("认证服务: token 已缓存 [TTL: %v]", ttl)
}
w.Write(data)
return
}
}
io.Copy(w, resp.Body)
}
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()
path := strings.TrimPrefix(r.URL.Path, "/production-cloudflare/")
targetURL := &url.URL{
Scheme: "https",
Host: targetHost,
Path: "/" + path,
RawQuery: r.URL.RawQuery,
}
headers := copyHeaders(r.Header)
headers.Set("Host", targetHost)
logrus.Debugf("Cloudflare: 转发请求至 %s", targetURL.String())
resp, err := sendRequestWithContext(ctx, r.Method, targetURL.String(), headers, r.Body)
if err != nil {
logrus.Errorf("Cloudflare: 请求失败 - %v", err)
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)
io.Copy(w, resp.Body)
}
func handleDisguise(w http.ResponseWriter, r *http.Request) {
targetURL := &url.URL{
Scheme: "https",
Host: config.DisguiseURL,
Path: r.URL.Path,
RawQuery: r.URL.RawQuery,
}
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)
io.Copy(w, resp.Body)
}
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
return client.Do(req)
}
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
}