add support for multi-target docker image(oci) proxy
This commit is contained in:
53
main.go
53
main.go
@@ -17,6 +17,7 @@ import (
|
||||
"ghproxy/middleware/loggin"
|
||||
"ghproxy/proxy"
|
||||
"ghproxy/rate"
|
||||
"ghproxy/weakcache"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/logger"
|
||||
"github.com/hertz-contrib/http2/factory"
|
||||
@@ -50,6 +51,10 @@ var (
|
||||
pagesFS embed.FS
|
||||
)
|
||||
|
||||
var (
|
||||
wcache *weakcache.Cache[string] // docker token缓存
|
||||
)
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
logDump = logger.LogDump
|
||||
@@ -360,6 +365,9 @@ func init() {
|
||||
setMemLimit(cfg)
|
||||
loadlist(cfg)
|
||||
setupRateLimit(cfg)
|
||||
if cfg.Docker.Enabled {
|
||||
wcache = proxy.InitWeakCache()
|
||||
}
|
||||
|
||||
if cfg.Server.Debug {
|
||||
runMode = "dev"
|
||||
@@ -468,23 +476,7 @@ func main() {
|
||||
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
|
||||
// for 3.4.0
|
||||
|
||||
/*
|
||||
r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) {
|
||||
proxy.GhcrRouting(cfg)(ctx, c)
|
||||
|
||||
/*
|
||||
//proxy.GhcrRouting(cfg)(ctx, c)
|
||||
// 返回200与空json
|
||||
//c.JSON(200, map[string]interface{}{})
|
||||
emptyJSON := "{}"
|
||||
//emptyJSON := `{"name":"disable-list-tags","tags":[]}`
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.Header("Content-Length", fmt.Sprint(len(emptyJSON)))
|
||||
c.String(200, emptyJSON)
|
||||
*/
|
||||
/*
|
||||
r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) {
|
||||
emptyJSON := "{}"
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.Header("Content-Length", fmt.Sprint(len(emptyJSON)))
|
||||
@@ -493,30 +485,18 @@ func main() {
|
||||
|
||||
c.Status(200)
|
||||
c.Write([]byte(emptyJSON))
|
||||
*/
|
||||
})
|
||||
|
||||
r.Any("/v2/:target/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||
proxy.GhcrWithImageRouting(cfg)(ctx, c)
|
||||
})
|
||||
|
||||
/*
|
||||
w := adaptor.GetCompatResponseWriter(&c.Response)
|
||||
|
||||
const emptyJSON = "{}"
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON)))
|
||||
w.Header().Del("Server")
|
||||
|
||||
fmt.Fprint(w, emptyJSON)
|
||||
*/
|
||||
/*
|
||||
})
|
||||
|
||||
r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||
proxy.GhcrRouting(cfg)(ctx, c)
|
||||
})
|
||||
*/
|
||||
|
||||
r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||
proxy.GhcrRouting(cfg)(ctx, c)
|
||||
})
|
||||
|
||||
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
@@ -531,7 +511,7 @@ func main() {
|
||||
}()
|
||||
}
|
||||
|
||||
r.Spin()
|
||||
defer wcache.StopCleanup()
|
||||
defer logger.Close()
|
||||
defer func() {
|
||||
if hertZfile != nil {
|
||||
@@ -541,5 +521,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
r.Spin()
|
||||
|
||||
fmt.Println("Program Exit")
|
||||
}
|
||||
|
||||
62
proxy/authparse.go
Normal file
62
proxy/authparse.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BearerAuthParams 用于存放解析出的 Bearer 认证参数
|
||||
type BearerAuthParams struct {
|
||||
Realm string
|
||||
Service string
|
||||
Scope string
|
||||
}
|
||||
|
||||
// parseBearerWWWAuthenticateHeader 解析 Bearer 方案的 Www-Authenticate Header。
|
||||
// 它期望格式为 'Bearer key1="value1",key2="value2",...'
|
||||
// 并尝试将已知参数解析到 BearerAuthParams struct 中。
|
||||
func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, error) {
|
||||
if headerValue == "" {
|
||||
return nil, fmt.Errorf("header value is empty")
|
||||
}
|
||||
|
||||
// 检查 Scheme 是否是 "Bearer"
|
||||
parts := strings.SplitN(headerValue, " ", 2)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
return nil, fmt.Errorf("invalid or non-bearer header format: got '%s'", headerValue)
|
||||
}
|
||||
paramsStr := parts[1]
|
||||
|
||||
paramPairs := strings.Split(paramsStr, ",")
|
||||
tempMap := make(map[string]string)
|
||||
|
||||
for _, pair := range paramPairs {
|
||||
trimmedPair := strings.TrimSpace(pair)
|
||||
keyValue := strings.SplitN(trimmedPair, "=", 2)
|
||||
if len(keyValue) != 2 {
|
||||
logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue)
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(keyValue[0])
|
||||
value := strings.TrimSpace(keyValue[1])
|
||||
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
|
||||
value = value[1 : len(value)-1]
|
||||
}
|
||||
tempMap[key] = value
|
||||
}
|
||||
|
||||
//从 map 中提取值并填充到 struct
|
||||
authParams := &BearerAuthParams{}
|
||||
|
||||
if realm, ok := tempMap["realm"]; ok {
|
||||
authParams.Realm = realm
|
||||
}
|
||||
if service, ok := tempMap["service"]; ok {
|
||||
authParams.Service = service
|
||||
}
|
||||
if scope, ok := tempMap["scope"]; ok {
|
||||
authParams.Scope = scope
|
||||
}
|
||||
|
||||
return authParams, nil
|
||||
}
|
||||
247
proxy/docker.go
247
proxy/docker.go
@@ -2,39 +2,132 @@ package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"ghproxy/config"
|
||||
"ghproxy/weakcache"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
var (
|
||||
dockerhubTarget = "registry-1.docker.io"
|
||||
ghcrTarget = "ghcr.io"
|
||||
)
|
||||
|
||||
var cache *weakcache.Cache[string]
|
||||
|
||||
type imageInfo struct {
|
||||
User string
|
||||
Repo string
|
||||
Image string
|
||||
}
|
||||
|
||||
func InitWeakCache() *weakcache.Cache[string] {
|
||||
cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
|
||||
return cache
|
||||
}
|
||||
|
||||
/*
|
||||
func GhcrRouting(cfg *config.Config) app.HandlerFunc {
|
||||
return func(ctx context.Context, c *app.RequestContext) {
|
||||
if cfg.Docker.Enabled {
|
||||
|
||||
charToFind := '.'
|
||||
reqTarget := c.Param("target")
|
||||
path := ""
|
||||
target := ""
|
||||
|
||||
if strings.ContainsRune(reqTarget, charToFind) {
|
||||
|
||||
path = c.Param("filepath")
|
||||
if reqTarget == "docker.io" {
|
||||
target = dockerhubTarget
|
||||
} else if reqTarget == "ghcr.io" {
|
||||
target = ghcrTarget
|
||||
} else {
|
||||
target = reqTarget
|
||||
}
|
||||
} else {
|
||||
path = string(c.Request.RequestURI())
|
||||
}
|
||||
|
||||
GhcrToTarget(ctx, c, cfg, target, path, nil)
|
||||
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
|
||||
return func(ctx context.Context, c *app.RequestContext) {
|
||||
|
||||
charToFind := '.'
|
||||
reqTarget := c.Param("target")
|
||||
reqImageUser := c.Param("user")
|
||||
reqImageName := c.Param("repo")
|
||||
reqFilePath := c.Param("filepath")
|
||||
|
||||
path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
|
||||
target := ""
|
||||
|
||||
if strings.ContainsRune(reqTarget, charToFind) {
|
||||
|
||||
if reqTarget == "docker.io" {
|
||||
target = dockerhubTarget
|
||||
} else if reqTarget == "ghcr.io" {
|
||||
target = ghcrTarget
|
||||
} else {
|
||||
target = reqTarget
|
||||
}
|
||||
} else {
|
||||
path = string(c.Request.RequestURI())
|
||||
reqImageUser = c.Param("target")
|
||||
reqImageName = c.Param("user")
|
||||
}
|
||||
image := &imageInfo{
|
||||
User: reqImageUser,
|
||||
Repo: reqImageName,
|
||||
Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
|
||||
}
|
||||
|
||||
GhcrToTarget(ctx, c, cfg, target, path, image)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) {
|
||||
if cfg.Docker.Enabled {
|
||||
if target != "" {
|
||||
GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+string(c.Request.QueryString()), image, cfg, target)
|
||||
|
||||
} else {
|
||||
if cfg.Docker.Target == "ghcr" {
|
||||
GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr")
|
||||
GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget)
|
||||
} else if cfg.Docker.Target == "dockerhub" {
|
||||
GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub")
|
||||
GhcrRequest(ctx, c, "https://"+dockerhubTarget+string(c.Request.RequestURI()), image, cfg, dockerhubTarget)
|
||||
} else if cfg.Docker.Target != "" {
|
||||
// 自定义taget
|
||||
GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), cfg, "custom")
|
||||
GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), image, cfg, cfg.Docker.Target)
|
||||
} else {
|
||||
// 配置为空
|
||||
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set"))
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
|
||||
func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *imageInfo, cfg *config.Config, target string) {
|
||||
|
||||
var (
|
||||
method []byte
|
||||
@@ -55,12 +148,11 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
|
||||
|
||||
method = c.Request.Method()
|
||||
|
||||
rb := client.NewRequestBuilder(string(method), u)
|
||||
rb := ghcrclient.NewRequestBuilder(string(method), u)
|
||||
rb.NoDefaultHeaders()
|
||||
rb.SetBody(c.Request.BodyStream())
|
||||
rb.WithContext(ctx)
|
||||
|
||||
//req, err = client.NewRequest(string(method), u, c.Request.BodyStream())
|
||||
req, err = rb.Build()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
@@ -73,14 +165,62 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
|
||||
req.Header.Add(headerKey, headerValue)
|
||||
})
|
||||
|
||||
resp, err = client.Do(req)
|
||||
req.Header.Set("Host", target)
|
||||
token, exist := cache.Get(image.Image)
|
||||
if exist {
|
||||
logDebug("Use Cache Token: %s", token)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
resp, err = ghcrclient.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 错误处理(404)
|
||||
if resp.StatusCode == 404 {
|
||||
// 处理状态码
|
||||
if resp.StatusCode == 401 {
|
||||
// 请求target /v2/路径
|
||||
if string(c.Request.URI().Path()) != "/v2/" {
|
||||
resp.Body.Close()
|
||||
token := ChallengeReq(target, image, ctx, c)
|
||||
|
||||
// 更新kv
|
||||
if token != "" {
|
||||
logDump("Update Cache Token: %s", token)
|
||||
cache.Put(image.Image, token)
|
||||
}
|
||||
|
||||
rb := ghcrclient.NewRequestBuilder(string(method), u)
|
||||
rb.NoDefaultHeaders()
|
||||
rb.SetBody(c.Request.BodyStream())
|
||||
rb.WithContext(ctx)
|
||||
|
||||
req, err = rb.Build()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Request.Header.VisitAll(func(key, value []byte) {
|
||||
headerKey := string(key)
|
||||
headerValue := string(value)
|
||||
req.Header.Add(headerKey, headerValue)
|
||||
})
|
||||
|
||||
req.Header.Set("Host", target)
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
resp, err = ghcrclient.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
} else if resp.StatusCode == 404 { // 错误处理(404)
|
||||
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
|
||||
return
|
||||
}
|
||||
@@ -101,8 +241,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
|
||||
bodySize = -1
|
||||
}
|
||||
if err == nil && bodySize > sizelimit {
|
||||
var finalURL string
|
||||
finalURL = resp.Request.URL.String()
|
||||
finalURL := resp.Request.URL.String()
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
logError("Failed to close response body: %v", err)
|
||||
@@ -116,7 +255,6 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
|
||||
// 复制响应头,排除需要移除的 header
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
//c.Header(key, value)
|
||||
c.Response.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
@@ -136,3 +274,78 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
|
||||
c.SetBodyStream(bodyReader, -1)
|
||||
|
||||
}
|
||||
|
||||
type AuthToken struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) {
|
||||
var resp401 *http.Response
|
||||
var req401 *http.Request
|
||||
var err error
|
||||
|
||||
rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
|
||||
rb401.NoDefaultHeaders()
|
||||
rb401.WithContext(ctx)
|
||||
rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ")
|
||||
req401, err = rb401.Build()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
req401.Header.Set("Host", target)
|
||||
|
||||
resp401, err = ghcrclient.Do(req401)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp401.Body.Close()
|
||||
bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
|
||||
if err != nil {
|
||||
logError("Failed to parse Www-Authenticate header: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
scope := fmt.Sprintf("repository:%s:pull", image.Image)
|
||||
|
||||
getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm).
|
||||
NoDefaultHeaders().
|
||||
WithContext(ctx).
|
||||
AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ").
|
||||
SetHeader("Host", bearer.Service).
|
||||
AddQueryParam("service", bearer.Service).
|
||||
AddQueryParam("scope", scope)
|
||||
|
||||
getAuthReq, err := getAuthRB.Build()
|
||||
if err != nil {
|
||||
logError("Failed to create request: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
authResp, err := ghcrclient.Do(getAuthReq)
|
||||
if err != nil {
|
||||
logError("Failed to send request: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer authResp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(authResp.Body)
|
||||
if err != nil {
|
||||
logError("Failed to read auth response body: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 解码json
|
||||
var authToken AuthToken
|
||||
err = json.Unmarshal(bodyBytes, &authToken)
|
||||
if err != nil {
|
||||
logError("Failed to decode auth response body: %v", err)
|
||||
return
|
||||
}
|
||||
token = authToken.Token
|
||||
|
||||
return token
|
||||
|
||||
}
|
||||
|
||||
@@ -12,10 +12,12 @@ import (
|
||||
var BufferSize int = 32 * 1024 // 32KB
|
||||
|
||||
var (
|
||||
tr *http.Transport
|
||||
gittr *http.Transport
|
||||
client *httpc.Client
|
||||
gitclient *httpc.Client
|
||||
tr *http.Transport
|
||||
gittr *http.Transport
|
||||
client *httpc.Client
|
||||
gitclient *httpc.Client
|
||||
ghcrtr *http.Transport
|
||||
ghcrclient *httpc.Client
|
||||
)
|
||||
|
||||
func InitReq(cfg *config.Config) error {
|
||||
@@ -23,11 +25,13 @@ func InitReq(cfg *config.Config) error {
|
||||
if cfg.GitClone.Mode == "cache" {
|
||||
initGitHTTPClient(cfg)
|
||||
}
|
||||
initGhcrHTTPClient(cfg)
|
||||
err := SetGlobalRateLimit(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func initHTTPClient(cfg *config.Config) {
|
||||
@@ -77,6 +81,7 @@ func initHTTPClient(cfg *config.Config) {
|
||||
httpc.WithTransport(tr),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func initGitHTTPClient(cfg *config.Config) {
|
||||
@@ -147,3 +152,51 @@ func initGitHTTPClient(cfg *config.Config) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func initGhcrHTTPClient(cfg *config.Config) {
|
||||
var proTolcols = new(http.Protocols)
|
||||
proTolcols.SetHTTP1(true)
|
||||
proTolcols.SetHTTP2(true)
|
||||
if cfg.Httpc.Mode == "auto" {
|
||||
|
||||
ghcrtr = &http.Transport{
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
Protocols: proTolcols,
|
||||
}
|
||||
} else if cfg.Httpc.Mode == "advanced" {
|
||||
ghcrtr = &http.Transport{
|
||||
MaxIdleConns: cfg.Httpc.MaxIdleConns,
|
||||
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
|
||||
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
Protocols: proTolcols,
|
||||
}
|
||||
} else {
|
||||
// 错误的模式
|
||||
logError("unknown httpc mode: %s", cfg.Httpc.Mode)
|
||||
fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
|
||||
logWarning("use Auto to Run HTTP Client")
|
||||
fmt.Println("use Auto to Run HTTP Client")
|
||||
ghcrtr = &http.Transport{
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
}
|
||||
}
|
||||
if cfg.Outbound.Enabled {
|
||||
initTransport(cfg, ghcrtr)
|
||||
}
|
||||
if cfg.Server.Debug {
|
||||
ghcrclient = httpc.New(
|
||||
httpc.WithTransport(ghcrtr),
|
||||
httpc.WithDumpLog(),
|
||||
)
|
||||
} else {
|
||||
ghcrclient = httpc.New(
|
||||
httpc.WithTransport(ghcrtr),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,12 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro
|
||||
// 匹配 "https://github.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://github.com") {
|
||||
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
|
||||
if strings.HasPrefix(remainingPath, "/") {
|
||||
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||
}
|
||||
/*
|
||||
if strings.HasPrefix(remainingPath, "/") {
|
||||
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||
}
|
||||
*/
|
||||
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||
// 预期格式/user/repo/more...
|
||||
// 取出user和repo和最后部分
|
||||
parts := strings.Split(remainingPath, "/")
|
||||
|
||||
@@ -13,8 +13,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
|
||||
|
||||
// 白名单检查
|
||||
if cfg.Whitelist.Enabled {
|
||||
var whitelist bool
|
||||
whitelist = auth.CheckWhitelist(user, repo)
|
||||
whitelist := auth.CheckWhitelist(user, repo)
|
||||
if !whitelist {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo)))
|
||||
logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
|
||||
@@ -24,8 +23,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
|
||||
|
||||
// 黑名单检查
|
||||
if cfg.Blacklist.Enabled {
|
||||
var blacklist bool
|
||||
blacklist = auth.CheckBlacklist(user, repo)
|
||||
blacklist := auth.CheckBlacklist(user, repo)
|
||||
if blacklist {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo)))
|
||||
logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
|
||||
|
||||
258
weakcache/weakcache.go
Normal file
258
weakcache/weakcache.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package weakcache
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
"time"
|
||||
"weak" // Go 1.24 引入的 weak 包
|
||||
)
|
||||
|
||||
// DefaultExpiration 默认过期时间,这里设置为 15 分钟。
|
||||
// 这是一个导出的常量,方便用户使用包时引用默认值。
|
||||
const DefaultExpiration = 5 * time.Minute
|
||||
|
||||
// cleanupInterval 是后台清理 Go routine 的扫描间隔,这里设置为 5 分钟。
|
||||
// 这是一个内部常量,不导出。
|
||||
const cleanupInterval = 2 * time.Minute
|
||||
|
||||
// cacheEntry 缓存项的内部结构。不导出。
|
||||
type cacheEntry[T any] struct {
|
||||
Value T
|
||||
Expiration time.Time
|
||||
key string // 存储key,方便在list.Element中引用
|
||||
}
|
||||
|
||||
// Cache 是一个基于 weak.Pointer, 带有过期和大小上限 (FIFO) 的泛型缓存。
|
||||
// 这是一个导出的类型。
|
||||
type Cache[T any] struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// 修正:缓存存储:key -> weak.Pointer 到 cacheEntry 结构体 (而不是指向结构体的指针)
|
||||
// weak.Make(*cacheEntry[T]) 返回 weak.Pointer[cacheEntry[T]]
|
||||
data map[string]weak.Pointer[cacheEntry[T]]
|
||||
|
||||
// FIFO 链表:存储 key 的 list.Element
|
||||
// 链表头部是最近放入的,尾部是最早放入的(最老的)
|
||||
fifoList *list.List
|
||||
// FIFO 元素的映射:key -> *list.Element
|
||||
fifoMap map[string]*list.Element
|
||||
|
||||
defaultExpiration time.Duration
|
||||
maxSize int // 缓存最大容量,0 表示无限制
|
||||
|
||||
stopCleanup chan struct{}
|
||||
wg sync.WaitGroup // 用于等待清理 Go routine 退出
|
||||
}
|
||||
|
||||
// NewCache 创建一个新的缓存实例。
|
||||
// expiration: 新添加项的默认过期时间。如果为 0,则使用 DefaultExpiration。
|
||||
// maxSize: 缓存的最大容量,0 表示无限制。当达到上限时,采用 FIFO 策略淘汰。
|
||||
// 这是一个导出的构造函数。
|
||||
func NewCache[T any](expiration time.Duration, maxSize int) *Cache[T] {
|
||||
if expiration <= 0 {
|
||||
expiration = DefaultExpiration
|
||||
}
|
||||
|
||||
c := &Cache[T]{
|
||||
// 修正:初始化 map,值类型已修正
|
||||
data: make(map[string]weak.Pointer[cacheEntry[T]]),
|
||||
fifoList: list.New(),
|
||||
fifoMap: make(map[string]*list.Element),
|
||||
defaultExpiration: expiration,
|
||||
maxSize: maxSize,
|
||||
stopCleanup: make(chan struct{}),
|
||||
}
|
||||
// 启动后台清理 Go routine
|
||||
c.wg.Add(1)
|
||||
go c.cleanupLoop()
|
||||
return c
|
||||
}
|
||||
|
||||
// Put 将值放入缓存。如果 key 已存在,会更新其值和过期时间。
|
||||
// 这是导出的方法。
|
||||
func (c *Cache[T]) Put(key string, value T) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
expiration := now.Add(c.defaultExpiration)
|
||||
|
||||
// 如果 key 已经存在,更新其值和过期时间。
|
||||
// 在 FIFO 策略中, Put 更新不改变其在链表中的位置,除非旧的 entry 已经被 GC。
|
||||
if elem, ok := c.fifoMap[key]; ok {
|
||||
// 从 data map 中获取弱引用,wp 的类型现在是 weak.Pointer[cacheEntry[T]]
|
||||
if wp, dataOk := c.data[key]; dataOk {
|
||||
// wp.Value() 返回 *cacheEntry[T], entry 的类型现在是 *cacheEntry[T]
|
||||
entry := wp.Value()
|
||||
if entry != nil {
|
||||
// 旧的 cacheEntry 仍在内存中,直接更新
|
||||
entry.Value = value
|
||||
entry.Expiration = expiration
|
||||
// 在严格 FIFO 中,更新不移动位置
|
||||
return
|
||||
}
|
||||
// 如果 weak.Pointer.Value() 为 nil,说明之前的 cacheEntry 已经被 GC 了
|
||||
// 此时需要创建一个新的 entry,并将其从旧位置移除,再重新添加
|
||||
c.fifoList.Remove(elem)
|
||||
delete(c.fifoMap, key)
|
||||
} else {
|
||||
c.fifoList.Remove(elem)
|
||||
delete(c.fifoMap, key)
|
||||
}
|
||||
}
|
||||
|
||||
// 新建缓存项 (注意这里是结构体值,而不是指针)
|
||||
// weak.Make 接收的是指针 *T
|
||||
entry := &cacheEntry[T]{ // 创建结构体指针
|
||||
Value: value,
|
||||
Expiration: expiration,
|
||||
key: key, // 存储 key
|
||||
}
|
||||
|
||||
// 将新的 *cacheEntry[T] 包装成 weak.Pointer[cacheEntry[T]] 存入 data map
|
||||
// weak.Make(entry) 现在返回 weak.Pointer[cacheEntry[T]],类型匹配 data map 的值类型
|
||||
c.data[key] = weak.Make(entry)
|
||||
|
||||
// 添加到 FIFO 链表头部 (最近放入/更新的在头部)
|
||||
// PushFront 返回新的 list.Element
|
||||
c.fifoMap[key] = c.fifoList.PushFront(key)
|
||||
|
||||
// 检查大小上限并进行淘汰 (淘汰尾部的最老项)
|
||||
c.evictIfNeeded()
|
||||
}
|
||||
|
||||
// Get 从缓存中获取值。返回获取到的值和是否存在/是否有效。
|
||||
// 这是导出的方法。
|
||||
func (c *Cache[T]) Get(key string) (T, bool) {
|
||||
c.mu.RLock() // 先读锁
|
||||
// 从 data map 中获取弱引用,wp 的类型现在是 weak.Pointer[cacheEntry[T]]
|
||||
wp, ok := c.data[key]
|
||||
c.mu.RUnlock() // 立即释放读锁,如果需要写操作(removeEntry)可以获得锁
|
||||
|
||||
var zero T // 零值
|
||||
|
||||
if !ok {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// 尝试获取实际的 cacheEntry 指针
|
||||
// wp.Value() 返回 *cacheEntry[T], entry 的类型现在是 *cacheEntry[T]
|
||||
entry := wp.Value()
|
||||
|
||||
if entry == nil {
|
||||
// 对象已被GC回收,需要清理此弱引用
|
||||
c.removeEntry(key) // 内部会加写锁
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// 检查过期时间 (通过 entry 指针访问字段)
|
||||
if time.Now().After(entry.Expiration) {
|
||||
// 逻辑上已过期
|
||||
c.removeEntry(key) // 内部会加写锁
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// 在 FIFO 缓存中,Get 操作不改变项在链表中的位置
|
||||
return entry.Value, true // 通过 entry 指针访问值字段
|
||||
}
|
||||
|
||||
// removeEntry 从缓存中移除项。
|
||||
// 这个方法是内部使用的,不导出。需要被调用者确保持有写锁,或者内部自己加锁。
|
||||
// 考虑到 Get 和 cleanupLoop 可能会调用,让其内部自己加锁更安全。
|
||||
func (c *Cache[T]) removeEntry(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// 从 data map 中删除
|
||||
delete(c.data, key)
|
||||
|
||||
// 从 FIFO 链表和 fifoMap 中删除
|
||||
if elem, ok := c.fifoMap[key]; ok {
|
||||
c.fifoList.Remove(elem)
|
||||
delete(c.fifoMap, key)
|
||||
}
|
||||
}
|
||||
|
||||
// evictIfNeeded 检查是否需要淘汰最老(FIFO 链表尾部)的项。
|
||||
// 这个方法是内部使用的,不导出。必须在持有写锁的情况下调用。
|
||||
func (c *Cache[T]) evictIfNeeded() {
|
||||
if c.maxSize > 0 && c.fifoList.Len() > c.maxSize {
|
||||
// 淘汰 FIFO 链表尾部的元素 (最老的)
|
||||
oldest := c.fifoList.Back()
|
||||
if oldest != nil {
|
||||
keyToEvict := oldest.Value.(string) // 链表元素存储的是 key
|
||||
c.fifoList.Remove(oldest)
|
||||
delete(c.fifoMap, keyToEvict)
|
||||
delete(c.data, keyToEvict) // 移除弱引用
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size 返回当前缓存中的弱引用项数量。
|
||||
// 注意:这个数量可能包含已被 GC 回收但尚未清理的项。
|
||||
// 这是一个导出的方法。
|
||||
func (c *Cache[T]) Size() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.data)
|
||||
}
|
||||
|
||||
// cleanupLoop 后台清理 Go routine。不导出。
|
||||
func (c *Cache[T]) cleanupLoop() {
|
||||
defer c.wg.Done()
|
||||
// 使用内部常量 cleanupInterval
|
||||
ticker := time.NewTicker(cleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.cleanupExpiredAndGCed()
|
||||
case <-c.stopCleanup:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupExpiredAndGCed 扫描并清理已过期或已被 GC 回收的项。不导出。
|
||||
func (c *Cache[T]) cleanupExpiredAndGCed() {
|
||||
c.mu.Lock() // 清理时需要写锁
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
keysToRemove := make([]string, 0, len(c.data)) // 预估容量
|
||||
|
||||
// 遍历 data map 查找需要清理的键
|
||||
for key, wp := range c.data {
|
||||
// wp 的类型是 weak.Pointer[cacheEntry[T]]
|
||||
// wp.Value() 返回 *cacheEntry[T], entry 的类型是 *cacheEntry[T]
|
||||
entry := wp.Value() // 尝试获取强引用
|
||||
|
||||
if entry == nil {
|
||||
// 已被 GC 回收
|
||||
keysToRemove = append(keysToRemove, key)
|
||||
} else if now.After(entry.Expiration) {
|
||||
// 逻辑过期 (通过 entry 指针访问字段)
|
||||
keysToRemove = append(keysToRemove, key)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行删除操作
|
||||
for _, key := range keysToRemove {
|
||||
// 从 data map 中删除
|
||||
delete(c.data, key)
|
||||
// 从 FIFO 链表和 fifoMap 中删除
|
||||
// 需要再次检查 fifoMap,因为在持有锁期间,evictIfNeeded 可能已经移除了这个 key
|
||||
if elem, ok := c.fifoMap[key]; ok {
|
||||
c.fifoList.Remove(elem)
|
||||
delete(c.fifoMap, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StopCleanup 停止后台清理 Go routine。
|
||||
// 这是一个导出的方法。
|
||||
func (c *Cache[T]) StopCleanup() {
|
||||
close(c.stopCleanup)
|
||||
c.wg.Wait() // 等待 Go routine 退出
|
||||
}
|
||||
Reference in New Issue
Block a user