4.1.7-rc.0

This commit is contained in:
wjqserver 2025-07-20 22:13:05 +08:00
parent b033079553
commit 1f3a036267
10 changed files with 98 additions and 162 deletions

View file

@ -48,12 +48,12 @@ func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc {
target := ""
if strings.ContainsRune(reqTarget, charToFind) {
if reqTarget == "docker.io" {
switch reqTarget {
case "docker.io":
target = dockerhubTarget
} else if reqTarget == "ghcr.io" {
case "ghcr.io":
target = ghcrTarget
} else {
default:
target = reqTarget
}
} else {
@ -132,11 +132,6 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn
return
}
//c.Request.Header.VisitAll(func(key, value []byte) {
// headerKey := string(key)
// headerValue := string(value)
// req.Header.Add(headerKey, headerValue)
//})
copyHeader(c.Request.Header, req.Header)
req.Header.Set("Host", target)
@ -154,8 +149,9 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn
return
}
// 处理状态码
if resp.StatusCode == 401 {
switch resp.StatusCode {
case 401:
// 请求target /v2/路径
if string(c.GetRequestURIPath()) != "/v2/" {
resp.Body.Close()
@ -181,13 +177,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn
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)
})
*/
copyHeader(c.Request.Header, req.Header)
req.Header.Set("Host", target)
@ -202,9 +192,20 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn
}
}
} else if resp.StatusCode == 404 { // 错误处理(404)
case 404: // 错误处理(404)
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
return
case 302, 301:
finalURL := resp.Header.Get("Location")
if finalURL != "" {
err = resp.Body.Close()
if err != nil {
c.Errorf("Failed to close response body: %v", err)
}
c.Infof("Internal Redirecting to %s", finalURL)
GhcrRequest(ctx, c, finalURL, image, cfg, target)
return
}
}
var (
@ -234,14 +235,6 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn
}
}
// 复制响应头,排除需要移除的 header
/*
for key, values := range resp.Header {
for _, value := range values {
c.Response.Header.Add(key, value)
}
}
*/
c.SetHeaders(resp.Header)
c.Status(resp.StatusCode)

View file

