4.0.0-beta.0

This commit is contained in:
wjqserver 2025-06-16 08:28:02 +08:00
parent 91c3ad7fd8
commit a4d324a361
38 changed files with 497 additions and 1428 deletions

View file

@ -34,7 +34,7 @@ func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, er
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)
//logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue)
continue
}
key := strings.TrimSpace(keyValue[0])

View file

@ -4,20 +4,19 @@ import (
"ghproxy/config"
"net/http"
"github.com/cloudwego/hertz/pkg/app"
"github.com/infinite-iroha/touka"
)
func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Request) {
func AuthPassThrough(c *touka.Context, cfg *config.Config, req *http.Request) {
if cfg.Auth.PassThrough {
token := c.Query("token")
if token != "" {
logDebug("%s %s %s %s %s Auth-PassThrough: token %s", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol(), token)
switch cfg.Auth.Method {
case "parameters":
if !cfg.Auth.Enabled {
req.Header.Set("Authorization", "token "+token)
} else {
logWarning("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol())
c.Warnf("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto)
ErrorPage(c, NewErrorWithStatusLookup(500, "Conflict Auth Method"))
return
}
@ -26,7 +25,7 @@ func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Reques
req.Header.Set("Authorization", "token "+token)
}
default:
logWarning("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol())
c.Warnf("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto)
ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid Auth Method / Auth Method is not be set"))
return
}

View file

@ -15,7 +15,6 @@ var (
func UnDefiendRateStringErrHandle(err error) error {
if errors.Is(err, &limitreader.UnDefiendRateStringErr{}) {
logWarning("UnDefiendRateStringErr: %s", err)
return nil
}
return err
@ -28,18 +27,15 @@ func SetGlobalRateLimit(cfg *config.Config) error {
var totalBurst rate.Limit
totalLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalLimit)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse total bandwidth limit: %v", err)
return err
}
totalBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalBurst)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse total bandwidth burst: %v", err)
return err
}
limitreader.SetGlobalRateLimit(totalLimit, int(totalBurst))
err = SetBandwidthLimit(cfg)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to set bandwidth limit: %v", err)
return err
}
} else {
@ -52,12 +48,10 @@ func SetBandwidthLimit(cfg *config.Config) error {
var err error
bandwidthLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleLimit)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse bandwidth limit: %v", err)
return err
}
bandwidthBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleBurst)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse bandwidth burst: %v", err)
return err
}
return nil

View file

@ -9,10 +9,10 @@ import (
"strconv"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app"
"github.com/infinite-iroha/touka"
)
func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
func ChunkedProxyRequest(ctx context.Context, c *touka.Context, u string, cfg *config.Config, matcher string) {
var (
req *http.Request
@ -23,18 +23,16 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
go func() {
<-ctx.Done()
if resp != nil && resp.Body != nil {
err := resp.Body.Close()
if err != nil {
logError("Failed to close response body: %v", err)
}
resp.Body.Close()
}
if req != nil && req.Body != nil {
req.Body.Close()
}
c.Abort()
}()
rb := client.NewRequestBuilder(string(c.Request.Method()), u)
rb := client.NewRequestBuilder(c.Request.Method, u)
rb.NoDefaultHeaders()
//rb.SetBody(bytes.NewBuffer(c.Request.Body()))
rb.SetBody(c.RequestBodyStream())
rb.SetBody(c.Request.Body)
rb.WithContext(ctx)
req, err = rb.Build()
@ -60,19 +58,21 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
// 处理302情况
if resp.StatusCode == 302 || resp.StatusCode == 301 {
//c.Debugf("resp header %s", resp.Header)
finalURL := resp.Header.Get("Location")
if finalURL != "" {
err = resp.Body.Close()
if err != nil {
logError("Failed to close response body: %v", err)
c.Errorf("Failed to close response body: %v", err)
}
c.Request.Header.Del("Referer")
logInfo("Internal Redirecting to %s", finalURL)
c.Infof("Internal Redirecting to %s", finalURL)
ChunkedProxyRequest(ctx, c, finalURL, cfg, matcher)
return
}
}
// 处理响应体大小限制
var (
bodySize int
contentLength string
@ -84,17 +84,17 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
var err error
bodySize, err = strconv.Atoi(contentLength)
if err != nil {
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err)
c.Warnf("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, err)
bodySize = -1
}
if err == nil && bodySize > sizelimit {
finalURL := resp.Request.URL.String()
err = resp.Body.Close()
if err != nil {
logError("Failed to close response body: %v", err)
c.Errorf("Failed to close response body: %v", err)
}
c.Redirect(301, []byte(finalURL))
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize)
c.Redirect(301, finalURL)
c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, bodySize)
return
}
}
@ -127,6 +127,8 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
}
defer bodyReader.Close()
if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor {
// 判断body是不是gzip
var compress string
@ -134,26 +136,26 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
compress = "gzip"
}
logDebug("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol())
c.Debugf("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), c.Request.Method, u, c.UserAgent(), c.Request.Proto)
c.Header("Content-Length", "")
var reader io.Reader
reader, _, err = processLinks(bodyReader, compress, string(c.Request.Host()), cfg)
c.SetBodyStream(reader, -1)
reader, _, err = processLinks(bodyReader, compress, c.Request.Host, cfg, c)
c.WriteStream(reader)
if err != nil {
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), err)
c.Errorf("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method, u, c.UserAgent(), c.Request.Proto, err)
ErrorPage(c, NewErrorWithStatusLookup(500, fmt.Sprintf("Failed to copy response body: %v", err)))
return
}
} else {
if contentLength != "" {
c.SetBodyStream(bodyReader, bodySize)
c.SetHeader("Content-Length", contentLength)
c.WriteStream(bodyReader)
return
}
c.SetBodyStream(bodyReader, -1)
bodyReader.Close()
c.WriteStream(bodyReader)
}
}

