Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af5c624d74 | |||
| b4685de9fc | |||
| d431bc8419 | |||
| 6f16865eb5 | |||
| 9baac19825 | |||
| 558fbf5f42 | |||
| d565ec678a | |||
| c4e7c50b8c | |||
| ae0afdbdda | |||
| 8d34f49fdf | |||
| e1b9a7fcf5 | |||
| ac63bfec60 | |||
| 7337693dc4 | |||
| fc0e774ef5 | |||
| b950b29578 | |||
| d8649dadc7 |
@@ -1,61 +0,0 @@
|
||||
# .gitea/workflows/build.yml
|
||||
name: Build GHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
|
||||
- name: 获取版本信息
|
||||
id: version
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
else
|
||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "构建版本: $VERSION"
|
||||
|
||||
- name: 编译 Windows ARM64
|
||||
run: |
|
||||
GOOS=windows GOARCH=arm64 go build -ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" -o GHub-windows-arm64.exe
|
||||
|
||||
- name: 编译 Linux AMD64
|
||||
run: |
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" -o GHub-linux-amd64
|
||||
|
||||
- name: 上传构建产物
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: GHub-${{ steps.version.outputs.version }}
|
||||
path: |
|
||||
GHub-windows-arm64.exe
|
||||
GHub-linux-amd64
|
||||
|
||||
- name: 创建 Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: actions/gitea-release@v1
|
||||
with:
|
||||
files: |
|
||||
GHub-windows-arm64.exe
|
||||
GHub-linux-amd64
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Dockerfile
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN go mod tidy && CGO_ENABLED=0 go build -ldflags="-s -w" -o GHub
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /build/GHub .
|
||||
|
||||
EXPOSE 18187
|
||||
|
||||
CMD ["./GHub", "-l", "0.0.0.0", "-p", "18187", "-ll", "info", "-w", "time.is"]
|
||||
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
# docker-compose.yml
|
||||
services:
|
||||
ghub:
|
||||
build: .
|
||||
container_name: ghub
|
||||
ports:
|
||||
- "18187:18187"
|
||||
restart: unless-stopped
|
||||
command: ["./GHub", "-l", "0.0.0.0", "-p", "18187", "-ll", "info", "-w", "time.is"]
|
||||
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module git.zku.me/xofine/Ghub
|
||||
|
||||
go 1.25.4
|
||||
|
||||
require github.com/sirupsen/logrus v1.9.3
|
||||
|
||||
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
15
go.sum
Normal file
15
go.sum
Normal file
@@ -0,0 +1,15 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
500
main.go
500
main.go
@@ -3,6 +3,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,16 +13,18 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// Version 用于嵌入构建版本号
|
||||
var Version = "dev"
|
||||
|
||||
// Config 定义配置结构体
|
||||
type Config struct {
|
||||
ListenAddress string
|
||||
Port int
|
||||
@@ -32,31 +35,104 @@ type Config struct {
|
||||
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,
|
||||
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:
|
||||
@@ -70,16 +146,9 @@ func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
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
|
||||
return []byte(fmt.Sprintf("%s %s[%s]%s %s\n",
|
||||
timestamp, levelColor, strings.ToUpper(entry.Level.String()), resetColor, entry.Message)), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -94,15 +163,10 @@ func init() {
|
||||
|
||||
func preprocessArgs() {
|
||||
alias := map[string]string{
|
||||
"--listen": "-l",
|
||||
"--port": "-p",
|
||||
"--log-level": "-ll",
|
||||
"--disguise": "-w",
|
||||
"--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)
|
||||
@@ -114,16 +178,13 @@ func preprocessArgs() {
|
||||
}
|
||||
newArgs = append(newArgs, arg)
|
||||
}
|
||||
|
||||
if len(newArgs) > 0 {
|
||||
if len(newArgs) > 1 {
|
||||
os.Args = newArgs
|
||||
} else {
|
||||
logrus.Warn("命令行参数为空,使用原始参数")
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
const helpText = `HubP - Docker Hub 代理服务器
|
||||
fmt.Fprintf(os.Stderr, `HubP - Docker Hub 代理服务器
|
||||
|
||||
参数说明:
|
||||
-l, --listen 监听地址 (默认: 0.0.0.0)
|
||||
@@ -133,9 +194,7 @@ func usage() {
|
||||
|
||||
示例:
|
||||
./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 {
|
||||
@@ -152,15 +211,10 @@ 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")
|
||||
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)
|
||||
@@ -182,10 +236,7 @@ func main() {
|
||||
addr := fmt.Sprintf("%s:%d", config.ListenAddress, config.Port)
|
||||
http.HandleFunc("/", handleRequest)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: http.DefaultServeMux,
|
||||
}
|
||||
server := &http.Server{Addr: addr, Handler: http.DefaultServeMux}
|
||||
|
||||
go func() {
|
||||
logrus.Info("服务启动成功")
|
||||
@@ -209,10 +260,7 @@ func main() {
|
||||
}
|
||||
|
||||
func printStartupInfo() {
|
||||
const blue = "\033[34m"
|
||||
const green = "\033[32m"
|
||||
const reset = "\033[0m"
|
||||
|
||||
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)
|
||||
@@ -228,7 +276,6 @@ func printStartupInfo() {
|
||||
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"))
|
||||
@@ -246,9 +293,7 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
routeTag = "[伪装]"
|
||||
}
|
||||
|
||||
logrus.Debugf("%s 请求: [%s %s] 来自 %s",
|
||||
routeTag, r.Method, r.URL.String(), r.RemoteAddr)
|
||||
logrus.Debugf("%s 请求: [%s %s] 来自 %s", routeTag, r.Method, r.URL.String(), r.RemoteAddr)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/v2/") {
|
||||
@@ -262,102 +307,208 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const targetHost = "registry-1.docker.io"
|
||||
path := strings.TrimPrefix(r.URL.Path, "/v2/")
|
||||
|
||||
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)
|
||||
// /v2/ 端点
|
||||
if path == "" {
|
||||
w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("{}"))
|
||||
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)
|
||||
imageName, apiType, reference := parseRegistryPath(path)
|
||||
if imageName == "" || apiType == "" {
|
||||
http.Error(w, "Invalid path format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if logrus.IsLevelEnabled(logrus.DebugLevel) {
|
||||
logrus.Debugf("Docker镜像: 响应完成 [状态: %d] [大小: %.2f KB]",
|
||||
resp.StatusCode, float64(written)/1024)
|
||||
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 handleAuthRequest(w http.ResponseWriter, r *http.Request) {
|
||||
const targetHost = "auth.docker.io"
|
||||
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()
|
||||
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
authPathParts := pathParts[2:]
|
||||
pathString := strings.Join(authPathParts, "/")
|
||||
|
||||
url := &url.URL{
|
||||
path := strings.TrimPrefix(r.URL.Path, "/auth/")
|
||||
targetURL := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: targetHost,
|
||||
Path: "/" + pathString,
|
||||
Path: "/" + path,
|
||||
RawQuery: r.URL.RawQuery,
|
||||
}
|
||||
|
||||
headers := copyHeaders(r.Header)
|
||||
headers.Set("Host", targetHost)
|
||||
|
||||
logrus.Debugf("认证服务: 转发请求至 %s", url.String())
|
||||
logrus.Debugf("认证服务: 转发请求至 %s", targetURL.String())
|
||||
|
||||
resp, err := sendRequestWithContext(ctx, r.Method, url.String(), headers, r.Body)
|
||||
resp, err := sendRequestWithContext(ctx, r.Method, targetURL.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()
|
||||
@@ -369,48 +520,48 @@ func handleAuthRequest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
written, err := io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
logrus.Errorf("认证服务: 传输响应失败 - %v", err)
|
||||
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
|
||||
}
|
||||
|
||||
if logrus.IsLevelEnabled(logrus.DebugLevel) {
|
||||
logrus.Debugf("认证服务: 响应完成 [状态: %d] [大小: %.2f KB]",
|
||||
resp.StatusCode, float64(written)/1024)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
cfPathParts := pathParts[2:]
|
||||
pathString := strings.Join(cfPathParts, "/")
|
||||
|
||||
url := &url.URL{
|
||||
path := strings.TrimPrefix(r.URL.Path, "/production-cloudflare/")
|
||||
targetURL := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: targetHost,
|
||||
Path: "/" + pathString,
|
||||
Path: "/" + path,
|
||||
RawQuery: r.URL.RawQuery,
|
||||
}
|
||||
|
||||
headers := copyHeaders(r.Header)
|
||||
headers.Set("Host", targetHost)
|
||||
|
||||
logrus.Debugf("Cloudflare: 转发请求至 %s", url.String())
|
||||
logrus.Debugf("Cloudflare: 转发请求至 %s", targetURL.String())
|
||||
|
||||
resp, err := sendRequestWithContext(ctx, r.Method, url.String(), headers, r.Body)
|
||||
resp, err := sendRequestWithContext(ctx, r.Method, targetURL.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()
|
||||
@@ -421,38 +572,7 @@ func handleCloudflareRequest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
func handleDisguise(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -463,10 +583,6 @@ func handleDisguise(w http.ResponseWriter, r *http.Request) {
|
||||
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()
|
||||
|
||||
@@ -487,17 +603,7 @@ func handleDisguise(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
func sendRequestWithContext(ctx context.Context, method, url string, headers http.Header, body io.ReadCloser) (*http.Response, error) {
|
||||
@@ -505,18 +611,8 @@ func sendRequestWithContext(ctx context.Context, method, url string, headers htt
|
||||
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
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func copyHeaders(src http.Header) http.Header {
|
||||
|
||||
Reference in New Issue
Block a user