@ -20,6 +20,19 @@ func HandleError(c *touka.Context, message string) {
c.Errorf("%s %s %s %s %s Error: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, message)
}
func UnifiedToukaErrorHandler(c *touka.Context, code int, err error) {
errMsg := ""
if err != nil {
errMsg = err.Error()
}
c.Errorf("%s %s %s %s %s Error: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, errMsg)
constructedGHErr := NewErrorWithStatusLookup(code, errMsg)
ErrorPage(c, constructedGHErr)
}
type GHProxyErrors struct {
StatusCode int
StatusDesc string
@ -65,6 +78,25 @@ var (
StatusText: "服务器内部错误",
HelpInfo: "服务器处理您的请求时发生错误,请稍后重试或联系管理员。",
}
// 502
ErrBadGateway = &GHProxyErrors{
StatusCode: 502,
StatusDesc: "Bad Gateway",
StatusText: "网关错误",
HelpInfo: "代理服务器从上游服务器接收到无效响应。",
}
ErrServiceUnavailable = &GHProxyErrors{
StatusCode: 503,
StatusDesc: "Service Unavailable",
StatusText: "服务不可用",
HelpInfo: "服务器目前无法处理请求,通常是由于服务器过载或停机维护。",
}
ErrGatewayTimeout = &GHProxyErrors{
StatusCode: 504,
StatusDesc: "Gateway Timeout",
StatusText: "网关超时",
HelpInfo: "代理服务器未能及时从上游服务器接收到响应。",
}
)
var statusErrorMap map[int]*GHProxyErrors
@ -169,11 +201,11 @@ func NewSizedLRUCache(maxBytes int64) (*SizedLRUCache, error) {
// 当内部 LRU 缓存因其自身的条目容量限制或 RemoveOldest 方法被调用而逐出条目时,
// 此回调函数会被执行,从而更新 currentBytes。
var err error
c.cache, err = lru.NewWithEvict[string, []byte](10000, func(key string, value []byte) {
//c.cache, err = lru.NewWithEvict[string, []byte](10000, func(key string, value []byte) {
c.cache, err = lru.NewWithEvict(10000, func(key string, value []byte) {
c.mu.Lock()
defer c.mu.Unlock()
c.currentBytes -= int64(len(value))
//logDebug("LRU evicted key: %s, size: %d, current total: %d", key, len(value), c.currentBytes)
})
if err != nil {
return nil, err
@ -195,7 +227,6 @@ func (c *SizedLRUCache) Add(key string, value []byte) {
// 如果待添加的条目本身就大于缓存的最大容量,则不进行缓存。
if itemSize > c.maxBytes {
//c.Warnf("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes)
return
}
@ -203,23 +234,19 @@ func (c *SizedLRUCache) Add(key string, value []byte) {
if oldVal, ok := c.cache.Get(key); ok {
c.currentBytes -= int64(len(oldVal))
c.cache.Remove(key)
//logDebug("Key %s exists, removed old size %d. Current total: %d", key, len(oldVal), c.currentBytes)
}
// 主动逐出最旧的条目,直到有足够的空间容纳新条目。
for c.currentBytes+itemSize > c.maxBytes && c.cache.Len() > 0 {
_, _, existed := c.cache.RemoveOldest()
if !existed {
//c.Warnf("Attempted to remove oldest, but item not found.")
break
}
//logDebug("Proactively evicted item (size %d) to free space. Current total: %d", len(oldVal), c.currentBytes)
}
// 添加新条目到内部 LRU 缓存。
c.cache.Add(key, value)
c.currentBytes += itemSize // 手动增加新条目的大小到 currentBytes。
//logDebug("Item added: key %s, size: %d, current total: %d", key, itemSize, c.currentBytes)
}
const maxErrorPageCacheBytes = 512 * 1024 // 错误页面缓存的最大容量512KB
@ -231,7 +258,6 @@ func init() {
var err error
errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes)
if err != nil {
// logError("Failed to initialize error page LRU cache: %v", err)
panic(err)
}
}
@ -283,6 +309,16 @@ func htmlTemplateRender(data interface{}) ([]byte, error) {
}
func ErrorPage(c *touka.Context, errInfo *GHProxyErrors) {
select {
case <-c.Request.Context().Done():
return
default:
if c.Writer.Written() {
return
}
}
// 将 errInfo 转换为 ErrorPageData 结构体
var err error
var cacheKey string

View file

@ -30,7 +30,11 @@ func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config,
return
}
// 构建新url
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode()
var paramStr string
if len(queryParams) > 0 {
paramStr = "?" + queryParams.Encode()
}
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + paramStr
}
if cfg.GitClone.Mode == "cache" {

View file

@ -28,7 +28,6 @@ func init() {
gistPrefixLen = len(gistPrefix)
gistContentPrefixLen = len(gistContentPrefix)
apiPrefixLen = len(apiPrefix)
//log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen)
}
// Matcher 从原始URL路径中高效地解析并匹配代理规则.
@ -159,105 +158,6 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro
return "", "", "", NewErrorWithStatusLookup(404, "no matcher found for the given path")
}
// 原实现
/*
func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) {
var (
user string
repo string
matcher string
)
// 匹配 "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, "/")
//}
remainingPath = strings.TrimPrefix(remainingPath, "/")
// 预期格式/user/repo/more...
// 取出user和repo和最后部分
parts := strings.Split(remainingPath, "/")
if len(parts) <= 2 {
errMsg := "Not enough parts in path after matching 'https://github.com*'"
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
}
user = parts[0]
repo = parts[1]
// 匹配 "https://github.com"开头的链接
if len(parts) >= 3 {
switch parts[2] {
case "releases", "archive":
matcher = "releases"
case "blob":
matcher = "blob"
case "raw":
matcher = "raw"
case "info", "git-upload-pack":
matcher = "clone"
default:
errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher"
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
}
}
return user, repo, matcher, nil
}
// 匹配 "https://raw"开头的链接
if strings.HasPrefix(rawPath, "https://raw") {
remainingPath := strings.TrimPrefix(rawPath, "https://")
parts := strings.Split(remainingPath, "/")
if len(parts) <= 3 {
errMsg := "URL after matched 'https://raw*' should have at least 4 parts (user/repo/branch/file)."
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
}
user = parts[1]
repo = parts[2]
matcher = "raw"
return user, repo, matcher, nil
}
// 匹配 "https://gist"开头的链接
if strings.HasPrefix(rawPath, "https://gist") {
remainingPath := strings.TrimPrefix(rawPath, "https://")
parts := strings.Split(remainingPath, "/")
if len(parts) <= 3 {
errMsg := "URL after matched 'https://gist*' should have at least 4 parts (user/gist_id)."
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
}
user = parts[1]
repo = ""
matcher = "gist"
return user, repo, matcher, nil
}
// 匹配 "https://api.github.com/"开头的链接
if strings.HasPrefix(rawPath, "https://api.github.com/") {
matcher = "api"
remainingPath := strings.TrimPrefix(rawPath, "https://api.github.com/")
parts := strings.Split(remainingPath, "/")
if parts[0] == "repos" {
user = parts[1]
repo = parts[2]
}
if parts[0] == "users" {
user = parts[1]
}
if !cfg.Auth.ForceAllowApi {
if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
//return "", "", "", ErrAuthHeaderUnavailable
errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy"
return "", "", "", NewErrorWithStatusLookup(403, errMsg)
}
}
return user, repo, matcher, nil
}
//return "", "", "", ErrNotFound
errMsg := "Didn't match any matcher"
return "", "", "", NewErrorWithStatusLookup(404, errMsg)
}
*/
var (
proxyableMatchersMap map[string]struct{}
initMatchersOnce sync.Once