View file

@ -7,6 +7,7 @@ package proxy
import (
"ghproxy/config"
"log"
"net/http"
"net/url"
"strings"
@ -24,7 +25,8 @@ func initTransport(cfg *config.Config, transport *http.Transport) {
// 如果代理 URL 未设置,使用环境变量中的代理配置
if cfg.Outbound.Url == "" {
transport.Proxy = http.ProxyFromEnvironment
logWarning("Outbound proxy is not set, using environment variables")
//logWarning("Outbound proxy is not set, using environment variables")
log.Printf("Outbound proxy is not set, using environment variables")
return
}
@ -32,7 +34,7 @@ func initTransport(cfg *config.Config, transport *http.Transport) {
proxyInfo, err := url.Parse(cfg.Outbound.Url)
if err != nil {
// 如果解析失败,记录错误日志并使用环境变量中的代理配置
logError("Failed to parse outbound proxy URL %v", err)
log.Printf("Failed to parse outbound proxy URL %v", err)
transport.Proxy = http.ProxyFromEnvironment
return
}
@ -41,7 +43,7 @@ func initTransport(cfg *config.Config, transport *http.Transport) {
switch strings.ToLower(proxyInfo.Scheme) {
case "http", "https": // 如果是 HTTP/HTTPS 代理
transport.Proxy = http.ProxyURL(proxyInfo) // 设置 HTTP(S) 代理
logInfo("Using HTTP(S) proxy: %s", proxyInfo.Redacted())
log.Printf("Using HTTP(S) proxy: %s", cfg.Outbound.Url)
case "socks5": // 如果是 SOCKS5 代理
// 调用 newProxyDial 创建 SOCKS5 代理拨号器
proxyDialer := newProxyDial(cfg.Outbound.Url)
@ -53,11 +55,14 @@ func initTransport(cfg *config.Config, transport *http.Transport) {
} else {
// 如果不支持 ContextDialer则回退到传统的 Dial 方法
transport.Dial = proxyDialer.Dial
logWarning("SOCKS5 dialer does not support ContextDialer, using legacy Dial")
//logWarning("SOCKS5 dialer does not support ContextDialer, using legacy Dial")
log.Printf("SOCKS5 dialer does not support ContextDialer, using legacy Dial")
}
logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url)
//logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url)
log.Printf("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url)
default: // 如果代理协议不支持
logError("Unsupported proxy scheme: %s", proxyInfo.Scheme)
//logError("Unsupported proxy scheme: %s", proxyInfo.Scheme)
log.Printf("Unsupported proxy scheme: %s", proxyInfo.Scheme)
transport.Proxy = http.ProxyFromEnvironment // 回退到环境变量代理
}
}
@ -77,13 +82,15 @@ func newProxyDial(proxyUrls string) proxy.Dialer {
urlInfo, err := url.Parse(proxyUrl)
if err != nil {
// 如果 URL 解析失败,记录错误日志并跳过
logError("Failed to parse proxy URL %q: %v", proxyUrl, err)
//logError("Failed to parse proxy URL %q: %v", proxyUrl, err)
log.Printf("Failed to parse proxy URL %q: %v", proxyUrl, err)
continue
}
// 检查代理协议是否为 SOCKS5
if urlInfo.Scheme != "socks5" {
logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme)
// logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme)
log.Printf("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme)
continue
}
@ -94,7 +101,8 @@ func newProxyDial(proxyUrls string) proxy.Dialer {
dialer, err := createSocksDialer(urlInfo.Host, auth, proxyDialer)
if err != nil {
// 如果创建失败,记录错误日志并跳过
logError("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err)
//logError("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err)
log.Printf("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err)
continue
}

View file

@ -2,9 +2,10 @@ package proxy
import (
"context"
"encoding/json"
"fmt"
json "github.com/bytedance/sonic"
"github.com/infinite-iroha/touka"
"ghproxy/config"
"ghproxy/weakcache"
@ -14,7 +15,6 @@ import (
"strings"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app"
)
var (
@ -35,8 +35,8 @@ func InitWeakCache() *weakcache.Cache[string] {
return cache
}
func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc {
return func(c *touka.Context) {
charToFind := '.'
reqTarget := c.Param("target")
@ -57,7 +57,7 @@ func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
target = reqTarget
}
} else {
path = string(c.Request.RequestURI())
path = c.GetRequestURI()
reqImageUser = c.Param("target")
reqImageName = c.Param("user")
}
@ -67,24 +67,25 @@ func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
}
GhcrToTarget(ctx, c, cfg, target, path, image)
GhcrToTarget(c, cfg, target, path, image)
}
}
func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) {
func GhcrToTarget(c *touka.Context, cfg *config.Config, target string, path string, image *imageInfo) {
if cfg.Docker.Enabled {
var ctx = c.Request.Context()
if target != "" {
GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+string(c.Request.QueryString()), image, cfg, target)
GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+c.GetReqQueryString(), image, cfg, target)
} else {
if cfg.Docker.Target == "ghcr" {
GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget)
GhcrRequest(ctx, c, "https://"+ghcrTarget+c.GetRequestURI(), image, cfg, ghcrTarget)
} else if cfg.Docker.Target == "dockerhub" {
GhcrRequest(ctx, c, "https://"+dockerhubTarget+string(c.Request.RequestURI()), image, cfg, dockerhubTarget)
GhcrRequest(ctx, c, "https://"+dockerhubTarget+c.GetRequestURI(), image, cfg, dockerhubTarget)
} else if cfg.Docker.Target != "" {
// 自定义taget
GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), image, cfg, cfg.Docker.Target)
GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+c.GetRequestURI(), image, cfg, cfg.Docker.Target)
} else {
// 配置为空
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set"))
@ -98,10 +99,10 @@ func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config
}
}
func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *imageInfo, cfg *config.Config, target string) {
func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageInfo, cfg *config.Config, target string) {
var (
method []byte
method string
req *http.Request
resp *http.Response
err error
@ -117,11 +118,11 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
}
}()
method = c.Request.Method()
method = c.Request.Method
rb := ghcrclient.NewRequestBuilder(string(method), u)
rb := ghcrclient.NewRequestBuilder(method, u)
rb.NoDefaultHeaders()
rb.SetBody(c.Request.BodyStream())
rb.SetBody(c.Request.Body)
rb.WithContext(ctx)
req, err = rb.Build()
@ -130,17 +131,18 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
return
}
c.Request.Header.VisitAll(func(key, value []byte) {
headerKey := string(key)
headerValue := string(value)
req.Header.Add(headerKey, headerValue)
})
//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)
if image != nil {
token, exist := cache.Get(image.Image)
if exist {
logDebug("Use Cache Token: %s", token)
c.Debugf("Use Cache Token: %s", token)
req.Header.Set("Authorization", "Bearer "+token)
}
}
@ -154,7 +156,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
// 处理状态码
if resp.StatusCode == 401 {
// 请求target /v2/路径
if string(c.Request.URI().Path()) != "/v2/" {
if string(c.GetRequestURIPath()) != "/v2/" {
resp.Body.Close()
if image == nil {
ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized"))
@ -164,13 +166,13 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
// 更新kv
if token != "" {
logDump("Update Cache Token: %s", token)
c.Debugf("Update Cache Token: %s", token)
cache.Put(image.Image, token)
}
rb := ghcrclient.NewRequestBuilder(string(method), u)
rb.NoDefaultHeaders()
rb.SetBody(c.Request.BodyStream())
rb.SetBody(c.Request.Body)
rb.WithContext(ctx)
req, err = rb.Build()
@ -178,12 +180,14 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
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)
})
/*
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)
if token != "" {
@ -214,27 +218,30 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im
var err error
bodySize, err = strconv.Atoi(contentLength)
if err != nil {
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err)
c.Warnf("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, err)
bodySize = -1
}
if err == nil && bodySize > sizelimit {
finalURL := resp.Request.URL.String()
err = resp.Body.Close()
if err != nil {
logError("Failed to close response body: %v", err)
c.Errorf("Failed to close response body: %v", err)
}
c.Redirect(301, []byte(finalURL))
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize)
c.Redirect(301, finalURL)
c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, bodySize)
return
}
}
// 复制响应头,排除需要移除的 header
for key, values := range resp.Header {
for _, value := range values {
c.Response.Header.Add(key, value)
/*
for key, values := range resp.Header {
for _, value := range values {
c.Response.Header.Add(key, value)
}
}
}
*/
copyHeader(resp.Header, c.GetAllReqHeader())
c.Status(resp.StatusCode)
@ -256,7 +263,7 @@ type AuthToken struct {
Token string `json:"token"`
}
func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) {
func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka.Context) (token string) {
var resp401 *http.Response
var req401 *http.Request
var err error
@ -280,7 +287,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R
defer resp401.Body.Close()
bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
if err != nil {
logError("Failed to parse Www-Authenticate header: %v", err)
c.Errorf("Failed to parse Www-Authenticate header: %v", err)
return
}
@ -296,13 +303,13 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R
getAuthReq, err := getAuthRB.Build()
if err != nil {
logError("Failed to create request: %v", err)
c.Errorf("Failed to create request: %v", err)
return
}
authResp, err := ghcrclient.Do(getAuthReq)
if err != nil {
logError("Failed to send request: %v", err)
c.Errorf("Failed to send request: %v", err)
return
}
@ -310,7 +317,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R
bodyBytes, err := io.ReadAll(authResp.Body)
if err != nil {
logError("Failed to read auth response body: %v", err)
c.Errorf("Failed to read auth response body: %v", err)
return
}
@ -318,7 +325,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R
var authToken AuthToken
err = json.Unmarshal(bodyBytes, &authToken)
if err != nil {
logError("Failed to decode auth response body: %v", err)
c.Errorf("Failed to decode auth response body: %v", err)
return
}
token = authToken.Token

View file

@ -11,24 +11,13 @@ import (
"html/template"
"io/fs"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/cloudwego/hertz/pkg/app"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/infinite-iroha/touka"
)
// 日志模块
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
func HandleError(c *app.RequestContext, message string) {
func HandleError(c *touka.Context, message string) {
ErrorPage(c, NewErrorWithStatusLookup(500, message))
logError("Error handled: %s", message)
c.Errorf("%s %s %s %s %s Error: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, message)
}
type GHProxyErrors struct {
@ -131,18 +120,18 @@ type ErrorPageData struct {
// ToCacheKey 为 ErrorPageData 生成一个唯一的 SHA256 字符串键。
// 使用 gob 序列化来确保结构体内容到字节序列的顺序一致性,然后计算哈希。
func (d ErrorPageData) ToCacheKey() string {
func (d ErrorPageData) ToCacheKey() (string, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(d)
if err != nil {
logError("Failed to gob encode ErrorPageData for cache key: %v", err)
return ""
//logError("Failed to gob encode ErrorPageData for cache key: %v", err)
return "", fmt.Errorf("failed to gob encode ErrorPageData for cache key: %w", err)
}
hasher := sha256.New()
hasher.Write(buf.Bytes())
return hex.EncodeToString(hasher.Sum(nil))
return hex.EncodeToString(hasher.Sum(nil)), nil
}
func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData {
@ -184,7 +173,7 @@ func NewSizedLRUCache(maxBytes int64) (*SizedLRUCache, error) {
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)
//logDebug("LRU evicted key: %s, size: %d, current total: %d", key, len(value), c.currentBytes)
})
if err != nil {
return nil, err
@ -206,7 +195,7 @@ func (c *SizedLRUCache) Add(key string, value []byte) {
// 如果待添加的条目本身就大于缓存的最大容量,则不进行缓存。
if itemSize > c.maxBytes {
logWarning("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes)
//c.Warnf("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes)
return
}
@ -214,23 +203,23 @@ 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)
//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 {
_, oldVal, existed := c.cache.RemoveOldest()
_, _, existed := c.cache.RemoveOldest()
if !existed {
logWarning("Attempted to remove oldest, but item not found.")
//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)
//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)
//logDebug("Item added: key %s, size: %d, current total: %d", key, itemSize, c.currentBytes)
}
const maxErrorPageCacheBytes = 512 * 1024 // 错误页面缓存的最大容量512KB
@ -242,7 +231,7 @@ func init() {
var err error
errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes)
if err != nil {
logError("Failed to initialize error page LRU cache: %v", err)
// logError("Failed to initialize error page LRU cache: %v", err)
panic(err)
}
}
@ -293,37 +282,50 @@ func htmlTemplateRender(data interface{}) ([]byte, error) {
return buf.Bytes(), nil
}
func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) {
func ErrorPage(c *touka.Context, errInfo *GHProxyErrors) {
// 将 errInfo 转换为 ErrorPageData 结构体
var err error
var cacheKey string
pageDataStruct := ErrPageUnwarper(errInfo)
// 使用 ErrorPageData 生成一个唯一的 SHA256 缓存键
cacheKey := pageDataStruct.ToCacheKey()
cacheKey, err = pageDataStruct.ToCacheKey()
if err != nil {
c.Warnf("Failed to generate cache key for error page: %v", err)
fallbackErrorJson(c, errInfo)
return
}
// 检查生成的缓存键是否为空,这可能表示序列化或哈希计算失败
if cacheKey == "" {
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
logWarning("Failed to generate cache key for error page: %v", errInfo)
c.Warnf("Failed to generate cache key for error page: %v", errInfo)
return
}
var pageData []byte
var err error
// 尝试从缓存中获取页面数据
if cachedPage, found := errorPageCache.Get(cacheKey); found {
pageData = cachedPage
logDebug("Serving error page from cache (Key: %s)", cacheKey)
c.Debugf("Serving error page from cache (Key: %s)", cacheKey)
} else {
// 如果不在缓存中,则渲染页面
pageData, err = htmlTemplateRender(pageDataStruct)
if err != nil {
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
logWarning("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err)
c.Warnf("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err)
return
}
// 将渲染结果存入缓存
errorPageCache.Add(cacheKey, pageData)
logDebug("Cached error page (Key: %s, Size: %d bytes)", cacheKey, len(pageData))
c.Debugf("Cached error page (Key: %s, Size: %d bytes)", cacheKey, len(pageData))
}
c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData)
c.Raw(errInfo.StatusCode, "text/html; charset=utf-8", pageData)
}
func fallbackErrorJson(c *touka.Context, errInfo *GHProxyErrors) {
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
}

View file

@ -1,7 +1,6 @@
package proxy
import (
"bytes"
"context"
"fmt"
"ghproxy/config"
@ -9,30 +8,36 @@ import (
"strconv"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app"
"github.com/infinite-iroha/touka"
)
func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) {
func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, mode string) {
var (
req *http.Request
resp *http.Response
err error
)
go func() {
<-ctx.Done()
if resp != nil && resp.Body != nil {
err = resp.Body.Close()
if err != nil {
logError("Failed to close response body: %v", err)
}
resp.Body.Close()
}
}()
method := string(c.Request.Method())
/*
fullBody, err := c.GetReqBodyFull()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to read request body: %v", err))
return
}
reqBodyReader := bytes.NewBuffer(fullBody)
*/
reqBodyReader := bytes.NewBuffer(c.Request.Body())
reqBodyReader, err := c.GetReqBodyBuffer()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to read request body: %v", err))
return
}
//bodyReader := c.Request.BodyStream() // 不可替换为此实现
@ -47,12 +52,12 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
}
if cfg.GitClone.Mode == "cache" {
rb := gitclient.NewRequestBuilder(method, u)
rb := gitclient.NewRequestBuilder(c.Request.Method, u)
rb.NoDefaultHeaders()
rb.SetBody(reqBodyReader)
rb.WithContext(ctx)
req, err = rb.Build()
req, err := rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
@ -66,8 +71,9 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
defer resp.Body.Close()
} else {
rb := client.NewRequestBuilder(string(c.Request.Method()), u)
rb := client.NewRequestBuilder(c.Request.Method, u)
rb.NoDefaultHeaders()
rb.SetBody(reqBodyReader)
rb.WithContext(ctx)
@ -86,6 +92,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
defer resp.Body.Close()
}
contentLength := resp.Header.Get("Content-Length")
@ -93,21 +100,25 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
size, err := strconv.Atoi(contentLength)
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
if err != nil {
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err)
c.Warnf("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, err)
}
if err == nil && size > sizelimit {
finalURL := []byte(resp.Request.URL.String())
finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), finalURL, size)
c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, size)
return
}
}
for key, values := range resp.Header {
for _, value := range values {
c.Response.Header.Add(key, value)
/*
for key, values := range resp.Header {
for _, value := range values {
c.Response.Header.Add(key, value)
}
}
}
*/
//copyHeader( resp.Header)
c.SetHeaders(resp.Header)
headersToRemove := map[string]struct{}{
"Content-Security-Policy": {},
@ -132,17 +143,20 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
c.Status(resp.StatusCode)
if cfg.GitClone.Mode == "cache" {
c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate")
c.Response.Header.Set("Pragma", "no-cache")
c.Response.Header.Set("Expires", "0")
c.SetHeader("Cache-Control", "no-store, no-cache, must-revalidate")
c.SetHeader("Pragma", "no-cache")
c.SetHeader("Expires", "0")
}
bodyReader := resp.Body
// 读取body内容
//bodyContent, _ := io.ReadAll(bodyReader)
// c.Infof("%s", bodyContent)
if cfg.RateLimit.BandwidthLimit.Enabled {
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
}
c.SetBodyStream(bodyReader, -1)
bodyReader.Close()
}

View file

@ -1,39 +1,37 @@
package proxy
import (
"context"
"fmt"
"ghproxy/config"
"ghproxy/rate"
"regexp"
"strings"
"github.com/cloudwego/hertz/pkg/app"
"github.com/infinite-iroha/touka"
)
var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
func NoRouteHandler(cfg *config.Config) touka.HandlerFunc {
return func(c *touka.Context) {
var ctx = c.Request.Context()
var shoudBreak bool
shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
if shoudBreak {
return
}
// shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
// if shoudBreak {
// return
// }
var (
rawPath string
matches []string
)
rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
matches = re.FindStringSubmatch(rawPath) // 匹配路径
rawPath = strings.TrimPrefix(c.GetRequestURI(), "/") // 去掉前缀/
matches = re.FindStringSubmatch(rawPath) // 匹配路径
// 匹配路径错误处理
if len(matches) < 3 {
logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.Path())))
c.Warnf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto)
ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.GetRequestURI())))
return
}
@ -53,9 +51,6 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
return
}
logDump("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
logDump("%s", c.Request.Header.Header())
shoudBreak = listCheck(cfg, c, user, repo, rawPath)
if shoudBreak {
return
@ -74,8 +69,6 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
matcher = "raw"
}
logDebug("Matched: %v", matcher)
switch matcher {
case "releases", "blob", "raw", "gist", "api":
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
@ -83,7 +76,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
GitReq(ctx, c, rawPath, cfg, "git")
default:
ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher)
c.Errorf("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.GetRequestURIPath(), rawPath, matcher)
return
}
}

