Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3917b2503a | ||
|
|
bb61eb5025 | ||
|
|
11c34459ca | ||
|
|
6659e977ae |
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -62,12 +62,13 @@ jobs:
|
|||||||
- name: 编译二进制文件
|
- name: 编译二进制文件
|
||||||
run: |
|
run: |
|
||||||
cd src
|
cd src
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
# Linux AMD64
|
# Linux AMD64
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ../build/hubproxy/hubproxy-linux-amd64 .
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.Version=${VERSION}" -o ../build/hubproxy/hubproxy-linux-amd64 .
|
||||||
|
|
||||||
# Linux ARM64
|
# Linux ARM64
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ../build/hubproxy/hubproxy-linux-arm64 .
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.Version=${VERSION}" -o ../build/hubproxy/hubproxy-linux-arm64 .
|
||||||
|
|
||||||
# 压缩二进制文件
|
# 压缩二进制文件
|
||||||
upx -9 ../build/hubproxy/hubproxy-linux-amd64
|
upx -9 ../build/hubproxy/hubproxy-linux-amd64
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
FROM golang:1.25-alpine AS builder
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
ARG VERSION=dev
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY src/go.mod src/go.sum ./
|
COPY src/go.mod src/go.sum ./
|
||||||
@@ -8,7 +9,7 @@ RUN go mod download && apk add upx
|
|||||||
|
|
||||||
COPY src/ .
|
COPY src/ .
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags="-s -w" -trimpath -o hubproxy . && upx -9 hubproxy
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags="-s -w -X main.Version=${VERSION}" -trimpath -o hubproxy . && upx -9 hubproxy
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -114,6 +114,8 @@ port = 5000
|
|||||||
fileSize = 2147483648
|
fileSize = 2147483648
|
||||||
# HTTP/2 多路复用,提升下载速度
|
# HTTP/2 多路复用,提升下载速度
|
||||||
enableH2C = false
|
enableH2C = false
|
||||||
|
# 是否启用前端静态页面
|
||||||
|
enableFrontend = true
|
||||||
|
|
||||||
[rateLimit]
|
[rateLimit]
|
||||||
# 每个IP每周期允许的请求数(注意Docker镜像会有多个层,会消耗多个次数)
|
# 每个IP每周期允许的请求数(注意Docker镜像会有多个层,会消耗多个次数)
|
||||||
@@ -204,6 +206,23 @@ defaultTTL = "20m"
|
|||||||
|
|
||||||
脚本部署配置文件位于 `/opt/hubproxy/config.toml`
|
脚本部署配置文件位于 `/opt/hubproxy/config.toml`
|
||||||
|
|
||||||
|
### 环境变量(可选)
|
||||||
|
|
||||||
|
支持通过环境变量覆盖部分配置,优先级高于`config.toml`,以下是默认值:
|
||||||
|
|
||||||
|
```
|
||||||
|
SERVER_HOST=0.0.0.0 # 监听地址
|
||||||
|
SERVER_PORT=5000 # 监听端口
|
||||||
|
ENABLE_H2C=false # 是否启用 H2C
|
||||||
|
ENABLE_FRONTEND=true # 是否启用前端静态页面
|
||||||
|
MAX_FILE_SIZE=2147483648 # GitHub 文件大小限制(字节)
|
||||||
|
RATE_LIMIT=500 # 每周期请求数
|
||||||
|
RATE_PERIOD_HOURS=3 # 限流周期(小时)
|
||||||
|
IP_WHITELIST=127.0.0.1,192.168.1.0/24 # IP 白名单(逗号分隔)
|
||||||
|
IP_BLACKLIST=192.168.100.1,192.168.100.0/24 # IP 黑名单(逗号分隔)
|
||||||
|
MAX_IMAGES=10 # 批量下载镜像数量限制
|
||||||
|
```
|
||||||
|
|
||||||
为了IP限流能够正常运行,反向代理需要传递IP头用来获取访客真实IP,以caddy为例:
|
为了IP限流能够正常运行,反向代理需要传递IP头用来获取访客真实IP,以caddy为例:
|
||||||
```
|
```
|
||||||
example.com {
|
example.com {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ port = 5000
|
|||||||
fileSize = 2147483648
|
fileSize = 2147483648
|
||||||
# HTTP/2 多路复用
|
# HTTP/2 多路复用
|
||||||
enableH2C = false
|
enableH2C = false
|
||||||
|
enableFrontend = true
|
||||||
|
|
||||||
[rateLimit]
|
[rateLimit]
|
||||||
# 每个IP每周期允许的请求数
|
# 每个IP每周期允许的请求数
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ type RegistryMapping struct {
|
|||||||
// AppConfig 应用配置结构体
|
// AppConfig 应用配置结构体
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Server struct {
|
Server struct {
|
||||||
Host string `toml:"host"`
|
Host string `toml:"host"`
|
||||||
Port int `toml:"port"`
|
Port int `toml:"port"`
|
||||||
FileSize int64 `toml:"fileSize"`
|
FileSize int64 `toml:"fileSize"`
|
||||||
EnableH2C bool `toml:"enableH2C"`
|
EnableH2C bool `toml:"enableH2C"`
|
||||||
|
EnableFrontend bool `toml:"enableFrontend"`
|
||||||
} `toml:"server"`
|
} `toml:"server"`
|
||||||
|
|
||||||
RateLimit struct {
|
RateLimit struct {
|
||||||
@@ -70,15 +71,17 @@ var (
|
|||||||
func DefaultConfig() *AppConfig {
|
func DefaultConfig() *AppConfig {
|
||||||
return &AppConfig{
|
return &AppConfig{
|
||||||
Server: struct {
|
Server: struct {
|
||||||
Host string `toml:"host"`
|
Host string `toml:"host"`
|
||||||
Port int `toml:"port"`
|
Port int `toml:"port"`
|
||||||
FileSize int64 `toml:"fileSize"`
|
FileSize int64 `toml:"fileSize"`
|
||||||
EnableH2C bool `toml:"enableH2C"`
|
EnableH2C bool `toml:"enableH2C"`
|
||||||
|
EnableFrontend bool `toml:"enableFrontend"`
|
||||||
}{
|
}{
|
||||||
Host: "0.0.0.0",
|
Host: "0.0.0.0",
|
||||||
Port: 5000,
|
Port: 5000,
|
||||||
FileSize: 2 * 1024 * 1024 * 1024, // 2GB
|
FileSize: 2 * 1024 * 1024 * 1024,
|
||||||
EnableH2C: false, // 默认关闭H2C
|
EnableH2C: false,
|
||||||
|
EnableFrontend: true,
|
||||||
},
|
},
|
||||||
RateLimit: struct {
|
RateLimit: struct {
|
||||||
RequestLimit int `toml:"requestLimit"`
|
RequestLimit int `toml:"requestLimit"`
|
||||||
@@ -227,6 +230,11 @@ func overrideFromEnv(cfg *AppConfig) {
|
|||||||
cfg.Server.EnableH2C = enable
|
cfg.Server.EnableH2C = enable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if val := os.Getenv("ENABLE_FRONTEND"); val != "" {
|
||||||
|
if enable, err := strconv.ParseBool(val); err == nil {
|
||||||
|
cfg.Server.EnableFrontend = enable
|
||||||
|
}
|
||||||
|
}
|
||||||
if val := os.Getenv("MAX_FILE_SIZE"); val != "" {
|
if val := os.Getenv("MAX_FILE_SIZE"); val != "" {
|
||||||
if size, err := strconv.ParseInt(val, 10, 64); err == nil && size > 0 {
|
if size, err := strconv.ParseInt(val, 10, 64); err == nil && size > 0 {
|
||||||
cfg.Server.FileSize = size
|
cfg.Server.FileSize = size
|
||||||
|
|||||||
@@ -274,7 +274,9 @@ func handleBlobRequest(c *gin.Context, imageRef, digest string) {
|
|||||||
c.Header("Docker-Content-Digest", digest)
|
c.Header("Docker-Content-Digest", digest)
|
||||||
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
io.Copy(c.Writer, reader)
|
if _, err := io.Copy(c.Writer, reader); err != nil {
|
||||||
|
fmt.Printf("复制layer内容失败: %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleTagsRequest 处理tags列表请求
|
// handleTagsRequest 处理tags列表请求
|
||||||
@@ -416,7 +418,9 @@ func proxyDockerAuthOriginal(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Status(resp.StatusCode)
|
c.Status(resp.StatusCode)
|
||||||
io.Copy(c.Writer, resp.Body)
|
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
|
||||||
|
fmt.Printf("复制认证响应失败: %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rewriteAuthHeader 重写认证头
|
// rewriteAuthHeader 重写认证头
|
||||||
@@ -569,7 +573,9 @@ func handleUpstreamBlobRequest(c *gin.Context, imageRef, digest string, mapping
|
|||||||
c.Header("Docker-Content-Digest", digest)
|
c.Header("Docker-Content-Digest", digest)
|
||||||
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
io.Copy(c.Writer, reader)
|
if _, err := io.Copy(c.Writer, reader); err != nil {
|
||||||
|
fmt.Printf("复制layer内容失败: %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleUpstreamTagsRequest 处理上游Registry的tags请求
|
// handleUpstreamTagsRequest 处理上游Registry的tags请求
|
||||||
|
|||||||
@@ -227,6 +227,8 @@ func proxyGitHubWithRedirect(c *gin.Context, u string, redirectCount int) {
|
|||||||
c.Status(resp.StatusCode)
|
c.Status(resp.StatusCode)
|
||||||
|
|
||||||
// 直接流式转发
|
// 直接流式转发
|
||||||
io.Copy(c.Writer, resp.Body)
|
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
|
||||||
|
fmt.Printf("转发响应体失败: %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -160,51 +159,6 @@ func init() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterSearchResults(results []Repository, query string) []Repository {
|
|
||||||
searchTerm := strings.ToLower(strings.TrimPrefix(query, "library/"))
|
|
||||||
filtered := make([]Repository, 0)
|
|
||||||
|
|
||||||
for _, repo := range results {
|
|
||||||
repoName := strings.ToLower(repo.Name)
|
|
||||||
repoDesc := strings.ToLower(repo.Description)
|
|
||||||
|
|
||||||
score := 0
|
|
||||||
|
|
||||||
if repoName == searchTerm {
|
|
||||||
score += 100
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(repoName, searchTerm) {
|
|
||||||
score += 50
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(repoName, searchTerm) {
|
|
||||||
score += 30
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(repoDesc, searchTerm) {
|
|
||||||
score += 10
|
|
||||||
}
|
|
||||||
|
|
||||||
if repo.IsOfficial {
|
|
||||||
score += 20
|
|
||||||
}
|
|
||||||
|
|
||||||
if score > 0 {
|
|
||||||
filtered = append(filtered, repo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(filtered, func(i, j int) bool {
|
|
||||||
if filtered[i].IsOfficial != filtered[j].IsOfficial {
|
|
||||||
return filtered[i].IsOfficial
|
|
||||||
}
|
|
||||||
return filtered[i].PullCount > filtered[j].PullCount
|
|
||||||
})
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalizeRepository 统一规范化仓库信息
|
// normalizeRepository 统一规范化仓库信息
|
||||||
func normalizeRepository(repo *Repository) {
|
func normalizeRepository(repo *Repository) {
|
||||||
if repo.IsOfficial {
|
if repo.IsOfficial {
|
||||||
@@ -487,10 +441,14 @@ func parsePaginationParams(c *gin.Context, defaultPageSize int) (page, pageSize
|
|||||||
pageSize = defaultPageSize
|
pageSize = defaultPageSize
|
||||||
|
|
||||||
if p := c.Query("page"); p != "" {
|
if p := c.Query("page"); p != "" {
|
||||||
fmt.Sscanf(p, "%d", &page)
|
if _, err := fmt.Sscanf(p, "%d", &page); err != nil {
|
||||||
|
fmt.Printf("解析page参数失败: %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ps := c.Query("page_size"); ps != "" {
|
if ps := c.Query("page_size"); ps != "" {
|
||||||
fmt.Sscanf(ps, "%d", &pageSize)
|
if _, err := fmt.Sscanf(ps, "%d", &pageSize); err != nil {
|
||||||
|
fmt.Printf("解析page_size参数失败: %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return page, pageSize
|
return page, pageSize
|
||||||
|
|||||||
134
src/main.go
134
src/main.go
@@ -40,6 +40,82 @@ var (
|
|||||||
serviceStartTime = time.Now()
|
serviceStartTime = time.Now()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
|
func buildRouter(cfg *config.AppConfig) *gin.Engine {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
// 全局Panic恢复保护
|
||||||
|
router.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
|
||||||
|
log.Printf("🚨 Panic recovered: %v", recovered)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": "Internal server error",
|
||||||
|
"code": "INTERNAL_ERROR",
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 全局限流中间件
|
||||||
|
router.Use(utils.RateLimitMiddleware(globalLimiter))
|
||||||
|
|
||||||
|
// 初始化监控端点
|
||||||
|
initHealthRoutes(router)
|
||||||
|
|
||||||
|
// 初始化镜像tar下载路由
|
||||||
|
handlers.InitImageTarRoutes(router)
|
||||||
|
|
||||||
|
if cfg.Server.EnableFrontend {
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
serveEmbedFile(c, "public/index.html")
|
||||||
|
})
|
||||||
|
router.GET("/public/*filepath", func(c *gin.Context) {
|
||||||
|
filepath := strings.TrimPrefix(c.Param("filepath"), "/")
|
||||||
|
serveEmbedFile(c, "public/"+filepath)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.GET("/images.html", func(c *gin.Context) {
|
||||||
|
serveEmbedFile(c, "public/images.html")
|
||||||
|
})
|
||||||
|
router.GET("/search.html", func(c *gin.Context) {
|
||||||
|
serveEmbedFile(c, "public/search.html")
|
||||||
|
})
|
||||||
|
router.GET("/favicon.ico", func(c *gin.Context) {
|
||||||
|
serveEmbedFile(c, "public/favicon.ico")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.Status(http.StatusNotFound)
|
||||||
|
})
|
||||||
|
router.GET("/public/*filepath", func(c *gin.Context) {
|
||||||
|
c.Status(http.StatusNotFound)
|
||||||
|
})
|
||||||
|
router.GET("/images.html", func(c *gin.Context) {
|
||||||
|
c.Status(http.StatusNotFound)
|
||||||
|
})
|
||||||
|
router.GET("/search.html", func(c *gin.Context) {
|
||||||
|
c.Status(http.StatusNotFound)
|
||||||
|
})
|
||||||
|
router.GET("/favicon.ico", func(c *gin.Context) {
|
||||||
|
c.Status(http.StatusNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册dockerhub搜索路由
|
||||||
|
handlers.RegisterSearchRoute(router)
|
||||||
|
|
||||||
|
// 注册Docker认证路由
|
||||||
|
router.Any("/token", handlers.ProxyDockerAuthGin)
|
||||||
|
router.Any("/token/*path", handlers.ProxyDockerAuthGin)
|
||||||
|
|
||||||
|
// 注册Docker Registry代理路由
|
||||||
|
router.Any("/v2/*path", handlers.ProxyDockerRegistryGin)
|
||||||
|
|
||||||
|
// 注册GitHub代理路由(NoRoute处理器)
|
||||||
|
router.NoRoute(handlers.GitHubProxyHandler)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 加载配置
|
// 加载配置
|
||||||
if err := config.LoadConfig(); err != nil {
|
if err := config.LoadConfig(); err != nil {
|
||||||
@@ -62,60 +138,9 @@ func main() {
|
|||||||
// 初始化防抖器
|
// 初始化防抖器
|
||||||
handlers.InitDebouncer()
|
handlers.InitDebouncer()
|
||||||
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
router := gin.Default()
|
|
||||||
|
|
||||||
// 全局Panic恢复保护
|
|
||||||
router.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
|
|
||||||
log.Printf("🚨 Panic recovered: %v", recovered)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"error": "Internal server error",
|
|
||||||
"code": "INTERNAL_ERROR",
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 全局限流中间件
|
|
||||||
router.Use(utils.RateLimitMiddleware(globalLimiter))
|
|
||||||
|
|
||||||
// 初始化监控端点
|
|
||||||
initHealthRoutes(router)
|
|
||||||
|
|
||||||
// 初始化镜像tar下载路由
|
|
||||||
handlers.InitImageTarRoutes(router)
|
|
||||||
|
|
||||||
// 静态文件路由
|
|
||||||
router.GET("/", func(c *gin.Context) {
|
|
||||||
serveEmbedFile(c, "public/index.html")
|
|
||||||
})
|
|
||||||
router.GET("/public/*filepath", func(c *gin.Context) {
|
|
||||||
filepath := strings.TrimPrefix(c.Param("filepath"), "/")
|
|
||||||
serveEmbedFile(c, "public/"+filepath)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.GET("/images.html", func(c *gin.Context) {
|
|
||||||
serveEmbedFile(c, "public/images.html")
|
|
||||||
})
|
|
||||||
router.GET("/search.html", func(c *gin.Context) {
|
|
||||||
serveEmbedFile(c, "public/search.html")
|
|
||||||
})
|
|
||||||
router.GET("/favicon.ico", func(c *gin.Context) {
|
|
||||||
serveEmbedFile(c, "public/favicon.ico")
|
|
||||||
})
|
|
||||||
|
|
||||||
// 注册dockerhub搜索路由
|
|
||||||
handlers.RegisterSearchRoute(router)
|
|
||||||
|
|
||||||
// 注册Docker认证路由
|
|
||||||
router.Any("/token", handlers.ProxyDockerAuthGin)
|
|
||||||
router.Any("/token/*path", handlers.ProxyDockerAuthGin)
|
|
||||||
|
|
||||||
// 注册Docker Registry代理路由
|
|
||||||
router.Any("/v2/*path", handlers.ProxyDockerRegistryGin)
|
|
||||||
|
|
||||||
// 注册GitHub代理路由(NoRoute处理器)
|
|
||||||
router.NoRoute(handlers.GitHubProxyHandler)
|
|
||||||
|
|
||||||
cfg := config.GetConfig()
|
cfg := config.GetConfig()
|
||||||
|
router := buildRouter(cfg)
|
||||||
|
|
||||||
fmt.Printf("HubProxy 启动成功\n")
|
fmt.Printf("HubProxy 启动成功\n")
|
||||||
fmt.Printf("监听地址: %s:%d\n", cfg.Server.Host, cfg.Server.Port)
|
fmt.Printf("监听地址: %s:%d\n", cfg.Server.Host, cfg.Server.Port)
|
||||||
fmt.Printf("限流配置: %d请求/%g小时\n", cfg.RateLimit.RequestLimit, cfg.RateLimit.PeriodHours)
|
fmt.Printf("限流配置: %d请求/%g小时\n", cfg.RateLimit.RequestLimit, cfg.RateLimit.PeriodHours)
|
||||||
@@ -125,7 +150,7 @@ func main() {
|
|||||||
fmt.Printf("H2c: 已启用\n")
|
fmt.Printf("H2c: 已启用\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("版本号: v1.2.1\n")
|
fmt.Printf("版本号: %s\n", Version)
|
||||||
fmt.Printf("项目地址: https://github.com/sky22333/hubproxy\n")
|
fmt.Printf("项目地址: https://github.com/sky22333/hubproxy\n")
|
||||||
|
|
||||||
// 创建HTTP2服务器
|
// 创建HTTP2服务器
|
||||||
@@ -182,6 +207,7 @@ func initHealthRoutes(router *gin.Engine) {
|
|||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"ready": true,
|
"ready": true,
|
||||||
"service": "hubproxy",
|
"service": "hubproxy",
|
||||||
|
"version": Version,
|
||||||
"start_time_unix": serviceStartTime.Unix(),
|
"start_time_unix": serviceStartTime.Unix(),
|
||||||
"uptime_sec": uptimeSec,
|
"uptime_sec": uptimeSec,
|
||||||
"uptime_human": uptimeHuman,
|
"uptime_human": uptimeHuman,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"hubproxy/config"
|
"hubproxy/config"
|
||||||
)
|
)
|
||||||
@@ -17,7 +16,6 @@ const (
|
|||||||
|
|
||||||
// AccessController 统一访问控制器
|
// AccessController 统一访问控制器
|
||||||
type AccessController struct {
|
type AccessController struct {
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerImageInfo Docker镜像信息
|
// DockerImageInfo Docker镜像信息
|
||||||
|
|||||||
@@ -176,8 +176,9 @@ func (i *IPRateLimiter) GetLimiter(ip string) (*rate.Limiter, bool) {
|
|||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
|
var entry *rateLimiterEntry
|
||||||
i.mu.RLock()
|
i.mu.RLock()
|
||||||
entry, exists := i.ips[normalizedIP]
|
_, exists := i.ips[normalizedIP]
|
||||||
i.mu.RUnlock()
|
i.mu.RUnlock()
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
|
|||||||
Reference in New Issue
Block a user