diff --git a/.github/workflows/build-nocache.yml b/.github/workflows/build-nocache.yml new file mode 100644 index 0000000..8f84886 --- /dev/null +++ b/.github/workflows/build-nocache.yml @@ -0,0 +1,51 @@ +name: Build NoCache Docker Image + +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - 'VERSION' + +jobs: + docker: + runs-on: ubuntu-latest + env: + IMAGE_NAME: wjqserver/ghproxy # 定义镜像名称变量 + DOCKERFILE: docker/dockerfile/nocache/Dockerfile # 定义 Dockerfile 路径变量 + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Load VERSION + run: | + if [ -f VERSION ]; then + echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV + else + echo "VERSION file not found!" && exit 1 + fi + - name: Wait for Compile + run: sleep 300s + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: 构建镜像 + uses: docker/build-push-action@v6 + with: + file: ./${{ env.DOCKERFILE }} + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ env.IMAGE_NAME }}:${{ env.VERSION }}-nocache + ${{ env.IMAGE_NAME }}:nocache diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e2e96..22d7583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +24w19d +--- +- PRE-RELEASE: 此版本是v1.6.1的预发布版本,请勿在生产环境中使用 +- ADD: 新增nocache版本,供由用户自行优化缓存策略 +- CHANGE: 优化`Proxy`核心模块内部结构,提升性能 +- REMOVE: 移除`Proxy`模块内部分无用`logInfo` + 24w19c --- - PRE-RELEASE: 此版本是v1.6.1的预发布版本,请勿在生产环境中使用 diff --git a/caddyfile/nocache/Caddyfile b/caddyfile/nocache/Caddyfile new file mode 100644 index 0000000..14f5bc5 --- /dev/null +++ b/caddyfile/nocache/Caddyfile @@ -0,0 +1,99 @@ +{ + debug + http_port 80 + https_port 443 + order cache before rewrite + cache { + cache_name GhProxyCache + } + log { + level INFO + output file /data/caddy/log/caddy.log { + roll_size 5MB + roll_keep 10 + } + } + server :80 { + protocols h1 h2 h2c + } +} + + +(log) { + log { + format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` { + time_format "02/Jan/2006:15:04:05 -0700" + } + output file /data/caddy/log/{args[0]}/access.log { + roll_size 5MB + roll_keep 10 + roll_keep_for 24h + } + } +} + +(error_page) { + handle_errors { + rewrite * /{err.status_code}.html + root * /data/caddy/pages/errors + file_server + } +} + +(encode) { + encode { + zstd best + br 5 v2 + gzip 5 + minimum_length 256 + } +} + +(cache) { + cache { + allowed_http_verbs GET + stale {args[0]} + ttl {args[1]} + } +} + +(header_realip) { + header_up X-Real-IP {remote_host} + header_up X-Real-IP {http.request.header.CF-Connecting-IP} + header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} + header_up X-Forwarded-Proto {http.request.header.CF-Visitor} +} + +(rate_limit) { + route /* { + rate_limit {remote.ip} {args[0]}r/m 10000 429 + } +} + +:80 { + reverse_proxy { + to h2c://127.0.0.1:8080 + import header_realip + } + import log ghproxy + import error_page + import encode + import rate_limit 60 + route / { + root /data/www + file_server + import cache 300s + } + route /favicon.ico { + root /data/www + file_server + import cache 300s + } + + route /api* { + rate_limit {remote.ip} 15r/m 10000 429 + import cache 300s + } +} + +import /data/caddy/config.d/* diff --git a/docker/dockerfile/nocache/Dockerfile b/docker/dockerfile/nocache/Dockerfile new file mode 100644 index 0000000..31680a6 --- /dev/null +++ b/docker/dockerfile/nocache/Dockerfile @@ -0,0 +1,49 @@ +FROM wjqserver/caddy:2.9.0-rc-alpine AS builder + +ARG USER=WJQSERVER-STUDIO +ARG REPO=ghproxy +ARG APPLICATION=ghproxy +ARG TARGETOS +ARG TARGETARCH +ARG TARGETPLATFORM + +# 创建文件夹 +RUN mkdir -p /data/www +RUN mkdir -p /data/${APPLICATION}/config +RUN mkdir -p /data/${APPLICATION}/log + +# 安装依赖 +RUN apk add --no-cache curl wget + +# 前端 +RUN wget -O /data/www/index.html https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/index.html +RUN wget -O /data/www/favicon.ico https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/favicon.ico + +# 后端 +RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/main/VERSION) && \ + wget -O /data/${APPLICATION}/${APPLICATION} https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH} +RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/docker/dockerfile/nocache/init.sh + +# 拉取配置 +RUN wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/nocache/Caddyfile +RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/main/config/config.toml +RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/blacklist.json +RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/whitelist.json + +# 权限 +RUN chmod +x /data/${APPLICATION}/${APPLICATION} +RUN chmod +x /usr/local/bin/init.sh + +FROM wjqserver/caddy:2.9.0-rc-alpine + +COPY --from=builder /data/www /data/www +COPY --from=builder /data/caddy /data/caddy +COPY --from=builder /data/${APPLICATION} /data/${APPLICATION} +COPY --from=builder /usr/local/bin/init.sh /usr/local/bin/init.sh + +# 权限 +RUN chmod +x /data/${APPLICATION}/${APPLICATION} +RUN chmod +x /usr/local/bin/init.sh + +CMD ["/usr/local/bin/init.sh"] + diff --git a/docker/dockerfile/nocache/init.sh b/docker/dockerfile/nocache/init.sh new file mode 100644 index 0000000..34d3477 --- /dev/null +++ b/docker/dockerfile/nocache/init.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +APPLICATION=ghproxy + +if [ ! -f /data/caddy/config/Caddyfile ]; then + cp /data/caddy/Caddyfile /data/caddy/config/Caddyfile +fi + +if [ ! -f /data/${APPLICATION}/config/blacklist.json ]; then + cp /data/${APPLICATION}/blacklist.json /data/${APPLICATION}/config/blacklist.json +fi + +if [ ! -f /data/${APPLICATION}/config/whitelist.json ]; then + cp /data/${APPLICATION}/whitelist.json /data/${APPLICATION}/config/whitelist.json +fi + +if [ ! -f /data/${APPLICATION}/config/config.toml ]; then + cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml +fi + +/data/caddy/caddy run --config /data/caddy/config/Caddyfile > /data/${APPLICATION}/log/caddy.log 2>&1 & + +/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1 & + +while true; do + sleep 1 +done \ No newline at end of file diff --git a/logger/logger.go b/logger/logger.go index 81aa5a3..5eac8e8 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -22,7 +22,7 @@ var ( logFilePath = "/data/ghproxy/log/ghproxy.log" ) -// 初始化,接受日志文件路径作为参数 +// 初始化 func Init(logFilePath_input string, maxLogsize int) error { logFileMutex.Lock() defer logFileMutex.Unlock() @@ -121,7 +121,6 @@ func rotateLogFile(logFilePath string) error { } } - // 打开当前日志文件 logFile, err := os.Open(logFilePath) if err != nil { return fmt.Errorf("failed to open log file: %s, error: %w", logFilePath, err) @@ -168,7 +167,6 @@ func rotateLogFile(logFilePath string) error { return fmt.Errorf("failed to truncate log file: %s, error: %w", logFilePath, err) } - // 重新打开日志文件 logFile, err = os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { return fmt.Errorf("failed to reopen log file: %s, error: %w", logFilePath, err) diff --git a/proxy/proxy.go b/proxy/proxy.go index 12b157c..a511dca 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -94,10 +94,8 @@ func NoRouteHandler(cfg *config.Config) gin.HandlerFunc { switch { case exps[0].MatchString(rawPath), exps[1].MatchString(rawPath), exps[3].MatchString(rawPath), exps[4].MatchString(rawPath): - logInfo("%s Matched - USE proxy-chrome", rawPath) ProxyRequest(c, rawPath, cfg, "chrome") case exps[2].MatchString(rawPath): - logInfo("%s Matched - USE proxy-git", rawPath) ProxyRequest(c, rawPath, cfg, "git") default: c.String(http.StatusForbidden, "Invalid input.") @@ -106,7 +104,7 @@ func NoRouteHandler(cfg *config.Config) gin.HandlerFunc { } } -// 提取用户名和仓库名,格式为 handle///* +// 提取用户名和仓库名 func MatchUserRepo(rawPath string, cfg *config.Config, c *gin.Context, matches []string) (string, string) { var gistregex = regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.*`) var gistmatches []string @@ -115,16 +113,16 @@ func MatchUserRepo(rawPath string, cfg *config.Config, c *gin.Context, matches [ logInfo("Gist Matched > Username: %s, URL: %s", gistmatches[1], rawPath) return gistmatches[1], "" } - pathmatches := regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`) - pathParts := pathmatches.FindStringSubmatch(matches[2]) - - if len(pathParts) < 4 { - logWarning("Invalid path: %s", rawPath) - c.String(http.StatusForbidden, "Invalid path; expected username/repo.") - return "", "" - } else { - return pathParts[2], pathParts[3] + // 定义路径匹配的正则表达式 + pathRegex := regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`) + if pathMatches := pathRegex.FindStringSubmatch(matches[2]); len(pathMatches) >= 4 { + return pathMatches[2], pathMatches[3] } + + // 返回错误信息 + logWarning("Invalid path: %s", rawPath) + c.String(http.StatusForbidden, "Invalid path; expected username/repo.") + return "", "" } func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string) { @@ -166,7 +164,7 @@ func createHTTPClient(mode string) *req.Client { client := req.C() switch mode { case "chrome": - client.SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"). + client.SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"). SetTLSFingerprintChrome(). ImpersonateChrome() case "git": @@ -175,7 +173,7 @@ func createHTTPClient(mode string) *req.Client { return client } -// readRequestBody 读取请求体 +// 读取请求体 func readRequestBody(c *gin.Context) ([]byte, error) { body, err := io.ReadAll(c.Request.Body) if err != nil { @@ -185,7 +183,7 @@ func readRequestBody(c *gin.Context) ([]byte, error) { return body, nil } -// setRequestHeaders 设置请求头 +// 设置请求头 func setRequestHeaders(c *gin.Context, req *req.Request) { for key, values := range c.Request.Header { for _, value := range values { @@ -194,7 +192,7 @@ func setRequestHeaders(c *gin.Context, req *req.Request) { } } -// copyResponseBody 复制响应体到客户端 +// 复制响应体 func copyResponseBody(c *gin.Context, respBody io.Reader) error { _, err := io.Copy(c.Writer, respBody) return err @@ -242,7 +240,7 @@ func CopyResponseHeaders(resp *req.Response, c *gin.Context, cfg *config.Config) setDefaultHeaders(c) } -// removeHeaders 移除指定的响应头 +// 移除指定响应头 func removeHeaders(resp *req.Response) { headersToRemove := map[string]struct{}{ "Content-Security-Policy": {}, @@ -255,7 +253,7 @@ func removeHeaders(resp *req.Response) { } } -// copyHeaders 复制响应头到 Gin 上下文 +// 复制响应头 func copyHeaders(resp *req.Response, c *gin.Context) { for key, values := range resp.Header { for _, value := range values { @@ -264,7 +262,7 @@ func copyHeaders(resp *req.Response, c *gin.Context) { } } -// setCORSHeaders 设置 CORS 相关的响应头 +// CORS配置 func setCORSHeaders(c *gin.Context, cfg *config.Config) { if cfg.CORS.Enabled { c.Header("Access-Control-Allow-Origin", "*") @@ -273,7 +271,7 @@ func setCORSHeaders(c *gin.Context, cfg *config.Config) { } } -// setDefaultHeaders 设置默认的响应头 +// 默认响应 func setDefaultHeaders(c *gin.Context) { c.Header("Age", "10") c.Header("Cache-Control", "max-age=300")