View file

@ -39,7 +39,7 @@ func initHTTPClient(cfg *config.Config) {
proTolcols.SetHTTP1(true)
proTolcols.SetHTTP2(true)
proTolcols.SetUnencryptedHTTP2(true)
if cfg.Httpc.Mode == "auto" {
if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" {
tr = &http.Transport{
IdleConnTimeout: 30 * time.Second,
@ -57,16 +57,7 @@ func initHTTPClient(cfg *config.Config) {
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")
tr = &http.Transport{
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
}
panic("unknown httpc mode: " + cfg.Httpc.Mode)
}
if cfg.Outbound.Enabled {
initTransport(cfg, tr)
@ -86,7 +77,7 @@ func initHTTPClient(cfg *config.Config) {
func initGitHTTPClient(cfg *config.Config) {
if cfg.Httpc.Mode == "auto" {
if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" {
gittr = &http.Transport{
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
@ -101,17 +92,7 @@ func initGitHTTPClient(cfg *config.Config) {
ReadBufferSize: 32 * 1024, // 32KB
}
} 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")
gittr = &http.Transport{
//MaxIdleConns: 160,
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
}
panic("unknown httpc mode: " + cfg.Httpc.Mode)
}
if cfg.Outbound.Enabled {
initTransport(cfg, gittr)
@ -157,7 +138,7 @@ func initGhcrHTTPClient(cfg *config.Config) {
var proTolcols = new(http.Protocols)
proTolcols.SetHTTP1(true)
proTolcols.SetHTTP2(true)
if cfg.Httpc.Mode == "auto" {
if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" {
ghcrtr = &http.Transport{
IdleConnTimeout: 30 * time.Second,
@ -175,16 +156,7 @@ func initGhcrHTTPClient(cfg *config.Config) {
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
}
panic(fmt.Sprintf("unknown httpc mode: %s", cfg.Httpc.Mode))
}
if cfg.Outbound.Enabled {
initTransport(cfg, ghcrtr)

View file

@ -26,8 +26,8 @@ func init() {
githubPrefixLen = len(githubPrefix)
rawPrefixLen = len(rawPrefix)
gistPrefixLen = len(gistPrefix)
apiPrefixLen = len(apiPrefix)
gistContentPrefixLen = len(gistContentPrefix)
apiPrefixLen = len(apiPrefix)
//log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen)
}

View file

@ -7,6 +7,8 @@ import (
"ghproxy/config"
"io"
"strings"
"github.com/infinite-iroha/touka"
)
func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) {
@ -52,21 +54,19 @@ func modifyURL(url string, host string, cfg *config.Config) string {
// 去除url内的https://或http://
matched, err := EditorMatcher(url, cfg)
if err != nil {
logDump("Invalid URL: %s", url)
return url
}
if matched {
var u = url
u = strings.TrimPrefix(u, "https://")
u = strings.TrimPrefix(u, "http://")
logDump("Modified URL: %s", "https://"+host+"/"+u)
return "https://" + host + "/" + u
}
return url
}
// processLinks 处理链接,返回包含处理后数据的 io.Reader
func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) {
func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config, c *touka.Context) (readerOut io.Reader, written int64, err error) {
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
readerOut = pipeReader
@ -75,11 +75,11 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config
if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误
if err != nil {
if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader
logError("pipeWriter close with error failed: %v, original error: %v", closeErr, err)
c.Errorf("pipeWriter close with error failed: %v, original error: %v", closeErr, err)
}
} else {
if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭
logError("pipeWriter close failed: %v", closeErr)
c.Errorf("pipeWriter close failed: %v", closeErr)
if err == nil { // 如果之前没有错误,记录关闭错误
err = closeErr
}
@ -90,7 +90,7 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config
defer func() {
if err := input.Close(); err != nil {
logError("input close failed: %v", err)
c.Errorf("input close failed: %v", err)
}
}()
@ -127,7 +127,7 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config
if gzipWriter != nil {
if closeErr = gzipWriter.Close(); closeErr != nil {
logError("gzipWriter close failed %v", closeErr)
c.Errorf("gzipWriter close failed %v", closeErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = closeErr
@ -135,7 +135,7 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config
}
}
if flushErr := bufWriter.Flush(); flushErr != nil {
logError("writer flush failed %v", flushErr)
c.Errorf("writer flush failed %v", flushErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = flushErr
@ -156,7 +156,6 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config
// 替换所有匹配的 URL
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
logDump("originalURL: %s", originalURL)
return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义
})

View file

@ -4,7 +4,7 @@ import (
"ghproxy/config"
"net/http"
"github.com/cloudwego/hertz/pkg/app"
"github.com/infinite-iroha/touka"
)
var (
@ -59,28 +59,19 @@ func copyHeader(dst, src http.Header) {
}
}
func setRequestHeaders(c *app.RequestContext, req *http.Request, cfg *config.Config, matcher string) {
func setRequestHeaders(c *touka.Context, req *http.Request, cfg *config.Config, matcher string) {
if matcher == "raw" && cfg.Httpc.UseCustomRawHeaders {
// 使用预定义Header
for key, value := range defaultHeaders {
req.Header.Set(key, value)
}
} else if matcher == "clone" {
c.Request.Header.VisitAll(func(key, value []byte) {
headerKey := string(key)
headerValue := string(value)
req.Header.Set(headerKey, headerValue)
})
copyHeader(req.Header, c.Request.Header)
for key := range cloneHeadersToRemove {
req.Header.Del(key)
}
} else {
c.Request.Header.VisitAll(func(key, value []byte) {
headerKey := string(key)
headerValue := string(value)
req.Header.Set(headerKey, headerValue)
})
copyHeader(req.Header, c.Request.Header)
for key := range reqHeadersToRemove {
req.Header.Del(key)
}

View file

@ -1,42 +1,43 @@
package proxy
import (
"context"
"ghproxy/config"
"ghproxy/rate"
"strings"
"github.com/cloudwego/hertz/pkg/app"
"github.com/infinite-iroha/touka"
)
func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
func RoutingHandler(cfg *config.Config) touka.HandlerFunc {
return func(c *touka.Context) {
var shoudBreak bool
shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
if shoudBreak {
return
}
// shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
// if shoudBreak {
// return
//}
var (
rawPath string
)
rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
rawPath = strings.TrimPrefix(c.GetRequestURI(), "/") // 去掉前缀/
var (
user string
repo string
matcher string
user string
repo string
)
user = c.Param("user")
repo = c.Param("repo")
matcher = c.GetString("matcher")
matcher, exists := c.GetString("matcher")
if !exists {
ErrorPage(c, NewErrorWithStatusLookup(500, "Matcher Not Found in Context"))
c.Errorf("Matcher Not Found in Context Path: %s", c.GetRequestURIPath())
return
}
logDump("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
logDump("%s", c.Request.Header.Header())
ctx := c.Request.Context()
shoudBreak = listCheck(cfg, c, user, repo, rawPath)
if shoudBreak {
@ -48,7 +49,6 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
return
}
// 处理blob/raw路径
// 处理blob/raw路径
if matcher == "blob" {
rawPath = rawPath[10:]
@ -60,8 +60,6 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
// 为rawpath加入https:// 头
rawPath = "https://" + rawPath
logDebug("Matched: %v", matcher)
switch matcher {
case "releases", "blob", "raw", "gist", "api":
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
@ -69,7 +67,7 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
GitReq(ctx, c, rawPath, cfg, "git")
default:
ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher)
c.Errorf("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.GetRequestURIPath(), rawPath, matcher)
return
}
}

View file

@ -4,12 +4,11 @@ import (
"fmt"
"ghproxy/auth"
"ghproxy/config"
"ghproxy/rate"
"github.com/cloudwego/hertz/pkg/app"
"github.com/infinite-iroha/touka"
)
func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) bool {
func listCheck(cfg *config.Config, c *touka.Context, user string, repo string, rawPath string) bool {
if cfg.Auth.ForceAllowApi && cfg.Auth.ForceAllowApiPassList {
return false
}
@ -18,7 +17,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
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)
c.Infof("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, user, repo)
return true
}
}
@ -28,7 +27,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
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)
c.Infof("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, user, repo)
return true
}
}
@ -37,13 +36,13 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
}
// 鉴权
func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPath string) bool {
func authCheck(c *touka.Context, cfg *config.Config, matcher string, rawPath string) bool {
var err error
if matcher == "api" && !cfg.Auth.ForceAllowApi {
if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
ErrorPage(c, NewErrorWithStatusLookup(403, "Github API Req without AuthHeader is Not Allowed"))
logInfo("%s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath)
c.Infof("%s %s %s AuthHeader Unavailable", c.ClientIP(), c.Request.Method, rawPath)
return true
}
}
@ -54,34 +53,7 @@ func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPat
authcheck, err = auth.AuthHandler(c, cfg)
if !authcheck {
ErrorPage(c, NewErrorWithStatusLookup(401, fmt.Sprintf("Unauthorized: %v", err)))
logInfo("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err)
return true
}
}
return false
}
func rateCheck(cfg *config.Config, c *app.RequestContext, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) bool {
// 限制访问频率
if cfg.RateLimit.Enabled {
var allowed bool
switch cfg.RateLimit.RateMethod {
case "ip":
allowed = iplimiter.Allow(c.ClientIP())
case "total":
allowed = limiter.Allow()
default:
logWarning("Invalid RateLimit Method")
ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid RateLimit Method"))
return true
}
if !allowed {
ErrorPage(c, NewErrorWithStatusLookup(429, fmt.Sprintf("Too Many Requests; Rate Limit is %d per minute", cfg.RateLimit.RatePerMinute)))
logInfo("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Infof("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, err)
return true
}
}