From 2072a4b146f05b2ef4b1926e9beabdf2673078bb Mon Sep 17 00:00:00 2001 From: Awen Date: Wed, 16 Apr 2025 12:29:07 +0800 Subject: [PATCH 1/6] first commit code --- .dockerignore | 8 + .github/.github/docker.yml | 75 ++ .gitignore | 2 + .idea/.gitignore | 8 + .idea/go-captcha-service.iml | 9 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + Dockerfile | 29 + Makefile | 158 ++++ README.md | 4 +- cmd/go-captcha-service/main.go | 415 +++++++++ config.json | 19 + docker-compose.yml | 52 ++ docs/cache.md | 0 docs/openapi.yaml | 0 ecosystem.config.js | 22 + go.mod | 118 +++ go.sum | 836 +++++++++++++++++ internal/adapt/capt.go | 42 + internal/cache/cache.go | 18 + internal/cache/etcd_client.go | 82 ++ internal/cache/etcd_client_test.go | 37 + internal/cache/memcache_client.go | 59 ++ internal/cache/memcache_client_test.go | 32 + internal/cache/memory_cache.go | 97 ++ internal/cache/memory_cache_test.go | 46 + internal/cache/redis_client.go | 58 ++ internal/cache/redis_client_test.go | 35 + internal/common/consts.go | 22 + internal/common/svc_context.go | 19 + internal/config/config.go | 305 ++++++ internal/config/config_test.go | 128 +++ internal/helper/helper.go | 70 ++ internal/load_balancer/consistent_hash.go | 24 + internal/load_balancer/load_balancer.go | 10 + internal/load_balancer/load_balancer_test.go | 43 + internal/load_balancer/round_robin.go | 26 + internal/logic/click.go | 151 +++ internal/logic/click_test.go | 101 ++ internal/logic/common.go | 88 ++ internal/logic/rotate.go | 134 +++ internal/logic/slide.go | 144 +++ internal/middleware/grpc_middleware.go | 69 ++ internal/middleware/http_niddleware.go | 216 +++++ internal/middleware/middleware_test.go | 220 +++++ internal/pkg/gocaptcha/click.go | 69 ++ internal/pkg/gocaptcha/gocaptcha.go | 51 + internal/pkg/gocaptcha/rotate.go | 28 + internal/pkg/gocaptcha/slide.go | 78 ++ internal/server/grpc_server.go | 198 ++++ internal/server/grpc_server_test.go | 66 ++ internal/server/http_handler.go | 283 ++++++ internal/server/http_handler_test.go | 100 ++ .../service_discovery/consul_discovery.go | 76 ++ internal/service_discovery/etcd_discovery.go | 100 ++ internal/service_discovery/nacos_discovery.go | 93 ++ .../service_discovery/service_discovery.go | 22 + .../service_discovery_test.go | 26 + .../service_discovery/zookeeper_discovery.go | 95 ++ modd.conf | 4 + proto/api.pb.go | 875 ++++++++++++++++++ proto/api.proto | 79 ++ proto/api_grpc.pb.go | 257 +++++ 63 files changed, 6543 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/.github/docker.yml create mode 100644 .idea/.gitignore create mode 100644 .idea/go-captcha-service.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 cmd/go-captcha-service/main.go create mode 100644 config.json create mode 100644 docker-compose.yml create mode 100644 docs/cache.md create mode 100644 docs/openapi.yaml create mode 100644 ecosystem.config.js create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/adapt/capt.go create mode 100644 internal/cache/cache.go create mode 100644 internal/cache/etcd_client.go create mode 100644 internal/cache/etcd_client_test.go create mode 100644 internal/cache/memcache_client.go create mode 100644 internal/cache/memcache_client_test.go create mode 100644 internal/cache/memory_cache.go create mode 100644 internal/cache/memory_cache_test.go create mode 100644 internal/cache/redis_client.go create mode 100644 internal/cache/redis_client_test.go create mode 100644 internal/common/consts.go create mode 100644 internal/common/svc_context.go create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/helper/helper.go create mode 100644 internal/load_balancer/consistent_hash.go create mode 100644 internal/load_balancer/load_balancer.go create mode 100644 internal/load_balancer/load_balancer_test.go create mode 100644 internal/load_balancer/round_robin.go create mode 100644 internal/logic/click.go create mode 100644 internal/logic/click_test.go create mode 100644 internal/logic/common.go create mode 100644 internal/logic/rotate.go create mode 100644 internal/logic/slide.go create mode 100644 internal/middleware/grpc_middleware.go create mode 100644 internal/middleware/http_niddleware.go create mode 100644 internal/middleware/middleware_test.go create mode 100644 internal/pkg/gocaptcha/click.go create mode 100644 internal/pkg/gocaptcha/gocaptcha.go create mode 100644 internal/pkg/gocaptcha/rotate.go create mode 100644 internal/pkg/gocaptcha/slide.go create mode 100644 internal/server/grpc_server.go create mode 100644 internal/server/grpc_server_test.go create mode 100644 internal/server/http_handler.go create mode 100644 internal/server/http_handler_test.go create mode 100644 internal/service_discovery/consul_discovery.go create mode 100644 internal/service_discovery/etcd_discovery.go create mode 100644 internal/service_discovery/nacos_discovery.go create mode 100644 internal/service_discovery/service_discovery.go create mode 100644 internal/service_discovery/service_discovery_test.go create mode 100644 internal/service_discovery/zookeeper_discovery.go create mode 100644 modd.conf create mode 100644 proto/api.pb.go create mode 100644 proto/api.proto create mode 100644 proto/api_grpc.pb.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3719cf4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.gitignore +*.md +*.log +tmp/ +vendor/ +*.swp +*.swo \ No newline at end of file diff --git a/.github/.github/docker.yml b/.github/.github/docker.yml new file mode 100644 index 0000000..01ef381 --- /dev/null +++ b/.github/.github/docker.yml @@ -0,0 +1,75 @@ +name: Build and Push Docker Image and Binaries + +on: + push: + branches: + - main + tags: + - 'v*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Install Protoc + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + + - name: Run Tests + run: make test + + - name: Generate Coverage + run: make cover + + - name: Upload Coverage + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage.html + + - name: Build and Package Binaries + run: | + make proto + make package VERSION=${{ github.ref_name }} + + - name: Upload Binaries + uses: actions/upload-artifact@v4 + with: + name: binaries + path: build/packages/ + + - 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.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker (binary) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:latest + ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:amd64 + ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:arm64 + ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:armv7 + ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:${{ github.ref_name }} diff --git a/.gitignore b/.gitignore index 6f72f89..1e7d993 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,11 @@ # Test binary, built with `go test -c` *.test +*.cache # Output of the go coverage tool, specifically when used with LiteIDE *.out +*build # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/go-captcha-service.iml b/.idea/go-captcha-service.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/go-captcha-service.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8bb0faf --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8129b20 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Build phase +FROM --platform=$BUILDPLATFORM golang:1.21 AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +ARG TARGETOS +ARG TARGETARCH +RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s" -o go-captcha-service ./cmd/go-captcha-service + +# Run phase (default binary) +FROM scratch AS binary + +WORKDIR /app + +COPY --from=builder /app/go-captcha-service . +COPY config.json . +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +EXPOSE 8080 50051 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD ["/app/go-captcha-service", "--health-check"] || exit 1 + +CMD ["/app/go-captcha-service"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2d0d74c --- /dev/null +++ b/Makefile @@ -0,0 +1,158 @@ +# Makefile for go-captcha-service project + +# Variables +BINARY_NAME=go-captcha-service +VERSION?=0.1.0 +BUILD_DIR=build +PLATFORMS=linux/amd64 linux/arm64 linux/arm/v7 darwin/amd64 darwin/arm64 windows/amd64 +DOCKER_IMAGE?=wenlng/go-captcha-service +GO=go +GOFLAGS=-ldflags="-w -s" -v -a -gcflags=-trimpath=$(PWD) -asmflags=-trimpath=$(PWD) +COPY_BUILD_FILES=config.json ecosystem.config.js + +# Default Target +.PHONY: all +all: build + +# Install Dependencies +.PHONY: deps +deps: + $(GO) mod tidy + $(GO) mod download + npm install -g pm2 + @if ! command -v protoc >/dev/null; then \ + echo "Installing protoc..."; \ + $(GO) install github.com/golang/protobuf/protoc-gen-go@latest; \ + fi + @if ! command -v grpcurl >/dev/null; then \ + echo "Installing grpcurl..."; \ + $(GO) install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest; \ + fi + @if ! command -v yq >/dev/null; then \ + echo "Installing yq..."; \ + $(GO) install github.com/mikefarah/yq/v4@latest; \ + fi + @if ! command -v modd >/dev/null; then \ + echo "Installing modd..."; \ + $(GO) install github.com/cortesi/modd/cmd/modd@latest; \ + fi + +# Generate gRPC code +.PHONY: proto +proto: + protoc --go_out=. --go-grpc_out=. proto/api.proto + +.PHONY: start-dev +start-dev: + modd -f modd.conf + @echo "Starting modd successfully" + +# Build the application +.PHONY: build +build: proto + $(GO) build $(GOFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/go-captcha-service + +# Cross-platform construction +.PHONY: build-multi +build-multi: proto + @mkdir -p $(BUILD_DIR) + @for platform in $(PLATFORMS); do \ + os=$$(echo $$platform | cut -d'/' -f1); \ + arch=$$(echo $$platform | cut -d'/' -f2); \ + output=$(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch; \ + if [ "$$os" = "windows" ]; then output=$$output.exe; fi; \ + echo "Building $$os/$$arch..."; \ + CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build $(GOFLAGS) -o $$output ./cmd/go-captcha-service || exit 1; \ + done + +# Packaging Binaries +.PHONY: package +package: build-multi + @mkdir -p $(BUILD_DIR)/packages + @for platform in $(PLATFORMS); do \ + os=$$(echo $$platform | cut -d'/' -f1); \ + arch=$$(echo $$platform | cut -d'/' -f2); \ + binary=$(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch; \ + if [ "$$os" = "windows" ]; then binary=$$binary.exe; fi; \ + package=$(BUILD_DIR)/packages/$(BINARY_NAME)-$(VERSION)-$$os-$$arch.tar.gz; \ + echo "Packaging $$os/$$arch..."; \ + tar -czf $$package -C $(BUILD_DIR) $(BINARY_NAME)-$$os-$$arch config.json ecosystem.config.js; \ + done + +# Run tests +.PHONY: test +test: proto + $(GO) test -v ./... + +# Coverage report +.PHONY: cover +cover: proto + $(GO) test -cover -coverprofile=coverage.out ./... + $(GO) tool cover -html=coverage.out -o coverage.html + +# Clean up +.PHONY: clean +clean: + rm -rf $(BINARY_NAME) $(BUILD_DIR) coverage.out coverage.html testdata.etcd* + rm -rf bin + rm -f proto/*.pb.go + rm -f docs/openapi.json + +# Format code +fmt: + $(GO) fmt ./... + +# Local Docker build (binary) +.PHONY: docker-build +docker-build: + docker build -t $(DOCKER_IMAGE):latest . + +# Multi-architecture Docker build and push (binary) +.PHONY: docker-build-multi +docker-build-multi: + docker buildx build \ + --platform linux/amd64,linux/arm64,linux/arm/v7 \ + -t $(DOCKER_IMAGE):latest \ + -t $(DOCKER_IMAGE):amd64 \ + -t $(DOCKER_IMAGE):arm64 \ + -t $(DOCKER_IMAGE):armv7 \ + --push . + +# Run a local Docker container (binary) +.PHONY: docker-run +docker-run: + docker run -d -p 8080:8080 -p 50051:50051 $(DOCKER_IMAGE):latest + +# Run a local PM2 service (binary) +.PHONY: pm2-run +pm2-run: build + pm2 start ecosystem.config.js + +# PM2 deployment +.PHONY: pm2-deploy +pm2-deploy: build + pm2 start ecosystem.config.js --env production + +# Docker compose +compose: + docker-compose up -d + +# Help Information +.PHONY: help +help: + @echo "Available targets:" + @echo " deps : Install dependencies" + @echo " proto : Generate Protobuf code" + @echo " start-dev : Opening the development environment" + @echo " build : Build binary for current platform" + @echo " build-multi : Build binaries for all platforms" + @echo " package : Package binaries with config.json" + @echo " test : Run tests" + @echo " cover : Generate test coverage report" + @echo " clean : Remove build artifacts" + @echo " docker-build : Build Docker image locally (binary)" + @echo " docker-build-multi : Build and push multi-arch Docker image (binary)" + @echo " docker-run : Run Docker container locally (binary)" + @echo " pm2-deploy : Deploy with PM2 locally" + @echo " pm2-deploy-prod : Deploy with PM2 in production" + @echo " help : Show this help message" \ No newline at end of file diff --git a/README.md b/README.md index ca0f6d4..53a423d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# go-captcha-docker -This is a docker image deployment for GoCaptcha +# go-captcha-service + diff --git a/cmd/go-captcha-service/main.go b/cmd/go-captcha-service/main.go new file mode 100644 index 0000000..32bf7b5 --- /dev/null +++ b/cmd/go-captcha-service/main.go @@ -0,0 +1,415 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "net" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "github.com/google/uuid" + "github.com/sony/gobreaker" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + "go.uber.org/zap" + "google.golang.org/grpc" + + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/middleware" + "github.com/wenlng/go-captcha-service/internal/server" + "github.com/wenlng/go-captcha-service/internal/service_discovery" + "github.com/wenlng/go-captcha-service/proto" +) + +// App manages the application components +type App struct { + logger *zap.Logger + dynamicCfg *config.DynamicConfig + cache cache.Cache + discovery service_discovery.ServiceDiscovery + httpServer *http.Server + grpcServer *grpc.Server + cacheBreaker *gobreaker.CircuitBreaker + limiter *middleware.DynamicLimiter +} + +const ( + CacheTypeRedis string = "redis" + CacheTypeMemory = "memory" + CacheTypeEtcd = "etcd" + CacheTypeMemcache = "memcache" +) + +const ( + ServiceDiscoveryTypeEtcd string = "etcd" + ServiceDiscoveryTypeZookeeper = "zookeeper" + ServiceDiscoveryTypeConsul = "consul" + ServiceDiscoveryTypeNacos = "nacos" +) + +// NewApp initializes the application +func NewApp() (*App, error) { + // Initialize logger + logger, err := zap.NewProduction() + if err != nil { + return nil, fmt.Errorf("failed to initialize logger: %v", err) + } + + // Parse command-line flags + configFile := flag.String("config", "config.json", "Path to config file") + serviceName := flag.String("service-name", "", "Name for service") + httpPort := flag.String("http-port", "", "Port for HTTP server") + grpcPort := flag.String("grpc-port", "", "Port for gRPC server") + redisAddrs := flag.String("redis-addrs", "", "Comma-separated Redis cluster addresses") + etcdAddrs := flag.String("etcd-addrs", "", "Comma-separated etcd addresses") + memcacheAddrs := flag.String("memcache-addrs", "", "Comma-separated Memcached addresses") + cacheType := flag.String("cache-type", "", "Cache type: redis, memory, etcd, memcache") + cacheTTL := flag.Int("cache-ttl", 0, "Cache TTL in seconds") + cacheCleanupInt := flag.Int("cache-cleanup-interval", 0, "Cache cleanup interval in seconds") + cacheKeyPrefix := flag.Int("cache-key-prefix", 0, "Key prefix for cache") + serviceDiscovery := flag.String("service-discovery", "", "Service discovery: etcd, zookeeper, consul, nacos") + serviceDiscoveryAddrs := flag.String("service-discovery-addrs", "", "Service discovery addresses") + rateLimitQPS := flag.Int("rate-limit-qps", 0, "Rate limit QPS") + rateLimitBurst := flag.Int("rate-limit-burst", 0, "Rate limit burst") + loadBalancer := flag.String("load-balancer", "", "Load balancer: round-robin, consistent-hash") + apiKeys := flag.String("api-keys", "", "Comma-separated API keys") + healthCheckFlag := flag.Bool("health-check", false, "Run health check and exit") + enableCorsFlag := flag.Bool("enable-cors", false, "Enable cross-domain resources") + flag.Parse() + + // Load configuration + dc, err := config.NewDynamicConfig(*configFile) + if err != nil { + logger.Warn("Failed to load config, using defaults", zap.Error(err)) + dc = &config.DynamicConfig{Config: config.DefaultConfig()} + } + + // Merge command-line flags + cfg := dc.Get() + cfg = config.MergeWithFlags(cfg, map[string]interface{}{ + "service-name": *serviceName, + "http-port": *httpPort, + "grpc-port": *grpcPort, + "redis-addrs": *redisAddrs, + "etcd-addrs": *etcdAddrs, + "memcache-addrs": *memcacheAddrs, + "cache-type": *cacheType, + "cache-ttl": *cacheTTL, + "cache-cleanup-interval": *cacheCleanupInt, + "cache-key-prefix": *cacheKeyPrefix, + "service-discovery": *serviceDiscovery, + "service-discovery-addrs": *serviceDiscoveryAddrs, + "rate-limit-qps": *rateLimitQPS, + "rate-limit-burst": *rateLimitBurst, + "load-balancer": *loadBalancer, + "api-keys": *apiKeys, + "enable-cors": *enableCorsFlag, + }) + if err = dc.Update(cfg); err != nil { + logger.Fatal("Configuration validation failed", zap.Error(err)) + } + + // Initialize rate limiter + limiter := middleware.NewDynamicLimiter(cfg.RateLimitQPS, cfg.RateLimitBurst) + go func() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for range ticker.C { + newCfg := dc.Get() + limiter.Update(newCfg.RateLimitQPS, newCfg.RateLimitBurst) + } + }() + + // Initialize circuit breaker + cacheBreaker := gobreaker.NewCircuitBreaker(gobreaker.Settings{ + Name: *serviceName, + MaxRequests: 1, + Interval: 60 * time.Second, + Timeout: 5 * time.Second, + ReadyToTrip: func(counts gobreaker.Counts) bool { + return counts.ConsecutiveFailures > 3 + }, + }) + + // Initialize curCache + var curCache cache.Cache + ttl := time.Duration(cfg.CacheTTL) * time.Second + cleanInt := time.Duration(cfg.CacheCleanupInt) * time.Second + switch cfg.CacheType { + case CacheTypeRedis: + curCache, err = cache.NewRedisClient(cfg.RedisAddrs, cfg.CacheKeyPrefix, ttl) + if err != nil { + logger.Fatal("Failed to initialize Redis", zap.Error(err)) + } + case CacheTypeMemory: + curCache = cache.NewMemoryCache(cfg.CacheKeyPrefix, ttl, cleanInt) + case CacheTypeEtcd: + curCache, err = cache.NewEtcdClient(cfg.EtcdAddrs, cfg.CacheKeyPrefix, ttl) + if err != nil { + logger.Fatal("Failed to initialize etcd", zap.Error(err)) + } + case CacheTypeMemcache: + curCache, err = cache.NewMemcacheClient(cfg.MemcacheAddrs, cfg.CacheKeyPrefix, ttl) + if err != nil { + logger.Fatal("Failed to initialize Memcached", zap.Error(err)) + } + default: + logger.Fatal("Invalid curCache type", zap.String("type", cfg.CacheType)) + } + + // Initialize service discovery + var discovery service_discovery.ServiceDiscovery + if cfg.ServiceDiscovery != "" { + switch cfg.ServiceDiscovery { + case ServiceDiscoveryTypeEtcd: + discovery, err = service_discovery.NewEtcdDiscovery(cfg.ServiceDiscoveryAddrs, 10) + case ServiceDiscoveryTypeZookeeper: + discovery, err = service_discovery.NewZookeeperDiscovery(cfg.ServiceDiscoveryAddrs, 10) + case ServiceDiscoveryTypeConsul: + discovery, err = service_discovery.NewConsulDiscovery(cfg.ServiceDiscoveryAddrs, 10) + case ServiceDiscoveryTypeNacos: + discovery, err = service_discovery.NewNacosDiscovery(cfg.ServiceDiscoveryAddrs, 10) + default: + logger.Fatal("Invalid service discovery type", zap.String("type", cfg.ServiceDiscovery)) + } + if err != nil { + logger.Fatal("Failed to initialize service discovery", zap.Error(err)) + } + } + + // Perform health check if requested + if *healthCheckFlag { + if err = healthCheck(":"+cfg.HTTPPort, ":"+cfg.GRPCPort); err != nil { + logger.Error("Health check failed", zap.Error(err)) + os.Exit(1) + } + os.Exit(0) + } + + return &App{ + logger: logger, + dynamicCfg: dc, + cache: curCache, + discovery: discovery, + cacheBreaker: cacheBreaker, + limiter: limiter, + }, nil +} + +// Start launches the HTTP and gRPC servers +func (a *App) Start(ctx context.Context) error { + cfg := a.dynamicCfg.Get() + + // setup captcha + captcha, err := gocaptcha.Setup() + if err != nil { + return errors.New("setup gocaptcha failed") + } + + // Register service with discovery + var instanceID string + if a.discovery != nil { + instanceID = uuid.New().String() + httpPortInt, _ := strconv.Atoi(cfg.HTTPPort) + grpcPortInt, _ := strconv.Atoi(cfg.GRPCPort) + if err = a.discovery.Register(ctx, cfg.ServiceName, instanceID, "127.0.0.1", httpPortInt, grpcPortInt); err != nil { + return fmt.Errorf("failed to register service: %v", err) + } + go a.updateInstances(ctx, instanceID) + } + + // service context + svcCtx := common.NewSvcContext() + svcCtx.Cache = a.cache + svcCtx.Config = &cfg + svcCtx.Logger = a.logger + svcCtx.Captcha = captcha + + // Register HTTP routes + handlers := server.NewHTTPHandlers(svcCtx) + mwChain := middleware.NewChainHTTP( + nil, // . + middleware.APIKeyMiddleware(a.dynamicCfg, a.logger), + middleware.LoggingMiddleware(a.logger), + middleware.RateLimitMiddleware(a.limiter, a.logger), + middleware.CircuitBreakerMiddleware(a.cacheBreaker, a.logger), + ) + + // Enable cross-domain resource + if cfg.EnableCors { + mwChain.AppendMiddleware(middleware.CORSMiddleware(a.logger)) + } + + // Logic Routes + http.Handle("/get-data", mwChain.Then(handlers.GetDataHandler)) + http.Handle("/check-data", mwChain.Then(handlers.CheckDataHandler)) + http.Handle("/check-status", mwChain.Then(handlers.CheckStatusHandler)) + http.Handle("/get-status-info", mwChain.Then(handlers.GetStatusInfoHandler)) + http.Handle("/del-status-data", mwChain.Then(handlers.DelStatusInfoHandler)) + http.Handle("/rate-limit", mwChain.Then(middleware.RateLimitHandler(a.limiter, a.logger))) + + // Start HTTP server + a.httpServer = &http.Server{ + Addr: ":" + cfg.HTTPPort, + } + go func() { + a.logger.Info("Starting HTTP server", zap.String("port", cfg.HTTPPort)) + if err := a.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + a.logger.Fatal("HTTP server failed", zap.Error(err)) + } + }() + + // Start gRPC server + lis, err := net.Listen("tcp", ":"+cfg.GRPCPort) + if err != nil { + return fmt.Errorf("failed to listen: %v", err) + } + a.grpcServer = grpc.NewServer( + grpc.UnaryInterceptor(middleware.UnaryServerInterceptor(a.dynamicCfg, a.logger, a.cacheBreaker)), + ) + proto.RegisterGoCaptchaServiceServer(a.grpcServer, server.NewGoCaptchaServer(svcCtx)) + go func() { + a.logger.Info("Starting gRPC server", zap.String("port", cfg.GRPCPort)) + if err := a.grpcServer.Serve(lis); err != nil && err != grpc.ErrServerStopped { + a.logger.Fatal("gRPC server failed", zap.Error(err)) + } + }() + + return nil +} + +// updateInstances periodically updates service instances +func (a *App) updateInstances(ctx context.Context, instanceID string) { + ticker := time.NewTicker(10 * time.Second) + cfg := a.dynamicCfg.Get() + + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + if a.discovery != nil { + if err := a.discovery.Deregister(ctx, instanceID); err != nil { + a.logger.Error("Failed to deregister service", zap.Error(err)) + } + } + return + case <-ticker.C: + if a.discovery == nil { + continue + } + instances, err := a.discovery.Discover(ctx, cfg.ServiceName) + if err != nil { + a.logger.Error("Failed to discover instances", zap.Error(err)) + continue + } + a.logger.Info("Discovered instances", zap.Int("count", len(instances))) + } + } +} + +// Shutdown gracefully stops the application +func (a *App) Shutdown() { + a.logger.Info("Received shutdown signal, shutting down gracefully") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Stop HTTP server + if a.httpServer != nil { + if err := a.httpServer.Shutdown(ctx); err != nil { + a.logger.Error("HTTP server shutdown error", zap.Error(err)) + } else { + a.logger.Info("HTTP server shut down successfully") + } + } + + // Stop gRPC server + if a.grpcServer != nil { + a.grpcServer.GracefulStop() + a.logger.Info("gRPC server shut down successfully") + } + + // Close cache + if redisClient, ok := a.cache.(*cache.RedisClient); ok { + if err := redisClient.Close(); err != nil { + a.logger.Error("Redis client close error", zap.Error(err)) + } else { + a.logger.Info("Redis client closed successfully") + } + } + if memoryCache, ok := a.cache.(*cache.MemoryCache); ok { + memoryCache.Stop() + a.logger.Info("Memory cache stopped successfully") + } + if etcdClient, ok := a.cache.(*cache.EtcdClient); ok { + if err := etcdClient.Close(); err != nil { + a.logger.Error("etcd client close error", zap.Error(err)) + } else { + a.logger.Info("etcd client closed successfully") + } + } + if memcacheClient, ok := a.cache.(*cache.MemcacheClient); ok { + if err := memcacheClient.Close(); err != nil { + a.logger.Error("Memcached client close error", zap.Error(err)) + } else { + a.logger.Info("Memcached client closed successfully") + } + } + + // Close service discovery + if a.discovery != nil { + if err := a.discovery.Close(); err != nil { + a.logger.Error("Service discovery close error", zap.Error(err)) + } else { + a.logger.Info("Service discovery closed successfully") + } + } +} + +// healthCheck performs a health check on HTTP and gRPC servers +func healthCheck(httpAddr, grpcAddr string) error { + resp, err := http.Get("http://localhost" + httpAddr + "/read?key=test") + if err != nil || resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("HTTP health check failed: %v", err) + } + resp.Body.Close() + + conn, err := net.DialTimeout("tcp", "localhost"+grpcAddr, 1*time.Second) + if err != nil { + return fmt.Errorf("gRPC health check failed: %v", err) + } + conn.Close() + + return nil +} + +func main() { + app, err := NewApp() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize app: %v\n", err) + os.Exit(1) + } + defer app.logger.Sync() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if err = app.Start(ctx); err != nil { + app.logger.Fatal("Failed to start app", zap.Error(err)) + } + + // Handle termination signals + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + <-sigCh + + app.Shutdown() + app.logger.Info("App exited") +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..35c06ac --- /dev/null +++ b/config.json @@ -0,0 +1,19 @@ +{ + "service_name": "go-captcha-service", + "http_port": "8080", + "grpc_port": "50051", + "redis_addrs": "localhost:6379", + "etcd_addrs": "localhost:2379", + "memcache_addrs": "localhost:11211", + "cache_type": "memory", + "cache_ttl": 60, + "cache_cleanup_interval": 10, + "cache_key_prefix": "GO_CAPTCHA_DATA:", + "service_discovery": "", + "service_discovery_addrs": "localhost:2379", + "rate_limit_qps": 1000, + "rate_limit_burst": 1000, + "load_balancer": "round-robin", + "api_keys": ["my-secret-key-123", "another-key-456"], + "enable_cors": true +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..addd9da --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + redis: + image: redis:latest + ports: + - "6379:6379" + + etcd: + image: quay.io/coreos/etcd:latest + ports: + - "2379:2379" + environment: + - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 + - ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379 + + memcached: + image: memcached:latest + ports: + - "11211:11211" + + zookeeper: + image: zookeeper:3.8 + ports: + - "2181:2181" + + consul: + image: consul:latest + ports: + - "8500:8500" + + nacos: + image: nacos/nacos-server:latest + ports: + - "8848:8848" + environment: + - MODE=standalone + + go-captcha-service: + build: . + ports: + - "8080:8080" + - "50051:50051" + depends_on: + - redis + - etcd + - memcached + - zookeeper + - consul + - nacos + volumes: + - ./config.json:/app/config.json \ No newline at end of file diff --git a/docs/cache.md b/docs/cache.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..e69de29 diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..925aac7 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,22 @@ +module.exports = { + apps: [{ + name: 'go-captcha-service', + script: './build/go-captcha-service', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + CONFIG: 'config.json', + CACHE_TYPE: 'redis', + CACHE_TTL: '60', + CACHE_CLEANUP_INTERVAL: '10', + }, + env_production: { + CONFIG: '/etc/go-captcha-service/config.json', + CACHE_TYPE: 'etcd', + CACHE_TTL: '30', + CACHE_CLEANUP_INTERVAL: '5', + } + }] +}; \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2c29832 --- /dev/null +++ b/go.mod @@ -0,0 +1,118 @@ +module github.com/wenlng/go-captcha-service + +go 1.23.0 + +require ( + github.com/alicebob/miniredis/v2 v2.32.1 + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 + github.com/fsnotify/fsnotify v1.9.0 + github.com/go-zookeeper/zk v1.0.3 + github.com/google/uuid v1.6.0 + github.com/hashicorp/consul/api v1.29.4 + github.com/nacos-group/nacos-sdk-go/v2 v2.2.5 + github.com/redis/go-redis/v9 v9.6.1 + github.com/sony/gobreaker v0.5.0 + github.com/stretchr/testify v1.9.0 + go.etcd.io/etcd/client/v3 v3.5.21 + go.etcd.io/etcd/server/v3 v3.5.21 + go.uber.org/zap v1.27.0 + golang.org/x/time v0.6.0 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 +) + +require ( + github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect + github.com/alibabacloud-go/tea v1.1.17 // indirect + github.com/alibabacloud-go/tea-utils v1.4.4 // indirect + github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect + github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 // indirect + github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.2.2 // indirect + github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.7 // indirect + github.com/armon/go-metrics v0.4.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/serf v0.10.1 // indirect + github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect + github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.12.2 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect + github.com/wenlng/go-captcha-assets v1.0.6 // indirect + github.com/wenlng/go-captcha/v2 v2.0.3 // indirect + github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + go.etcd.io/bbolt v1.3.11 // indirect + go.etcd.io/etcd/api/v3 v3.5.21 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect + go.etcd.io/etcd/client/v2 v2.305.21 // indirect + go.etcd.io/etcd/pkg/v3 v3.5.21 // indirect + go.etcd.io/etcd/raft/v3 v3.5.21 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect + go.opentelemetry.io/otel v1.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect + go.opentelemetry.io/otel/metric v1.20.0 // indirect + go.opentelemetry.io/otel/sdk v1.20.0 // indirect + go.opentelemetry.io/otel/trace v1.20.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect + golang.org/x/image v0.16.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + gopkg.in/ini.v1 v1.66.2 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f1a38bf --- /dev/null +++ b/go.sum @@ -0,0 +1,836 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v1.28.1 h1:XwPcZjgMCnU2tkwY10VleUjSAfpTj9RDn+kGrbYsi8o= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.17 h1:05R5DnaJXe9sCNIe8KUgWHC/z6w/VZIwczgUwzRnul8= +github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o= +github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoOR4JssBo= +github.com/alicebob/miniredis/v2 v2.32.1/go.mod h1:AqkLNAfUm0K07J28hnAyyQKf/x0YkCY/g5DCtuL01Mw= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 h1:ie/8RxBOfKZWcrbYSJi2Z8uX8TcOlSMwPlEJh83OeOw= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU= +github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.2.2 h1:rWkH6D2XlXb/Y+tNAQROxBzp3a0p92ni+pXcaHBe/WI= +github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.2.2/go.mod h1:GDtq+Kw+v0fO+j5BrrWiUHbBq7L+hfpzpPfXKOZMFE0= +github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.7 h1:olLiPI2iM8Hqq6vKnSxpM3awCrm9/BeOgHpzQkOYnI4= +github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.7/go.mod h1:oDg1j4kFxnhgftaiLJABkGeSvuEvSF5Lo6UmRAMruX4= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= +github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hashicorp/consul/api v1.29.4 h1:P6slzxDLBOxUSj3fWo2o65VuKtbtOXFi7TSSgtXutuE= +github.com/hashicorp/consul/api v1.29.4/go.mod h1:HUlfw+l2Zy68ceJavv2zAyArl2fqhGWnMycyt56sBgg= +github.com/hashicorp/consul/proto-public v0.6.2 h1:+DA/3g/IiKlJZb88NBn0ZgXrxJp2NlvCZdEyl+qxvL0= +github.com/hashicorp/consul/proto-public v0.6.2/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg= +github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= +github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= +github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nacos-group/nacos-sdk-go/v2 v2.2.5 h1:r0wwT7PayEjvEHzWXwr1ROi/JSqzujM4w+1L5ikThzQ= +github.com/nacos-group/nacos-sdk-go/v2 v2.2.5/go.mod h1:OObBon0prVJVPoIbSZxpEkFiBfL0d1LcBtuAMiNn+8c= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/wenlng/go-captcha-assets v1.0.6 h1:PSTTReE7QXsYdBnE/oZB91BllCZSvBKb4uWoJACuhYo= +github.com/wenlng/go-captcha-assets v1.0.6/go.mod h1:zinRACsdYcL/S6pHgI9Iv7FKTU41d00+43pNX+b9+MM= +github.com/wenlng/go-captcha/v2 v2.0.3 h1:QTZ39/gVDisPSgvL9O2X2HbTuj5P/z8QsdGB/aayg9c= +github.com/wenlng/go-captcha/v2 v2.0.3/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= +go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= +go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= +go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= +go.etcd.io/etcd/client/v2 v2.305.21 h1:eLiFfexc2mE+pTLz9WwnoEsX5JTTpLCYVivKkmVXIRA= +go.etcd.io/etcd/client/v2 v2.305.21/go.mod h1:OKkn4hlYNf43hpjEM3Ke3aRdUkhSl8xjKjSf8eCq2J8= +go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= +go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= +go.etcd.io/etcd/pkg/v3 v3.5.21 h1:jUItxeKyrDuVuWhdh0HtjUANwyuzcb7/FAeUfABmQsk= +go.etcd.io/etcd/pkg/v3 v3.5.21/go.mod h1:wpZx8Egv1g4y+N7JAsqi2zoUiBIUWznLjqJbylDjWgU= +go.etcd.io/etcd/raft/v3 v3.5.21 h1:dOmE0mT55dIUsX77TKBLq+RgyumsQuYeiRQnW/ylugk= +go.etcd.io/etcd/raft/v3 v3.5.21/go.mod h1:fmcuY5R2SNkklU4+fKVBQi2biVp5vafMrWUEj4TJ4Cs= +go.etcd.io/etcd/server/v3 v3.5.21 h1:9w0/k12majtgarGmlMVuhwXRI2ob3/d1Ik3X5TKo0yU= +go.etcd.io/etcd/server/v3 v3.5.21/go.mod h1:G1mOzdwuzKT1VRL7SqRchli/qcFrtLBTAQ4lV20sXXo= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M= +go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc= +go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0= +go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA= +go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM= +go.opentelemetry.io/otel/sdk v1.20.0 h1:5Jf6imeFZlZtKv9Qbo6qt2ZkmWtdWx/wzcCbNUlAWGM= +go.opentelemetry.io/otel/sdk v1.20.0/go.mod h1:rmkSx1cZCm/tn16iWDn1GQbLtsW/LvsdEEFzCSRM6V0= +go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ= +go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw= +golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/internal/adapt/capt.go b/internal/adapt/capt.go new file mode 100644 index 0000000..869c134 --- /dev/null +++ b/internal/adapt/capt.go @@ -0,0 +1,42 @@ +package adapt + +type CaptData struct { + CaptchaKey string `json:"captcha_key,omitempty"` + MasterImageBase64 string `json:"master_image_base64,omitempty"` + ThumbImageBase64 string `json:"thumb_image_base64,omitempty"` + MasterImageWidth int32 `json:"master_width,omitempty"` + MasterImageHeight int32 `json:"master_height,omitempty"` + ThumbImageWidth int32 `json:"thumb_width,omitempty"` + ThumbImageHeight int32 `json:"thumb_height,omitempty"` + ThumbImageSize int32 `json:"thumb_size,omitempty"` + DisplayX int32 `json:"display_x,omitempty"` + DisplayY int32 `json:"display_y,omitempty"` +} + +type CaptDataResponse struct { + Code int32 `json:"code" default:"200"` + Message string `json:"message"` + CaptchaKey string `json:"captcha_key,omitempty"` + MasterImageBase64 string `json:"master_image_base64,omitempty"` + ThumbImageBase64 string `json:"thumb_image_base64,omitempty"` + MasterImageWidth int32 `json:"master_width,omitempty"` + MasterImageHeight int32 `json:"master_height,omitempty"` + ThumbImageWidth int32 `json:"thumb_width,omitempty"` + ThumbImageHeight int32 `json:"thumb_height,omitempty"` + ThumbImageSize int32 `json:"thumb_size,omitempty"` + DisplayX int32 `json:"display_x,omitempty"` + DisplayY int32 `json:"display_y,omitempty"` + Type int32 `json:"type,omitempty"` +} + +type CaptNormalDataResponse struct { + Code int32 `json:"code" default:"200"` + Message string `json:"message" default:""` + Data interface{} `json:"data"` +} + +type CaptStatusDataResponse struct { + Code int32 `json:"code" default:"200"` + Message string `json:"message" default:""` + Data string `json:"status" default:""` +} diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..95c6117 --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,18 @@ +package cache + +import ( + "context" +) + +// Cache defines the interface for cache operations +type Cache interface { + GetCache(ctx context.Context, key string) (string, error) + SetCache(ctx context.Context, key, value string) error + DeleteCache(ctx context.Context, key string) error + Close() error +} + +type CaptCacheData struct { + Data interface{} `json:"data"` + Status int `json:"status"` +} diff --git a/internal/cache/etcd_client.go b/internal/cache/etcd_client.go new file mode 100644 index 0000000..bda05e9 --- /dev/null +++ b/internal/cache/etcd_client.go @@ -0,0 +1,82 @@ +package cache + +import ( + "context" + "fmt" + "strings" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/client/v3/concurrency" +) + +// EtcdClient implements the Cache interface for etcd +type EtcdClient struct { + client *clientv3.Client + prefix string + ttl time.Duration +} + +// NewEtcdClient creates a new etcd client +func NewEtcdClient(addrs, prefix string, ttl time.Duration) (*EtcdClient, error) { + client, err := clientv3.New(clientv3.Config{ + Endpoints: []string{addrs}, + DialTimeout: 5 * time.Second, + }) + if err != nil { + return nil, err + } + return &EtcdClient{client: client, prefix: prefix, ttl: ttl}, nil +} + +// GetCache retrieves a value from etcd +func (c *EtcdClient) GetCache(ctx context.Context, key string) (string, error) { + key = c.prefix + key + resp, err := c.client.Get(ctx, key) + if err != nil { + return "", err + } + if len(resp.Kvs) == 0 { + return "", nil + } + return string(resp.Kvs[0].Value), nil +} + +// SetCache stores a value in etcd +func (c *EtcdClient) SetCache(ctx context.Context, key, value string) error { + key = c.prefix + key + session, err := concurrency.NewSession(c.client, concurrency.WithTTL(int(c.ttl/time.Second))) + if err != nil { + return fmt.Errorf("failed to create etcd session: %v", err) + } + defer session.Close() + + prefix := "http" + if strings.Contains(key, ":grpc:") { + prefix = "grpc" + } + mutex := concurrency.NewMutex(session, "/go-captcha-cache-lock/"+prefix) + if err = mutex.Lock(ctx); err != nil { + return fmt.Errorf("failed to acquire etcd lock: %v", err) + } + defer mutex.Unlock(ctx) + + _, err = c.client.Put(ctx, key, value, clientv3.WithLease(session.Lease())) + if err != nil { + return err + } + return nil +} + +func (c *EtcdClient) DeleteCache(ctx context.Context, key string) error { + _, err := c.client.Delete(ctx, key) + if err != nil { + return fmt.Errorf("etcd delete error: %v", err) + } + return nil +} + +// Close closes the etcd client +func (c *EtcdClient) Close() error { + return c.client.Close() +} diff --git a/internal/cache/etcd_client_test.go b/internal/cache/etcd_client_test.go new file mode 100644 index 0000000..255461a --- /dev/null +++ b/internal/cache/etcd_client_test.go @@ -0,0 +1,37 @@ +package cache + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.etcd.io/etcd/server/v3/embed" +) + +func TestEtcdClient(t *testing.T) { + cfg := embed.NewConfig() + cfg.Dir = t.TempDir() + etcd, err := embed.StartEtcd(cfg) + assert.NoError(t, err) + defer etcd.Close() + + client, err := NewEtcdClient("localhost:2379", "TEST_KEY:", 60*time.Second) + assert.NoError(t, err) + defer client.Close() + + t.Run("SetAndGet", func(t *testing.T) { + err := client.SetCache(context.Background(), "key1", "value1") + assert.NoError(t, err) + + value, err := client.GetCache(context.Background(), "key1") + assert.NoError(t, err) + assert.Equal(t, "value1", value) + }) + + t.Run("GetNonExistent", func(t *testing.T) { + value, err := client.GetCache(context.Background(), "nonexistent") + assert.NoError(t, err) + assert.Equal(t, "", value) + }) +} diff --git a/internal/cache/memcache_client.go b/internal/cache/memcache_client.go new file mode 100644 index 0000000..875d0ef --- /dev/null +++ b/internal/cache/memcache_client.go @@ -0,0 +1,59 @@ +package cache + +import ( + "context" + "fmt" + "time" + + "github.com/bradfitz/gomemcache/memcache" +) + +// MemcacheClient implements the Cache interface for Memcached +type MemcacheClient struct { + client *memcache.Client + prefix string + ttl time.Duration +} + +// NewMemcacheClient creates a new Memcached client +func NewMemcacheClient(addrs, prefix string, ttl time.Duration) (*MemcacheClient, error) { + client := memcache.New(addrs) + return &MemcacheClient{client: client, prefix: prefix, ttl: ttl}, nil +} + +// GetCache retrieves a value from Memcached +func (c *MemcacheClient) GetCache(ctx context.Context, key string) (string, error) { + key = c.prefix + key + item, err := c.client.Get(key) + if err == memcache.ErrCacheMiss { + return "", nil + } + if err != nil { + return "", err + } + return string(item.Value), nil +} + +// SetCache stores a value in Memcached +func (c *MemcacheClient) SetCache(ctx context.Context, key, value string) error { + key = c.prefix + key + item := &memcache.Item{ + Key: key, + Value: []byte(value), + Expiration: int32(c.ttl / time.Second), + } + return c.client.Set(item) +} + +func (c *MemcacheClient) DeleteCache(ctx context.Context, key string) error { + err := c.client.Delete(key) + if err != nil && err != memcache.ErrCacheMiss { + return fmt.Errorf("memcache delete error: %v", err) + } + return nil +} + +// Close closes the Memcached client +func (c *MemcacheClient) Close() error { + return nil +} diff --git a/internal/cache/memcache_client_test.go b/internal/cache/memcache_client_test.go new file mode 100644 index 0000000..77cfeff --- /dev/null +++ b/internal/cache/memcache_client_test.go @@ -0,0 +1,32 @@ +package cache + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestMemcacheClient(t *testing.T) { + client, err := NewMemcacheClient("localhost:11211", "TEST_KEY:", 60*time.Second) + assert.NoError(t, err) + defer client.Close() + + t.Run("SetAndGet", func(t *testing.T) { + err := client.SetCache(context.Background(), "key1", "value1") + if err != nil { + t.Skip("Memcached not running") + } + + value, err := client.GetCache(context.Background(), "key1") + assert.NoError(t, err) + assert.Equal(t, "value1", value) + }) + + t.Run("GetNonExistent", func(t *testing.T) { + value, err := client.GetCache(context.Background(), "nonexistent") + assert.NoError(t, err) + assert.Equal(t, "", value) + }) +} diff --git a/internal/cache/memory_cache.go b/internal/cache/memory_cache.go new file mode 100644 index 0000000..5cb6987 --- /dev/null +++ b/internal/cache/memory_cache.go @@ -0,0 +1,97 @@ +package cache + +import ( + "context" + "sync" + "time" +) + +// MemoryCache is an in-memory cache with TTL and cleanup +type MemoryCache struct { + items map[string]cacheItem + mu sync.RWMutex + ttl time.Duration + prefix string + stop chan struct{} + cleanInt time.Duration +} + +type cacheItem struct { + value string + expiration int64 +} + +// NewMemoryCache creates a new memory cache +func NewMemoryCache(prefix string, ttl, cleanupInterval time.Duration) *MemoryCache { + cache := &MemoryCache{ + items: make(map[string]cacheItem), + ttl: ttl, + prefix: prefix, + stop: make(chan struct{}), + cleanInt: cleanupInterval, + } + go cache.startCleanup() + return cache +} + +// startCleanup runs periodic cleanup of expired items +func (c *MemoryCache) startCleanup() { + ticker := time.NewTicker(c.cleanInt) + defer ticker.Stop() + for { + select { + case <-c.stop: + return + case <-ticker.C: + c.mu.Lock() + for key, item := range c.items { + if item.expiration <= time.Now().UnixNano() { + delete(c.items, key) + } + } + c.mu.Unlock() + } + } +} + +// GetCache retrieves a value from memory cache +func (c *MemoryCache) GetCache(ctx context.Context, key string) (string, error) { + key = c.prefix + key + c.mu.RLock() + defer c.mu.RUnlock() + item, exists := c.items[key] + if !exists || item.expiration <= time.Now().UnixNano() { + return "", nil + } + return item.value, nil +} + +// SetCache stores a value in memory cache +func (c *MemoryCache) SetCache(ctx context.Context, key, value string) error { + key = c.prefix + key + c.mu.Lock() + defer c.mu.Unlock() + c.items[key] = cacheItem{ + value: value, + expiration: time.Now().Add(c.ttl).UnixNano(), + } + return nil +} + +func (c *MemoryCache) DeleteCache(ctx context.Context, key string) error { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.items, key) + return nil +} + +// Close stops the memory cache +func (c *MemoryCache) Close() error { + c.Stop() + return nil +} + +// Stop stops the memory cache cleanup routine +func (c *MemoryCache) Stop() { + close(c.stop) +} diff --git a/internal/cache/memory_cache_test.go b/internal/cache/memory_cache_test.go new file mode 100644 index 0000000..72270a9 --- /dev/null +++ b/internal/cache/memory_cache_test.go @@ -0,0 +1,46 @@ +package cache + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestMemoryCache(t *testing.T) { + cache := NewMemoryCache("TEST_KEY:", 1*time.Second, 500*time.Millisecond) + defer cache.Stop() + + t.Run("SetAndGet", func(t *testing.T) { + err := cache.SetCache(context.Background(), "key1", "value1") + assert.NoError(t, err) + + value, err := cache.GetCache(context.Background(), "key1") + assert.NoError(t, err) + assert.Equal(t, "value1", value) + }) + + t.Run("Expiration", func(t *testing.T) { + err := cache.SetCache(context.Background(), "key2", "value2") + assert.NoError(t, err) + + time.Sleep(1100 * time.Millisecond) + + value, err := cache.GetCache(context.Background(), "key2") + assert.NoError(t, err) + assert.Equal(t, "", value) + }) + + t.Run("Cleanup", func(t *testing.T) { + err := cache.SetCache(context.Background(), "key3", "value3") + assert.NoError(t, err) + + time.Sleep(600 * time.Millisecond) + + cache.mu.RLock() + _, exists := cache.items["key3"] + cache.mu.RUnlock() + assert.False(t, exists) + }) +} diff --git a/internal/cache/redis_client.go b/internal/cache/redis_client.go new file mode 100644 index 0000000..3041f42 --- /dev/null +++ b/internal/cache/redis_client.go @@ -0,0 +1,58 @@ +package cache + +import ( + "context" + "fmt" + "time" + + "github.com/redis/go-redis/v9" +) + +// RedisClient implements the Cache interface for Redis +type RedisClient struct { + client *redis.Client + prefix string + ttl time.Duration +} + +// NewRedisClient creates a new Redis client +func NewRedisClient(addrs, prefix string, ttl time.Duration) (*RedisClient, error) { + client := redis.NewClient(&redis.Options{ + Addr: addrs, + }) + _, err := client.Ping(context.Background()).Result() + if err != nil { + return nil, err + } + return &RedisClient{client: client, prefix: prefix, ttl: ttl}, nil +} + +// GetCache retrieves a value from Redis +func (c *RedisClient) GetCache(ctx context.Context, key string) (string, error) { + key = c.prefix + key + val, err := c.client.Get(ctx, key).Result() + if err == redis.Nil { + return "", nil + } + return val, err +} + +// SetCache stores a value in Redis +func (c *RedisClient) SetCache(ctx context.Context, key, value string) error { + key = c.prefix + key + return c.client.Set(ctx, key, value, c.ttl).Err() +} + +// DeleteCache stores a value in Redis +func (c *RedisClient) DeleteCache(ctx context.Context, key string) error { + err := c.client.Del(ctx, key).Err() + if err != nil && err != redis.Nil { + return fmt.Errorf("redis delete error: %v", err) + } + return nil +} + +// Close closes the Redis client +func (c *RedisClient) Close() error { + return c.client.Close() +} diff --git a/internal/cache/redis_client_test.go b/internal/cache/redis_client_test.go new file mode 100644 index 0000000..00ff5fb --- /dev/null +++ b/internal/cache/redis_client_test.go @@ -0,0 +1,35 @@ +package cache + +import ( + "context" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" +) + +func TestRedisClient(t *testing.T) { + mr, err := miniredis.Run() + assert.NoError(t, err) + defer mr.Close() + + client, err := NewRedisClient(mr.Addr(), "TEST_KEY:", 60*time.Second) + assert.NoError(t, err) + defer client.Close() + + t.Run("SetAndGet", func(t *testing.T) { + err := client.SetCache(context.Background(), "key1", "value1") + assert.NoError(t, err) + + value, err := client.GetCache(context.Background(), "key1") + assert.NoError(t, err) + assert.Equal(t, "value1", value) + }) + + t.Run("GetNonExistent", func(t *testing.T) { + value, err := client.GetCache(context.Background(), "nonexistent") + assert.NoError(t, err) + assert.Equal(t, "", value) + }) +} diff --git a/internal/common/consts.go b/internal/common/consts.go new file mode 100644 index 0000000..ac3f5b3 --- /dev/null +++ b/internal/common/consts.go @@ -0,0 +1,22 @@ +package common + +// Type +const ( + GoCaptchaTypeClick = 0 + GoCaptchaTypeClickShape = 1 + GoCaptchaTypeSlide = 2 + GoCaptchaTypeDrag = 3 + GoCaptchaTypeRotate = 4 +) + +// Theme +const ( + GoCaptchaThemeDefault = 0 + GoCaptchaThemeDark = 1 +) + +// Lang +const ( + GoCaptchaLangDefault = 0 + GoCaptchaLangEnglish = 1 +) diff --git a/internal/common/svc_context.go b/internal/common/svc_context.go new file mode 100644 index 0000000..df1c32a --- /dev/null +++ b/internal/common/svc_context.go @@ -0,0 +1,19 @@ +package common + +import ( + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + "go.uber.org/zap" +) + +type SvcContext struct { + Cache cache.Cache + Config *config.Config + Logger *zap.Logger + Captcha *gocaptcha.GoCaptcha +} + +func NewSvcContext() *SvcContext { + return &SvcContext{} +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..0e42513 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,305 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + + "github.com/fsnotify/fsnotify" +) + +// Config defines the configuration structure for the application +type Config struct { + ServiceName string `json:"service_name"` + HTTPPort string `json:"http_port"` + GRPCPort string `json:"grpc_port"` + RedisAddrs string `json:"redis_addrs"` + EtcdAddrs string `json:"etcd_addrs"` + MemcacheAddrs string `json:"memcache_addrs"` + CacheType string `json:"cache_type"` // redis, memory, etcd, memcache + CacheTTL int `json:"cache_ttl"` // seconds + CacheCleanupInt int `json:"cache_cleanup_interval"` // seconds + CacheKeyPrefix string `json:"cache_key_prefix"` + ServiceDiscovery string `json:"service_discovery"` // etcd, zookeeper, consul, nacos + ServiceDiscoveryAddrs string `json:"service_discovery_addrs"` + RateLimitQPS int `json:"rate_limit_qps"` + RateLimitBurst int `json:"rate_limit_burst"` + LoadBalancer string `json:"load_balancer"` // round-robin, consistent-hash + APIKeys []string `json:"api_keys"` // API keys for authentication + EnableCors bool `json:"enable_cors"` // cross-domain resources +} + +// DynamicConfig . +type DynamicConfig struct { + Config Config + mu sync.RWMutex +} + +// NewDynamicConfig . +func NewDynamicConfig(file string) (*DynamicConfig, error) { + cfg, err := Load(file) + if err != nil { + return nil, err + } + dc := &DynamicConfig{Config: cfg} + go dc.watchFile(file) + return dc, nil +} + +// Get retrieves the current configuration +func (dc *DynamicConfig) Get() Config { + dc.mu.RLock() + defer dc.mu.RUnlock() + return dc.Config +} + +// Update updates the configuration +func (dc *DynamicConfig) Update(cfg Config) error { + if err := Validate(cfg); err != nil { + return err + } + dc.mu.Lock() + defer dc.mu.Unlock() + dc.Config = cfg + return nil +} + +// watchFile monitors the Config file for changes +func (dc *DynamicConfig) watchFile(file string) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create watcher: %v\n", err) + return + } + defer watcher.Close() + + absPath, err := filepath.Abs(file) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get absolute path: %v\n", err) + return + } + dir := filepath.Dir(absPath) + + if err := watcher.Add(dir); err != nil { + fmt.Fprintf(os.Stderr, "Failed to watch directory: %v\n", err) + return + } + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Name == absPath && (event.Op&fsnotify.Write == fsnotify.Write) { + cfg, err := Load(file) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to reload Config: %v\n", err) + continue + } + if err := dc.Update(cfg); err != nil { + fmt.Fprintf(os.Stderr, "Failed to update Config: %v\n", err) + continue + } + fmt.Printf("Configuration reloaded successfully\n") + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + fmt.Fprintf(os.Stderr, "Watcher error: %v\n", err) + } + } +} + +// Load reads the configuration from a file +func Load(file string) (Config, error) { + var config Config + data, err := os.ReadFile(file) + if err != nil { + return config, fmt.Errorf("failed to read Config file: %v", err) + } + if err := json.Unmarshal(data, &config); err != nil { + return config, fmt.Errorf("failed to parse Config file: %v", err) + } + return config, nil +} + +// Validate checks the configuration for validity +func Validate(config Config) error { + if !isValidPort(config.HTTPPort) { + return fmt.Errorf("invalid http_port: %s", config.HTTPPort) + } + if !isValidPort(config.GRPCPort) { + return fmt.Errorf("invalid grpc_port: %s", config.GRPCPort) + } + + validCacheTypes := map[string]bool{ + "redis": true, + "memory": true, + "etcd": true, + "memcache": true, + } + if !validCacheTypes[config.CacheType] { + return fmt.Errorf("invalid cache_type: %s, must be redis, memory, etcd, or memcache", config.CacheType) + } + + switch config.CacheType { + case "redis": + if !isValidAddrs(config.RedisAddrs) { + return fmt.Errorf("invalid redis_addrs: %s", config.RedisAddrs) + } + case "etcd": + if !isValidAddrs(config.EtcdAddrs) { + return fmt.Errorf("invalid etcd_addrs: %s", config.EtcdAddrs) + } + case "memcache": + if !isValidAddrs(config.MemcacheAddrs) { + return fmt.Errorf("invalid memcache_addrs: %s", config.MemcacheAddrs) + } + } + + if config.CacheTTL <= 0 { + return fmt.Errorf("cache_ttl must be positive: %d", config.CacheTTL) + } + if config.CacheCleanupInt <= 0 && config.CacheType == "memory" { + return fmt.Errorf("cache_cleanup_interval must be positive for memory cache: %d", config.CacheCleanupInt) + } + + validDiscoveryTypes := map[string]bool{ + "etcd": true, + "zookeeper": true, + "consul": true, + "nacos": true, + } + if config.ServiceDiscovery != "" && !validDiscoveryTypes[config.ServiceDiscovery] { + return fmt.Errorf("invalid service_discovery: %s, must be etcd, zookeeper, consul, or nacos", config.ServiceDiscovery) + } + if config.ServiceDiscovery != "" && !isValidAddrs(config.ServiceDiscoveryAddrs) { + return fmt.Errorf("invalid service_discovery_addrs: %s", config.ServiceDiscoveryAddrs) + } + + if config.RateLimitQPS <= 0 { + return fmt.Errorf("rate_limit_qps must be positive: %d", config.RateLimitQPS) + } + if config.RateLimitBurst <= 0 { + return fmt.Errorf("rate_limit_burst must be positive: %d", config.RateLimitBurst) + } + + if len(config.APIKeys) == 0 { + return fmt.Errorf("api_keys must not be empty") + } + for _, key := range config.APIKeys { + if key == "" { + return fmt.Errorf("api_keys contain empty key") + } + } + + validBalancerTypes := map[string]bool{ + "round-robin": true, + "consistent-hash": true, + } + if config.LoadBalancer != "" && !validBalancerTypes[config.LoadBalancer] { + return fmt.Errorf("invalid load_balancer: %s, must be round-robin or consistent-hash", config.LoadBalancer) + } + + return nil +} + +// isValidPort checks if a port number is valid +func isValidPort(port string) bool { + p, err := strconv.Atoi(port) + return err == nil && p > 0 && p <= 65535 +} + +// isValidAddrs checks if addresses are valid +func isValidAddrs(addrs string) bool { + if addrs == "" { + return false + } + addrRegex := regexp.MustCompile(`^([a-zA-Z0-9.-]+:[0-9]+)(,[a-zA-Z0-9.-]+:[0-9]+)*$`) + return addrRegex.MatchString(addrs) +} + +// MergeWithFlags merges command-line flags into the configuration +func MergeWithFlags(config Config, flags map[string]interface{}) Config { + if v, ok := flags["http-port"].(string); ok && v != "" { + config.HTTPPort = v + } + if v, ok := flags["service-name"].(string); ok && v != "" { + config.ServiceName = v + } + if v, ok := flags["grpc-port"].(string); ok && v != "" { + config.GRPCPort = v + } + if v, ok := flags["redis-addrs"].(string); ok && v != "" { + config.RedisAddrs = v + } + if v, ok := flags["etcd-addrs"].(string); ok && v != "" { + config.EtcdAddrs = v + } + if v, ok := flags["memcache-addrs"].(string); ok && v != "" { + config.MemcacheAddrs = v + } + if v, ok := flags["cache-type"].(string); ok && v != "" { + config.CacheType = v + } + if v, ok := flags["cache-ttl"].(int); ok && v != 0 { + config.CacheTTL = v + } + if v, ok := flags["cache-cleanup-interval"].(int); ok && v != 0 { + config.CacheCleanupInt = v + } + if v, ok := flags["cache-key-prefix"].(string); ok && v != "" { + config.CacheKeyPrefix = v + } + if v, ok := flags["service-discovery"].(string); ok && v != "" { + config.ServiceDiscovery = v + } + if v, ok := flags["service-discovery-addrs"].(string); ok && v != "" { + config.ServiceDiscoveryAddrs = v + } + if v, ok := flags["rate-limit-qps"].(int); ok && v != 0 { + config.RateLimitQPS = v + } + if v, ok := flags["rate-limit-burst"].(int); ok && v != 0 { + config.RateLimitBurst = v + } + if v, ok := flags["load-balancer"].(string); ok && v != "" { + config.LoadBalancer = v + } + if v, ok := flags["api-keys"].(string); ok && v != "" { + config.APIKeys = strings.Split(v, ",") + } + if v, ok := flags["enable-cors"].(bool); ok { + config.EnableCors = v + } + return config +} + +func DefaultConfig() Config { + return Config{ + ServiceName: "go-captcha-service", + HTTPPort: "8080", + GRPCPort: "50051", + RedisAddrs: "localhost:6379", + EtcdAddrs: "localhost:2379", + MemcacheAddrs: "localhost:11211", + CacheType: "memory", + CacheTTL: 60, + CacheCleanupInt: 10, + CacheKeyPrefix: "GO_CAPTCHA_DATA", + ServiceDiscovery: "", + ServiceDiscoveryAddrs: "localhost:2379", + RateLimitQPS: 1000, + RateLimitBurst: 1000, + LoadBalancer: "round-robin", + APIKeys: []string{"my-secret-key-123"}, + EnableCors: false, + } +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..e7fa1a7 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,128 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestLoad(t *testing.T) { + configContent := ` +{ + "http_port": "8080", + "grpc_port": "50051", + "redis_addrs": "localhost:6379", + "etcd_addrs": "localhost:2379", + "memcache_addrs": "localhost:11211", + "cache_type": "redis", + "cache_ttl": 60, + "cache_cleanup_interval": 10, + "service_discovery": "etcd", + "service_discovery_addrs": "localhost:2379", + "rate_limit_qps": 1000, + "rate_limit_burst": 1000, + "load_balancer": "round-robin", + "api_keys": ["key1", "key2"] +}` + tmpFile, err := os.CreateTemp("", "Config.json") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + _, err = tmpFile.WriteString(configContent) + assert.NoError(t, err) + tmpFile.Close() + + config, err := Load(tmpFile.Name()) + assert.NoError(t, err) + assert.Equal(t, "8080", config.HTTPPort) + assert.Equal(t, "redis", config.CacheType) + assert.Equal(t, []string{"key1", "key2"}, config.APIKeys) +} + +func TestValidate(t *testing.T) { + config := Config{ + HTTPPort: "8080", + GRPCPort: "50051", + RedisAddrs: "localhost:6379", + CacheType: "redis", + CacheTTL: 60, + CacheCleanupInt: 10, + ServiceDiscovery: "etcd", + ServiceDiscoveryAddrs: "localhost:2379", + RateLimitQPS: 1000, + RateLimitBurst: 1000, + LoadBalancer: "round-robin", + APIKeys: []string{"key1"}, + } + assert.NoError(t, Validate(config)) + + config.APIKeys = nil + assert.Error(t, Validate(config)) + + config.APIKeys = []string{""} + assert.Error(t, Validate(config)) +} + +func TestDynamicConfig(t *testing.T) { + configContent := ` +{ + "http_port": "8080", + "grpc_port": "50051", + "redis_addrs": "localhost:6379", + "etcd_addrs": "localhost:2379", + "memcache_addrs": "localhost:11211", + "cache_type": "redis", + "cache_ttl": 60, + "cache_cleanup_interval": 10, + "service_discovery": "etcd", + "service_discovery_addrs": "localhost:2379", + "rate_limit_qps": 1000, + "rate_limit_burst": 1000, + "load_balancer": "round-robin", + "api_keys": ["key1"] +}` + tmpDir, err := os.MkdirTemp("", "config_test") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) + + configPath := filepath.Join(tmpDir, "Config.json") + err = os.WriteFile(configPath, []byte(configContent), 0644) + assert.NoError(t, err) + + dc, err := NewDynamicConfig(configPath) + assert.NoError(t, err) + + cfg := dc.Get() + assert.Equal(t, []string{"key1"}, cfg.APIKeys) + assert.Equal(t, 1000, cfg.RateLimitQPS) + + // Update Config file + newContent := ` +{ + "http_port": "8080", + "grpc_port": "50051", + "redis_addrs": "localhost:6379", + "etcd_addrs": "localhost:2379", + "memcache_addrs": "localhost:11211", + "cache_type": "redis", + "cache_ttl": 60, + "cache_cleanup_interval": 10, + "service_discovery": "etcd", + "service_discovery_addrs": "localhost:2379", + "rate_limit_qps": 2000, + "rate_limit_burst": 2000, + "load_balancer": "round-robin", + "api_keys": ["key2", "key3"] +}` + err = os.WriteFile(configPath, []byte(newContent), 0644) + assert.NoError(t, err) + + // Wait for reload + time.Sleep(100 * time.Millisecond) + + cfg = dc.Get() + assert.Equal(t, []string{"key2", "key3"}, cfg.APIKeys) + assert.Equal(t, 2000, cfg.RateLimitQPS) +} diff --git a/internal/helper/helper.go b/internal/helper/helper.go new file mode 100644 index 0000000..41b6fb3 --- /dev/null +++ b/internal/helper/helper.go @@ -0,0 +1,70 @@ +package helper + +import ( + "encoding/json" + "reflect" + "strconv" + + "github.com/google/uuid" +) + +// GenUniqueId . +func GenUniqueId() (string, error) { + uid, err := uuid.NewUUID() + if err != nil { + return "", err + } + return uid.String(), nil +} + +// Marshal . +func Marshal(data interface{}) interface{} { + typeof := reflect.TypeOf(data) + valueof := reflect.ValueOf(data) + + for i := 0; i < typeof.Elem().NumField(); i++ { + if valueof.Elem().Field(i).IsZero() { + def := typeof.Elem().Field(i).Tag.Get("default") + if def != "" { + switch typeof.Elem().Field(i).Type.String() { + case "int": + result, _ := strconv.Atoi(def) + valueof.Elem().Field(i).SetInt(int64(result)) + case "uint": + result, _ := strconv.ParseUint(def, 10, 64) + valueof.Elem().Field(i).SetUint(result) + case "string": + valueof.Elem().Field(i).SetString(def) + case "interface {}": + valueof.Elem().Field(i).SetZero() + } + } + } + } + return data +} + +// MarshalJson . +func MarshalJson(data interface{}) ([]byte, error) { + typeof := reflect.TypeOf(data) + valueof := reflect.ValueOf(data) + + for i := 0; i < typeof.Elem().NumField(); i++ { + if valueof.Elem().Field(i).IsZero() { + def := typeof.Elem().Field(i).Tag.Get("default") + if def != "" { + switch typeof.Elem().Field(i).Type.String() { + case "int": + result, _ := strconv.Atoi(def) + valueof.Elem().Field(i).SetInt(int64(result)) + case "uint": + result, _ := strconv.ParseUint(def, 10, 64) + valueof.Elem().Field(i).SetUint(result) + case "string": + valueof.Elem().Field(i).SetString(def) + } + } + } + } + return json.Marshal(data) +} diff --git a/internal/load_balancer/consistent_hash.go b/internal/load_balancer/consistent_hash.go new file mode 100644 index 0000000..26ef55c --- /dev/null +++ b/internal/load_balancer/consistent_hash.go @@ -0,0 +1,24 @@ +package load_balancer + +import ( + "fmt" + + "github.com/wenlng/go-captcha-service/internal/service_discovery" +) + +// ConsistentHash . +type ConsistentHash struct{} + +// NewConsistentHash . +func NewConsistentHash() *ConsistentHash { + return &ConsistentHash{} +} + +// Select selects an instance using consistent hashing +func (lb *ConsistentHash) Select(instances []service_discovery.Instance) (service_discovery.Instance, error) { + if len(instances) == 0 { + return service_discovery.Instance{}, fmt.Errorf("no instances available") + } + // select first instance + return instances[0], nil +} diff --git a/internal/load_balancer/load_balancer.go b/internal/load_balancer/load_balancer.go new file mode 100644 index 0000000..2570ae1 --- /dev/null +++ b/internal/load_balancer/load_balancer.go @@ -0,0 +1,10 @@ +package load_balancer + +import ( + "github.com/wenlng/go-captcha-service/internal/service_discovery" +) + +// LoadBalancer . +type LoadBalancer interface { + Select(instances []service_discovery.Instance) (service_discovery.Instance, error) +} diff --git a/internal/load_balancer/load_balancer_test.go b/internal/load_balancer/load_balancer_test.go new file mode 100644 index 0000000..a17bec5 --- /dev/null +++ b/internal/load_balancer/load_balancer_test.go @@ -0,0 +1,43 @@ +package load_balancer + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/wenlng/go-captcha-service/internal/service_discovery" +) + +func TestRoundRobin(t *testing.T) { + lb := NewRoundRobin() + instances := []service_discovery.Instance{ + {InstanceID: "1", Host: "127.0.0.1", HTTPPort: 8080, GRPCPort: 50051}, + {InstanceID: "2", Host: "127.0.0.2", HTTPPort: 8081, GRPCPort: 50052}, + } + + instance, err := lb.Select(instances) + assert.NoError(t, err) + assert.Equal(t, "1", instance.InstanceID) + + instance, err = lb.Select(instances) + assert.NoError(t, err) + assert.Equal(t, "2", instance.InstanceID) + + instance, err = lb.Select(instances) + assert.NoError(t, err) + assert.Equal(t, "1", instance.InstanceID) +} + +func TestConsistentHash(t *testing.T) { + lb := NewConsistentHash() + instances := []service_discovery.Instance{ + {InstanceID: "1", Host: "127.0.0.1", HTTPPort: 8080, GRPCPort: 50051}, + } + + instance, err := lb.Select(instances) + assert.NoError(t, err) + assert.Equal(t, "1", instance.InstanceID) + + _, err = lb.Select([]service_discovery.Instance{}) + assert.Error(t, err) +} diff --git a/internal/load_balancer/round_robin.go b/internal/load_balancer/round_robin.go new file mode 100644 index 0000000..10d03e2 --- /dev/null +++ b/internal/load_balancer/round_robin.go @@ -0,0 +1,26 @@ +package load_balancer + +import ( + "fmt" + + "github.com/wenlng/go-captcha-service/internal/service_discovery" +) + +// RoundRobin implements round-robin load balancing +type RoundRobin struct { + index int +} + +// NewRoundRobin . +func NewRoundRobin() *RoundRobin { + return &RoundRobin{} +} + +// Select selects an instance using round-robin +func (lb *RoundRobin) Select(instances []service_discovery.Instance) (service_discovery.Instance, error) { + if len(instances) == 0 { + return service_discovery.Instance{}, fmt.Errorf("no instances available") + } + lb.index = (lb.index + 1) % len(instances) + return instances[lb.index], nil +} diff --git a/internal/logic/click.go b/internal/logic/click.go new file mode 100644 index 0000000..8277c9b --- /dev/null +++ b/internal/logic/click.go @@ -0,0 +1,151 @@ +package logic + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/wenlng/go-captcha-service/internal/adapt" + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/helper" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + "github.com/wenlng/go-captcha/v2/click" + "go.uber.org/zap" +) + +// ClickCaptLogic . +type ClickCaptLogic struct { + svcCtx *common.SvcContext + + cache cache.Cache + config *config.Config + logger *zap.Logger + captcha *gocaptcha.GoCaptcha +} + +// NewClickCaptLogic . +func NewClickCaptLogic(svcCtx *common.SvcContext) *ClickCaptLogic { + return &ClickCaptLogic{ + svcCtx: svcCtx, + cache: svcCtx.Cache, + config: svcCtx.Config, + logger: svcCtx.Logger, + captcha: svcCtx.Captcha, + } +} + +// GetData . +func (cl *ClickCaptLogic) GetData(ctx context.Context, ctype, theme, lang int) (res *adapt.CaptData, err error) { + res = &adapt.CaptData{} + + if ctype < 0 { + return nil, fmt.Errorf("missing parameter") + } + + captData, err := cl.captcha.ClickCaptInstance.Generate() + if err != nil { + return nil, fmt.Errorf("generate captcha data failed: %v", err) + } + + data := captData.GetData() + if data == nil { + return nil, fmt.Errorf("generate captcha data failed: %v", err) + } + + res.MasterImageBase64, err = captData.GetMasterImage().ToBase64() + if err != nil { + return nil, fmt.Errorf("failed to convert base64 encoding: %v", err) + } + + res.ThumbImageBase64, err = captData.GetThumbImage().ToBase64() + if err != nil { + return nil, fmt.Errorf("failed to convert base64 encoding: %v", err) + } + + cacheData := &cache.CaptCacheData{ + Data: data, + Status: 0, + } + cacheDataByte, err := json.Marshal(cacheData) + if err != nil { + return nil, fmt.Errorf("failed to json marshal: %v", err) + } + + key, err := helper.GenUniqueId() + if err != nil { + return nil, fmt.Errorf("failed to generate uuid: %v", err) + } + + err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + if err != nil { + return res, fmt.Errorf("failed to write cache:: %v", err) + } + + opts := cl.captcha.ClickCaptInstance.GetOptions() + res.MasterImageWidth = int32(opts.GetImageSize().Width) + res.MasterImageHeight = int32(opts.GetImageSize().Height) + res.ThumbImageWidth = int32(opts.GetThumbImageSize().Width) + res.ThumbImageHeight = int32(opts.GetThumbImageSize().Height) + res.CaptchaKey = key + return res, nil +} + +// CheckData . +func (cl *ClickCaptLogic) CheckData(ctx context.Context, key string, dots string) (bool, error) { + if key == "" { + return false, fmt.Errorf("invalid key") + } + + cacheData, err := cl.cache.GetCache(ctx, key) + if err != nil { + return false, fmt.Errorf("failed to get cache: %v", err) + } + + src := strings.Split(dots, ",") + + var captData *cache.CaptCacheData + err = json.Unmarshal([]byte(cacheData), &captData) + if err != nil { + return false, fmt.Errorf("failed to json unmarshal: %v", err) + } + + dct, ok := captData.Data.(map[int]*click.Dot) + if !ok { + return false, fmt.Errorf("cache data invalid: %v", err) + } + + ret := false + if (len(dct) * 2) == len(src) { + for i := 0; i < len(dct); i++ { + dot := dct[i] + j := i * 2 + k := i*2 + 1 + sx, _ := strconv.ParseInt(src[j], 10, 64) + sy, _ := strconv.ParseInt(src[k], 10, 64) + + ret = click.CheckPoint(sx, sy, int64(dot.X), int64(dot.Y), int64(dot.Width), int64(dot.Height), 0) + if !ret { + break + } + } + } + + if ret { + captData.Status = 1 + cacheDataByte, err := json.Marshal(captData) + if err != nil { + return ret, fmt.Errorf("failed to json marshal: %v", err) + } + + err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + if err != nil { + return ret, fmt.Errorf("failed to update cache:: %v", err) + } + } + + return ret, nil +} diff --git a/internal/logic/click_test.go b/internal/logic/click_test.go new file mode 100644 index 0000000..9d3159b --- /dev/null +++ b/internal/logic/click_test.go @@ -0,0 +1,101 @@ +package logic + +import ( + "context" + "encoding/json" + "strconv" + "strings" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + "github.com/wenlng/go-captcha/v2/click" + "go.uber.org/zap" +) + +func TestCacheLogic(t *testing.T) { + mr, err := miniredis.Run() + assert.NoError(t, err) + defer mr.Close() + + ttl := time.Duration(10) * time.Second + cleanInt := time.Duration(30) * time.Second + cacheClient := cache.NewMemoryCache("TEST_CAPTCHA_DATA:", ttl, cleanInt) + defer cacheClient.Close() + + dc := &config.DynamicConfig{Config: config.DefaultConfig()} + cnf := dc.Get() + + logger, err := zap.NewProduction() + assert.NoError(t, err) + + captcha, err := gocaptcha.Setup() + assert.NoError(t, err) + + svcCtx := &common.SvcContext{ + Cache: cacheClient, + Config: &cnf, + Logger: logger, + Captcha: captcha, + } + logic := NewClickCaptLogic(svcCtx) + + t.Run("GetData", func(t *testing.T) { + _, err := logic.GetData(context.Background(), 0, 1, 1) + assert.NoError(t, err) + }) + + t.Run("GetData_Miss", func(t *testing.T) { + _, err := logic.GetData(context.Background(), -1, 1, 1) + assert.Error(t, err) + }) + + t.Run("CheckData", func(t *testing.T) { + data, err := logic.GetData(context.Background(), 1, 1, 1) + assert.NoError(t, err) + + cacheData, err := svcCtx.Cache.GetCache(context.Background(), data.CaptchaKey) + assert.NoError(t, err) + + var dct map[int]*click.Dot + err = json.Unmarshal([]byte(cacheData), &dct) + assert.NoError(t, err) + + var dots []string + for i := 0; i < len(dct); i++ { + dot := dct[i] + dots = append(dots, strconv.Itoa(dot.X), strconv.Itoa(dot.Y)) + } + + dotStr := strings.Join(dots, ",") + result, err := logic.CheckData(context.Background(), data.CaptchaKey, dotStr) + assert.NoError(t, err) + assert.Equal(t, true, result) + }) + + t.Run("CheckData_MISS", func(t *testing.T) { + data, err := logic.GetData(context.Background(), 1, 1, 1) + assert.NoError(t, err) + + cacheData, err := svcCtx.Cache.GetCache(context.Background(), data.CaptchaKey) + assert.NoError(t, err) + + var dct map[int]*click.Dot + err = json.Unmarshal([]byte(cacheData), &dct) + assert.NoError(t, err) + + var dots = []string{ + "111", + "222", + } + dotStr := strings.Join(dots, ",") + result, err := logic.CheckData(context.Background(), data.CaptchaKey, dotStr) + assert.NoError(t, err) + assert.Equal(t, false, result) + }) +} diff --git a/internal/logic/common.go b/internal/logic/common.go new file mode 100644 index 0000000..716b0e4 --- /dev/null +++ b/internal/logic/common.go @@ -0,0 +1,88 @@ +package logic + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + "go.uber.org/zap" +) + +// CommonLogic . +type CommonLogic struct { + svcCtx *common.SvcContext + + cache cache.Cache + config *config.Config + logger *zap.Logger + captcha *gocaptcha.GoCaptcha +} + +// NewCommonLogic . +func NewCommonLogic(svcCtx *common.SvcContext) *CommonLogic { + return &CommonLogic{ + svcCtx: svcCtx, + cache: svcCtx.Cache, + config: svcCtx.Config, + logger: svcCtx.Logger, + captcha: svcCtx.Captcha, + } +} + +// CheckStatus . +func (cl *CommonLogic) CheckStatus(ctx context.Context, key string) (ret bool, err error) { + if key == "" { + return false, fmt.Errorf("invalid key") + } + + cacheData, err := cl.cache.GetCache(ctx, key) + if err != nil { + return false, fmt.Errorf("failed to get cache: %v", err) + } + + var captData *cache.CaptCacheData + err = json.Unmarshal([]byte(cacheData), &captData) + if err != nil { + return false, fmt.Errorf("failed to json unmarshal: %v", err) + } + + return captData.Status == 1, nil +} + +// GetStatusInfo . +func (cl *CommonLogic) GetStatusInfo(ctx context.Context, key string) (data *cache.CaptCacheData, err error) { + if key == "" { + return nil, fmt.Errorf("invalid key") + } + + cacheData, err := cl.cache.GetCache(ctx, key) + if err != nil { + return nil, fmt.Errorf("failed to get cache: %v", err) + } + + var captData *cache.CaptCacheData + err = json.Unmarshal([]byte(cacheData), &captData) + if err != nil { + return nil, fmt.Errorf("failed to json unmarshal: %v", err) + } + + return captData, nil +} + +// DelStatusInfo . +func (cl *CommonLogic) DelStatusInfo(ctx context.Context, key string) (ret bool, err error) { + if key == "" { + return false, fmt.Errorf("invalid key") + } + + err = cl.cache.DeleteCache(ctx, key) + if err != nil { + return false, fmt.Errorf("failed to get cache: %v", err) + } + + return true, nil +} diff --git a/internal/logic/rotate.go b/internal/logic/rotate.go new file mode 100644 index 0000000..9caeb5f --- /dev/null +++ b/internal/logic/rotate.go @@ -0,0 +1,134 @@ +package logic + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/wenlng/go-captcha-service/internal/adapt" + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/helper" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + "github.com/wenlng/go-captcha/v2/rotate" + "go.uber.org/zap" +) + +// RotateCaptLogic . +type RotateCaptLogic struct { + svcCtx *common.SvcContext + + cache cache.Cache + config *config.Config + logger *zap.Logger + captcha *gocaptcha.GoCaptcha +} + +// NewRotateCaptLogic . +func NewRotateCaptLogic(svcCtx *common.SvcContext) *RotateCaptLogic { + return &RotateCaptLogic{ + svcCtx: svcCtx, + cache: svcCtx.Cache, + config: svcCtx.Config, + logger: svcCtx.Logger, + captcha: svcCtx.Captcha, + } +} + +// GetData . +func (cl *RotateCaptLogic) GetData(ctx context.Context, ctype, theme, lang int) (res *adapt.CaptData, err error) { + res = &adapt.CaptData{} + + if ctype < 0 { + return nil, fmt.Errorf("missing parameter") + } + + captData, err := cl.captcha.RotateCaptInstance.Generate() + if err != nil { + return nil, fmt.Errorf("generate captcha data failed: %v", err) + } + + data := captData.GetData() + if data == nil { + return nil, fmt.Errorf("generate captcha data failed: %v", err) + } + + res.MasterImageBase64, err = captData.GetMasterImage().ToBase64() + if err != nil { + return nil, fmt.Errorf("failed to convert base64 encoding: %v", err) + } + + res.ThumbImageBase64, err = captData.GetThumbImage().ToBase64() + if err != nil { + return nil, fmt.Errorf("failed to convert base64 encoding: %v", err) + } + + cacheData := &cache.CaptCacheData{ + Data: data, + Status: 0, + } + cacheDataByte, err := json.Marshal(cacheData) + if err != nil { + return nil, fmt.Errorf("failed to json marshal: %v", err) + } + + key, err := helper.GenUniqueId() + if err != nil { + return nil, fmt.Errorf("failed to generate uuid: %v", err) + } + + err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + if err != nil { + return res, fmt.Errorf("failed to write cache:: %v", err) + } + + opts := cl.captcha.ClickCaptInstance.GetOptions() + res.MasterImageWidth = int32(opts.GetImageSize().Width) + res.MasterImageHeight = int32(opts.GetImageSize().Height) + res.ThumbImageWidth = int32(data.Width) + res.ThumbImageHeight = int32(data.Height) + res.ThumbImageSize = int32(data.Width) + res.CaptchaKey = key + return res, nil +} + +// CheckData . +func (cl *RotateCaptLogic) CheckData(ctx context.Context, key string, angle int) (bool, error) { + if key == "" { + return false, fmt.Errorf("invalid key") + } + + cacheData, err := cl.cache.GetCache(ctx, key) + if err != nil { + return false, fmt.Errorf("failed to get cache: %v", err) + } + + var captData *cache.CaptCacheData + err = json.Unmarshal([]byte(cacheData), &captData) + if err != nil { + return false, fmt.Errorf("failed to json unmarshal: %v", err) + } + + dct, ok := captData.Data.(*rotate.Block) + if !ok { + return false, fmt.Errorf("cache data invalid: %v", err) + } + + ret := rotate.CheckAngle(int64(angle), int64(dct.Angle), 2) + + if ret { + captData.Status = 1 + cacheDataByte, err := json.Marshal(captData) + if err != nil { + return ret, fmt.Errorf("failed to json marshal: %v", err) + } + + err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + if err != nil { + return ret, fmt.Errorf("failed to update cache:: %v", err) + } + } + + return ret, nil +} diff --git a/internal/logic/slide.go b/internal/logic/slide.go new file mode 100644 index 0000000..d265b94 --- /dev/null +++ b/internal/logic/slide.go @@ -0,0 +1,144 @@ +package logic + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/wenlng/go-captcha-service/internal/adapt" + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/helper" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + "github.com/wenlng/go-captcha/v2/slide" + "go.uber.org/zap" +) + +// SlideCaptLogic . +type SlideCaptLogic struct { + svcCtx *common.SvcContext + + cache cache.Cache + config *config.Config + logger *zap.Logger + captcha *gocaptcha.GoCaptcha +} + +// NewSlideCaptLogic . +func NewSlideCaptLogic(svcCtx *common.SvcContext) *SlideCaptLogic { + return &SlideCaptLogic{ + svcCtx: svcCtx, + cache: svcCtx.Cache, + config: svcCtx.Config, + logger: svcCtx.Logger, + captcha: svcCtx.Captcha, + } +} + +// GetData . +func (cl *SlideCaptLogic) GetData(ctx context.Context, ctype, theme, lang int) (res *adapt.CaptData, err error) { + res = &adapt.CaptData{} + + if ctype < 0 { + return nil, fmt.Errorf("missing parameter") + } + + captData, err := cl.captcha.SlideCaptInstance.Generate() + if err != nil { + return nil, fmt.Errorf("generate captcha data failed: %v", err) + } + + data := captData.GetData() + if data == nil { + return nil, fmt.Errorf("generate captcha data failed: %v", err) + } + + res.MasterImageBase64, err = captData.GetMasterImage().ToBase64() + if err != nil { + return nil, fmt.Errorf("failed to convert base64 encoding: %v", err) + } + + res.ThumbImageBase64, err = captData.GetTileImage().ToBase64() + if err != nil { + return nil, fmt.Errorf("failed to convert base64 encoding: %v", err) + } + + cacheData := &cache.CaptCacheData{ + Data: data, + Status: 0, + } + cacheDataByte, err := json.Marshal(cacheData) + if err != nil { + return nil, fmt.Errorf("failed to json marshal: %v", err) + } + + key, err := helper.GenUniqueId() + if err != nil { + return nil, fmt.Errorf("failed to generate uuid: %v", err) + } + + err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + if err != nil { + return res, fmt.Errorf("failed to write cache:: %v", err) + } + + opts := cl.captcha.ClickCaptInstance.GetOptions() + res.MasterImageWidth = int32(opts.GetImageSize().Width) + res.MasterImageHeight = int32(opts.GetImageSize().Height) + res.ThumbImageWidth = int32(data.Width) + res.ThumbImageHeight = int32(data.Height) + res.DisplayX = int32(data.TileX) + res.DisplayY = int32(data.TileY) + res.CaptchaKey = key + return res, nil +} + +// CheckData . +func (cl *SlideCaptLogic) CheckData(ctx context.Context, key string, dots string) (bool, error) { + if key == "" { + return false, fmt.Errorf("invalid key") + } + + cacheData, err := cl.cache.GetCache(ctx, key) + if err != nil { + return false, fmt.Errorf("failed to get cache: %v", err) + } + + src := strings.Split(dots, ",") + + var captData *cache.CaptCacheData + err = json.Unmarshal([]byte(cacheData), &captData) + if err != nil { + return false, fmt.Errorf("failed to json unmarshal: %v", err) + } + + dct, ok := captData.Data.(*slide.Block) + if !ok { + return false, fmt.Errorf("cache data invalid: %v", err) + } + + ret := false + if 2 == len(src) { + sx, _ := strconv.ParseInt(src[0], 10, 64) + sy, _ := strconv.ParseInt(src[1], 10, 64) + ret = slide.CheckPoint(sx, sy, int64(dct.X), int64(dct.Y), 4) + } + + if ret { + captData.Status = 1 + cacheDataByte, err := json.Marshal(captData) + if err != nil { + return ret, fmt.Errorf("failed to json marshal: %v", err) + } + + err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + if err != nil { + return ret, fmt.Errorf("failed to update cache:: %v", err) + } + } + + return ret, nil +} diff --git a/internal/middleware/grpc_middleware.go b/internal/middleware/grpc_middleware.go new file mode 100644 index 0000000..b41c148 --- /dev/null +++ b/internal/middleware/grpc_middleware.go @@ -0,0 +1,69 @@ +package middleware + +import ( + "context" + "time" + + "github.com/sony/gobreaker" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "github.com/wenlng/go-captcha-service/internal/config" +) + +// UnaryServerInterceptor implements gRPC unary interceptor +func UnaryServerInterceptor(dc *config.DynamicConfig, logger *zap.Logger, breaker *gobreaker.CircuitBreaker) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + start := time.Now() + + // Validate API key + cfg := dc.Get() + apiKeyMap := make(map[string]struct{}) + for _, key := range cfg.APIKeys { + apiKeyMap[key] = struct{}{} + } + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + logger.Warn("Missing metadata") + return nil, status.Error(codes.Unauthenticated, "missing API Key") + } + apiKeys := md.Get("x-api-key") + if len(apiKeys) == 0 { + logger.Warn("Missing API Key") + return nil, status.Error(codes.Unauthenticated, "missing API Key") + } + if _, exists := apiKeyMap[apiKeys[0]]; !exists { + logger.Warn("Invalid API Key", zap.String("key", apiKeys[0])) + return nil, status.Error(codes.Unauthenticated, "invalid API Key") + } + + // Apply circuit breaker + var resp interface{} + var err error + _, cbErr := breaker.Execute(func() (interface{}, error) { + resp, err = handler(ctx, req) + return nil, nil + }) + if cbErr == gobreaker.ErrOpenState || cbErr == gobreaker.ErrTooManyRequests { + logger.Warn("gRPC circuit breaker tripped", zap.Error(cbErr)) + return nil, status.Error(codes.Unavailable, "service unavailable") + } + if cbErr != nil { + logger.Error("gRPC circuit breaker error", zap.Error(cbErr)) + return nil, status.Error(codes.Internal, "internal server error") + } + + // Log request + logger.Info("gRPC request", + zap.String("method", info.FullMethod), + zap.Duration("duration", time.Since(start)), + zap.Error(err), + ) + + return resp, err + } +} diff --git a/internal/middleware/http_niddleware.go b/internal/middleware/http_niddleware.go new file mode 100644 index 0000000..9721a16 --- /dev/null +++ b/internal/middleware/http_niddleware.go @@ -0,0 +1,216 @@ +package middleware + +import ( + "context" + "encoding/json" + "net/http" + "sync" + "time" + + "github.com/sony/gobreaker" + "go.uber.org/zap" + "golang.org/x/time/rate" + + "github.com/wenlng/go-captcha-service/internal/config" +) + +// ErrorResponse defines the standard error response format +type ErrorResponse struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// HandlerFunc . +type HandlerFunc func(http.ResponseWriter, *http.Request) + +// HTTPMiddleware defines the middleware function signature +type HTTPMiddleware func(HandlerFunc) HandlerFunc + +// MwChain . +type MwChain struct { + middlewares []HTTPMiddleware +} + +// NewChainHTTP . +func NewChainHTTP(mws ...HTTPMiddleware) *MwChain { + return &MwChain{middlewares: mws} +} + +// AppendMiddleware new middlewares +func (c *MwChain) AppendMiddleware(final HTTPMiddleware) *MwChain { + c.middlewares = append(c.middlewares, final) + + return c +} + +// Then chains multiple middlewares +func (c *MwChain) Then(final HandlerFunc) http.HandlerFunc { + for i := len(c.middlewares) - 1; i >= 0; i-- { + if c.middlewares[i] != nil { + final = c.middlewares[i](final) + } + } + + return http.HandlerFunc(final) +} + +// APIKeyMiddleware validates API keys +func APIKeyMiddleware(dc *config.DynamicConfig, logger *zap.Logger) HTTPMiddleware { + return func(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cfg := dc.Get() + apiKeyMap := make(map[string]struct{}) + for _, key := range cfg.APIKeys { + apiKeyMap[key] = struct{}{} + } + + apiKey := r.Header.Get("X-API-Key") + if apiKey == "" { + logger.Warn("Missing API Key") + WriteError(w, http.StatusUnauthorized, "missing API Key") + return + } + if _, exists := apiKeyMap[apiKey]; !exists { + logger.Warn("Invalid API Key", zap.String("key", apiKey)) + WriteError(w, http.StatusUnauthorized, "invalid API Key") + return + } + next(w, r) + } + } +} + +// RateLimitMiddleware enforces rate limiting +func RateLimitMiddleware(limiter *DynamicLimiter, logger *zap.Logger) HTTPMiddleware { + return func(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := limiter.Wait(r.Context()); err != nil { + logger.Warn("Rate limit exceeded", zap.String("client", r.RemoteAddr)) + WriteError(w, http.StatusTooManyRequests, "rate limit exceeded") + return + } + next(w, r) + } + } +} + +// LoggingMiddleware logs HTTP requests +func LoggingMiddleware(logger *zap.Logger) HTTPMiddleware { + return func(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + next(w, r) + logger.Info("HTTP request", + zap.String("method", r.Method), + zap.String("path", r.URL.Path), + zap.String("client", r.RemoteAddr), + zap.Duration("duration", time.Since(start)), + ) + } + } +} + +// CircuitBreakerMiddleware implements circuit breaking +func CircuitBreakerMiddleware(breaker *gobreaker.CircuitBreaker, logger *zap.Logger) HTTPMiddleware { + return func(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + _, err := breaker.Execute(func() (interface{}, error) { + next(w, r) + return nil, nil + }) + if err == gobreaker.ErrOpenState || err == gobreaker.ErrTooManyRequests { + logger.Warn("Circuit breaker tripped", zap.Error(err)) + WriteError(w, http.StatusServiceUnavailable, "service unavailable") + return + } + if err != nil { + logger.Error("Circuit breaker error", zap.Error(err)) + WriteError(w, http.StatusInternalServerError, "internal server error") + return + } + } + } +} + +// CORSMiddleware implements cross-origin resource sharing +func CORSMiddleware(logger *zap.Logger) HTTPMiddleware { + return func(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + next(w, r) + } + } +} + +// DynamicLimiter manages dynamic rate limiting +type DynamicLimiter struct { + limiter *rate.Limiter + mu sync.RWMutex +} + +// NewDynamicLimiter creates a new dynamic rate limiter +func NewDynamicLimiter(qps, burst int) *DynamicLimiter { + return &DynamicLimiter{ + limiter: rate.NewLimiter(rate.Limit(qps), burst), + } +} + +// Wait checks the rate limit +func (d *DynamicLimiter) Wait(ctx context.Context) error { + d.mu.RLock() + defer d.mu.RUnlock() + return d.limiter.Wait(ctx) +} + +// Update updates rate limit parameters +func (d *DynamicLimiter) Update(qps, burst int) { + if qps <= 0 || burst <= 0 { + return + } + d.mu.Lock() + defer d.mu.Unlock() + d.limiter.SetLimit(rate.Limit(qps)) + d.limiter.SetBurst(burst) +} + +// RateLimitHandler handles dynamic rate limit updates +func RateLimitHandler(limiter *DynamicLimiter, logger *zap.Logger) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + var params struct { + QPS int `json:"qps"` + Burst int `json:"burst"` + } + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + logger.Warn("Invalid rate limit params", zap.Error(err)) + WriteError(w, http.StatusBadRequest, "invalid parameters") + return + } + if params.QPS <= 0 || params.Burst <= 0 { + WriteError(w, http.StatusBadRequest, "qps and burst must be positive") + return + } + limiter.Update(params.QPS, params.Burst) + logger.Info("Rate limit updated", + zap.Int("qps", params.QPS), + zap.Int("burst", params.Burst), + ) + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + } +} + +// WriteError sends an error response +func WriteError(w http.ResponseWriter, code int, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(ErrorResponse{ + Code: code, + Message: message, + }) +} diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go new file mode 100644 index 0000000..7bb31c6 --- /dev/null +++ b/internal/middleware/middleware_test.go @@ -0,0 +1,220 @@ +package middleware + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/sony/gobreaker" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "github.com/wenlng/go-captcha-service/internal/config" +) + +func TestAPIKeyMiddleware(t *testing.T) { + logger, _ := zap.NewDevelopment() + dc := &config.DynamicConfig{ + Config: config.Config{ + APIKeys: []string{"valid-key"}, + }, + } + + mw := APIKeyMiddleware(dc, logger) + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + } + + t.Run("ValidKey", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("X-API-Key", "valid-key") + rr := httptest.NewRecorder() + mw(handler)(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + }) + + t.Run("MissingKey", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test", nil) + rr := httptest.NewRecorder() + mw(handler)(rr, req) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + var resp ErrorResponse + json.NewDecoder(rr.Body).Decode(&resp) + assert.Equal(t, "missing API Key", resp.Message) + }) + + t.Run("InvalidKey", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("X-API-Key", "invalid-key") + rr := httptest.NewRecorder() + mw(handler)(rr, req) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + var resp ErrorResponse + json.NewDecoder(rr.Body).Decode(&resp) + assert.Equal(t, "invalid API Key", resp.Message) + }) +} + +func TestRateLimitMiddleware(t *testing.T) { + logger, _ := zap.NewDevelopment() + limiter := NewDynamicLimiter(1, 1) // 1 QPS, 1 burst + mw := RateLimitMiddleware(limiter, logger) + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + } + + t.Run("WithinLimit", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test", nil) + rr := httptest.NewRecorder() + mw(handler)(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + }) + + t.Run("ExceedLimit", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test", nil) + rr := httptest.NewRecorder() + mw(handler)(rr, req) // First request + rr = httptest.NewRecorder() + mw(handler)(rr, req) // Second request exceeds limit + assert.Equal(t, http.StatusTooManyRequests, rr.Code) + var resp ErrorResponse + json.NewDecoder(rr.Body).Decode(&resp) + assert.Equal(t, "rate limit exceeded", resp.Message) + }) +} + +func TestLoggingMiddleware(t *testing.T) { + logger, _ := zap.NewDevelopment() + mw := LoggingMiddleware(logger) + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + } + + req := httptest.NewRequest("GET", "/test", nil) + rr := httptest.NewRecorder() + mw(handler)(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) +} + +func TestCircuitBreakerMiddleware(t *testing.T) { + logger, _ := zap.NewDevelopment() + breaker := gobreaker.NewCircuitBreaker(gobreaker.Settings{ + Name: "test", + MaxRequests: 1, + Interval: 60 * time.Second, + Timeout: 5 * time.Second, + ReadyToTrip: func(counts gobreaker.Counts) bool { + return counts.ConsecutiveFailures > 1 + }, + }) + mw := CircuitBreakerMiddleware(breaker, logger) + handler := func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "error", http.StatusInternalServerError) + } + + req := httptest.NewRequest("GET", "/test", nil) + rr := httptest.NewRecorder() + mw(handler)(rr, req) + assert.Equal(t, http.StatusInternalServerError, rr.Code) + + // Simulate multiple failures to trip breaker + for i := 0; i < 2; i++ { + rr = httptest.NewRecorder() + mw(handler)(rr, req) + } + + // Breaker should be open + rr = httptest.NewRecorder() + mw(handler)(rr, req) + assert.Equal(t, http.StatusServiceUnavailable, rr.Code) + var resp ErrorResponse + json.NewDecoder(rr.Body).Decode(&resp) + assert.Equal(t, "service unavailable", resp.Message) +} + +func TestRateLimitHandler(t *testing.T) { + logger, _ := zap.NewDevelopment() + limiter := NewDynamicLimiter(1000, 1000) + handler := RateLimitHandler(limiter, logger) + + t.Run("ValidUpdate", func(t *testing.T) { + body := strings.NewReader(`{"qps":10,"burst":10}`) + req := httptest.NewRequest("POST", "/rate-limit", body) + rr := httptest.NewRecorder() + handler(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + var resp map[string]string + json.NewDecoder(rr.Body).Decode(&resp) + assert.Equal(t, "success", resp["status"]) + }) + + t.Run("InvalidMethod", func(t *testing.T) { + req := httptest.NewRequest("GET", "/rate-limit", nil) + rr := httptest.NewRecorder() + handler(rr, req) + assert.Equal(t, http.StatusMethodNotAllowed, rr.Code) + var resp ErrorResponse + json.NewDecoder(rr.Body).Decode(&resp) + assert.Equal(t, "method not allowed", resp.Message) + }) + + t.Run("InvalidParams", func(t *testing.T) { + body := strings.NewReader(`{"qps":0,"burst":0}`) + req := httptest.NewRequest("POST", "/rate-limit", body) + rr := httptest.NewRecorder() + handler(rr, req) + assert.Equal(t, http.StatusBadRequest, rr.Code) + var resp ErrorResponse + json.NewDecoder(rr.Body).Decode(&resp) + assert.Equal(t, "qps and burst must be positive", resp.Message) + }) +} + +func TestGRPCInterceptor(t *testing.T) { + logger, _ := zap.NewDevelopment() + dc := &config.DynamicConfig{ + Config: config.Config{ + APIKeys: []string{"valid-key"}, + }, + } + breaker := gobreaker.NewCircuitBreaker(gobreaker.Settings{ + Name: "grpc-test", + MaxRequests: 1, + Interval: 60 * time.Second, + Timeout: 5 * time.Second, + }) + interceptor := UnaryServerInterceptor(dc, logger, breaker) + + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return "success", nil + } + + t.Run("ValidKey", func(t *testing.T) { + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-api-key", "valid-key")) + resp, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{FullMethod: "TestMethod"}, handler) + assert.NoError(t, err) + assert.Equal(t, "success", resp) + }) + + t.Run("MissingKey", func(t *testing.T) { + ctx := context.Background() + _, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{FullMethod: "TestMethod"}, handler) + assert.Error(t, err) + assert.Equal(t, codes.Unauthenticated, status.Code(err)) + }) + + t.Run("InvalidKey", func(t *testing.T) { + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-api-key", "invalid-key")) + _, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{FullMethod: "TestMethod"}, handler) + assert.Error(t, err) + assert.Equal(t, codes.Unauthenticated, status.Code(err)) + }) +} diff --git a/internal/pkg/gocaptcha/click.go b/internal/pkg/gocaptcha/click.go new file mode 100644 index 0000000..18f4b10 --- /dev/null +++ b/internal/pkg/gocaptcha/click.go @@ -0,0 +1,69 @@ +package gocaptcha + +import ( + "github.com/golang/freetype/truetype" + "github.com/wenlng/go-captcha-assets/bindata/chars" + "github.com/wenlng/go-captcha-assets/resources/fonts/fzshengsksjw" + "github.com/wenlng/go-captcha-assets/resources/images_v2" + "github.com/wenlng/go-captcha-assets/resources/shapes" + + "github.com/wenlng/go-captcha/v2/base/option" + "github.com/wenlng/go-captcha/v2/click" +) + +func setupClick() (capt click.Captcha, err error) { + builder := click.NewBuilder( + click.WithRangeLen(option.RangeVal{Min: 4, Max: 6}), + click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 4}), + ) + + // fonts + fonts, err := fzshengsksjw.GetFont() + if err != nil { + return nil, err + } + + // background images + imgs, err := images.GetImages() + if err != nil { + return nil, err + } + + // set resources + builder.SetResources( + click.WithChars(chars.GetChineseChars()), + click.WithFonts([]*truetype.Font{fonts}), + click.WithBackgrounds(imgs), + ) + + return builder.Make(), nil +} + +func setupClickShape() (capt click.Captcha, err error) { + builder := click.NewBuilder( + click.WithRangeLen(option.RangeVal{Min: 3, Max: 6}), + click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 3}), + click.WithRangeThumbBgDistort(1), + click.WithIsThumbNonDeformAbility(true), + ) + + // shape + shapeMaps, err := shapes.GetShapes() + if err != nil { + return nil, err + } + + // background images + imgs, err := images.GetImages() + if err != nil { + return nil, err + } + + // set resources + builder.SetResources( + click.WithShapes(shapeMaps), + click.WithBackgrounds(imgs), + ) + + return builder.Make(), nil +} diff --git a/internal/pkg/gocaptcha/gocaptcha.go b/internal/pkg/gocaptcha/gocaptcha.go new file mode 100644 index 0000000..41fe8b2 --- /dev/null +++ b/internal/pkg/gocaptcha/gocaptcha.go @@ -0,0 +1,51 @@ +package gocaptcha + +import ( + "github.com/wenlng/go-captcha/v2/click" + "github.com/wenlng/go-captcha/v2/rotate" + "github.com/wenlng/go-captcha/v2/slide" +) + +type GoCaptcha struct { + ClickCaptInstance click.Captcha + ClickShapeCaptInstance click.Captcha + SlideCaptInstance slide.Captcha + DragCaptInstance slide.Captcha + RotateCaptInstance rotate.Captcha +} + +func Setup() (*GoCaptcha, error) { + var gc = &GoCaptcha{} + + cc, err := setupClick() + if err != nil { + return nil, err + } + gc.ClickCaptInstance = cc + + ccs, err := setupClickShape() + if err != nil { + return nil, err + } + gc.ClickShapeCaptInstance = ccs + + sc, err := setupSlide() + if err != nil { + return nil, err + } + gc.SlideCaptInstance = sc + + scc, err := setupDrag() + if err != nil { + return nil, err + } + gc.DragCaptInstance = scc + + rc, err := setupRotate() + if err != nil { + return nil, err + } + gc.RotateCaptInstance = rc + + return gc, nil +} diff --git a/internal/pkg/gocaptcha/rotate.go b/internal/pkg/gocaptcha/rotate.go new file mode 100644 index 0000000..e738f0f --- /dev/null +++ b/internal/pkg/gocaptcha/rotate.go @@ -0,0 +1,28 @@ +package gocaptcha + +import ( + "github.com/wenlng/go-captcha-assets/resources/images_v2" + "github.com/wenlng/go-captcha/v2/base/option" + "github.com/wenlng/go-captcha/v2/rotate" +) + +func setupRotate() (capt rotate.Captcha, err error) { + builder := rotate.NewBuilder( + rotate.WithRangeAnglePos([]option.RangeVal{ + {Min: 20, Max: 330}, + }), + ) + + // background images + imgs, err := images.GetImages() + if err != nil { + return nil, err + } + + // set resources + builder.SetResources( + rotate.WithImages(imgs), + ) + + return builder.Make(), nil +} diff --git a/internal/pkg/gocaptcha/slide.go b/internal/pkg/gocaptcha/slide.go new file mode 100644 index 0000000..9e7cfa9 --- /dev/null +++ b/internal/pkg/gocaptcha/slide.go @@ -0,0 +1,78 @@ +package gocaptcha + +import ( + "log" + + "github.com/wenlng/go-captcha-assets/resources/images_v2" + "github.com/wenlng/go-captcha-assets/resources/tiles" + "github.com/wenlng/go-captcha/v2/slide" +) + +func setupSlide() (capt slide.Captcha, err error) { + builder := slide.NewBuilder() + + // background images + imgs, err := images.GetImages() + if err != nil { + return nil, err + } + + graphs, err := tiles.GetTiles() + if err != nil { + log.Fatalln(err) + } + + var newGraphs = make([]*slide.GraphImage, 0, len(graphs)) + for i := 0; i < len(graphs); i++ { + graph := graphs[i] + newGraphs = append(newGraphs, &slide.GraphImage{ + OverlayImage: graph.OverlayImage, + MaskImage: graph.MaskImage, + ShadowImage: graph.ShadowImage, + }) + } + + // set resources + builder.SetResources( + slide.WithGraphImages(newGraphs), + slide.WithBackgrounds(imgs), + ) + + return builder.Make(), nil +} + +func setupDrag() (capt slide.Captcha, err error) { + builder := slide.NewBuilder( + slide.WithGenGraphNumber(2), + slide.WithEnableGraphVerticalRandom(true), + ) + + // background images + imgs, err := images.GetImages() + if err != nil { + return nil, err + } + + graphs, err := tiles.GetTiles() + if err != nil { + log.Fatalln(err) + } + + var newGraphs = make([]*slide.GraphImage, 0, len(graphs)) + for i := 0; i < len(graphs); i++ { + graph := graphs[i] + newGraphs = append(newGraphs, &slide.GraphImage{ + OverlayImage: graph.OverlayImage, + MaskImage: graph.MaskImage, + ShadowImage: graph.ShadowImage, + }) + } + + // set resources + builder.SetResources( + slide.WithGraphImages(newGraphs), + slide.WithBackgrounds(imgs), + ) + + return builder.Make(), nil +} diff --git a/internal/server/grpc_server.go b/internal/server/grpc_server.go new file mode 100644 index 0000000..e618a8c --- /dev/null +++ b/internal/server/grpc_server.go @@ -0,0 +1,198 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "github.com/wenlng/go-captcha-service/internal/adapt" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/logic" + "github.com/wenlng/go-captcha-service/proto" + "go.uber.org/zap" +) + +// GrpcServer implements the gRPC cache service +type GrpcServer struct { + proto.UnimplementedGoCaptchaServiceServer + config *config.Config + logger *zap.Logger + + // Initialize logic + clickCaptLogic *logic.ClickCaptLogic + slideCaptLogic *logic.SlideCaptLogic + rotateCaptLogic *logic.RotateCaptLogic + commonLogic *logic.CommonLogic +} + +// NewGoCaptchaServer creates a new gRPC cache server +func NewGoCaptchaServer(svcCtx *common.SvcContext) *GrpcServer { + return &GrpcServer{ + config: svcCtx.Config, + logger: svcCtx.Logger, + clickCaptLogic: logic.NewClickCaptLogic(svcCtx), + slideCaptLogic: logic.NewSlideCaptLogic(svcCtx), + rotateCaptLogic: logic.NewRotateCaptLogic(svcCtx), + commonLogic: logic.NewCommonLogic(svcCtx), + } +} + +// GetData handle +func (s *GrpcServer) GetData(ctx context.Context, req *proto.GetDataRequest) (*proto.GetDataResponse, error) { + resp := &proto.GetDataResponse{Code: 0} + var err error + + var data = &adapt.CaptData{} + + ctype := int(req.GetType()) + theme := int(req.GetTheme()) + lang := int(req.GetLang()) + + switch req.GetType() { + case proto.GoCaptchaType_GoCaptchaTypeClick: + data, err = s.clickCaptLogic.GetData(ctx, ctype, theme, lang) + break + case proto.GoCaptchaType_GoCaptchaTypeClickShape: + data, err = s.clickCaptLogic.GetData(ctx, ctype, theme, lang) + break + case proto.GoCaptchaType_GoCaptchaTypeSlide: + data, err = s.slideCaptLogic.GetData(ctx, ctype, theme, lang) + break + case proto.GoCaptchaType_GoCaptchaTypeDrag: + data, err = s.slideCaptLogic.GetData(ctx, ctype, theme, lang) + break + case proto.GoCaptchaType_GoCaptchaTypeRotate: + data, err = s.rotateCaptLogic.GetData(ctx, ctype, theme, lang) + break + default: + // + } + + if err != nil || data == nil { + s.logger.Error("failed to get captcha data, err: ", zap.Error(err)) + return &proto.GetDataResponse{Code: 0, Message: "failed to get captcha data"}, nil + } + + resp.Type = req.GetType() + return resp, nil +} + +// CheckData handle +func (s *GrpcServer) CheckData(ctx context.Context, req *proto.CheckDataRequest) (*proto.CheckDataResponse, error) { + resp := &proto.CheckDataResponse{Code: 0} + + if req.GetCaptchaKey() == "" || req.GetValue() == "" { + return &proto.CheckDataResponse{Code: 1, Message: "captchaKey and value are required"}, nil + } + + var err error + var ok bool + switch req.GetType() { + case proto.GoCaptchaType_GoCaptchaTypeClick: + ok, err = s.clickCaptLogic.CheckData(ctx, req.GetCaptchaKey(), req.GetValue()) + break + case proto.GoCaptchaType_GoCaptchaTypeClickShape: + ok, err = s.clickCaptLogic.CheckData(ctx, req.GetCaptchaKey(), req.GetValue()) + break + case proto.GoCaptchaType_GoCaptchaTypeSlide: + ok, err = s.slideCaptLogic.CheckData(ctx, req.GetCaptchaKey(), req.GetValue()) + break + case proto.GoCaptchaType_GoCaptchaTypeDrag: + ok, err = s.slideCaptLogic.CheckData(ctx, req.GetCaptchaKey(), req.GetValue()) + break + case proto.GoCaptchaType_GoCaptchaTypeRotate: + var angle int64 + angle, err = strconv.ParseInt(req.GetValue(), 10, 64) + if err == nil { + ok, err = s.rotateCaptLogic.CheckData(ctx, req.GetCaptchaKey(), int(angle)) + } + break + default: + //... + } + + if err != nil { + s.logger.Error("failed to check captcha data, err: ", zap.Error(err)) + return &proto.CheckDataResponse{Code: 1, Message: "failed to check captcha data"}, nil + } + + if ok { + resp.Data = "ok" + } else { + resp.Data = "failure" + } + + return resp, nil +} + +// CheckStatus handle +func (s *GrpcServer) CheckStatus(ctx context.Context, req *proto.StatusInfoRequest) (*proto.StatusInfoResponse, error) { + resp := &proto.StatusInfoResponse{Code: 0} + + if req.GetCaptchaKey() == "" { + return &proto.StatusInfoResponse{Code: 1, Message: "captchaKey is required"}, nil + } + + data, err := s.commonLogic.GetStatusInfo(ctx, req.GetCaptchaKey()) + if err != nil { + s.logger.Error("failed to check status, err: ", zap.Error(err)) + return &proto.StatusInfoResponse{Code: 1}, nil + } + + if data != nil && data.Status == 1 { + resp.Data = "ok" + } else { + resp.Data = "failure" + } + + return resp, nil +} + +// GetStatusInfo handle +func (s *GrpcServer) GetStatusInfo(ctx context.Context, req *proto.StatusInfoRequest) (*proto.StatusInfoResponse, error) { + resp := &proto.StatusInfoResponse{Code: 0} + + if req.CaptchaKey == "" { + return &proto.StatusInfoResponse{Code: 1, Message: "captchaKey is required"}, nil + } + + data, err := s.commonLogic.GetStatusInfo(ctx, req.GetCaptchaKey()) + if err != nil { + s.logger.Error("failed to check status, err: ", zap.Error(err)) + return &proto.StatusInfoResponse{Code: 1}, nil + } + + if data != nil && data.Status == 1 { + dataByte, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to json marshal: %v", err) + } + + resp.Data = string(dataByte) + } + + return resp, nil +} + +// DelStatusInfo handle +func (s *GrpcServer) DelStatusInfo(ctx context.Context, req *proto.StatusInfoRequest) (*proto.StatusInfoResponse, error) { + resp := &proto.StatusInfoResponse{Code: 0} + + if req.CaptchaKey == "" { + return &proto.StatusInfoResponse{Code: 1, Message: "captchaKey is required"}, nil + } + + ret, err := s.commonLogic.DelStatusInfo(ctx, req.GetCaptchaKey()) + if err != nil { + s.logger.Error("failed to delete status info, err: ", zap.Error(err)) + return &proto.StatusInfoResponse{Code: 1}, nil + } + + if ret { + resp.Data = "ok" + } + + return resp, nil +} diff --git a/internal/server/grpc_server_test.go b/internal/server/grpc_server_test.go new file mode 100644 index 0000000..8fdeeef --- /dev/null +++ b/internal/server/grpc_server_test.go @@ -0,0 +1,66 @@ +package server + +import ( + "context" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/proto" +) + +func TestCacheServer(t *testing.T) { + mr, err := miniredis.Run() + assert.NoError(t, err) + defer mr.Close() + + ttl := time.Duration(10) * time.Second + cleanInt := time.Duration(30) * time.Second + cacheClient := cache.NewMemoryCache("TEST_CAPTCHA_DATA:", ttl, cleanInt) + defer cacheClient.Close() + + dc := &config.DynamicConfig{Config: config.DefaultConfig()} + cnf := dc.Get() + + logger, err := zap.NewProduction() + assert.NoError(t, err) + + captcha, err := gocaptcha.Setup() + assert.NoError(t, err) + + svcCtx := &common.SvcContext{ + Cache: cacheClient, + Config: &cnf, + Logger: logger, + Captcha: captcha, + } + server := NewGoCaptchaServer(svcCtx) + + t.Run("GetData", func(t *testing.T) { + req := &proto.GetDataRequest{ + Type: proto.GoCaptchaType_GoCaptchaTypeClick, + } + resp, err := server.GetData(context.Background(), req) + assert.NoError(t, err) + assert.Equal(t, 200, resp.Code) + }) + + t.Run("GetData_Miss", func(t *testing.T) { + req := &proto.GetDataRequest{ + Type: -1, + } + _, err := server.GetData(context.Background(), req) + assert.Error(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) + //assert.Equal(t, codes.InvalidArgument, status.Code(err)) + }) +} diff --git a/internal/server/http_handler.go b/internal/server/http_handler.go new file mode 100644 index 0000000..43d7d46 --- /dev/null +++ b/internal/server/http_handler.go @@ -0,0 +1,283 @@ +package server + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/wenlng/go-captcha-service/internal/adapt" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/helper" + "github.com/wenlng/go-captcha-service/internal/logic" + "github.com/wenlng/go-captcha-service/internal/middleware" + "go.uber.org/zap" +) + +// HTTPHandlers manages HTTP request handlers +type HTTPHandlers struct { + config *config.Config + logger *zap.Logger + + // Initialize logic + clickCaptLogic *logic.ClickCaptLogic + slideCaptLogic *logic.SlideCaptLogic + rotateCaptLogic *logic.RotateCaptLogic + commonLogic *logic.CommonLogic +} + +// NewHTTPHandlers creates a new HTTP handlers instance +func NewHTTPHandlers(svcCtx *common.SvcContext) *HTTPHandlers { + return &HTTPHandlers{ + config: svcCtx.Config, + logger: svcCtx.Logger, + clickCaptLogic: logic.NewClickCaptLogic(svcCtx), + slideCaptLogic: logic.NewSlideCaptLogic(svcCtx), + rotateCaptLogic: logic.NewRotateCaptLogic(svcCtx), + commonLogic: logic.NewCommonLogic(svcCtx), + } +} + +// GetDataHandler . +func (h *HTTPHandlers) GetDataHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptDataResponse{Code: http.StatusOK, Message: ""} + + if r.Method != http.MethodGet { + middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + query := r.URL.Query() + + typeStr := query.Get("type") + ctype, err := strconv.Atoi(typeStr) + if err != nil { + middleware.WriteError(w, http.StatusBadRequest, "missing type parameter") + return + } + + themeStr := query.Get("theme") + var theme int + if themeStr != "" { + theme, err = strconv.Atoi(themeStr) + if err != nil { + middleware.WriteError(w, http.StatusBadRequest, "missing theme parameter") + return + } + } + + langStr := query.Get("lang") + var lang int + if langStr != "" { + lang, err = strconv.Atoi(langStr) + if err != nil { + middleware.WriteError(w, http.StatusBadRequest, "missing lang parameter") + return + } + } + + var data *adapt.CaptData + switch ctype { + case common.GoCaptchaTypeClick: + data, err = h.clickCaptLogic.GetData(r.Context(), ctype, theme, lang) + break + case common.GoCaptchaTypeClickShape: + data, err = h.clickCaptLogic.GetData(r.Context(), ctype, theme, lang) + break + case common.GoCaptchaTypeSlide: + data, err = h.slideCaptLogic.GetData(r.Context(), ctype, theme, lang) + break + case common.GoCaptchaTypeDrag: + data, err = h.slideCaptLogic.GetData(r.Context(), ctype, theme, lang) + break + case common.GoCaptchaTypeRotate: + data, err = h.rotateCaptLogic.GetData(r.Context(), ctype, theme, lang) + break + default: + //... + } + + if err != nil || data == nil { + h.logger.Error("failed to get captcha data, err: ", zap.Error(err)) + middleware.WriteError(w, http.StatusNotFound, "v") + return + } + + resp.Code = http.StatusOK + resp.Message = "success" + resp.Type = int32(ctype) + + resp.CaptchaKey = data.CaptchaKey + resp.MasterImageBase64 = data.MasterImageBase64 + resp.ThumbImageBase64 = data.ThumbImageBase64 + resp.MasterImageWidth = data.MasterImageWidth + resp.MasterImageHeight = data.MasterImageHeight + resp.ThumbImageWidth = data.ThumbImageWidth + resp.ThumbImageHeight = data.ThumbImageHeight + resp.ThumbImageSize = data.ThumbImageSize + resp.DisplayX = data.DisplayX + resp.DisplayY = data.DisplayY + + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} + +// CheckDataHandler . +func (h *HTTPHandlers) CheckDataHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: ""} + + if r.Method != http.MethodPost { + middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + var req struct { + Type int32 `json:"type"` + CaptchaKey string `json:"captchaKey"` + Value string `json:"value"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + middleware.WriteError(w, http.StatusBadRequest, "invalid request body") + return + } + if req.CaptchaKey == "" || req.Value == "" { + middleware.WriteError(w, http.StatusBadRequest, "captchaKey and value are required") + return + } + + var err error + var ok bool + switch req.Type { + case common.GoCaptchaTypeClick: + ok, err = h.clickCaptLogic.CheckData(r.Context(), req.CaptchaKey, req.Value) + break + case common.GoCaptchaTypeClickShape: + ok, err = h.clickCaptLogic.CheckData(r.Context(), req.CaptchaKey, req.Value) + break + case common.GoCaptchaTypeSlide: + ok, err = h.slideCaptLogic.CheckData(r.Context(), req.CaptchaKey, req.Value) + break + case common.GoCaptchaTypeDrag: + ok, err = h.slideCaptLogic.CheckData(r.Context(), req.CaptchaKey, req.Value) + break + case common.GoCaptchaTypeRotate: + var angle int64 + angle, err = strconv.ParseInt(req.Value, 10, 64) + if err == nil { + ok, err = h.rotateCaptLogic.CheckData(r.Context(), req.CaptchaKey, int(angle)) + } + break + default: + //... + } + + if err != nil { + middleware.WriteError(w, http.StatusBadRequest, "failed to check captcha data") + return + } + + if ok { + resp.Data = "ok" + } else { + resp.Data = "failure" + } + resp.Code = http.StatusOK + + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} + +// CheckStatusHandler . +func (h *HTTPHandlers) CheckStatusHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: "success"} + + if r.Method != http.MethodGet { + middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + query := r.URL.Query() + captchaKey := query.Get("captchaKey") + if captchaKey == "" { + middleware.WriteError(w, http.StatusBadRequest, "captchaKey is required") + return + } + + data, err := h.commonLogic.GetStatusInfo(r.Context(), captchaKey) + if err != nil { + h.logger.Error("failed to check status, err: ", zap.Error(err)) + middleware.WriteError(w, http.StatusBadRequest, "failed to check status") + return + } + + if data != nil && data.Status == 1 { + resp.Code = http.StatusOK + resp.Data = "ok" + } else { + resp.Data = "failure" + } + + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} + +// GetStatusInfoHandler . +func (h *HTTPHandlers) GetStatusInfoHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: "success"} + if r.Method != http.MethodGet { + middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + query := r.URL.Query() + captchaKey := query.Get("captchaKey") + if captchaKey == "" { + middleware.WriteError(w, http.StatusBadRequest, "captchaKey is required") + return + } + + data, err := h.commonLogic.GetStatusInfo(r.Context(), captchaKey) + if err != nil { + h.logger.Error("failed to get status info, err: ", zap.Error(err)) + middleware.WriteError(w, http.StatusNotFound, "not found status info") + return + } + + resp.Code = http.StatusOK + if data != nil { + resp.Data = data + } + + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} + +// DelStatusInfoHandler . +func (h *HTTPHandlers) DelStatusInfoHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: "success"} + if r.Method != http.MethodDelete { + middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + query := r.URL.Query() + captchaKey := query.Get("captchaKey") + if captchaKey == "" { + middleware.WriteError(w, http.StatusBadRequest, "captchaKey is required") + return + } + + ret, err := h.commonLogic.DelStatusInfo(r.Context(), captchaKey) + if err != nil { + h.logger.Error("failed to del status data, err: ", zap.Error(err)) + middleware.WriteError(w, http.StatusBadRequest, "not found status info") + return + } + + if ret { + resp.Data = "ok" + } + + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} diff --git a/internal/server/http_handler_test.go b/internal/server/http_handler_test.go new file mode 100644 index 0000000..8047868 --- /dev/null +++ b/internal/server/http_handler_test.go @@ -0,0 +1,100 @@ +package server + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/middleware" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + "go.uber.org/zap" + + "github.com/wenlng/go-captcha-service/internal/cache" +) + +func TestHTTPHandlers(t *testing.T) { + mr, err := miniredis.Run() + assert.NoError(t, err) + defer mr.Close() + + ttl := time.Duration(10) * time.Second + cleanInt := time.Duration(30) * time.Second + cacheClient := cache.NewMemoryCache("TEST_CAPTCHA_DATA:", ttl, cleanInt) + defer cacheClient.Close() + + dc := &config.DynamicConfig{Config: config.DefaultConfig()} + cnf := dc.Get() + + logger, err := zap.NewProduction() + assert.NoError(t, err) + + captcha, err := gocaptcha.Setup() + assert.NoError(t, err) + + svcCtx := &common.SvcContext{ + Cache: cacheClient, + Config: &cnf, + Logger: logger, + Captcha: captcha, + } + handlers := NewHTTPHandlers(svcCtx) + + t.Run("GetDataHandler", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/get-data?key=key1", nil) + rr := httptest.NewRecorder() + handlers.GetDataHandler(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + var resp map[string]string + json.Unmarshal(rr.Body.Bytes(), &resp) + fmt.Println(resp) + assert.Equal(t, "value1", resp["value"]) + }) + + t.Run("ReadHandler_Miss", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/read?key=nonexistent", nil) + rr := httptest.NewRecorder() + handlers.GetDataHandler(rr, req) + + assert.Equal(t, http.StatusNotFound, rr.Code) + var resp middleware.ErrorResponse + json.Unmarshal(rr.Body.Bytes(), &resp) + assert.Contains(t, resp.Message, "cache miss") + }) + + t.Run("CheckDataHandler", func(t *testing.T) { + reqBody, _ := json.Marshal(map[string]string{"key": "key2", "value": "value2"}) + req := httptest.NewRequest(http.MethodPost, "/write", bytes.NewReader(reqBody)) + rr := httptest.NewRecorder() + handlers.CheckDataHandler(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + var resp map[string]string + json.Unmarshal(rr.Body.Bytes(), &resp) + assert.Equal(t, "success", resp["status"]) + + value, err := cacheClient.GetCache(context.Background(), "key2") + assert.NoError(t, err) + assert.Equal(t, "value2", value) + }) + + t.Run("WriteHandler_InvalidBody", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/write", bytes.NewReader([]byte("invalid"))) + rr := httptest.NewRecorder() + handlers.CheckDataHandler(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + var resp middleware.ErrorResponse + json.Unmarshal(rr.Body.Bytes(), &resp) + assert.Equal(t, "invalid request body", resp.Message) + }) +} diff --git a/internal/service_discovery/consul_discovery.go b/internal/service_discovery/consul_discovery.go new file mode 100644 index 0000000..096fd9f --- /dev/null +++ b/internal/service_discovery/consul_discovery.go @@ -0,0 +1,76 @@ +package service_discovery + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/consul/api" +) + +type ConsulDiscovery struct { + client *api.Client + ttl int +} + +func NewConsulDiscovery(addrs string, ttl int) (*ConsulDiscovery, error) { + cfg := api.DefaultConfig() + cfg.Address = strings.Split(addrs, ",")[0] + client, err := api.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf("failed to connect to Consul: %v", err) + } + _, err = client.Status().Leader() + if err != nil { + return nil, fmt.Errorf("Consul health check failed: %v", err) + } + return &ConsulDiscovery{client: client, ttl: ttl}, nil +} + +func (d *ConsulDiscovery) Register(ctx context.Context, serviceName, instanceID, host string, httpPort, grpcPort int) error { + reg := &api.AgentServiceRegistration{ + ID: instanceID, + Name: serviceName, + Address: host, + Port: httpPort, + Tags: []string{"http", "grpc"}, + Meta: map[string]string{ + "grpc_port": fmt.Sprintf("%d", grpcPort), + }, + Check: &api.AgentServiceCheck{ + HTTP: fmt.Sprintf("http://%s:%d/hello", host, httpPort), + Interval: fmt.Sprintf("%ds", d.ttl), + Timeout: "5s", + TTL: fmt.Sprintf("%ds", d.ttl), + }, + } + return d.client.Agent().ServiceRegister(reg) +} + +func (d *ConsulDiscovery) Deregister(ctx context.Context, instanceID string) error { + return d.client.Agent().ServiceDeregister(instanceID) +} + +func (d *ConsulDiscovery) Discover(ctx context.Context, serviceName string) ([]Instance, error) { + services, _, err := d.client.Health().Service(serviceName, "", true, nil) + if err != nil { + return nil, fmt.Errorf("failed to discover instances: %v", err) + } + var instances []Instance + for _, entry := range services { + grpcPort, _ := strconv.Atoi(entry.Service.Meta["grpc_port"]) + instances = append(instances, Instance{ + InstanceID: entry.Service.ID, + Host: entry.Service.Address, + HTTPPort: entry.Service.Port, + GRPCPort: grpcPort, + Metadata: entry.Service.Meta, + }) + } + return instances, nil +} + +func (d *ConsulDiscovery) Close() error { + return nil +} diff --git a/internal/service_discovery/etcd_discovery.go b/internal/service_discovery/etcd_discovery.go new file mode 100644 index 0000000..1ede2f2 --- /dev/null +++ b/internal/service_discovery/etcd_discovery.go @@ -0,0 +1,100 @@ +package service_discovery + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +type EtcdDiscovery struct { + client *clientv3.Client + ttl int64 // seconds +} + +func NewEtcdDiscovery(addrs string, ttl int64) (*EtcdDiscovery, error) { + client, err := clientv3.New(clientv3.Config{ + Endpoints: strings.Split(addrs, ","), + DialTimeout: 5 * time.Second, + }) + if err != nil { + return nil, fmt.Errorf("failed to connect to etcd: %v", err) + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + _, err = client.Status(ctx, addrs) + if err != nil { + client.Close() + return nil, fmt.Errorf("etcd health check failed: %v", err) + } + return &EtcdDiscovery{client: client, ttl: ttl}, nil +} + +func (d *EtcdDiscovery) Register(ctx context.Context, serviceName, instanceID, host string, httpPort, grpcPort int) error { + lease, err := d.client.Grant(ctx, d.ttl) + if err != nil { + return fmt.Errorf("failed to create lease: %v", err) + } + + instance := Instance{ + InstanceID: instanceID, + Host: host, + HTTPPort: httpPort, + GRPCPort: grpcPort, + } + data, err := json.Marshal(instance) + if err != nil { + return fmt.Errorf("failed to marshal instance: %v", err) + } + + key := fmt.Sprintf("/services/%s/%s", serviceName, instanceID) + _, err = d.client.Put(ctx, key, string(data), clientv3.WithLease(lease.ID)) + if err != nil { + return fmt.Errorf("failed to register instance: %v", err) + } + + go func() { + for { + select { + case <-ctx.Done(): + return + default: + _, err := d.client.KeepAliveOnce(context.Background(), lease.ID) + if err != nil { + return + } + time.Sleep(time.Duration(d.ttl/2) * time.Second) + } + } + }() + return nil +} + +func (d *EtcdDiscovery) Deregister(ctx context.Context, instanceID string) error { + key := fmt.Sprintf("/services/%s/%s", "go-captcha-service", instanceID) + _, err := d.client.Delete(ctx, key) + return err +} + +func (d *EtcdDiscovery) Discover(ctx context.Context, serviceName string) ([]Instance, error) { + resp, err := d.client.Get(ctx, fmt.Sprintf("/services/%s/", serviceName), clientv3.WithPrefix()) + if err != nil { + return nil, fmt.Errorf("failed to discover instances: %v", err) + } + var instances []Instance + for _, kv := range resp.Kvs { + var instance Instance + if err := json.Unmarshal(kv.Value, &instance); err != nil { + continue + } + instances = append(instances, instance) + } + return instances, nil +} + +func (d *EtcdDiscovery) Close() error { + return d.client.Close() +} diff --git a/internal/service_discovery/nacos_discovery.go b/internal/service_discovery/nacos_discovery.go new file mode 100644 index 0000000..963b812 --- /dev/null +++ b/internal/service_discovery/nacos_discovery.go @@ -0,0 +1,93 @@ +package service_discovery + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/nacos-group/nacos-sdk-go/v2/clients" + "github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client" + "github.com/nacos-group/nacos-sdk-go/v2/common/constant" + "github.com/nacos-group/nacos-sdk-go/v2/vo" +) + +type NacosDiscovery struct { + client naming_client.INamingClient +} + +func NewNacosDiscovery(addrs string, ttl int64) (*NacosDiscovery, error) { + clientConfig := *constant.NewClientConfig( + constant.WithNamespaceId(""), + constant.WithTimeoutMs(5000), + constant.WithNotLoadCacheAtStart(true), + ) + serverConfigs := []constant.ServerConfig{} + for _, addr := range strings.Split(addrs, ",") { + hostPort := strings.Split(addr, ":") + host := hostPort[0] + port, _ := strconv.Atoi(hostPort[1]) + serverConfigs = append(serverConfigs, *constant.NewServerConfig(host, uint64(port))) + } + namingClient, err := clients.NewNamingClient( + vo.NacosClientParam{ + ClientConfig: &clientConfig, + ServerConfigs: serverConfigs, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to connect to Nacos: %v", err) + } + return &NacosDiscovery{client: namingClient}, nil +} + +func (d *NacosDiscovery) Register(ctx context.Context, serviceName, instanceID, host string, httpPort, grpcPort int) error { + _, err := d.client.RegisterInstance(vo.RegisterInstanceParam{ + Ip: host, + Port: uint64(httpPort), + ServiceName: serviceName, + GroupName: instanceID, + Weight: 1, + Enable: true, + Healthy: true, + Ephemeral: true, + Metadata: map[string]string{ + "grpc_port": fmt.Sprintf("%d", grpcPort), + }, + }) + + return err +} + +func (d *NacosDiscovery) Deregister(ctx context.Context, instanceID string) error { + _, err := d.client.DeregisterInstance(vo.DeregisterInstanceParam{ + GroupName: instanceID, + ServiceName: "go-captcha-service", + }) + return err +} + +func (d *NacosDiscovery) Discover(ctx context.Context, serviceName string) ([]Instance, error) { + instances, err := d.client.GetService(vo.GetServiceParam{ + ServiceName: serviceName, + }) + if err != nil { + return nil, fmt.Errorf("failed to discover instances: %v", err) + } + var result []Instance + for _, inst := range instances.Hosts { + grpcPort, _ := strconv.Atoi(inst.Metadata["grpc_port"]) + result = append(result, Instance{ + InstanceID: inst.InstanceId, + Host: inst.Ip, + HTTPPort: int(inst.Port), + GRPCPort: grpcPort, + Metadata: inst.Metadata, + }) + } + return result, nil +} + +func (d *NacosDiscovery) Close() error { + return nil +} diff --git a/internal/service_discovery/service_discovery.go b/internal/service_discovery/service_discovery.go new file mode 100644 index 0000000..da214ae --- /dev/null +++ b/internal/service_discovery/service_discovery.go @@ -0,0 +1,22 @@ +package service_discovery + +import ( + "context" +) + +// ServiceDiscovery defines the interface for service discovery +type ServiceDiscovery interface { + Register(ctx context.Context, serviceName, instanceID, host string, httpPort, grpcPort int) error + Deregister(ctx context.Context, instanceID string) error + Discover(ctx context.Context, serviceName string) ([]Instance, error) + Close() error +} + +// Instance represents a service instance +type Instance struct { + InstanceID string + Host string + HTTPPort int + GRPCPort int + Metadata map[string]string +} diff --git a/internal/service_discovery/service_discovery_test.go b/internal/service_discovery/service_discovery_test.go new file mode 100644 index 0000000..a9b8f35 --- /dev/null +++ b/internal/service_discovery/service_discovery_test.go @@ -0,0 +1,26 @@ +package service_discovery + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEtcdDiscovery(t *testing.T) { + discovery, err := NewEtcdDiscovery("localhost:2379", 10) + assert.NoError(t, err) + + err = discovery.Register(context.Background(), "go-captcha-service", "id1", "127.0.0.1", 8080, 50051) + assert.NoError(t, err) + + instances, err := discovery.Discover(context.Background(), "go-captcha-service") + assert.NoError(t, err) + assert.Empty(t, instances) + + err = discovery.Deregister(context.Background(), "id1") + assert.NoError(t, err) + + err = discovery.Close() + assert.NoError(t, err) +} diff --git a/internal/service_discovery/zookeeper_discovery.go b/internal/service_discovery/zookeeper_discovery.go new file mode 100644 index 0000000..a97e568 --- /dev/null +++ b/internal/service_discovery/zookeeper_discovery.go @@ -0,0 +1,95 @@ +package service_discovery + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/go-zookeeper/zk" +) + +type ZookeeperDiscovery struct { + conn *zk.Conn + ttl int64 // seconds +} + +func NewZookeeperDiscovery(addrs string, ttl int64) (*ZookeeperDiscovery, error) { + conn, _, err := zk.Connect(strings.Split(addrs, ","), 5*time.Second) + if err != nil { + return nil, fmt.Errorf("failed to connect to ZooKeeper: %v", err) + } + _, _, err = conn.Children("/") + if err != nil { + conn.Close() + return nil, fmt.Errorf("ZooKeeper health check failed: %v", err) + } + return &ZookeeperDiscovery{conn: conn, ttl: ttl}, nil +} + +func (d *ZookeeperDiscovery) Register(ctx context.Context, serviceName, instanceID, host string, httpPort, grpcPort int) error { + instance := Instance{ + InstanceID: instanceID, + Host: host, + HTTPPort: httpPort, + GRPCPort: grpcPort, + } + data, err := json.Marshal(instance) + if err != nil { + return fmt.Errorf("failed to marshal instance: %v", err) + } + + path := fmt.Sprintf("/services/%s/%s", serviceName, instanceID) + _, err = d.conn.Create(path, data, zk.FlagEphemeral, zk.WorldACL(zk.PermAll)) + if err != nil && err != zk.ErrNodeExists { + return fmt.Errorf("failed to register instance: %v", err) + } + + go func() { + for { + select { + case <-ctx.Done(): + return + default: + exists, _, err := d.conn.Exists(path) + if err != nil || !exists { + d.conn.Create(path, data, zk.FlagEphemeral, zk.WorldACL(zk.PermAll)) + } + time.Sleep(time.Duration(d.ttl/2) * time.Second) + } + } + }() + return nil +} + +func (d *ZookeeperDiscovery) Deregister(ctx context.Context, instanceID string) error { + path := fmt.Sprintf("/services/%s/%s", "go-captcha-service", instanceID) + return d.conn.Delete(path, -1) +} + +func (d *ZookeeperDiscovery) Discover(ctx context.Context, serviceName string) ([]Instance, error) { + path := fmt.Sprintf("/services/%s", serviceName) + children, _, err := d.conn.Children(path) + if err != nil { + return nil, fmt.Errorf("failed to discover instances: %v", err) + } + var instances []Instance + for _, child := range children { + data, _, err := d.conn.Get(path + "/" + child) + if err != nil { + continue + } + var instance Instance + if err := json.Unmarshal(data, &instance); err != nil { + continue + } + instances = append(instances, instance) + } + return instances, nil +} + +func (d *ZookeeperDiscovery) Close() error { + d.conn.Close() + return nil +} diff --git a/modd.conf b/modd.conf new file mode 100644 index 0000000..b503538 --- /dev/null +++ b/modd.conf @@ -0,0 +1,4 @@ +**/*.go { + prep: go build -o ./.cache/go-captcha-service -v cmd/go-captcha-service/main.go + daemon +sigkill: ./.cache/go-captcha-service -config config.json +} \ No newline at end of file diff --git a/proto/api.pb.go b/proto/api.pb.go new file mode 100644 index 0000000..3454d45 --- /dev/null +++ b/proto/api.pb.go @@ -0,0 +1,875 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v4.22.2 +// source: proto/api.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Type +type GoCaptchaType int32 + +const ( + GoCaptchaType_GoCaptchaTypeClick GoCaptchaType = 0 + GoCaptchaType_GoCaptchaTypeClickShape GoCaptchaType = 1 + GoCaptchaType_GoCaptchaTypeSlide GoCaptchaType = 2 + GoCaptchaType_GoCaptchaTypeDrag GoCaptchaType = 3 + GoCaptchaType_GoCaptchaTypeRotate GoCaptchaType = 4 +) + +// Enum value maps for GoCaptchaType. +var ( + GoCaptchaType_name = map[int32]string{ + 0: "GoCaptchaTypeClick", + 1: "GoCaptchaTypeClickShape", + 2: "GoCaptchaTypeSlide", + 3: "GoCaptchaTypeDrag", + 4: "GoCaptchaTypeRotate", + } + GoCaptchaType_value = map[string]int32{ + "GoCaptchaTypeClick": 0, + "GoCaptchaTypeClickShape": 1, + "GoCaptchaTypeSlide": 2, + "GoCaptchaTypeDrag": 3, + "GoCaptchaTypeRotate": 4, + } +) + +func (x GoCaptchaType) Enum() *GoCaptchaType { + p := new(GoCaptchaType) + *p = x + return p +} + +func (x GoCaptchaType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (GoCaptchaType) Descriptor() protoreflect.EnumDescriptor { + return file_proto_api_proto_enumTypes[0].Descriptor() +} + +func (GoCaptchaType) Type() protoreflect.EnumType { + return &file_proto_api_proto_enumTypes[0] +} + +func (x GoCaptchaType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use GoCaptchaType.Descriptor instead. +func (GoCaptchaType) EnumDescriptor() ([]byte, []int) { + return file_proto_api_proto_rawDescGZIP(), []int{0} +} + +// Theme +type GoCaptchaTheme int32 + +const ( + GoCaptchaTheme_GoCaptchaThemeDefault GoCaptchaTheme = 0 + GoCaptchaTheme_GoCaptchaThemeDark GoCaptchaTheme = 1 +) + +// Enum value maps for GoCaptchaTheme. +var ( + GoCaptchaTheme_name = map[int32]string{ + 0: "GoCaptchaThemeDefault", + 1: "GoCaptchaThemeDark", + } + GoCaptchaTheme_value = map[string]int32{ + "GoCaptchaThemeDefault": 0, + "GoCaptchaThemeDark": 1, + } +) + +func (x GoCaptchaTheme) Enum() *GoCaptchaTheme { + p := new(GoCaptchaTheme) + *p = x + return p +} + +func (x GoCaptchaTheme) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (GoCaptchaTheme) Descriptor() protoreflect.EnumDescriptor { + return file_proto_api_proto_enumTypes[1].Descriptor() +} + +func (GoCaptchaTheme) Type() protoreflect.EnumType { + return &file_proto_api_proto_enumTypes[1] +} + +func (x GoCaptchaTheme) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use GoCaptchaTheme.Descriptor instead. +func (GoCaptchaTheme) EnumDescriptor() ([]byte, []int) { + return file_proto_api_proto_rawDescGZIP(), []int{1} +} + +// Lang +type GoCaptchaLang int32 + +const ( + GoCaptchaLang_GoCaptchaLangDefault GoCaptchaLang = 0 + GoCaptchaLang_GoCaptchaLangEnglish GoCaptchaLang = 1 +) + +// Enum value maps for GoCaptchaLang. +var ( + GoCaptchaLang_name = map[int32]string{ + 0: "GoCaptchaLangDefault", + 1: "GoCaptchaLangEnglish", + } + GoCaptchaLang_value = map[string]int32{ + "GoCaptchaLangDefault": 0, + "GoCaptchaLangEnglish": 1, + } +) + +func (x GoCaptchaLang) Enum() *GoCaptchaLang { + p := new(GoCaptchaLang) + *p = x + return p +} + +func (x GoCaptchaLang) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (GoCaptchaLang) Descriptor() protoreflect.EnumDescriptor { + return file_proto_api_proto_enumTypes[2].Descriptor() +} + +func (GoCaptchaLang) Type() protoreflect.EnumType { + return &file_proto_api_proto_enumTypes[2] +} + +func (x GoCaptchaLang) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use GoCaptchaLang.Descriptor instead. +func (GoCaptchaLang) EnumDescriptor() ([]byte, []int) { + return file_proto_api_proto_rawDescGZIP(), []int{2} +} + +type GetDataRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type GoCaptchaType `protobuf:"varint,1,opt,name=type,proto3,enum=gocaptcha.GoCaptchaType" json:"type,omitempty"` + Theme *GoCaptchaTheme `protobuf:"varint,2,opt,name=theme,proto3,enum=gocaptcha.GoCaptchaTheme,oneof" json:"theme,omitempty"` + Lang *GoCaptchaLang `protobuf:"varint,3,opt,name=lang,proto3,enum=gocaptcha.GoCaptchaLang,oneof" json:"lang,omitempty"` +} + +func (x *GetDataRequest) Reset() { + *x = GetDataRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetDataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDataRequest) ProtoMessage() {} + +func (x *GetDataRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_api_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDataRequest.ProtoReflect.Descriptor instead. +func (*GetDataRequest) Descriptor() ([]byte, []int) { + return file_proto_api_proto_rawDescGZIP(), []int{0} +} + +func (x *GetDataRequest) GetType() GoCaptchaType { + if x != nil { + return x.Type + } + return GoCaptchaType_GoCaptchaTypeClick +} + +func (x *GetDataRequest) GetTheme() GoCaptchaTheme { + if x != nil && x.Theme != nil { + return *x.Theme + } + return GoCaptchaTheme_GoCaptchaThemeDefault +} + +func (x *GetDataRequest) GetLang() GoCaptchaLang { + if x != nil && x.Lang != nil { + return *x.Lang + } + return GoCaptchaLang_GoCaptchaLangDefault +} + +type GetDataResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Type GoCaptchaType `protobuf:"varint,3,opt,name=type,proto3,enum=gocaptcha.GoCaptchaType" json:"type,omitempty"` + CaptchaKey string `protobuf:"bytes,4,opt,name=captchaKey,proto3" json:"captchaKey,omitempty"` + MasterImageBase64 string `protobuf:"bytes,5,opt,name=masterImageBase64,proto3" json:"masterImageBase64,omitempty"` + ThumbImageBase64 string `protobuf:"bytes,6,opt,name=thumbImageBase64,proto3" json:"thumbImageBase64,omitempty"` + MasterWidth int32 `protobuf:"varint,7,opt,name=masterWidth,proto3" json:"masterWidth,omitempty"` + MasterHeight int32 `protobuf:"varint,8,opt,name=masterHeight,proto3" json:"masterHeight,omitempty"` + ThumbWidth int32 `protobuf:"varint,9,opt,name=thumbWidth,proto3" json:"thumbWidth,omitempty"` + ThumbHeight int32 `protobuf:"varint,10,opt,name=thumbHeight,proto3" json:"thumbHeight,omitempty"` + ThumbSize int32 `protobuf:"varint,11,opt,name=thumbSize,proto3" json:"thumbSize,omitempty"` + DisplayX int32 `protobuf:"varint,12,opt,name=displayX,proto3" json:"displayX,omitempty"` + DisplayY int32 `protobuf:"varint,13,opt,name=displayY,proto3" json:"displayY,omitempty"` +} + +func (x *GetDataResponse) Reset() { + *x = GetDataResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetDataResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDataResponse) ProtoMessage() {} + +func (x *GetDataResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_api_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDataResponse.ProtoReflect.Descriptor instead. +func (*GetDataResponse) Descriptor() ([]byte, []int) { + return file_proto_api_proto_rawDescGZIP(), []int{1} +} + +func (x *GetDataResponse) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *GetDataResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *GetDataResponse) GetType() GoCaptchaType { + if x != nil { + return x.Type + } + return GoCaptchaType_GoCaptchaTypeClick +} + +func (x *GetDataResponse) GetCaptchaKey() string { + if x != nil { + return x.CaptchaKey + } + return "" +} + +func (x *GetDataResponse) GetMasterImageBase64() string { + if x != nil { + return x.MasterImageBase64 + } + return "" +} + +func (x *GetDataResponse) GetThumbImageBase64() string { + if x != nil { + return x.ThumbImageBase64 + } + return "" +} + +func (x *GetDataResponse) GetMasterWidth() int32 { + if x != nil { + return x.MasterWidth + } + return 0 +} + +func (x *GetDataResponse) GetMasterHeight() int32 { + if x != nil { + return x.MasterHeight + } + return 0 +} + +func (x *GetDataResponse) GetThumbWidth() int32 { + if x != nil { + return x.ThumbWidth + } + return 0 +} + +func (x *GetDataResponse) GetThumbHeight() int32 { + if x != nil { + return x.ThumbHeight + } + return 0 +} + +func (x *GetDataResponse) GetThumbSize() int32 { + if x != nil { + return x.ThumbSize + } + return 0 +} + +func (x *GetDataResponse) GetDisplayX() int32 { + if x != nil { + return x.DisplayX + } + return 0 +} + +func (x *GetDataResponse) GetDisplayY() int32 { + if x != nil { + return x.DisplayY + } + return 0 +} + +type CheckDataRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type GoCaptchaType `protobuf:"varint,1,opt,name=type,proto3,enum=gocaptcha.GoCaptchaType" json:"type,omitempty"` + CaptchaKey string `protobuf:"bytes,2,opt,name=captchaKey,proto3" json:"captchaKey,omitempty"` + Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *CheckDataRequest) Reset() { + *x = CheckDataRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckDataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckDataRequest) ProtoMessage() {} + +func (x *CheckDataRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_api_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckDataRequest.ProtoReflect.Descriptor instead. +func (*CheckDataRequest) Descriptor() ([]byte, []int) { + return file_proto_api_proto_rawDescGZIP(), []int{2} +} + +func (x *CheckDataRequest) GetType() GoCaptchaType { + if x != nil { + return x.Type + } + return GoCaptchaType_GoCaptchaTypeClick +} + +func (x *CheckDataRequest) GetCaptchaKey() string { + if x != nil { + return x.CaptchaKey + } + return "" +} + +func (x *CheckDataRequest) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type CheckDataResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *CheckDataResponse) Reset() { + *x = CheckDataResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckDataResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckDataResponse) ProtoMessage() {} + +func (x *CheckDataResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_api_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckDataResponse.ProtoReflect.Descriptor instead. +func (*CheckDataResponse) Descriptor() ([]byte, []int) { + return file_proto_api_proto_rawDescGZIP(), []int{3} +} + +func (x *CheckDataResponse) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *CheckDataResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *CheckDataResponse) GetData() string { + if x != nil { + return x.Data + } + return "" +} + +type StatusInfoRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CaptchaKey string `protobuf:"bytes,1,opt,name=captchaKey,proto3" json:"captchaKey,omitempty"` +} + +func (x *StatusInfoRequest) Reset() { + *x = StatusInfoRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatusInfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatusInfoRequest) ProtoMessage() {} + +func (x *StatusInfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_api_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatusInfoRequest.ProtoReflect.Descriptor instead. +func (*StatusInfoRequest) Descriptor() ([]byte, []int) { + return file_proto_api_proto_rawDescGZIP(), []int{4} +} + +func (x *StatusInfoRequest) GetCaptchaKey() string { + if x != nil { + return x.CaptchaKey + } + return "" +} + +type StatusInfoResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *StatusInfoResponse) Reset() { + *x = StatusInfoResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatusInfoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatusInfoResponse) ProtoMessage() {} + +func (x *StatusInfoResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_api_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatusInfoResponse.ProtoReflect.Descriptor instead. +func (*StatusInfoResponse) Descriptor() ([]byte, []int) { + return file_proto_api_proto_rawDescGZIP(), []int{5} +} + +func (x *StatusInfoResponse) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *StatusInfoResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *StatusInfoResponse) GetData() string { + if x != nil { + return x.Data + } + return "" +} + +var File_proto_api_proto protoreflect.FileDescriptor + +var file_proto_api_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x09, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x22, 0xba, 0x01, 0x0a, + 0x0e, 0x47, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x2c, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, + 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, + 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x34, 0x0a, + 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x67, + 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, + 0x68, 0x61, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x48, 0x00, 0x52, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, + 0x88, 0x01, 0x01, 0x12, 0x31, 0x0a, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x18, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x47, 0x6f, + 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4c, 0x61, 0x6e, 0x67, 0x48, 0x01, 0x52, 0x04, 0x6c, + 0x61, 0x6e, 0x67, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x74, 0x68, 0x65, 0x6d, 0x65, + 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6c, 0x61, 0x6e, 0x67, 0x22, 0xc5, 0x03, 0x0a, 0x0f, 0x47, 0x65, + 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x67, 0x6f, 0x63, 0x61, + 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x70, + 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, + 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x11, 0x6d, 0x61, 0x73, + 0x74, 0x65, 0x72, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6d, 0x61, 0x67, + 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x12, 0x2a, 0x0a, 0x10, 0x74, 0x68, 0x75, 0x6d, 0x62, + 0x49, 0x6d, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x10, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, + 0x65, 0x36, 0x34, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x57, 0x69, 0x64, + 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, + 0x57, 0x69, 0x64, 0x74, 0x68, 0x12, 0x22, 0x0a, 0x0c, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x48, + 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6d, 0x61, 0x73, + 0x74, 0x65, 0x72, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x68, 0x75, + 0x6d, 0x62, 0x57, 0x69, 0x64, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x74, + 0x68, 0x75, 0x6d, 0x62, 0x57, 0x69, 0x64, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x68, 0x75, + 0x6d, 0x62, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, + 0x74, 0x68, 0x75, 0x6d, 0x62, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, + 0x68, 0x75, 0x6d, 0x62, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, + 0x74, 0x68, 0x75, 0x6d, 0x62, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, + 0x70, 0x6c, 0x61, 0x79, 0x58, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x64, 0x69, 0x73, + 0x70, 0x6c, 0x61, 0x79, 0x58, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, + 0x59, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, + 0x59, 0x22, 0x76, 0x0a, 0x10, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, + 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, + 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x55, 0x0a, 0x11, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, + 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x22, 0x33, 0x0a, 0x11, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, + 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, + 0x68, 0x61, 0x4b, 0x65, 0x79, 0x22, 0x56, 0x0a, 0x12, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, + 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x2a, 0x8c, 0x01, + 0x0a, 0x0d, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x16, 0x0a, 0x12, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, + 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x47, 0x6f, 0x43, 0x61, 0x70, + 0x74, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x53, 0x68, 0x61, + 0x70, 0x65, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, + 0x61, 0x54, 0x79, 0x70, 0x65, 0x53, 0x6c, 0x69, 0x64, 0x65, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, + 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x44, 0x72, 0x61, + 0x67, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, + 0x54, 0x79, 0x70, 0x65, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x10, 0x04, 0x2a, 0x43, 0x0a, 0x0e, + 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x19, + 0x0a, 0x15, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x68, 0x65, 0x6d, 0x65, + 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x47, 0x6f, 0x43, + 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x44, 0x61, 0x72, 0x6b, 0x10, + 0x01, 0x2a, 0x43, 0x0a, 0x0d, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4c, 0x61, + 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x14, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4c, + 0x61, 0x6e, 0x67, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, + 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4c, 0x61, 0x6e, 0x67, 0x45, 0x6e, 0x67, + 0x6c, 0x69, 0x73, 0x68, 0x10, 0x01, 0x32, 0x8e, 0x03, 0x0a, 0x10, 0x47, 0x6f, 0x43, 0x61, 0x70, + 0x74, 0x63, 0x68, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x42, 0x0a, 0x07, 0x47, + 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x19, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, + 0x68, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x47, 0x65, + 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x48, 0x0a, 0x09, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x2e, 0x67, + 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x61, + 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x63, 0x61, + 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x61, 0x74, 0x61, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4c, 0x0a, 0x0b, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, + 0x74, 0x63, 0x68, 0x61, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, + 0x68, 0x61, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4e, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, + 0x74, 0x63, 0x68, 0x61, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, + 0x68, 0x61, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4e, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, + 0x74, 0x63, 0x68, 0x61, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, + 0x68, 0x61, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proto_api_proto_rawDescOnce sync.Once + file_proto_api_proto_rawDescData = file_proto_api_proto_rawDesc +) + +func file_proto_api_proto_rawDescGZIP() []byte { + file_proto_api_proto_rawDescOnce.Do(func() { + file_proto_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_api_proto_rawDescData) + }) + return file_proto_api_proto_rawDescData +} + +var file_proto_api_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_proto_api_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_proto_api_proto_goTypes = []interface{}{ + (GoCaptchaType)(0), // 0: gocaptcha.GoCaptchaType + (GoCaptchaTheme)(0), // 1: gocaptcha.GoCaptchaTheme + (GoCaptchaLang)(0), // 2: gocaptcha.GoCaptchaLang + (*GetDataRequest)(nil), // 3: gocaptcha.GetDataRequest + (*GetDataResponse)(nil), // 4: gocaptcha.GetDataResponse + (*CheckDataRequest)(nil), // 5: gocaptcha.CheckDataRequest + (*CheckDataResponse)(nil), // 6: gocaptcha.CheckDataResponse + (*StatusInfoRequest)(nil), // 7: gocaptcha.StatusInfoRequest + (*StatusInfoResponse)(nil), // 8: gocaptcha.StatusInfoResponse +} +var file_proto_api_proto_depIdxs = []int32{ + 0, // 0: gocaptcha.GetDataRequest.type:type_name -> gocaptcha.GoCaptchaType + 1, // 1: gocaptcha.GetDataRequest.theme:type_name -> gocaptcha.GoCaptchaTheme + 2, // 2: gocaptcha.GetDataRequest.lang:type_name -> gocaptcha.GoCaptchaLang + 0, // 3: gocaptcha.GetDataResponse.type:type_name -> gocaptcha.GoCaptchaType + 0, // 4: gocaptcha.CheckDataRequest.type:type_name -> gocaptcha.GoCaptchaType + 3, // 5: gocaptcha.GoCaptchaService.GetData:input_type -> gocaptcha.GetDataRequest + 5, // 6: gocaptcha.GoCaptchaService.CheckData:input_type -> gocaptcha.CheckDataRequest + 7, // 7: gocaptcha.GoCaptchaService.CheckStatus:input_type -> gocaptcha.StatusInfoRequest + 7, // 8: gocaptcha.GoCaptchaService.GetStatusInfo:input_type -> gocaptcha.StatusInfoRequest + 7, // 9: gocaptcha.GoCaptchaService.DelStatusInfo:input_type -> gocaptcha.StatusInfoRequest + 4, // 10: gocaptcha.GoCaptchaService.GetData:output_type -> gocaptcha.GetDataResponse + 6, // 11: gocaptcha.GoCaptchaService.CheckData:output_type -> gocaptcha.CheckDataResponse + 8, // 12: gocaptcha.GoCaptchaService.CheckStatus:output_type -> gocaptcha.StatusInfoResponse + 8, // 13: gocaptcha.GoCaptchaService.GetStatusInfo:output_type -> gocaptcha.StatusInfoResponse + 8, // 14: gocaptcha.GoCaptchaService.DelStatusInfo:output_type -> gocaptcha.StatusInfoResponse + 10, // [10:15] is the sub-list for method output_type + 5, // [5:10] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_proto_api_proto_init() } +func file_proto_api_proto_init() { + if File_proto_api_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetDataRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetDataResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckDataRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckDataResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_api_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StatusInfoRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_api_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StatusInfoResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_proto_api_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proto_api_proto_rawDesc, + NumEnums: 3, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_api_proto_goTypes, + DependencyIndexes: file_proto_api_proto_depIdxs, + EnumInfos: file_proto_api_proto_enumTypes, + MessageInfos: file_proto_api_proto_msgTypes, + }.Build() + File_proto_api_proto = out.File + file_proto_api_proto_rawDesc = nil + file_proto_api_proto_goTypes = nil + file_proto_api_proto_depIdxs = nil +} diff --git a/proto/api.proto b/proto/api.proto new file mode 100644 index 0000000..d1851ee --- /dev/null +++ b/proto/api.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package gocaptcha; + +option go_package = "./proto"; + +// GoCaptchaService provides cache operations +service GoCaptchaService { + rpc GetData(GetDataRequest) returns (GetDataResponse) {} + rpc CheckData(CheckDataRequest) returns (CheckDataResponse) {} + rpc CheckStatus(StatusInfoRequest) returns (StatusInfoResponse) {} + rpc GetStatusInfo(StatusInfoRequest) returns (StatusInfoResponse) {} + rpc DelStatusInfo(StatusInfoRequest) returns (StatusInfoResponse) {} +} + +// Type +enum GoCaptchaType { + GoCaptchaTypeClick = 0; + GoCaptchaTypeClickShape = 1; + GoCaptchaTypeSlide = 2; + GoCaptchaTypeDrag = 3; + GoCaptchaTypeRotate = 4; +} + +// Theme +enum GoCaptchaTheme { + GoCaptchaThemeDefault = 0; + GoCaptchaThemeDark = 1; +} + +// Lang +enum GoCaptchaLang { + GoCaptchaLangDefault = 0; + GoCaptchaLangEnglish = 1; +} + +message GetDataRequest { + GoCaptchaType type = 1; + optional GoCaptchaTheme theme = 2; + optional GoCaptchaLang lang = 3; +} + +message GetDataResponse { + int32 code = 1; + string message = 2; + GoCaptchaType type = 3; + string captchaKey = 4; + string masterImageBase64 = 5; + string thumbImageBase64 = 6; + int32 masterWidth = 7; + int32 masterHeight = 8; + int32 thumbWidth = 9; + int32 thumbHeight = 10; + int32 thumbSize = 11; + int32 displayX = 12; + int32 displayY = 13; +} + +message CheckDataRequest { + GoCaptchaType type = 1; + string captchaKey = 2; + string value = 3; +} + +message CheckDataResponse { + int32 code = 1; + string message = 2; + string data = 3; +} + +message StatusInfoRequest { + string captchaKey = 1; +} + +message StatusInfoResponse { + int32 code = 1; + string message = 2; + string data = 3; +} diff --git a/proto/api_grpc.pb.go b/proto/api_grpc.pb.go new file mode 100644 index 0000000..ce9f08b --- /dev/null +++ b/proto/api_grpc.pb.go @@ -0,0 +1,257 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v4.22.2 +// source: proto/api.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + GoCaptchaService_GetData_FullMethodName = "/gocaptcha.GoCaptchaService/GetData" + GoCaptchaService_CheckData_FullMethodName = "/gocaptcha.GoCaptchaService/CheckData" + GoCaptchaService_CheckStatus_FullMethodName = "/gocaptcha.GoCaptchaService/CheckStatus" + GoCaptchaService_GetStatusInfo_FullMethodName = "/gocaptcha.GoCaptchaService/GetStatusInfo" + GoCaptchaService_DelStatusInfo_FullMethodName = "/gocaptcha.GoCaptchaService/DelStatusInfo" +) + +// GoCaptchaServiceClient is the client API for GoCaptchaService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GoCaptchaServiceClient interface { + GetData(ctx context.Context, in *GetDataRequest, opts ...grpc.CallOption) (*GetDataResponse, error) + CheckData(ctx context.Context, in *CheckDataRequest, opts ...grpc.CallOption) (*CheckDataResponse, error) + CheckStatus(ctx context.Context, in *StatusInfoRequest, opts ...grpc.CallOption) (*StatusInfoResponse, error) + GetStatusInfo(ctx context.Context, in *StatusInfoRequest, opts ...grpc.CallOption) (*StatusInfoResponse, error) + DelStatusInfo(ctx context.Context, in *StatusInfoRequest, opts ...grpc.CallOption) (*StatusInfoResponse, error) +} + +type goCaptchaServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGoCaptchaServiceClient(cc grpc.ClientConnInterface) GoCaptchaServiceClient { + return &goCaptchaServiceClient{cc} +} + +func (c *goCaptchaServiceClient) GetData(ctx context.Context, in *GetDataRequest, opts ...grpc.CallOption) (*GetDataResponse, error) { + out := new(GetDataResponse) + err := c.cc.Invoke(ctx, GoCaptchaService_GetData_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCaptchaServiceClient) CheckData(ctx context.Context, in *CheckDataRequest, opts ...grpc.CallOption) (*CheckDataResponse, error) { + out := new(CheckDataResponse) + err := c.cc.Invoke(ctx, GoCaptchaService_CheckData_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCaptchaServiceClient) CheckStatus(ctx context.Context, in *StatusInfoRequest, opts ...grpc.CallOption) (*StatusInfoResponse, error) { + out := new(StatusInfoResponse) + err := c.cc.Invoke(ctx, GoCaptchaService_CheckStatus_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCaptchaServiceClient) GetStatusInfo(ctx context.Context, in *StatusInfoRequest, opts ...grpc.CallOption) (*StatusInfoResponse, error) { + out := new(StatusInfoResponse) + err := c.cc.Invoke(ctx, GoCaptchaService_GetStatusInfo_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCaptchaServiceClient) DelStatusInfo(ctx context.Context, in *StatusInfoRequest, opts ...grpc.CallOption) (*StatusInfoResponse, error) { + out := new(StatusInfoResponse) + err := c.cc.Invoke(ctx, GoCaptchaService_DelStatusInfo_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GoCaptchaServiceServer is the server API for GoCaptchaService service. +// All implementations must embed UnimplementedGoCaptchaServiceServer +// for forward compatibility +type GoCaptchaServiceServer interface { + GetData(context.Context, *GetDataRequest) (*GetDataResponse, error) + CheckData(context.Context, *CheckDataRequest) (*CheckDataResponse, error) + CheckStatus(context.Context, *StatusInfoRequest) (*StatusInfoResponse, error) + GetStatusInfo(context.Context, *StatusInfoRequest) (*StatusInfoResponse, error) + DelStatusInfo(context.Context, *StatusInfoRequest) (*StatusInfoResponse, error) + mustEmbedUnimplementedGoCaptchaServiceServer() +} + +// UnimplementedGoCaptchaServiceServer must be embedded to have forward compatible implementations. +type UnimplementedGoCaptchaServiceServer struct { +} + +func (UnimplementedGoCaptchaServiceServer) GetData(context.Context, *GetDataRequest) (*GetDataResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetData not implemented") +} +func (UnimplementedGoCaptchaServiceServer) CheckData(context.Context, *CheckDataRequest) (*CheckDataResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckData not implemented") +} +func (UnimplementedGoCaptchaServiceServer) CheckStatus(context.Context, *StatusInfoRequest) (*StatusInfoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckStatus not implemented") +} +func (UnimplementedGoCaptchaServiceServer) GetStatusInfo(context.Context, *StatusInfoRequest) (*StatusInfoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetStatusInfo not implemented") +} +func (UnimplementedGoCaptchaServiceServer) DelStatusInfo(context.Context, *StatusInfoRequest) (*StatusInfoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DelStatusInfo not implemented") +} +func (UnimplementedGoCaptchaServiceServer) mustEmbedUnimplementedGoCaptchaServiceServer() {} + +// UnsafeGoCaptchaServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GoCaptchaServiceServer will +// result in compilation errors. +type UnsafeGoCaptchaServiceServer interface { + mustEmbedUnimplementedGoCaptchaServiceServer() +} + +func RegisterGoCaptchaServiceServer(s grpc.ServiceRegistrar, srv GoCaptchaServiceServer) { + s.RegisterService(&GoCaptchaService_ServiceDesc, srv) +} + +func _GoCaptchaService_GetData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetDataRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCaptchaServiceServer).GetData(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GoCaptchaService_GetData_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCaptchaServiceServer).GetData(ctx, req.(*GetDataRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCaptchaService_CheckData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckDataRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCaptchaServiceServer).CheckData(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GoCaptchaService_CheckData_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCaptchaServiceServer).CheckData(ctx, req.(*CheckDataRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCaptchaService_CheckStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StatusInfoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCaptchaServiceServer).CheckStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GoCaptchaService_CheckStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCaptchaServiceServer).CheckStatus(ctx, req.(*StatusInfoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCaptchaService_GetStatusInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StatusInfoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCaptchaServiceServer).GetStatusInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GoCaptchaService_GetStatusInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCaptchaServiceServer).GetStatusInfo(ctx, req.(*StatusInfoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCaptchaService_DelStatusInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StatusInfoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCaptchaServiceServer).DelStatusInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GoCaptchaService_DelStatusInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCaptchaServiceServer).DelStatusInfo(ctx, req.(*StatusInfoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GoCaptchaService_ServiceDesc is the grpc.ServiceDesc for GoCaptchaService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GoCaptchaService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "gocaptcha.GoCaptchaService", + HandlerType: (*GoCaptchaServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetData", + Handler: _GoCaptchaService_GetData_Handler, + }, + { + MethodName: "CheckData", + Handler: _GoCaptchaService_CheckData_Handler, + }, + { + MethodName: "CheckStatus", + Handler: _GoCaptchaService_CheckStatus_Handler, + }, + { + MethodName: "GetStatusInfo", + Handler: _GoCaptchaService_GetStatusInfo_Handler, + }, + { + MethodName: "DelStatusInfo", + Handler: _GoCaptchaService_DelStatusInfo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/api.proto", +} From 55cee133fef25d1be60722ecdfe3936c2763d649 Mon Sep 17 00:00:00 2001 From: Awen Date: Wed, 16 Apr 2025 12:30:01 +0800 Subject: [PATCH 2/6] first commit code --- .idea/.gitignore | 8 -------- .idea/go-captcha-service.iml | 9 --------- .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ 4 files changed, 31 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/go-captcha-service.iml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/go-captcha-service.iml b/.idea/go-captcha-service.iml deleted file mode 100644 index 5e764c4..0000000 --- a/.idea/go-captcha-service.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 8bb0faf..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 4e7956591ff09e0c5043d2555ff3943447e6ae4c Mon Sep 17 00:00:00 2001 From: Awen Date: Thu, 17 Apr 2025 13:36:22 +0800 Subject: [PATCH 3/6] add configuration gocaptcha and resource management --- .gitignore | 24 + Makefile | 4 +- README.md | 3 + cmd/go-captcha-service/main.go | 393 +------------ config.json | 4 +- gocaptcha.json | 541 ++++++++++++++++++ internal/adapt/capt.go | 7 +- internal/app/app.go | 472 +++++++++++++++ internal/common/consts.go | 22 - internal/common/svc_context.go | 8 +- internal/config/config.go | 46 +- internal/consts/consts.go | 11 + internal/helper/helper.go | 313 ++++++++++ internal/helper/path.go | 8 + internal/logic/click.go | 46 +- internal/logic/common.go | 32 +- internal/logic/resource.go | 133 +++++ internal/logic/rotate.go | 47 +- internal/logic/slide.go | 46 +- internal/middleware/http_niddleware.go | 41 +- internal/pkg/gocaptcha/click.go | 296 ++++++++-- internal/pkg/gocaptcha/config/base.config.go | 98 ++++ internal/pkg/gocaptcha/config/config.go | 273 +++++++++ internal/pkg/gocaptcha/config/config_test.go | 316 ++++++++++ .../pkg/gocaptcha/config/resrouce.config.go | 34 ++ internal/pkg/gocaptcha/gocaptcha.go | 249 +++++++- internal/pkg/gocaptcha/rotate.go | 92 ++- internal/pkg/gocaptcha/slide.go | 237 ++++++-- internal/server/grpc_server.go | 59 +- internal/server/http_handler.go | 254 ++++++-- proto/api.pb.go | 404 ++++--------- proto/api.proto | 29 +- 32 files changed, 3533 insertions(+), 1009 deletions(-) create mode 100644 gocaptcha.json create mode 100644 internal/app/app.go delete mode 100644 internal/common/consts.go create mode 100644 internal/consts/consts.go create mode 100644 internal/helper/path.go create mode 100644 internal/logic/resource.go create mode 100644 internal/pkg/gocaptcha/config/base.config.go create mode 100644 internal/pkg/gocaptcha/config/config.go create mode 100644 internal/pkg/gocaptcha/config/config_test.go create mode 100644 internal/pkg/gocaptcha/config/resrouce.config.go diff --git a/.gitignore b/.gitignore index 1e7d993..4caf33d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,27 @@ go.work.sum # env file .env + +# Other +.cache/ +unpackage +.idea/ +.DS_Store +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.node +!.vscode/extensions.json + +logs +*.log + +# resources +resources/gocaptcha/fonts/* +resources/gocaptcha/master_images/* +resources/gocaptcha/shape_images/* +resources/gocaptcha/thumb_images/* +resources/gocaptcha/tile_images/* \ No newline at end of file diff --git a/Makefile b/Makefile index 2d0d74c..186e89a 100644 --- a/Makefile +++ b/Makefile @@ -153,6 +153,6 @@ help: @echo " docker-build : Build Docker image locally (binary)" @echo " docker-build-multi : Build and push multi-arch Docker image (binary)" @echo " docker-run : Run Docker container locally (binary)" - @echo " pm2-deploy : Deploy with PM2 locally" - @echo " pm2-deploy-prod : Deploy with PM2 in production" + @echo " pm2-run : Run with PM2 locally" + @echo " pm2-deploy : Deploy with PM2 in production" @echo " help : Show this help message" \ No newline at end of file diff --git a/README.md b/README.md index 53a423d..bd63f80 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # go-captcha-service + +# config 执重载有效字段 +api_keys diff --git a/cmd/go-captcha-service/main.go b/cmd/go-captcha-service/main.go index 32bf7b5..a7375d8 100644 --- a/cmd/go-captcha-service/main.go +++ b/cmd/go-captcha-service/main.go @@ -2,407 +2,26 @@ package main import ( "context" - "errors" - "flag" "fmt" - "net" - "net/http" "os" "os/signal" - "strconv" "syscall" - "time" - "github.com/google/uuid" - "github.com/sony/gobreaker" - "github.com/wenlng/go-captcha-service/internal/common" - "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" - "go.uber.org/zap" - "google.golang.org/grpc" - - "github.com/wenlng/go-captcha-service/internal/cache" - "github.com/wenlng/go-captcha-service/internal/config" - "github.com/wenlng/go-captcha-service/internal/middleware" - "github.com/wenlng/go-captcha-service/internal/server" - "github.com/wenlng/go-captcha-service/internal/service_discovery" - "github.com/wenlng/go-captcha-service/proto" -) - -// App manages the application components -type App struct { - logger *zap.Logger - dynamicCfg *config.DynamicConfig - cache cache.Cache - discovery service_discovery.ServiceDiscovery - httpServer *http.Server - grpcServer *grpc.Server - cacheBreaker *gobreaker.CircuitBreaker - limiter *middleware.DynamicLimiter -} - -const ( - CacheTypeRedis string = "redis" - CacheTypeMemory = "memory" - CacheTypeEtcd = "etcd" - CacheTypeMemcache = "memcache" + "github.com/wenlng/go-captcha-service/internal/app" ) -const ( - ServiceDiscoveryTypeEtcd string = "etcd" - ServiceDiscoveryTypeZookeeper = "zookeeper" - ServiceDiscoveryTypeConsul = "consul" - ServiceDiscoveryTypeNacos = "nacos" -) - -// NewApp initializes the application -func NewApp() (*App, error) { - // Initialize logger - logger, err := zap.NewProduction() - if err != nil { - return nil, fmt.Errorf("failed to initialize logger: %v", err) - } - - // Parse command-line flags - configFile := flag.String("config", "config.json", "Path to config file") - serviceName := flag.String("service-name", "", "Name for service") - httpPort := flag.String("http-port", "", "Port for HTTP server") - grpcPort := flag.String("grpc-port", "", "Port for gRPC server") - redisAddrs := flag.String("redis-addrs", "", "Comma-separated Redis cluster addresses") - etcdAddrs := flag.String("etcd-addrs", "", "Comma-separated etcd addresses") - memcacheAddrs := flag.String("memcache-addrs", "", "Comma-separated Memcached addresses") - cacheType := flag.String("cache-type", "", "Cache type: redis, memory, etcd, memcache") - cacheTTL := flag.Int("cache-ttl", 0, "Cache TTL in seconds") - cacheCleanupInt := flag.Int("cache-cleanup-interval", 0, "Cache cleanup interval in seconds") - cacheKeyPrefix := flag.Int("cache-key-prefix", 0, "Key prefix for cache") - serviceDiscovery := flag.String("service-discovery", "", "Service discovery: etcd, zookeeper, consul, nacos") - serviceDiscoveryAddrs := flag.String("service-discovery-addrs", "", "Service discovery addresses") - rateLimitQPS := flag.Int("rate-limit-qps", 0, "Rate limit QPS") - rateLimitBurst := flag.Int("rate-limit-burst", 0, "Rate limit burst") - loadBalancer := flag.String("load-balancer", "", "Load balancer: round-robin, consistent-hash") - apiKeys := flag.String("api-keys", "", "Comma-separated API keys") - healthCheckFlag := flag.Bool("health-check", false, "Run health check and exit") - enableCorsFlag := flag.Bool("enable-cors", false, "Enable cross-domain resources") - flag.Parse() - - // Load configuration - dc, err := config.NewDynamicConfig(*configFile) - if err != nil { - logger.Warn("Failed to load config, using defaults", zap.Error(err)) - dc = &config.DynamicConfig{Config: config.DefaultConfig()} - } - - // Merge command-line flags - cfg := dc.Get() - cfg = config.MergeWithFlags(cfg, map[string]interface{}{ - "service-name": *serviceName, - "http-port": *httpPort, - "grpc-port": *grpcPort, - "redis-addrs": *redisAddrs, - "etcd-addrs": *etcdAddrs, - "memcache-addrs": *memcacheAddrs, - "cache-type": *cacheType, - "cache-ttl": *cacheTTL, - "cache-cleanup-interval": *cacheCleanupInt, - "cache-key-prefix": *cacheKeyPrefix, - "service-discovery": *serviceDiscovery, - "service-discovery-addrs": *serviceDiscoveryAddrs, - "rate-limit-qps": *rateLimitQPS, - "rate-limit-burst": *rateLimitBurst, - "load-balancer": *loadBalancer, - "api-keys": *apiKeys, - "enable-cors": *enableCorsFlag, - }) - if err = dc.Update(cfg); err != nil { - logger.Fatal("Configuration validation failed", zap.Error(err)) - } - - // Initialize rate limiter - limiter := middleware.NewDynamicLimiter(cfg.RateLimitQPS, cfg.RateLimitBurst) - go func() { - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - for range ticker.C { - newCfg := dc.Get() - limiter.Update(newCfg.RateLimitQPS, newCfg.RateLimitBurst) - } - }() - - // Initialize circuit breaker - cacheBreaker := gobreaker.NewCircuitBreaker(gobreaker.Settings{ - Name: *serviceName, - MaxRequests: 1, - Interval: 60 * time.Second, - Timeout: 5 * time.Second, - ReadyToTrip: func(counts gobreaker.Counts) bool { - return counts.ConsecutiveFailures > 3 - }, - }) - - // Initialize curCache - var curCache cache.Cache - ttl := time.Duration(cfg.CacheTTL) * time.Second - cleanInt := time.Duration(cfg.CacheCleanupInt) * time.Second - switch cfg.CacheType { - case CacheTypeRedis: - curCache, err = cache.NewRedisClient(cfg.RedisAddrs, cfg.CacheKeyPrefix, ttl) - if err != nil { - logger.Fatal("Failed to initialize Redis", zap.Error(err)) - } - case CacheTypeMemory: - curCache = cache.NewMemoryCache(cfg.CacheKeyPrefix, ttl, cleanInt) - case CacheTypeEtcd: - curCache, err = cache.NewEtcdClient(cfg.EtcdAddrs, cfg.CacheKeyPrefix, ttl) - if err != nil { - logger.Fatal("Failed to initialize etcd", zap.Error(err)) - } - case CacheTypeMemcache: - curCache, err = cache.NewMemcacheClient(cfg.MemcacheAddrs, cfg.CacheKeyPrefix, ttl) - if err != nil { - logger.Fatal("Failed to initialize Memcached", zap.Error(err)) - } - default: - logger.Fatal("Invalid curCache type", zap.String("type", cfg.CacheType)) - } - - // Initialize service discovery - var discovery service_discovery.ServiceDiscovery - if cfg.ServiceDiscovery != "" { - switch cfg.ServiceDiscovery { - case ServiceDiscoveryTypeEtcd: - discovery, err = service_discovery.NewEtcdDiscovery(cfg.ServiceDiscoveryAddrs, 10) - case ServiceDiscoveryTypeZookeeper: - discovery, err = service_discovery.NewZookeeperDiscovery(cfg.ServiceDiscoveryAddrs, 10) - case ServiceDiscoveryTypeConsul: - discovery, err = service_discovery.NewConsulDiscovery(cfg.ServiceDiscoveryAddrs, 10) - case ServiceDiscoveryTypeNacos: - discovery, err = service_discovery.NewNacosDiscovery(cfg.ServiceDiscoveryAddrs, 10) - default: - logger.Fatal("Invalid service discovery type", zap.String("type", cfg.ServiceDiscovery)) - } - if err != nil { - logger.Fatal("Failed to initialize service discovery", zap.Error(err)) - } - } - - // Perform health check if requested - if *healthCheckFlag { - if err = healthCheck(":"+cfg.HTTPPort, ":"+cfg.GRPCPort); err != nil { - logger.Error("Health check failed", zap.Error(err)) - os.Exit(1) - } - os.Exit(0) - } - - return &App{ - logger: logger, - dynamicCfg: dc, - cache: curCache, - discovery: discovery, - cacheBreaker: cacheBreaker, - limiter: limiter, - }, nil -} - -// Start launches the HTTP and gRPC servers -func (a *App) Start(ctx context.Context) error { - cfg := a.dynamicCfg.Get() - - // setup captcha - captcha, err := gocaptcha.Setup() - if err != nil { - return errors.New("setup gocaptcha failed") - } - - // Register service with discovery - var instanceID string - if a.discovery != nil { - instanceID = uuid.New().String() - httpPortInt, _ := strconv.Atoi(cfg.HTTPPort) - grpcPortInt, _ := strconv.Atoi(cfg.GRPCPort) - if err = a.discovery.Register(ctx, cfg.ServiceName, instanceID, "127.0.0.1", httpPortInt, grpcPortInt); err != nil { - return fmt.Errorf("failed to register service: %v", err) - } - go a.updateInstances(ctx, instanceID) - } - - // service context - svcCtx := common.NewSvcContext() - svcCtx.Cache = a.cache - svcCtx.Config = &cfg - svcCtx.Logger = a.logger - svcCtx.Captcha = captcha - - // Register HTTP routes - handlers := server.NewHTTPHandlers(svcCtx) - mwChain := middleware.NewChainHTTP( - nil, // . - middleware.APIKeyMiddleware(a.dynamicCfg, a.logger), - middleware.LoggingMiddleware(a.logger), - middleware.RateLimitMiddleware(a.limiter, a.logger), - middleware.CircuitBreakerMiddleware(a.cacheBreaker, a.logger), - ) - - // Enable cross-domain resource - if cfg.EnableCors { - mwChain.AppendMiddleware(middleware.CORSMiddleware(a.logger)) - } - - // Logic Routes - http.Handle("/get-data", mwChain.Then(handlers.GetDataHandler)) - http.Handle("/check-data", mwChain.Then(handlers.CheckDataHandler)) - http.Handle("/check-status", mwChain.Then(handlers.CheckStatusHandler)) - http.Handle("/get-status-info", mwChain.Then(handlers.GetStatusInfoHandler)) - http.Handle("/del-status-data", mwChain.Then(handlers.DelStatusInfoHandler)) - http.Handle("/rate-limit", mwChain.Then(middleware.RateLimitHandler(a.limiter, a.logger))) - - // Start HTTP server - a.httpServer = &http.Server{ - Addr: ":" + cfg.HTTPPort, - } - go func() { - a.logger.Info("Starting HTTP server", zap.String("port", cfg.HTTPPort)) - if err := a.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - a.logger.Fatal("HTTP server failed", zap.Error(err)) - } - }() - - // Start gRPC server - lis, err := net.Listen("tcp", ":"+cfg.GRPCPort) - if err != nil { - return fmt.Errorf("failed to listen: %v", err) - } - a.grpcServer = grpc.NewServer( - grpc.UnaryInterceptor(middleware.UnaryServerInterceptor(a.dynamicCfg, a.logger, a.cacheBreaker)), - ) - proto.RegisterGoCaptchaServiceServer(a.grpcServer, server.NewGoCaptchaServer(svcCtx)) - go func() { - a.logger.Info("Starting gRPC server", zap.String("port", cfg.GRPCPort)) - if err := a.grpcServer.Serve(lis); err != nil && err != grpc.ErrServerStopped { - a.logger.Fatal("gRPC server failed", zap.Error(err)) - } - }() - - return nil -} - -// updateInstances periodically updates service instances -func (a *App) updateInstances(ctx context.Context, instanceID string) { - ticker := time.NewTicker(10 * time.Second) - cfg := a.dynamicCfg.Get() - - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - if a.discovery != nil { - if err := a.discovery.Deregister(ctx, instanceID); err != nil { - a.logger.Error("Failed to deregister service", zap.Error(err)) - } - } - return - case <-ticker.C: - if a.discovery == nil { - continue - } - instances, err := a.discovery.Discover(ctx, cfg.ServiceName) - if err != nil { - a.logger.Error("Failed to discover instances", zap.Error(err)) - continue - } - a.logger.Info("Discovered instances", zap.Int("count", len(instances))) - } - } -} - -// Shutdown gracefully stops the application -func (a *App) Shutdown() { - a.logger.Info("Received shutdown signal, shutting down gracefully") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // Stop HTTP server - if a.httpServer != nil { - if err := a.httpServer.Shutdown(ctx); err != nil { - a.logger.Error("HTTP server shutdown error", zap.Error(err)) - } else { - a.logger.Info("HTTP server shut down successfully") - } - } - - // Stop gRPC server - if a.grpcServer != nil { - a.grpcServer.GracefulStop() - a.logger.Info("gRPC server shut down successfully") - } - - // Close cache - if redisClient, ok := a.cache.(*cache.RedisClient); ok { - if err := redisClient.Close(); err != nil { - a.logger.Error("Redis client close error", zap.Error(err)) - } else { - a.logger.Info("Redis client closed successfully") - } - } - if memoryCache, ok := a.cache.(*cache.MemoryCache); ok { - memoryCache.Stop() - a.logger.Info("Memory cache stopped successfully") - } - if etcdClient, ok := a.cache.(*cache.EtcdClient); ok { - if err := etcdClient.Close(); err != nil { - a.logger.Error("etcd client close error", zap.Error(err)) - } else { - a.logger.Info("etcd client closed successfully") - } - } - if memcacheClient, ok := a.cache.(*cache.MemcacheClient); ok { - if err := memcacheClient.Close(); err != nil { - a.logger.Error("Memcached client close error", zap.Error(err)) - } else { - a.logger.Info("Memcached client closed successfully") - } - } - - // Close service discovery - if a.discovery != nil { - if err := a.discovery.Close(); err != nil { - a.logger.Error("Service discovery close error", zap.Error(err)) - } else { - a.logger.Info("Service discovery closed successfully") - } - } -} - -// healthCheck performs a health check on HTTP and gRPC servers -func healthCheck(httpAddr, grpcAddr string) error { - resp, err := http.Get("http://localhost" + httpAddr + "/read?key=test") - if err != nil || resp.StatusCode != http.StatusNotFound { - return fmt.Errorf("HTTP health check failed: %v", err) - } - resp.Body.Close() - - conn, err := net.DialTimeout("tcp", "localhost"+grpcAddr, 1*time.Second) - if err != nil { - return fmt.Errorf("gRPC health check failed: %v", err) - } - conn.Close() - - return nil -} - func main() { - app, err := NewApp() + a, err := app.NewApp() if err != nil { fmt.Fprintf(os.Stderr, "Failed to initialize app: %v\n", err) os.Exit(1) } - defer app.logger.Sync() ctx, cancel := context.WithCancel(context.Background()) defer cancel() - if err = app.Start(ctx); err != nil { - app.logger.Fatal("Failed to start app", zap.Error(err)) + if err = a.Start(ctx); err != nil { + fmt.Fprintf(os.Stderr, "Failed to start app: %v\n", err) } // Handle termination signals @@ -410,6 +29,6 @@ func main() { signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) <-sigCh - app.Shutdown() - app.logger.Info("App exited") + a.Shutdown() + fmt.Fprintf(os.Stderr, "App service exited") } diff --git a/config.json b/config.json index 35c06ac..ae4e4b9 100644 --- a/config.json +++ b/config.json @@ -14,6 +14,6 @@ "rate_limit_qps": 1000, "rate_limit_burst": 1000, "load_balancer": "round-robin", - "api_keys": ["my-secret-key-123", "another-key-456"], - "enable_cors": true + "enable_cors": true, + "api_keys": ["my-secret-key-123", "another-key-456"] } \ No newline at end of file diff --git a/gocaptcha.json b/gocaptcha.json new file mode 100644 index 0000000..3489045 --- /dev/null +++ b/gocaptcha.json @@ -0,0 +1,541 @@ +{ + "resources": { + "version": "0.0.1", + "char": { + "languages": { + "chinese": [], + "english": [] + } + }, + "font": { + "type": "load", + "file_dir": "./gocaptcha/fonts/", + "file_maps": { + "yrdzst_bold": "yrdzst-bold.ttf" + } + }, + "shape_image": { + "type": "load", + "file_dir": "./gocaptcha/shape_images/", + "file_maps": { + "shape_01": "shape_01.png", + "shape_01.png":"c.png" + } + }, + "master_image": { + "type": "load", + "file_dir": "./gocaptcha/master_images/", + "file_maps": { + "image_01": "image_01.jpg", + "image_02":"image_02.jpg" + } + }, + "thumb_image": { + "type": "load", + "file_dir": "./gocaptcha/thumb_images/", + "file_maps": { + + } + }, + "tile_image": { + "type": "load", + "file_dir": "./gocaptcha/tile_images/", + "file_maps": { + "tile_01": "tile_01.png", + "tile_02": "tile_02.png" + }, + "file_maps_02": { + "tile_mask_01": "tile_mask_01.png", + "tile_mask_02": "tile_mask_02.png" + }, + "file_maps_03": { + "tile_shadow_01": "tile_shadow_01.png", + "tile_shadow_02": "tile_shadow_02.png" + } + } + }, + "builder": { + "click_config_maps": { + "click_default_ch": { + "language": "chinese", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click_dark_ch": { + "language": "chinese", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click_default_en": { + "language": "english", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 34, + "max": 48 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 34, + "max": 48 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click_dark_en": { + "language": "english", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + } + }, + "click_shape_config_maps": { + "click_shape_default": { + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + } + }, + "slide_config_maps": { + "slide_default": {} + }, + "drag_config_maps": { + "drag_default": {} + }, + "rotate_config_maps": { + "rotate_default": {} + } + } +} \ No newline at end of file diff --git a/internal/adapt/capt.go b/internal/adapt/capt.go index 869c134..5401f80 100644 --- a/internal/adapt/capt.go +++ b/internal/adapt/capt.go @@ -26,7 +26,7 @@ type CaptDataResponse struct { ThumbImageSize int32 `json:"thumb_size,omitempty"` DisplayX int32 `json:"display_x,omitempty"` DisplayY int32 `json:"display_y,omitempty"` - Type int32 `json:"type,omitempty"` + Id string `json:"id,omitempty"` } type CaptNormalDataResponse struct { @@ -40,3 +40,8 @@ type CaptStatusDataResponse struct { Message string `json:"message" default:""` Data string `json:"status" default:""` } + +type CaptStatusInfo struct { + Info interface{} `json:"info"` + Status int `json:"status"` +} diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..30c9a7e --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,472 @@ +package app + +import ( + "context" + "errors" + "flag" + "fmt" + "net" + "net/http" + "os" + "strconv" + "time" + + "github.com/google/uuid" + "github.com/sony/gobreaker" + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/middleware" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + config2 "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha/config" + "github.com/wenlng/go-captcha-service/internal/server" + "github.com/wenlng/go-captcha-service/internal/service_discovery" + "github.com/wenlng/go-captcha-service/proto" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +// App manages the application components +type App struct { + logger *zap.Logger + dynamicCfg *config.DynamicConfig + dynamicCaptCfg *config2.DynamicCaptchaConfig + cache cache.Cache + discovery service_discovery.ServiceDiscovery + httpServer *http.Server + grpcServer *grpc.Server + cacheBreaker *gobreaker.CircuitBreaker + limiter *middleware.DynamicLimiter +} + +// CacheType . +const ( + CacheTypeRedis string = "redis" + CacheTypeMemory = "memory" + CacheTypeEtcd = "etcd" + CacheTypeMemcache = "memcache" +) + +// ServiceDiscoveryType . +const ( + ServiceDiscoveryTypeEtcd string = "etcd" + ServiceDiscoveryTypeZookeeper = "zookeeper" + ServiceDiscoveryTypeConsul = "consul" + ServiceDiscoveryTypeNacos = "nacos" +) + +// NewApp initializes the application +func NewApp() (*App, error) { + + // Parse command-line flags + configFile := flag.String("config", "config.json", "Path to config file") + gocaptchaConfigFile := flag.String("gocaptcha.json", "gocaptcha.json", "Path to gocaptcha config file") + serviceName := flag.String("service-name", "", "Name for service") + httpPort := flag.String("http-port", "", "Port for HTTP server") + grpcPort := flag.String("grpc-port", "", "Port for gRPC server") + redisAddrs := flag.String("redis-addrs", "", "Comma-separated Redis cluster addresses") + etcdAddrs := flag.String("etcd-addrs", "", "Comma-separated etcd addresses") + memcacheAddrs := flag.String("memcache-addrs", "", "Comma-separated Memcached addresses") + cacheType := flag.String("cache-type", "", "Cache type: redis, memory, etcd, memcache") + cacheTTL := flag.Int("cache-ttl", 0, "Cache TTL in seconds") + cacheCleanupInt := flag.Int("cache-cleanup-interval", 0, "Cache cleanup interval in seconds") + cacheKeyPrefix := flag.Int("cache-key-prefix", 0, "Key prefix for cache") + serviceDiscovery := flag.String("service-discovery", "", "Service discovery: etcd, zookeeper, consul, nacos") + serviceDiscoveryAddrs := flag.String("service-discovery-addrs", "", "Service discovery addresses") + rateLimitQPS := flag.Int("rate-limit-qps", 0, "Rate limit QPS") + rateLimitBurst := flag.Int("rate-limit-burst", 0, "Rate limit burst") + loadBalancer := flag.String("load-balancer", "", "Load balancer: round-robin, consistent-hash") + apiKeys := flag.String("api-keys", "", "Comma-separated API keys") + logLevel := flag.String("log-level", "", "Set log level: error, debug, warn, info") + healthCheckFlag := flag.Bool("health-check", false, "Run health check and exit") + enableCorsFlag := flag.Bool("enable-cors", false, "Enable cross-domain resources") + flag.Parse() + + // Initialize logger + logger, err := zap.NewProduction() + if err != nil { + return nil, fmt.Errorf("failed to initialize logger: %v", err) + } + setLoggerLevel(logger, *logLevel) + + // Load configuration + dc, err := config.NewDynamicConfig(*configFile) + if err != nil { + logger.Warn("Failed to load config, using defaults", zap.Error(err)) + dc = &config.DynamicConfig{Config: config.DefaultConfig()} + } + // Register hot update callback + dc.RegisterHotCallbackHook("UPDATE_LOG_LEVEL", func(dnCfg *config.DynamicConfig) { + setLoggerLevel(logger, dnCfg.Get().LogLevel) + }) + + // Load configuration + dgc, err := config2.NewDynamicConfig(*gocaptchaConfigFile) + if err != nil { + logger.Warn("Failed to load gocaptcha config, using defaults", zap.Error(err)) + dgc = &config2.DynamicCaptchaConfig{Config: config2.DefaultConfig()} + } + + // Merge command-line flags + cfg := dc.Get() + cfg = config.MergeWithFlags(cfg, map[string]interface{}{ + "service-name": *serviceName, + "http-port": *httpPort, + "grpc-port": *grpcPort, + "redis-addrs": *redisAddrs, + "etcd-addrs": *etcdAddrs, + "memcache-addrs": *memcacheAddrs, + "cache-type": *cacheType, + "cache-ttl": *cacheTTL, + "cache-cleanup-interval": *cacheCleanupInt, + "cache-key-prefix": *cacheKeyPrefix, + "service-discovery": *serviceDiscovery, + "service-discovery-addrs": *serviceDiscoveryAddrs, + "rate-limit-qps": *rateLimitQPS, + "rate-limit-burst": *rateLimitBurst, + "load-balancer": *loadBalancer, + "enable-cors": *enableCorsFlag, + "api-keys": *apiKeys, + }) + if err = dc.Update(cfg); err != nil { + logger.Fatal("Configuration validation failed", zap.Error(err)) + } + + // Initialize rate limiter + limiter := middleware.NewDynamicLimiter(cfg.RateLimitQPS, cfg.RateLimitBurst) + go func() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for range ticker.C { + newCfg := dc.Get() + limiter.Update(newCfg.RateLimitQPS, newCfg.RateLimitBurst) + } + }() + + // Initialize circuit breaker + cacheBreaker := gobreaker.NewCircuitBreaker(gobreaker.Settings{ + Name: *serviceName, + MaxRequests: 1, + Interval: 60 * time.Second, + Timeout: 5 * time.Second, + ReadyToTrip: func(counts gobreaker.Counts) bool { + return counts.ConsecutiveFailures > 3 + }, + }) + + // Initialize cache + var curCache cache.Cache + ttl := time.Duration(cfg.CacheTTL) * time.Second + cleanInt := time.Duration(cfg.CacheCleanupInt) * time.Second + switch cfg.CacheType { + case CacheTypeRedis: + curCache, err = cache.NewRedisClient(cfg.RedisAddrs, cfg.CacheKeyPrefix, ttl) + if err != nil { + logger.Fatal("Failed to initialize Redis", zap.Error(err)) + } + case CacheTypeMemory: + curCache = cache.NewMemoryCache(cfg.CacheKeyPrefix, ttl, cleanInt) + case CacheTypeEtcd: + curCache, err = cache.NewEtcdClient(cfg.EtcdAddrs, cfg.CacheKeyPrefix, ttl) + if err != nil { + logger.Fatal("Failed to initialize etcd", zap.Error(err)) + } + case CacheTypeMemcache: + curCache, err = cache.NewMemcacheClient(cfg.MemcacheAddrs, cfg.CacheKeyPrefix, ttl) + if err != nil { + logger.Fatal("Failed to initialize Memcached", zap.Error(err)) + } + default: + logger.Fatal("Invalid curCache type", zap.String("type", cfg.CacheType)) + } + + // Initialize service discovery + var discovery service_discovery.ServiceDiscovery + if cfg.ServiceDiscovery != "" { + switch cfg.ServiceDiscovery { + case ServiceDiscoveryTypeEtcd: + discovery, err = service_discovery.NewEtcdDiscovery(cfg.ServiceDiscoveryAddrs, 10) + case ServiceDiscoveryTypeZookeeper: + discovery, err = service_discovery.NewZookeeperDiscovery(cfg.ServiceDiscoveryAddrs, 10) + case ServiceDiscoveryTypeConsul: + discovery, err = service_discovery.NewConsulDiscovery(cfg.ServiceDiscoveryAddrs, 10) + case ServiceDiscoveryTypeNacos: + discovery, err = service_discovery.NewNacosDiscovery(cfg.ServiceDiscoveryAddrs, 10) + default: + logger.Fatal("Invalid service discovery type", zap.String("type", cfg.ServiceDiscovery)) + } + if err != nil { + logger.Fatal("Failed to initialize service discovery", zap.Error(err)) + } + } + + // Perform health check if requested + if *healthCheckFlag { + if err = healthCheck(":"+cfg.HTTPPort, ":"+cfg.GRPCPort); err != nil { + logger.Error("Health check failed", zap.Error(err)) + os.Exit(1) + } + os.Exit(0) + } + + return &App{ + logger: logger, + dynamicCfg: dc, + dynamicCaptCfg: dgc, + cache: curCache, + discovery: discovery, + cacheBreaker: cacheBreaker, + limiter: limiter, + }, nil +} + +// setLoggerLevel setting the log Level +func setLoggerLevel(logger *zap.Logger, level string) { + switch level { + case "error": + logger.WithOptions(zap.IncreaseLevel(zap.ErrorLevel)) + break + case "debug": + logger.WithOptions(zap.IncreaseLevel(zap.DebugLevel)) + break + case "warn": + logger.WithOptions(zap.IncreaseLevel(zap.WarnLevel)) + break + case "info": + logger.WithOptions(zap.IncreaseLevel(zap.InfoLevel)) + break + } +} + +// Start launches the HTTP and gRPC servers +func (a *App) Start(ctx context.Context) error { + cfg := a.dynamicCfg.Get() + + // Setup captcha + captcha, err := gocaptcha.Setup(a.dynamicCaptCfg) + if err != nil { + a.logger.Fatal("Failed to setup gocaptcha: ", zap.Error(err)) + return errors.New("setup gocaptcha failed") + } + captcha.DynamicCnf = a.dynamicCaptCfg + + // Register hot update callback + a.dynamicCaptCfg.RegisterHotCallbackHook("GENERATE_CAPTCHA", func(dnCfg *config2.DynamicCaptchaConfig) { + err = captcha.HotUpdate(dnCfg) + if err != nil { + a.logger.Fatal("Failed to hot update gocaptcha, without any change: ", zap.Error(err)) + } + }) + + // Register service with discovery + var instanceID string + if a.discovery != nil { + instanceID = uuid.New().String() + httpPortInt, _ := strconv.Atoi(cfg.HTTPPort) + grpcPortInt, _ := strconv.Atoi(cfg.GRPCPort) + if err = a.discovery.Register(ctx, cfg.ServiceName, instanceID, "127.0.0.1", httpPortInt, grpcPortInt); err != nil { + return fmt.Errorf("failed to register service: %v", err) + } + go a.updateInstances(ctx, instanceID) + } + + // Service context + svcCtx := common.NewSvcContext() + svcCtx.Cache = a.cache + svcCtx.DynamicConfig = a.dynamicCfg + svcCtx.Logger = a.logger + svcCtx.Captcha = captcha + + // Start HTTP server + if cfg.HTTPPort != "" && cfg.HTTPPort != "0" { + if err = a.setupHTTPServer(svcCtx, &cfg); err != nil { + return err + } + } + + // Start gRPC server + if cfg.GRPCPort != "" && cfg.GRPCPort != "0" { + if err = a.setupGRPCServer(svcCtx, &cfg); err != nil { + return err + } + } + + return nil +} + +// setupHttpServer start HTTP server +func (a *App) setupHTTPServer(svcCtx *common.SvcContext, cfg *config.Config) error { + // Register HTTP routes + handlers := server.NewHTTPHandlers(svcCtx) + + var middlewares = make([]middleware.HTTPMiddleware, 0) + + // Enable cross-domain resource + if cfg.EnableCors { + middlewares = append(middlewares, nil, middleware.CORSMiddleware(a.logger)) + } + + middlewares = append(middlewares, + middleware.APIKeyMiddleware(a.dynamicCfg, a.logger), + middleware.LoggingMiddleware(a.logger), + middleware.RateLimitMiddleware(a.limiter, a.logger), + middleware.CircuitBreakerMiddleware(a.cacheBreaker, a.logger), + ) + + mwChain := middleware.NewChainHTTP(middlewares...) + + // Logic Routes + http.Handle("/get-data", mwChain.Then(handlers.GetDataHandler)) + http.Handle("/check-data", mwChain.Then(handlers.CheckDataHandler)) + http.Handle("/check-status", mwChain.Then(handlers.CheckStatusHandler)) + http.Handle("/get-status-info", mwChain.Then(handlers.GetStatusInfoHandler)) + http.Handle("/del-status-data", mwChain.Then(handlers.DelStatusInfoHandler)) + http.Handle("/rate-limit", mwChain.Then(middleware.RateLimitHandler(a.limiter, a.logger))) + + http.Handle("/manage/upload-resource", mwChain.Then(handlers.UploadResourceHandler)) + http.Handle("/manage/delete-resource", mwChain.Then(handlers.DeleteResourceHandler)) + http.Handle("/manage/get-resource-list", mwChain.Then(handlers.GetResourceListHandler)) + http.Handle("/manage/get-config", mwChain.Then(handlers.GetGoCaptchaConfigHandler)) + http.Handle("/manage/update-hot-config", mwChain.Then(handlers.UpdateHotGoCaptchaConfigHandler)) + + // Start HTTP server + a.httpServer = &http.Server{ + Addr: ":" + cfg.HTTPPort, + } + go func() { + a.logger.Info("Starting HTTP server", zap.String("port", cfg.HTTPPort)) + if err := a.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + a.logger.Fatal("HTTP server failed", zap.Error(err)) + } + }() + + return nil +} + +// setupGRPCServer start gRPC server +func (a *App) setupGRPCServer(svcCtx *common.SvcContext, cfg *config.Config) error { + lis, err := net.Listen("tcp", ":"+cfg.GRPCPort) + if err != nil { + return fmt.Errorf("failed to listen: %v", err) + } + a.grpcServer = grpc.NewServer( + grpc.UnaryInterceptor(middleware.UnaryServerInterceptor(a.dynamicCfg, a.logger, a.cacheBreaker)), + ) + proto.RegisterGoCaptchaServiceServer(a.grpcServer, server.NewGoCaptchaServer(svcCtx)) + go func() { + a.logger.Info("Starting gRPC server", zap.String("port", cfg.GRPCPort)) + if err := a.grpcServer.Serve(lis); err != nil && err != grpc.ErrServerStopped { + a.logger.Fatal("gRPC server failed", zap.Error(err)) + } + }() + return nil +} + +// updateInstances periodically updates service instances +func (a *App) updateInstances(ctx context.Context, instanceID string) { + ticker := time.NewTicker(10 * time.Second) + cfg := a.dynamicCfg.Get() + + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + if a.discovery != nil { + if err := a.discovery.Deregister(ctx, instanceID); err != nil { + a.logger.Error("Failed to deregister service", zap.Error(err)) + } + } + return + case <-ticker.C: + if a.discovery == nil { + continue + } + instances, err := a.discovery.Discover(ctx, cfg.ServiceName) + if err != nil { + a.logger.Error("Failed to discover instances", zap.Error(err)) + continue + } + a.logger.Info("Discovered instances", zap.Int("count", len(instances))) + } + } +} + +// Shutdown gracefully stops the application +func (a *App) Shutdown() { + a.logger.Info("Received shutdown signal, shutting down gracefully") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + defer a.logger.Sync() + + // Stop HTTP server + if a.httpServer != nil { + if err := a.httpServer.Shutdown(ctx); err != nil { + a.logger.Error("HTTP server shutdown error", zap.Error(err)) + } else { + a.logger.Info("HTTP server shut down successfully") + } + } + + // Stop gRPC server + if a.grpcServer != nil { + a.grpcServer.GracefulStop() + a.logger.Info("gRPC server shut down successfully") + } + + // Close cache + if redisClient, ok := a.cache.(*cache.RedisClient); ok { + if err := redisClient.Close(); err != nil { + a.logger.Error("Redis client close error", zap.Error(err)) + } else { + a.logger.Info("Redis client closed successfully") + } + } + if memoryCache, ok := a.cache.(*cache.MemoryCache); ok { + memoryCache.Stop() + a.logger.Info("Memory cache stopped successfully") + } + if etcdClient, ok := a.cache.(*cache.EtcdClient); ok { + if err := etcdClient.Close(); err != nil { + a.logger.Error("etcd client close error", zap.Error(err)) + } else { + a.logger.Info("etcd client closed successfully") + } + } + if memcacheClient, ok := a.cache.(*cache.MemcacheClient); ok { + if err := memcacheClient.Close(); err != nil { + a.logger.Error("Memcached client close error", zap.Error(err)) + } else { + a.logger.Info("Memcached client closed successfully") + } + } + + // Close service discovery + if a.discovery != nil { + if err := a.discovery.Close(); err != nil { + a.logger.Error("Service discovery close error", zap.Error(err)) + } else { + a.logger.Info("Service discovery closed successfully") + } + } + + a.logger.Info("App service shutdown") +} + +// healthCheck performs a health check on HTTP and gRPC servers +func healthCheck(httpAddr, grpcAddr string) error { + resp, err := http.Get("http://localhost" + httpAddr + "/read?key=test") + if err != nil || resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("HTTP health check failed: %v", err) + } + resp.Body.Close() + + conn, err := net.DialTimeout("tcp", "localhost"+grpcAddr, 1*time.Second) + if err != nil { + return fmt.Errorf("gRPC health check failed: %v", err) + } + conn.Close() + + return nil +} diff --git a/internal/common/consts.go b/internal/common/consts.go deleted file mode 100644 index ac3f5b3..0000000 --- a/internal/common/consts.go +++ /dev/null @@ -1,22 +0,0 @@ -package common - -// Type -const ( - GoCaptchaTypeClick = 0 - GoCaptchaTypeClickShape = 1 - GoCaptchaTypeSlide = 2 - GoCaptchaTypeDrag = 3 - GoCaptchaTypeRotate = 4 -) - -// Theme -const ( - GoCaptchaThemeDefault = 0 - GoCaptchaThemeDark = 1 -) - -// Lang -const ( - GoCaptchaLangDefault = 0 - GoCaptchaLangEnglish = 1 -) diff --git a/internal/common/svc_context.go b/internal/common/svc_context.go index df1c32a..76aba3a 100644 --- a/internal/common/svc_context.go +++ b/internal/common/svc_context.go @@ -8,10 +8,10 @@ import ( ) type SvcContext struct { - Cache cache.Cache - Config *config.Config - Logger *zap.Logger - Captcha *gocaptcha.GoCaptcha + Cache cache.Cache + DynamicConfig *config.DynamicConfig + Logger *zap.Logger + Captcha *gocaptcha.GoCaptcha } func NewSvcContext() *SvcContext { diff --git a/internal/config/config.go b/internal/config/config.go index 0e42513..6c1cb5e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,23 +30,27 @@ type Config struct { RateLimitQPS int `json:"rate_limit_qps"` RateLimitBurst int `json:"rate_limit_burst"` LoadBalancer string `json:"load_balancer"` // round-robin, consistent-hash - APIKeys []string `json:"api_keys"` // API keys for authentication - EnableCors bool `json:"enable_cors"` // cross-domain resources + EnableCors bool `json:"enable_cors"` + APIKeys []string `json:"api_keys"` + LogLevel string `json:"log_level"` // error, debug, info, none } // DynamicConfig . type DynamicConfig struct { - Config Config - mu sync.RWMutex + Config Config + mu sync.RWMutex + hotCbsHooks map[string]HandleHotCallbackHookFnc } +type HandleHotCallbackHookFnc = func(*DynamicConfig) + // NewDynamicConfig . func NewDynamicConfig(file string) (*DynamicConfig, error) { cfg, err := Load(file) if err != nil { return nil, err } - dc := &DynamicConfig{Config: cfg} + dc := &DynamicConfig{Config: cfg, hotCbsHooks: make(map[string]HandleHotCallbackHookFnc)} go dc.watchFile(file) return dc, nil } @@ -69,6 +73,29 @@ func (dc *DynamicConfig) Update(cfg Config) error { return nil } +// RegisterHotCallbackHook callback when updating configuration +func (dc *DynamicConfig) RegisterHotCallbackHook(key string, callback HandleHotCallbackHookFnc) { + if _, ok := dc.hotCbsHooks[key]; !ok { + dc.hotCbsHooks[key] = callback + } +} + +// UnRegisterHotCallbackHook callback when updating configuration +func (dc *DynamicConfig) UnRegisterHotCallbackHook(key string) { + if _, ok := dc.hotCbsHooks[key]; !ok { + delete(dc.hotCbsHooks, key) + } +} + +// HandleHotCallbackHook . +func (dc *DynamicConfig) HandleHotCallbackHook() { + for _, fnc := range dc.hotCbsHooks { + if fnc != nil { + fnc(dc) + } + } +} + // watchFile monitors the Config file for changes func (dc *DynamicConfig) watchFile(file string) { watcher, err := fsnotify.NewWatcher() @@ -106,6 +133,8 @@ func (dc *DynamicConfig) watchFile(file string) { fmt.Fprintf(os.Stderr, "Failed to update Config: %v\n", err) continue } + + dc.HandleHotCallbackHook() fmt.Printf("Configuration reloaded successfully\n") } case err, ok := <-watcher.Errors: @@ -276,8 +305,8 @@ func MergeWithFlags(config Config, flags map[string]interface{}) Config { if v, ok := flags["api-keys"].(string); ok && v != "" { config.APIKeys = strings.Split(v, ",") } - if v, ok := flags["enable-cors"].(bool); ok { - config.EnableCors = v + if v, ok := flags["log-level"].(string); ok && v != "" { + config.LogLevel = v } return config } @@ -299,7 +328,8 @@ func DefaultConfig() Config { RateLimitQPS: 1000, RateLimitBurst: 1000, LoadBalancer: "round-robin", - APIKeys: []string{"my-secret-key-123"}, EnableCors: false, + APIKeys: []string{"my-secret-key-123"}, + LogLevel: "info", } } diff --git a/internal/consts/consts.go b/internal/consts/consts.go new file mode 100644 index 0000000..b938369 --- /dev/null +++ b/internal/consts/consts.go @@ -0,0 +1,11 @@ +package consts + +// Type +const ( + GoCaptchaTypeUnknown = 0 + GoCaptchaTypeClick = 1 + GoCaptchaTypeClickShape = 2 + GoCaptchaTypeSlide = 3 + GoCaptchaTypeDrag = 4 + GoCaptchaTypeRotate = 5 +) diff --git a/internal/helper/helper.go b/internal/helper/helper.go index 41b6fb3..302fd5d 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -1,11 +1,20 @@ package helper import ( + "bytes" "encoding/json" + "fmt" + "image" + "os" + "path" + "path/filepath" "reflect" + "regexp" "strconv" + "strings" "github.com/google/uuid" + "github.com/wenlng/go-captcha/v2/base/codec" ) // GenUniqueId . @@ -68,3 +77,307 @@ func MarshalJson(data interface{}) ([]byte, error) { } return json.Marshal(data) } + +// GetPWD . +func GetPWD() string { + path, err := os.Getwd() + if err != nil { + return "" + } + return path +} + +// ReadFileStream reads file contents using streaming, suitable for large files +func ReadFileStream(filePath string) ([]byte, error) { + cleanPath := filepath.Clean(filePath) + + info, err := os.Stat(cleanPath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("file %s does not exist: %w", cleanPath, err) + } + return nil, fmt.Errorf("cannot access file %s: %w", cleanPath, err) + } + + if info.IsDir() { + return nil, fmt.Errorf("%s is a directory, not a file", cleanPath) + } + + file, err := os.Open(cleanPath) + if err != nil { + return nil, fmt.Errorf("failed to open file %s: %w", cleanPath, err) + } + defer file.Close() + + var buffer bytes.Buffer + const chunkSize = 8192 // 8KB buffer size + chunk := make([]byte, chunkSize) + for { + n, err := file.Read(chunk) + if n > 0 { + buffer.Write(chunk[:n]) + } + if err != nil { + if err.Error() == "EOF" { + break // End of file reached + } + return nil, fmt.Errorf("failed to read file %s: %w", cleanPath, err) + } + } + return buffer.Bytes(), nil +} + +// LoadImageData . +func LoadImageData(filepath string) (image.Image, error) { + stream, err := ReadFileStream(filepath) + if err != nil { + return nil, err + } + if path.Ext(filepath) == ".png" { + png, err := codec.DecodeByteToPng(stream) + if err != nil { + return nil, err + } + return png, nil + } + + jpeg, err := codec.DecodeByteToJpeg(stream) + if err != nil { + return nil, err + } + return jpeg, nil +} + +// DeepEqual depth compares whether two values are exactly the same +func DeepEqual(a, b interface{}) bool { + v1 := reflect.ValueOf(a) + v2 := reflect.ValueOf(b) + + if v1.Type() != v2.Type() { + return false + } + + return deepEqualValue(v1, v2) +} + +// deepEqualValue recursively compares two reflection values +func deepEqualValue(v1, v2 reflect.Value) bool { + if !v1.IsValid() || !v2.IsValid() { + return v1.IsValid() == v2.IsValid() + } + + switch v1.Kind() { + case reflect.Ptr: + if v1.IsNil() && v2.IsNil() { + return true + } + if v1.IsNil() != v2.IsNil() { + return false + } + return deepEqualValue(v1.Elem(), v2.Elem()) + + case reflect.Struct: + for i := 0; i < v1.NumField(); i++ { + if !deepEqualValue(v1.Field(i), v2.Field(i)) { + return false + } + } + return true + + case reflect.Slice, reflect.Array: + if v1.Len() != v2.Len() { + return false + } + for i := 0; i < v1.Len(); i++ { + if !deepEqualValue(v1.Index(i), v2.Index(i)) { + return false + } + } + return true + + case reflect.Map: + if v1.Len() != v2.Len() { + return false + } + if v1.IsNil() != v2.IsNil() { + return false + } + for _, k := range v1.MapKeys() { + if !deepEqualValue(v1.MapIndex(k), v2.MapIndex(k)) { + return false + } + } + return true + + case reflect.Interface: + if v1.IsNil() || v2.IsNil() { + return v1.IsNil() == v2.IsNil() + } + return deepEqualValue(v1.Elem(), v2.Elem()) + + default: + return reflect.DeepEqual(v1.Interface(), v2.Interface()) + } +} + +// FileExists . +func FileExists(filepathStr string) bool { + p := filepath.Clean(filepathStr) + _, err := os.Stat(p) + if err == nil { + return true + } + if os.IsNotExist(err) { + return false + } + return false +} + +// IsFile . +func IsFile(filepathStr string) bool { + p := filepath.Clean(filepathStr) + info, err := os.Stat(p) + if err != nil { + return false + } + return !info.IsDir() +} + +// DeleteFile . +func DeleteFile(path string) error { + cleanPath := filepath.Clean(path) + + if cleanPath == "" || cleanPath == "." || cleanPath == "/" || cleanPath == string(os.PathSeparator) { + return os.ErrInvalid + } + + fileInfo, err := os.Stat(cleanPath) + if err != nil { + if os.IsNotExist(err) { + return err + } + return err + } + if fileInfo.IsDir() { + return os.ErrInvalid + } + + err = os.Remove(cleanPath) + if err != nil { + return err + } + + return nil +} + +// EnsureDir . +func EnsureDir(path string) error { + return EnsureDirWithPerm(path, 0755) +} + +// EnsureDirWithPerm make sure all directories in the specified path exist. If they do not exist, create them +func EnsureDirWithPerm(path string, perm os.FileMode) error { + cleanPath := filepath.Clean(path) + if cleanPath == "" || cleanPath == "." || cleanPath == "/" || cleanPath == string(os.PathSeparator) { + return os.ErrInvalid + } + fileInfo, err := os.Stat(cleanPath) + if err == nil { + if fileInfo.IsDir() { + return nil + } + return os.ErrExist + } + if !os.IsNotExist(err) { + return err + } + + err = os.MkdirAll(cleanPath, perm) + if err != nil { + return err + } + + return nil +} + +// IsValidDirName verify that the directory name is valid +// (only letters, numbers, hyphens, underscores allowed) +func IsValidDirName(dir string) bool { + re := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) + return re.MatchString(dir) +} + +// IsSubPath check whether the target path is in the root directory +func IsSubPath(path, root string) bool { + path = filepath.Clean(path) + + absPath, err := filepath.Abs(path) + if err != nil { + return false + } + absRoot, err := filepath.Abs(root) + if err != nil { + return false + } + + return strings.HasPrefix(absRoot, absPath) +} + +// TraverseDir recursively traverse all file paths in the directory +// and return the relative paths relative to the specified parent directory +func TraverseDir(root, baseDir string) ([]string, error) { + cleanRoot := filepath.Clean(root) + cleanBaseDir := filepath.Clean(baseDir) + + if cleanRoot == "" || cleanRoot == "/" || cleanRoot == string(os.PathSeparator) { + return nil, os.ErrInvalid + } + if cleanBaseDir == "" || cleanBaseDir == "/" || cleanBaseDir == string(os.PathSeparator) { + return nil, os.ErrInvalid + } + + absRoot, err := filepath.Abs(cleanRoot) + if err != nil { + return nil, err + } + absBaseDir, err := filepath.Abs(cleanBaseDir) + if err != nil { + return nil, err + } + + fileInfo, err := os.Stat(absRoot) + if err != nil { + return nil, err + } + if !fileInfo.IsDir() { + return nil, os.ErrInvalid + } + + if _, err := os.Stat(absBaseDir); err != nil { + return nil, err + } + + var filePaths []string + + err = filepath.Walk(absRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() { + relPath, err := filepath.Rel(absBaseDir, path) + if err != nil { + return err + } + filePaths = append(filePaths, relPath) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return filePaths, nil +} diff --git a/internal/helper/path.go b/internal/helper/path.go new file mode 100644 index 0000000..e01eff1 --- /dev/null +++ b/internal/helper/path.go @@ -0,0 +1,8 @@ +package helper + +import "path" + +// GetResourceDirAbsPath 。 +func GetResourceDirAbsPath() string { + return path.Join(GetPWD(), "resources") +} diff --git a/internal/logic/click.go b/internal/logic/click.go index 8277c9b..d811ae8 100644 --- a/internal/logic/click.go +++ b/internal/logic/click.go @@ -11,6 +11,7 @@ import ( "github.com/wenlng/go-captcha-service/internal/cache" "github.com/wenlng/go-captcha-service/internal/common" "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/consts" "github.com/wenlng/go-captcha-service/internal/helper" "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" "github.com/wenlng/go-captcha/v2/click" @@ -21,32 +22,45 @@ import ( type ClickCaptLogic struct { svcCtx *common.SvcContext - cache cache.Cache - config *config.Config - logger *zap.Logger - captcha *gocaptcha.GoCaptcha + cache cache.Cache + dynamicCfg *config.DynamicConfig + logger *zap.Logger + captcha *gocaptcha.GoCaptcha } // NewClickCaptLogic . func NewClickCaptLogic(svcCtx *common.SvcContext) *ClickCaptLogic { return &ClickCaptLogic{ - svcCtx: svcCtx, - cache: svcCtx.Cache, - config: svcCtx.Config, - logger: svcCtx.Logger, - captcha: svcCtx.Captcha, + svcCtx: svcCtx, + cache: svcCtx.Cache, + dynamicCfg: svcCtx.DynamicConfig, + logger: svcCtx.Logger, + captcha: svcCtx.Captcha, } } // GetData . -func (cl *ClickCaptLogic) GetData(ctx context.Context, ctype, theme, lang int) (res *adapt.CaptData, err error) { +func (cl *ClickCaptLogic) GetData(ctx context.Context, id string) (res *adapt.CaptData, err error) { res = &adapt.CaptData{} - if ctype < 0 { - return nil, fmt.Errorf("missing parameter") + if id == "" { + return nil, fmt.Errorf("missing id parameter") } - captData, err := cl.captcha.ClickCaptInstance.Generate() + var capt *gocaptcha.ClickCaptInstance + switch cl.svcCtx.Captcha.GetCaptTypeWithKey(id) { + case consts.GoCaptchaTypeClick: + capt = cl.svcCtx.Captcha.GetClickInstanceWithKey(id) + break + case consts.GoCaptchaTypeClickShape: + capt = cl.svcCtx.Captcha.GetClickShapeInstanceWithKey(id) + break + } + if capt == nil || capt.Instance == nil { + return nil, fmt.Errorf("missing captcha type") + } + + captData, err := capt.Instance.Generate() if err != nil { return nil, fmt.Errorf("generate captcha data failed: %v", err) } @@ -85,7 +99,7 @@ func (cl *ClickCaptLogic) GetData(ctx context.Context, ctype, theme, lang int) ( return res, fmt.Errorf("failed to write cache:: %v", err) } - opts := cl.captcha.ClickCaptInstance.GetOptions() + opts := capt.Instance.GetOptions() res.MasterImageWidth = int32(opts.GetImageSize().Width) res.MasterImageHeight = int32(opts.GetImageSize().Height) res.ThumbImageWidth = int32(opts.GetThumbImageSize().Width) @@ -105,6 +119,10 @@ func (cl *ClickCaptLogic) CheckData(ctx context.Context, key string, dots string return false, fmt.Errorf("failed to get cache: %v", err) } + if cacheData == "" { + return false, nil + } + src := strings.Split(dots, ",") var captData *cache.CaptCacheData diff --git a/internal/logic/common.go b/internal/logic/common.go index 716b0e4..239e393 100644 --- a/internal/logic/common.go +++ b/internal/logic/common.go @@ -16,20 +16,20 @@ import ( type CommonLogic struct { svcCtx *common.SvcContext - cache cache.Cache - config *config.Config - logger *zap.Logger - captcha *gocaptcha.GoCaptcha + cache cache.Cache + dynamicCfg *config.DynamicConfig + logger *zap.Logger + captcha *gocaptcha.GoCaptcha } // NewCommonLogic . func NewCommonLogic(svcCtx *common.SvcContext) *CommonLogic { return &CommonLogic{ - svcCtx: svcCtx, - cache: svcCtx.Cache, - config: svcCtx.Config, - logger: svcCtx.Logger, - captcha: svcCtx.Captcha, + svcCtx: svcCtx, + cache: svcCtx.Cache, + dynamicCfg: svcCtx.DynamicConfig, + logger: svcCtx.Logger, + captcha: svcCtx.Captcha, } } @@ -44,6 +44,10 @@ func (cl *CommonLogic) CheckStatus(ctx context.Context, key string) (ret bool, e return false, fmt.Errorf("failed to get cache: %v", err) } + if cacheData == "" { + return false, nil + } + var captData *cache.CaptCacheData err = json.Unmarshal([]byte(cacheData), &captData) if err != nil { @@ -59,12 +63,18 @@ func (cl *CommonLogic) GetStatusInfo(ctx context.Context, key string) (data *cac return nil, fmt.Errorf("invalid key") } + captData := &cache.CaptCacheData{} + cacheData, err := cl.cache.GetCache(ctx, key) if err != nil { return nil, fmt.Errorf("failed to get cache: %v", err) } - var captData *cache.CaptCacheData + if cacheData == "" { + captData.Data = struct{}{} + return captData, nil + } + err = json.Unmarshal([]byte(cacheData), &captData) if err != nil { return nil, fmt.Errorf("failed to json unmarshal: %v", err) @@ -81,7 +91,7 @@ func (cl *CommonLogic) DelStatusInfo(ctx context.Context, key string) (ret bool, err = cl.cache.DeleteCache(ctx, key) if err != nil { - return false, fmt.Errorf("failed to get cache: %v", err) + return false, fmt.Errorf("failed to delete cache: %v", err) } return true, nil diff --git a/internal/logic/resource.go b/internal/logic/resource.go new file mode 100644 index 0000000..c2b1487 --- /dev/null +++ b/internal/logic/resource.go @@ -0,0 +1,133 @@ +package logic + +import ( + "context" + "fmt" + "io" + "mime/multipart" + "os" + "path" + "path/filepath" + + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/internal/common" + "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/helper" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + "go.uber.org/zap" +) + +// ResourceLogic . +type ResourceLogic struct { + svcCtx *common.SvcContext + + cache cache.Cache + dynamicCfg *config.DynamicConfig + logger *zap.Logger + captcha *gocaptcha.GoCaptcha +} + +// NewResourceLogic . +func NewResourceLogic(svcCtx *common.SvcContext) *ResourceLogic { + return &ResourceLogic{ + svcCtx: svcCtx, + cache: svcCtx.Cache, + dynamicCfg: svcCtx.DynamicConfig, + logger: svcCtx.Logger, + captcha: svcCtx.Captcha, + } +} + +// SaveResource . +func (cl *ResourceLogic) SaveResource(ctx context.Context, dirname string, files []*multipart.FileHeader) (ret, allDone bool, err error) { + resourcePath := helper.GetResourceDirAbsPath() + dirPath := filepath.Join(resourcePath, dirname) + dirPath = filepath.Clean(dirPath) + + if !helper.IsSubPath(resourcePath, dirPath) { + return false, false, fmt.Errorf("invalid dirpath") + } + + err = helper.EnsureDir(dirPath) + if err != nil { + return false, false, err + } + + var hasSkipFileSave bool + for _, fileHeader := range files { + file, err := fileHeader.Open() + if err != nil { + return false, false, fmt.Errorf("failed to open file %s: %v", fileHeader.Filename, err) + } + defer file.Close() + + filename := filepath.Base(fileHeader.Filename) + if filename == "" { + return false, false, fmt.Errorf("invalid filename") + } + + dstPath := filepath.Join(dirPath, filename) + + if helper.FileExists(dstPath) { + hasSkipFileSave = true + continue + } + + dst, err := os.Create(dstPath) + if err != nil { + return false, false, fmt.Errorf("failed to create file %s: %v", filename, err) + } + defer dst.Close() + + if _, err := io.Copy(dst, file); err != nil { + return false, false, fmt.Errorf("failed to save file %s: %v", filename, err) + } + } + + if hasSkipFileSave { + return true, false, fmt.Errorf("some files failed to be uploaded. check if they already exist") + } + + return true, true, nil +} + +// GetResourceList . +func (cl *ResourceLogic) GetResourceList(ctx context.Context, filepath string) ([]string, error) { + resourcePath := helper.GetResourceDirAbsPath() + filepath = path.Join(resourcePath, filepath) + filepath = path.Clean(filepath) + + if !helper.IsSubPath(resourcePath, path.Dir(filepath)) { + return nil, fmt.Errorf("invalid filepath") + } + + fileList, err := helper.TraverseDir(filepath, resourcePath) + if err != nil { + return nil, nil + } + + return fileList, nil +} + +// DelResource . +func (cl *ResourceLogic) DelResource(ctx context.Context, filepath string) (ret bool, err error) { + resourcePath := helper.GetResourceDirAbsPath() + filepath = path.Join(resourcePath, filepath) + filepath = path.Clean(filepath) + + if !helper.IsSubPath(resourcePath, path.Dir(filepath)) { + return false, fmt.Errorf("invalid filepath") + } + + if helper.FileExists(filepath) { + err = helper.DeleteFile(filepath) + if err != nil { + cl.logger.Error("failed to delete resource, err: ", zap.Error(err)) + return false, err + } + } else { + return false, nil + } + + return true, nil +} diff --git a/internal/logic/rotate.go b/internal/logic/rotate.go index 9caeb5f..601ce6b 100644 --- a/internal/logic/rotate.go +++ b/internal/logic/rotate.go @@ -9,6 +9,7 @@ import ( "github.com/wenlng/go-captcha-service/internal/cache" "github.com/wenlng/go-captcha-service/internal/common" "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/consts" "github.com/wenlng/go-captcha-service/internal/helper" "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" "github.com/wenlng/go-captcha/v2/rotate" @@ -19,32 +20,42 @@ import ( type RotateCaptLogic struct { svcCtx *common.SvcContext - cache cache.Cache - config *config.Config - logger *zap.Logger - captcha *gocaptcha.GoCaptcha + cache cache.Cache + dynamicCfg *config.DynamicConfig + logger *zap.Logger + captcha *gocaptcha.GoCaptcha } // NewRotateCaptLogic . func NewRotateCaptLogic(svcCtx *common.SvcContext) *RotateCaptLogic { return &RotateCaptLogic{ - svcCtx: svcCtx, - cache: svcCtx.Cache, - config: svcCtx.Config, - logger: svcCtx.Logger, - captcha: svcCtx.Captcha, + svcCtx: svcCtx, + cache: svcCtx.Cache, + dynamicCfg: svcCtx.DynamicConfig, + logger: svcCtx.Logger, + captcha: svcCtx.Captcha, } } // GetData . -func (cl *RotateCaptLogic) GetData(ctx context.Context, ctype, theme, lang int) (res *adapt.CaptData, err error) { +func (cl *RotateCaptLogic) GetData(ctx context.Context, id string) (res *adapt.CaptData, err error) { res = &adapt.CaptData{} - if ctype < 0 { - return nil, fmt.Errorf("missing parameter") + if id == "" { + return nil, fmt.Errorf("missing id parameter") } - captData, err := cl.captcha.RotateCaptInstance.Generate() + var capt *gocaptcha.RotateCaptInstance + switch cl.svcCtx.Captcha.GetCaptTypeWithKey(id) { + case consts.GoCaptchaTypeRotate: + capt = cl.svcCtx.Captcha.GetRotateInstanceWithKey(id) + break + } + if capt == nil || capt.Instance == nil { + return nil, fmt.Errorf("missing captcha type") + } + + captData, err := capt.Instance.Generate() if err != nil { return nil, fmt.Errorf("generate captcha data failed: %v", err) } @@ -83,9 +94,9 @@ func (cl *RotateCaptLogic) GetData(ctx context.Context, ctype, theme, lang int) return res, fmt.Errorf("failed to write cache:: %v", err) } - opts := cl.captcha.ClickCaptInstance.GetOptions() - res.MasterImageWidth = int32(opts.GetImageSize().Width) - res.MasterImageHeight = int32(opts.GetImageSize().Height) + opts := capt.Instance.GetOptions() + res.MasterImageWidth = int32(opts.GetImageSize()) + res.MasterImageHeight = int32(opts.GetImageSize()) res.ThumbImageWidth = int32(data.Width) res.ThumbImageHeight = int32(data.Height) res.ThumbImageSize = int32(data.Width) @@ -104,6 +115,10 @@ func (cl *RotateCaptLogic) CheckData(ctx context.Context, key string, angle int) return false, fmt.Errorf("failed to get cache: %v", err) } + if cacheData == "" { + return false, nil + } + var captData *cache.CaptCacheData err = json.Unmarshal([]byte(cacheData), &captData) if err != nil { diff --git a/internal/logic/slide.go b/internal/logic/slide.go index d265b94..18a4937 100644 --- a/internal/logic/slide.go +++ b/internal/logic/slide.go @@ -11,6 +11,7 @@ import ( "github.com/wenlng/go-captcha-service/internal/cache" "github.com/wenlng/go-captcha-service/internal/common" "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/consts" "github.com/wenlng/go-captcha-service/internal/helper" "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" "github.com/wenlng/go-captcha/v2/slide" @@ -21,32 +22,45 @@ import ( type SlideCaptLogic struct { svcCtx *common.SvcContext - cache cache.Cache - config *config.Config - logger *zap.Logger - captcha *gocaptcha.GoCaptcha + cache cache.Cache + dynamicCfg *config.DynamicConfig + logger *zap.Logger + captcha *gocaptcha.GoCaptcha } // NewSlideCaptLogic . func NewSlideCaptLogic(svcCtx *common.SvcContext) *SlideCaptLogic { return &SlideCaptLogic{ - svcCtx: svcCtx, - cache: svcCtx.Cache, - config: svcCtx.Config, - logger: svcCtx.Logger, - captcha: svcCtx.Captcha, + svcCtx: svcCtx, + cache: svcCtx.Cache, + dynamicCfg: svcCtx.DynamicConfig, + logger: svcCtx.Logger, + captcha: svcCtx.Captcha, } } // GetData . -func (cl *SlideCaptLogic) GetData(ctx context.Context, ctype, theme, lang int) (res *adapt.CaptData, err error) { +func (cl *SlideCaptLogic) GetData(ctx context.Context, id string) (res *adapt.CaptData, err error) { res = &adapt.CaptData{} - if ctype < 0 { - return nil, fmt.Errorf("missing parameter") + if id == "" { + return nil, fmt.Errorf("missing id parameter") } - captData, err := cl.captcha.SlideCaptInstance.Generate() + var capt *gocaptcha.SlideCaptInstance + switch cl.svcCtx.Captcha.GetCaptTypeWithKey(id) { + case consts.GoCaptchaTypeSlide: + capt = cl.svcCtx.Captcha.GetSlideInstanceWithKey(id) + break + case consts.GoCaptchaTypeDrag: + capt = cl.svcCtx.Captcha.GetDragInstanceWithKey(id) + break + } + if capt == nil || capt.Instance == nil { + return nil, fmt.Errorf("missing captcha type") + } + + captData, err := capt.Instance.Generate() if err != nil { return nil, fmt.Errorf("generate captcha data failed: %v", err) } @@ -85,7 +99,7 @@ func (cl *SlideCaptLogic) GetData(ctx context.Context, ctype, theme, lang int) ( return res, fmt.Errorf("failed to write cache:: %v", err) } - opts := cl.captcha.ClickCaptInstance.GetOptions() + opts := capt.Instance.GetOptions() res.MasterImageWidth = int32(opts.GetImageSize().Width) res.MasterImageHeight = int32(opts.GetImageSize().Height) res.ThumbImageWidth = int32(data.Width) @@ -107,6 +121,10 @@ func (cl *SlideCaptLogic) CheckData(ctx context.Context, key string, dots string return false, fmt.Errorf("failed to get cache: %v", err) } + if cacheData == "" { + return false, nil + } + src := strings.Split(dots, ",") var captData *cache.CaptCacheData diff --git a/internal/middleware/http_niddleware.go b/internal/middleware/http_niddleware.go index 9721a16..45981b5 100644 --- a/internal/middleware/http_niddleware.go +++ b/internal/middleware/http_niddleware.go @@ -59,6 +59,12 @@ func APIKeyMiddleware(dc *config.DynamicConfig, logger *zap.Logger) HTTPMiddlewa return func(next HandlerFunc) HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cfg := dc.Get() + + if len(cfg.APIKeys) == 0 { + next(w, r) + return + } + apiKeyMap := make(map[string]struct{}) for _, key := range cfg.APIKeys { apiKeyMap[key] = struct{}{} @@ -136,9 +142,40 @@ func CircuitBreakerMiddleware(breaker *gobreaker.CircuitBreaker, logger *zap.Log func CORSMiddleware(logger *zap.Logger) HTTPMiddleware { return func(next HandlerFunc) HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") + origin := r.Header.Get("Origin") + + allowedOrigins := []string{"*"} + allowOrigin := "*" + for _, allowed := range allowedOrigins { + if allowed == "*" || allowed == origin { + allowOrigin = origin + break + } + } + + w.Header().Set("Access-Control-Allow-Origin", allowOrigin) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if requestedHeaders := r.Header.Get("Access-Control-Request-Headers"); requestedHeaders != "" { + w.Header().Set("Access-Control-Allow-Headers", requestedHeaders) + } else { + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Custom-Header") + } + + w.Header().Set("Access-Control-Max-Age", "86400") // Cache preflight response for 24 hours + w.Header().Set("Access-Control-Allow-Credentials", "true") // Allow credentials if needed + + // Handle preflight (OPTIONS) requests + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + + // Processing precheck requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } next(w, r) } } diff --git a/internal/pkg/gocaptcha/click.go b/internal/pkg/gocaptcha/click.go index 18f4b10..bc771fe 100644 --- a/internal/pkg/gocaptcha/click.go +++ b/internal/pkg/gocaptcha/click.go @@ -1,69 +1,295 @@ package gocaptcha import ( + "image" + "path" + "strings" + + "github.com/golang/freetype" "github.com/golang/freetype/truetype" "github.com/wenlng/go-captcha-assets/bindata/chars" "github.com/wenlng/go-captcha-assets/resources/fonts/fzshengsksjw" "github.com/wenlng/go-captcha-assets/resources/images_v2" "github.com/wenlng/go-captcha-assets/resources/shapes" - - "github.com/wenlng/go-captcha/v2/base/option" + "github.com/wenlng/go-captcha-service/internal/helper" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha/config" "github.com/wenlng/go-captcha/v2/click" ) -func setupClick() (capt click.Captcha, err error) { - builder := click.NewBuilder( - click.WithRangeLen(option.RangeVal{Min: 4, Max: 6}), - click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 4}), - ) +// ClickCaptInstance . +type ClickCaptInstance struct { + ResourcesVersion string + Version string + Instance click.Captcha +} + +// genClickOptions . +func genClickOptions(conf config.ClickConfig) ([]click.Option, error) { + options := make([]click.Option, 0) + + // Master image + if conf.Master.ImageSize.Height != 0 && conf.Master.ImageSize.Width != 0 { + options = append(options, click.WithImageSize(conf.Master.ImageSize)) + } + + if conf.Master.RangeLength.Min >= 0 && conf.Master.RangeLength.Max >= 0 { + options = append(options, click.WithRangeLen(conf.Master.RangeLength)) + } + + if conf.Master.RangeAngles != nil && len(conf.Master.RangeAngles) > 0 { + options = append(options, click.WithRangeAnglePos(conf.Master.RangeAngles)) + } + + if conf.Master.RangeSize.Min >= 0 && conf.Master.RangeSize.Max >= 0 { + options = append(options, click.WithRangeSize(conf.Master.RangeSize)) + } + + if conf.Master.RangeColors != nil && len(conf.Master.RangeColors) > 0 { + options = append(options, click.WithRangeColors(conf.Master.RangeColors)) + } + + if conf.Master.DisplayShadow != false { + options = append(options, click.WithDisplayShadow(conf.Master.DisplayShadow)) + } + + if conf.Master.ShadowColor != "" { + options = append(options, click.WithShadowColor(conf.Master.ShadowColor)) + } + + if conf.Master.ShadowPoint.X > -999 && + conf.Master.ShadowPoint.Y > -999 && + conf.Master.ShadowPoint.X < 999 && + conf.Master.ShadowPoint.Y < 999 { + options = append(options, click.WithShadowPoint(conf.Master.ShadowPoint)) + } - // fonts - fonts, err := fzshengsksjw.GetFont() + if conf.Master.ImageAlpha > 0 { + options = append(options, click.WithImageAlpha(conf.Master.ImageAlpha)) + } + + if conf.Master.UseShapeOriginalColor != false { + options = append(options, click.WithUseShapeOriginalColor(conf.Master.UseShapeOriginalColor)) + } + + // Thumb image + if conf.Thumb.ImageSize.Height != 0 && conf.Thumb.ImageSize.Width != 0 { + options = append(options, click.WithRangeThumbImageSize(conf.Thumb.ImageSize)) + } + + if conf.Thumb.RangeVerifyLength.Min != 0 && conf.Thumb.RangeVerifyLength.Max != 0 { + options = append(options, click.WithRangeVerifyLen(conf.Thumb.RangeVerifyLength)) + } + + if conf.Thumb.DisabledRangeVerifyLength != false { + options = append(options, click.WithDisabledRangeVerifyLen(conf.Thumb.DisabledRangeVerifyLength)) + } + + if conf.Thumb.RangeTextSize.Min != 0 && conf.Thumb.RangeTextSize.Max != 0 { + options = append(options, click.WithRangeThumbSize(conf.Thumb.RangeTextSize)) + } + + if conf.Thumb.RangeTextColors != nil && len(conf.Thumb.RangeTextColors) > 0 { + options = append(options, click.WithRangeThumbColors(conf.Thumb.RangeTextColors)) + } + + if conf.Thumb.RangeBackgroundColors != nil && len(conf.Thumb.RangeBackgroundColors) > 0 { + options = append(options, click.WithRangeThumbBgColors(conf.Thumb.RangeBackgroundColors)) + } + + if conf.Thumb.BackgroundDistort > 0 { + options = append(options, click.WithRangeThumbBgDistort(conf.Thumb.BackgroundDistort)) + } + + if conf.Thumb.BackgroundDistortAlpha > 0 { + options = append(options, click.WithThumbDisturbAlpha(conf.Thumb.BackgroundDistortAlpha)) + } + + if conf.Thumb.BackgroundCirclesNum > 0 { + options = append(options, click.WithRangeThumbBgCirclesNum(conf.Thumb.BackgroundCirclesNum)) + } + + if conf.Thumb.BackgroundSlimLineNum > 0 { + options = append(options, click.WithRangeThumbBgSlimLineNum(conf.Thumb.BackgroundSlimLineNum)) + } + + if conf.Thumb.IsThumbNonDeformAbility != false { + options = append(options, click.WithIsThumbNonDeformAbility(conf.Thumb.IsThumbNonDeformAbility)) + } + + return options, nil +} + +// GetMixinAlphaChars 数字+字母组合(双组合) +func GetMixinAlphaChars() []string { + var ret = make([]string, 0) + letterArr := strings.Split("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "") + numArr := strings.Split("0123456789", "") + + for _, s := range letterArr { + for _, n := range numArr { + ret = append(ret, s+n) + } + } + + for _, s := range numArr { + for _, n := range letterArr { + ret = append(ret, s+n) + } + } + + return ret +} + +// genClickResources. +func genClickResources(conf config.ClickConfig, resources config.ResourceConfig) ([]click.Resource, error) { + newResources := make([]click.Resource, 0) + + // Set chars resources + if newChars, ok := resources.Char.Languages[conf.Language]; ok && len(newChars) > 0 { + newResources = append(newResources, click.WithChars(newChars)) + } else { + if conf.Language == LanguageNameChinese { + newResources = append(newResources, click.WithChars(chars.GetChineseChars())) + } else { + newResources = append(newResources, click.WithChars(GetMixinAlphaChars())) + } + } + + // Set fonts resources + if len(resources.Font.FileMaps) > 0 { + var newFonts = make([]*truetype.Font, 0) + for _, file := range resources.Font.FileMaps { + resourcesPath := helper.GetResourceDirAbsPath() + rootDir := resources.Font.FileDir + filepath := path.Join(resourcesPath, rootDir, file) + stream, err := helper.ReadFileStream(filepath) + if err != nil { + return nil, err + } + + font, err := freetype.ParseFont(stream) + if err != nil { + return nil, err + } + + newFonts = append(newFonts, font) + } + + newResources = append(newResources, click.WithFonts(newFonts)) + } else { + fonts, err := fzshengsksjw.GetFont() + if err != nil { + return nil, err + } + newResources = append(newResources, click.WithFonts([]*truetype.Font{fonts})) + } + + // Set Background images resources + if len(resources.MasterImage.FileMaps) > 0 { + var newImages = make([]image.Image, 0) + for _, file := range resources.MasterImage.FileMaps { + resourcesPath := helper.GetResourceDirAbsPath() + rootDir := resources.MasterImage.FileDir + filepath := path.Join(resourcesPath, rootDir, file) + + img, err := helper.LoadImageData(filepath) + if err != nil { + return nil, err + } + + newImages = append(newImages, img) + } + newResources = append(newResources, click.WithBackgrounds(newImages)) + } else { + imgs, err := images.GetImages() + if err != nil { + return nil, err + } + newResources = append(newResources, click.WithBackgrounds(imgs)) + } + + // Set Thumb images resources + if len(resources.ThumbImage.FileMaps) > 0 { + var newImages = make([]image.Image, 0) + for _, file := range resources.ThumbImage.FileMaps { + resourcesPath := helper.GetResourceDirAbsPath() + rootDir := resources.ThumbImage.FileDir + filepath := path.Join(resourcesPath, rootDir, file) + + img, err := helper.LoadImageData(filepath) + if err != nil { + return nil, err + } + newImages = append(newImages, img) + } + newResources = append(newResources, click.WithThumbBackgrounds(newImages)) + } else { + //imgs, err := thumbs.GetThumbs() + //if err != nil { + // return nil, err + //} + //newResources = append(newResources, click.WithThumbBackgrounds(imgs)) + } + + return newResources, nil +} + +// setupClickCapt +func setupClickCapt(conf config.ClickConfig, resources config.ResourceConfig) (capt click.Captcha, err error) { + newOptions, err := genClickOptions(conf) if err != nil { return nil, err } - // background images - imgs, err := images.GetImages() + newResources, err := genClickResources(conf, resources) if err != nil { return nil, err } - // set resources - builder.SetResources( - click.WithChars(chars.GetChineseChars()), - click.WithFonts([]*truetype.Font{fonts}), - click.WithBackgrounds(imgs), - ) + // builder + builder := click.NewBuilder(newOptions...) + builder.SetResources(newResources...) return builder.Make(), nil } -func setupClickShape() (capt click.Captcha, err error) { - builder := click.NewBuilder( - click.WithRangeLen(option.RangeVal{Min: 3, Max: 6}), - click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 3}), - click.WithRangeThumbBgDistort(1), - click.WithIsThumbNonDeformAbility(true), - ) - - // shape - shapeMaps, err := shapes.GetShapes() +func setupClickShapeCapt(conf config.ClickConfig, resources config.ResourceConfig) (capt click.Captcha, err error) { + newOptions, err := genClickOptions(conf) if err != nil { return nil, err } - // background images - imgs, err := images.GetImages() + newResources, err := genClickResources(conf, resources) if err != nil { return nil, err } - // set resources - builder.SetResources( - click.WithShapes(shapeMaps), - click.WithBackgrounds(imgs), - ) + // Set Shape images resources + if len(resources.ShapeImage.FileMaps) > 0 { + var newImageMaps = make(map[string]image.Image, 0) + for name, file := range resources.ShapeImage.FileMaps { + resourcesPath := helper.GetResourceDirAbsPath() + rootDir := resources.ShapeImage.FileDir + filepath := path.Join(resourcesPath, rootDir, file) - return builder.Make(), nil + img, err := helper.LoadImageData(filepath) + if err != nil { + return nil, err + } + newImageMaps[name] = img + } + newResources = append(newResources, click.WithShapes(newImageMaps)) + } else { + imgs, err := shapes.GetShapes() + if err != nil { + return nil, err + } + newResources = append(newResources, click.WithShapes(imgs)) + } + + // builder + builder := click.NewBuilder(newOptions...) + builder.SetResources(newResources...) + + return builder.MakeWithShape(), nil } diff --git a/internal/pkg/gocaptcha/config/base.config.go b/internal/pkg/gocaptcha/config/base.config.go new file mode 100644 index 0000000..3892306 --- /dev/null +++ b/internal/pkg/gocaptcha/config/base.config.go @@ -0,0 +1,98 @@ +package config + +import ( + "github.com/wenlng/go-captcha/v2/base/option" +) + +// RangeVal . +type RangeVal struct { + Min, Max int +} + +// Size . +type Size struct { + Width, Height int +} + +// Point . +type Point struct { + X, Y int +} + +// ClickMasterOption . +type ClickMasterOption struct { + ImageSize option.Size `json:"image_size"` + RangeLength option.RangeVal `json:"range_length"` + RangeAngles []option.RangeVal `json:"range_angles"` + RangeSize option.RangeVal `json:"range_size"` + RangeColors []string `json:"range_colors"` + DisplayShadow bool `json:"display_shadow"` + ShadowColor string `json:"shadow_color"` + ShadowPoint option.Point `json:"shadow_point"` + ImageAlpha float32 `json:"image_alpha"` + UseShapeOriginalColor bool `json:"use_shape_original_color"` +} + +// ClickThumbOption . +type ClickThumbOption struct { + ImageSize option.Size `json:"image_size"` + RangeVerifyLength option.RangeVal `json:"range_verify_length"` + DisabledRangeVerifyLength bool `json:"disabled_range_verify_length"` + RangeTextSize option.RangeVal `json:"range_text_size"` + RangeTextColors []string `json:"range_text_colors"` + RangeBackgroundColors []string `json:"range_background_colors"` + BackgroundDistort int `json:"background_distort"` + BackgroundDistortAlpha float32 `json:"background_distort_alpha"` + BackgroundCirclesNum int `json:"background_circles_num"` + BackgroundSlimLineNum int `json:"background_slim_line_num"` + IsThumbNonDeformAbility bool `json:"is_thumb_non_deform_ability"` +} + +// ClickConfig . +type ClickConfig struct { + Version string `json:"version"` + Language string `json:"language"` + Master ClickMasterOption `json:"master"` + Thumb ClickThumbOption `json:"thumb"` +} + +// SlideMasterOption . +type SlideMasterOption struct { + ImageSize option.Size `json:"image_size"` + ImageAlpha float32 `json:"image_alpha"` +} + +// SlideThumbOption . +type SlideThumbOption struct { + RangeGraphSizes option.RangeVal `json:"range_graph_size"` + RangeGraphAngles []option.RangeVal `json:"range_graph_angles"` + GenerateGraphNumber int `json:"generate_graph_number"` + EnableGraphVerticalRandom bool `json:"enable_graph_vertical_random"` + RangeDeadZoneDirections []string `json:"range_dead_zone_directions"` +} + +// SlideConfig . +type SlideConfig struct { + Version string `json:"version"` + Master SlideMasterOption `json:"master"` + Thumb SlideThumbOption `json:"thumb"` +} + +// RotateMasterOption . +type RotateMasterOption struct { + ImageSquareSize int `json:"image_square_size"` +} + +// RotateThumbOption . +type RotateThumbOption struct { + RangeAngles []option.RangeVal `json:"range_angles"` + RangeImageSquareSizes []int `json:"range_image_square_sizes"` + ImageAlpha float32 `json:"image_alpha"` +} + +// RotateConfig . +type RotateConfig struct { + Version string `json:"version"` + Master RotateMasterOption `json:"master"` + Thumb RotateThumbOption `json:"thumb"` +} diff --git a/internal/pkg/gocaptcha/config/config.go b/internal/pkg/gocaptcha/config/config.go new file mode 100644 index 0000000..2d7cd0d --- /dev/null +++ b/internal/pkg/gocaptcha/config/config.go @@ -0,0 +1,273 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "path" + "path/filepath" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/wenlng/go-captcha-service/internal/helper" +) + +// BuilderConfig . +type BuilderConfig struct { + ClickConfigMaps map[string]ClickConfig `json:"click_config_maps"` + ClickShapeConfigMaps map[string]ClickConfig `json:"click_shape_config_maps"` + SlideConfigMaps map[string]SlideConfig `json:"slide_config_maps"` + DragConfigMaps map[string]SlideConfig `json:"drag_config_maps"` + RotateConfigMaps map[string]RotateConfig `json:"rotate_config_maps"` +} + +// CaptchaConfig defines the configuration structure for the gocaptcha +type CaptchaConfig struct { + Resources ResourceConfig `json:"resources"` + Builder BuilderConfig `json:"builder"` +} + +// DynamicCaptchaConfig . +type DynamicCaptchaConfig struct { + Config CaptchaConfig + mu sync.RWMutex + hotCbsHooks map[string]HandleHotCallbackHookFnc +} + +type HandleHotCallbackHookFnc = func(*DynamicCaptchaConfig) + +// NewDynamicConfig . +func NewDynamicConfig(file string) (*DynamicCaptchaConfig, error) { + cfg, err := Load(file) + if err != nil { + return nil, err + } + dc := &DynamicCaptchaConfig{Config: cfg, hotCbsHooks: make(map[string]HandleHotCallbackHookFnc)} + go dc.watchFile(file) + return dc, nil +} + +// Get retrieves the current configuration +func (dc *DynamicCaptchaConfig) Get() CaptchaConfig { + dc.mu.RLock() + defer dc.mu.RUnlock() + return dc.Config +} + +// Update updates the configuration +func (dc *DynamicCaptchaConfig) Update(cfg CaptchaConfig) error { + if err := Validate(cfg); err != nil { + return err + } + dc.mu.Lock() + defer dc.mu.Unlock() + dc.Config = cfg + return nil +} + +// RegisterHotCallbackHook callback when updating configuration +func (dc *DynamicCaptchaConfig) RegisterHotCallbackHook(key string, callback HandleHotCallbackHookFnc) { + if _, ok := dc.hotCbsHooks[key]; !ok { + dc.hotCbsHooks[key] = callback + } +} + +// UnRegisterHotCallbackHook callback when updating configuration +func (dc *DynamicCaptchaConfig) UnRegisterHotCallbackHook(key string) { + if _, ok := dc.hotCbsHooks[key]; !ok { + delete(dc.hotCbsHooks, key) + } +} + +// HandleHotCallbackHook . +func (dc *DynamicCaptchaConfig) HandleHotCallbackHook() { + for _, fnc := range dc.hotCbsHooks { + if fnc != nil { + fnc(dc) + } + } +} + +// watchFile monitors the CaptchaConfig file for changes +func (dc *DynamicCaptchaConfig) watchFile(file string) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create watcher: %v\n", err) + return + } + defer watcher.Close() + + absPath, err := filepath.Abs(file) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get absolute path: %v\n", err) + return + } + dir := filepath.Dir(absPath) + + if err := watcher.Add(dir); err != nil { + fmt.Fprintf(os.Stderr, "Failed to watch directory: %v\n", err) + return + } + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Name == absPath && (event.Op&fsnotify.Write == fsnotify.Write) { + cfg, err := Load(file) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to reload CaptchaConfig: %v\n", err) + continue + } + if err := dc.Update(cfg); err != nil { + fmt.Fprintf(os.Stderr, "Failed to update CaptchaConfig: %v\n", err) + continue + } + + // Instance update gocaptcha + dc.HandleHotCallbackHook() + + fmt.Printf("GoCaptcha Configuration reloaded successfully\n") + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + fmt.Fprintf(os.Stderr, "Watcher error: %v\n", err) + } + } +} + +// HotUpdate hot update configuration +func (dc *DynamicCaptchaConfig) HotUpdate(cfg CaptchaConfig) error { + if err := dc.Update(cfg); err != nil { + fmt.Fprintf(os.Stderr, "Failed to update CaptchaConfig: %v\n", err) + return err + } + + // Instance update gocaptcha + dc.HandleHotCallbackHook() + return nil +} + +// Load reads the configuration from a file +func Load(file string) (CaptchaConfig, error) { + var config CaptchaConfig + data, err := os.ReadFile(file) + if err != nil { + return config, fmt.Errorf("failed to read GoCaptcha config file: %v", err) + } + if err := json.Unmarshal(data, &config); err != nil { + return config, fmt.Errorf("failed to parse GoCaptcha config file: %v", err) + } + return config, nil +} + +// Validate checks the configuration for validity +func Validate(config CaptchaConfig) error { + filepathList := make([]string, 0, 0) + resourcePath := helper.GetResourceDirAbsPath() + + fontConfig := config.Resources.Font + for _, f := range fontConfig.FileMaps { + filepathList = append(filepathList, path.Join(resourcePath, fontConfig.FileDir, f)) + } + + shapeImageConfig := config.Resources.ShapeImage + for _, f := range shapeImageConfig.FileMaps { + filepathList = append(filepathList, path.Join(resourcePath, shapeImageConfig.FileDir, f)) + } + + MasterImageConfig := config.Resources.MasterImage + for _, f := range MasterImageConfig.FileMaps { + filepathList = append(filepathList, path.Join(resourcePath, MasterImageConfig.FileDir, f)) + } + + ThumbImageConfig := config.Resources.ThumbImage + for _, f := range ThumbImageConfig.FileMaps { + filepathList = append(filepathList, path.Join(resourcePath, ThumbImageConfig.FileDir, f)) + } + + TileImageConfig := config.Resources.TileImage + for _, f := range TileImageConfig.FileMaps { + filepathList = append(filepathList, path.Join(resourcePath, TileImageConfig.FileDir, f)) + } + for _, f := range TileImageConfig.FileMaps02 { + filepathList = append(filepathList, path.Join(resourcePath, TileImageConfig.FileDir, f)) + } + for _, f := range TileImageConfig.FileMaps03 { + filepathList = append(filepathList, path.Join(resourcePath, TileImageConfig.FileDir, f)) + } + + if err := isValidFileExist(filepathList); err != nil { + return err + } + + return nil +} + +// isValidFileExist checks if the file is existed +func isValidFileExist(filePaths []string) error { + for _, filePath := range filePaths { + if ok := helper.FileExists(filePath); !ok { + return fmt.Errorf("file not exist: %s", filePath) + } else if ok = helper.IsFile(filePath); !ok { + return fmt.Errorf("not file type: %s", filePath) + } + } + return nil +} + +// DefaultConfig . +func DefaultConfig() CaptchaConfig { + return CaptchaConfig{ + Resources: ResourceConfig{ + Char: ResourceChar{}, + Font: ResourceFileConfig{}, + ShapeImage: ResourceFileConfig{}, + MasterImage: ResourceFileConfig{}, + ThumbImage: ResourceFileConfig{}, + TileImage: ResourceMultiFileConfig{}, + }, + Builder: BuilderConfig{ + ClickConfigMaps: map[string]ClickConfig{ + "click_default_ch": { + Language: "chinese", + Master: ClickMasterOption{}, + Thumb: ClickThumbOption{}, + }, + "click_dark_ch": { + Language: "chinese", + Master: ClickMasterOption{}, + Thumb: ClickThumbOption{}, + }, + "click_default_en": { + Language: "english", + Master: ClickMasterOption{}, + Thumb: ClickThumbOption{}, + }, + "click_dark_en": { + Language: "english", + Master: ClickMasterOption{}, + Thumb: ClickThumbOption{}, + }, + "click_shape_light_default": {}, + "click_shape_dark_default": {}, + }, + ClickShapeConfigMaps: map[string]ClickConfig{ + "click_shape_default": {}, + }, + SlideConfigMaps: map[string]SlideConfig{ + "slide_default": {}, + }, + DragConfigMaps: map[string]SlideConfig{ + "drag_default": {}, + }, + RotateConfigMaps: map[string]RotateConfig{ + "rotate_default": {}, + }, + }, + } +} diff --git a/internal/pkg/gocaptcha/config/config_test.go b/internal/pkg/gocaptcha/config/config_test.go new file mode 100644 index 0000000..b8a842a --- /dev/null +++ b/internal/pkg/gocaptcha/config/config_test.go @@ -0,0 +1,316 @@ +package config + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoad(t *testing.T) { + configContent := ` +{ + "dir": "./resources", + "resources": { + "char": { + "type": "chinese", + "text": [] + }, + "font": { + "type": "load", + "file_dir": "./fonts/", + "file_maps": { + "aa": "aa.ttf", + "bb": "bb.ttf" + } + }, + "shape_image": { + "type": "load", + "file_dir": "./shape_images/", + "file_maps": { + "aa": "aa.png", + "bb":"bb.png" + } + }, + "master_image": { + "type": "load", + "file_dir": "./master_images/", + "file_maps": { + "aa": "aa.png", + "bb": "bb.png" + } + }, + "thumb_image": { + "type": "load", + "file_dir": "./thumb_images/", + "file_maps": { + "aa": "aa.png", + "bb": "bb.png" + } + }, + "tile_image": { + "type": "load", + "file_dir": "./tile_images/", + "file_maps_01": { + "overlay_image_01": "overlay_image_01.png", + "overlay_image_02": "overlay_image_02.png" + }, + "file_maps_02": { + "shadow_image_01": "shadow_image_01.png", + "shadow_image_02": "shadow_image_02.png" + }, + "file_maps_03": { + "mask_image_01": "mask_image_01.png" + } + } + }, + "builder": { + "click_config_maps": { + "default_cn_click": { + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "dark_cn_click": { + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + } + }, + "slide_config_maps": { + "default_cn_slide": { + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { + "min": 20, + "max": 100 + }, + "range_graph_angles": [ + { + "min": 20, + "max": 100 + } + ], + "generate_graph_number": 1, + "enable_graph_vertical_random": false, + "range_dead_zone_directions": ["left", "right"] + } + } + }, + "rotate_config_maps": { + "default_cn_rotate": { + "master": { + "image_square_size": 200 + }, + "thumb": { + "range_angles": [ + { + "min": 20, + "max": 100 + } + ], + "range_image_square_sizes": [140, 150, 170], + "image_alpha": 1 + } + } + } + } +}` + tmpFile, err := os.CreateTemp("", "CaptchaConfig.json") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + _, err = tmpFile.WriteString(configContent) + assert.NoError(t, err) + tmpFile.Close() + + config, err := Load(tmpFile.Name()) + assert.NoError(t, err) + + err = Validate(config) + assert.NoError(t, err) + + fmt.Println(config) +} diff --git a/internal/pkg/gocaptcha/config/resrouce.config.go b/internal/pkg/gocaptcha/config/resrouce.config.go new file mode 100644 index 0000000..95ff902 --- /dev/null +++ b/internal/pkg/gocaptcha/config/resrouce.config.go @@ -0,0 +1,34 @@ +package config + +// ResourceChar . +type ResourceChar struct { + Type string `json:"type"` + Languages map[string][]string `json:"languages"` +} + +// ResourceFileConfig . +type ResourceFileConfig struct { + Type string `json:"type"` + FileDir string `json:"file_dir"` + FileMaps map[string]string `json:"file_maps"` +} + +// ResourceMultiFileConfig . +type ResourceMultiFileConfig struct { + Type string `json:"type"` + FileDir string `json:"file_dir"` + FileMaps map[string]string `json:"file_maps"` + FileMaps02 map[string]string `json:"file_maps_02"` + FileMaps03 map[string]string `json:"file_maps_03"` +} + +// ResourceConfig defines the configuration structure for the gocaptcha resource +type ResourceConfig struct { + Version string `json:"version"` + Char ResourceChar `json:"char"` + Font ResourceFileConfig `json:"font"` + ShapeImage ResourceFileConfig `json:"shapes_image"` + MasterImage ResourceFileConfig `json:"master_image"` + ThumbImage ResourceFileConfig `json:"thumb_image"` + TileImage ResourceMultiFileConfig `json:"tile_image"` +} diff --git a/internal/pkg/gocaptcha/gocaptcha.go b/internal/pkg/gocaptcha/gocaptcha.go index 41fe8b2..0d69f90 100644 --- a/internal/pkg/gocaptcha/gocaptcha.go +++ b/internal/pkg/gocaptcha/gocaptcha.go @@ -1,51 +1,252 @@ package gocaptcha import ( - "github.com/wenlng/go-captcha/v2/click" - "github.com/wenlng/go-captcha/v2/rotate" - "github.com/wenlng/go-captcha/v2/slide" + "sync" + + "github.com/wenlng/go-captcha-service/internal/consts" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha/config" +) + +const ( + LanguageNameChinese = "chinese" + LanguageNameEnglish = "english" ) +// GoCaptcha . type GoCaptcha struct { - ClickCaptInstance click.Captcha - ClickShapeCaptInstance click.Captcha - SlideCaptInstance slide.Captcha - DragCaptInstance slide.Captcha - RotateCaptInstance rotate.Captcha + DynamicCnf *config.DynamicCaptchaConfig + + clickInstanceMaps map[string]*ClickCaptInstance + clickShapeInstanceMaps map[string]*ClickCaptInstance + slideInstanceMaps map[string]*SlideCaptInstance + dragInstanceMaps map[string]*SlideCaptInstance + rotateInstanceMaps map[string]*RotateCaptInstance + keyMaps map[string]int + + clickInstanceMutex sync.RWMutex + clickShapeInstanceMutex sync.RWMutex + slideInstanceMutex sync.RWMutex + dragInstanceMutex sync.RWMutex + rotateInstanceMutex sync.RWMutex +} + +// newGoCaptcha . +func newGoCaptcha() *GoCaptcha { + return &GoCaptcha{ + clickInstanceMaps: make(map[string]*ClickCaptInstance, 0), + clickShapeInstanceMaps: make(map[string]*ClickCaptInstance, 0), + slideInstanceMaps: make(map[string]*SlideCaptInstance, 0), + dragInstanceMaps: make(map[string]*SlideCaptInstance, 0), + rotateInstanceMaps: make(map[string]*RotateCaptInstance, 0), + keyMaps: make(map[string]int), + } +} + +// GetCaptTypeWithKey . +func (gc *GoCaptcha) GetCaptTypeWithKey(key string) int { + t, ok := gc.keyMaps[key] + if ok { + return t + } + + return consts.GoCaptchaTypeUnknown +} + +// GetClickInstanceWithKey . +func (gc *GoCaptcha) GetClickInstanceWithKey(key string) *ClickCaptInstance { + gc.clickInstanceMutex.RLock() + defer gc.clickInstanceMutex.RUnlock() + return gc.clickInstanceMaps[key] +} + +// GetClickShapeInstanceWithKey . +func (gc *GoCaptcha) GetClickShapeInstanceWithKey(key string) *ClickCaptInstance { + gc.clickInstanceMutex.RLock() + defer gc.clickInstanceMutex.RUnlock() + return gc.clickShapeInstanceMaps[key] +} + +// GetSlideInstanceWithKey . +func (gc *GoCaptcha) GetSlideInstanceWithKey(key string) *SlideCaptInstance { + gc.clickInstanceMutex.RLock() + defer gc.clickInstanceMutex.RUnlock() + return gc.slideInstanceMaps[key] +} + +// GetDragInstanceWithKey . +func (gc *GoCaptcha) GetDragInstanceWithKey(key string) *SlideCaptInstance { + gc.clickInstanceMutex.RLock() + defer gc.clickInstanceMutex.RUnlock() + return gc.dragInstanceMaps[key] +} + +// GetRotateInstanceWithKey . +func (gc *GoCaptcha) GetRotateInstanceWithKey(key string) *RotateCaptInstance { + gc.clickInstanceMutex.RLock() + defer gc.clickInstanceMutex.RUnlock() + return gc.rotateInstanceMaps[key] +} + +// UpdateClickInstance . +func (gc *GoCaptcha) UpdateClickInstance(configMaps map[string]config.ClickConfig, resources config.ResourceConfig) error { + gc.clickInstanceMutex.Lock() + defer gc.clickInstanceMutex.Unlock() + + for key, cnf := range configMaps { + ci, ok := gc.clickInstanceMaps[key] + + if !ok || ci.ResourcesVersion != resources.Version || ci.Version != cnf.Version { + instance, err := setupClickCapt(cnf, resources) + if err != nil { + return err + } + gc.clickInstanceMaps[key] = &ClickCaptInstance{ + ResourcesVersion: resources.Version, + Version: cnf.Version, + Instance: instance, + } + gc.keyMaps[key] = consts.GoCaptchaTypeClick + } + } + return nil +} + +// UpdateClickShapeInstance . +func (gc *GoCaptcha) UpdateClickShapeInstance(configMaps map[string]config.ClickConfig, resources config.ResourceConfig) error { + gc.clickShapeInstanceMutex.Lock() + defer gc.clickShapeInstanceMutex.Unlock() + + for key, cnf := range configMaps { + ci, ok := gc.clickShapeInstanceMaps[key] + + if !ok || ci.ResourcesVersion != resources.Version || ci.Version != cnf.Version { + instance, err := setupClickShapeCapt(cnf, resources) + if err != nil { + return err + } + gc.clickShapeInstanceMaps[key] = &ClickCaptInstance{ + ResourcesVersion: resources.Version, + Version: cnf.Version, + Instance: instance, + } + gc.keyMaps[key] = consts.GoCaptchaTypeClickShape + } + } + return nil +} + +// UpdateSlideInstance . +func (gc *GoCaptcha) UpdateSlideInstance(configMaps map[string]config.SlideConfig, resources config.ResourceConfig) error { + gc.slideInstanceMutex.Lock() + defer gc.slideInstanceMutex.Unlock() + + for key, cnf := range configMaps { + ci, ok := gc.slideInstanceMaps[key] + + if !ok || ci.ResourcesVersion != resources.Version || ci.Version != cnf.Version { + instance, err := setupSlideCapt(cnf, resources) + if err != nil { + return err + } + gc.slideInstanceMaps[key] = &SlideCaptInstance{ + ResourcesVersion: resources.Version, + Version: cnf.Version, + Instance: instance, + } + gc.keyMaps[key] = consts.GoCaptchaTypeSlide + } + } + return nil +} + +// UpdateDragInstance . +func (gc *GoCaptcha) UpdateDragInstance(configMaps map[string]config.SlideConfig, resources config.ResourceConfig) error { + gc.dragInstanceMutex.Lock() + defer gc.dragInstanceMutex.Unlock() + + for key, cnf := range configMaps { + ci, ok := gc.dragInstanceMaps[key] + + if !ok || ci.ResourcesVersion != resources.Version || ci.Version != cnf.Version { + instance, err := setupDragCapt(cnf, resources) + if err != nil { + return err + } + gc.dragInstanceMaps[key] = &SlideCaptInstance{ + ResourcesVersion: resources.Version, + Version: cnf.Version, + Instance: instance, + } + gc.keyMaps[key] = consts.GoCaptchaTypeDrag + } + } + return nil } -func Setup() (*GoCaptcha, error) { - var gc = &GoCaptcha{} +// UpdateRotateInstance . +func (gc *GoCaptcha) UpdateRotateInstance(configMaps map[string]config.RotateConfig, resources config.ResourceConfig) error { + gc.rotateInstanceMutex.Lock() + defer gc.rotateInstanceMutex.Unlock() - cc, err := setupClick() + for key, cnf := range configMaps { + ci, ok := gc.rotateInstanceMaps[key] + + if !ok || ci.ResourcesVersion != resources.Version || ci.Version != cnf.Version { + instance, err := setupRotateCapt(cnf, resources) + if err != nil { + return err + } + gc.rotateInstanceMaps[key] = &RotateCaptInstance{ + ResourcesVersion: resources.Version, + Version: cnf.Version, + Instance: instance, + } + gc.keyMaps[key] = consts.GoCaptchaTypeRotate + } + } + return nil +} + +// HotUpdate . +func (gc *GoCaptcha) HotUpdate(dyCnf *config.DynamicCaptchaConfig) error { + cnf := dyCnf.Get() + + var err error + err = gc.UpdateClickInstance(cnf.Builder.ClickConfigMaps, cnf.Resources) if err != nil { - return nil, err + return err } - gc.ClickCaptInstance = cc - ccs, err := setupClickShape() + err = gc.UpdateClickShapeInstance(cnf.Builder.ClickShapeConfigMaps, cnf.Resources) if err != nil { - return nil, err + return err } - gc.ClickShapeCaptInstance = ccs - sc, err := setupSlide() + err = gc.UpdateSlideInstance(cnf.Builder.SlideConfigMaps, cnf.Resources) if err != nil { - return nil, err + return err } - gc.SlideCaptInstance = sc - scc, err := setupDrag() + err = gc.UpdateDragInstance(cnf.Builder.DragConfigMaps, cnf.Resources) if err != nil { - return nil, err + return err } - gc.DragCaptInstance = scc - rc, err := setupRotate() + err = gc.UpdateRotateInstance(cnf.Builder.RotateConfigMaps, cnf.Resources) + if err != nil { + return err + } + + return nil +} + +// Setup initializes the captcha +func Setup(dyCnf *config.DynamicCaptchaConfig) (*GoCaptcha, error) { + gc := newGoCaptcha() + err := gc.HotUpdate(dyCnf) if err != nil { return nil, err } - gc.RotateCaptInstance = rc return gc, nil } diff --git a/internal/pkg/gocaptcha/rotate.go b/internal/pkg/gocaptcha/rotate.go index e738f0f..76dae83 100644 --- a/internal/pkg/gocaptcha/rotate.go +++ b/internal/pkg/gocaptcha/rotate.go @@ -1,28 +1,92 @@ package gocaptcha import ( - "github.com/wenlng/go-captcha-assets/resources/images_v2" - "github.com/wenlng/go-captcha/v2/base/option" + "image" + "path" + + images "github.com/wenlng/go-captcha-assets/resources/images_v2" + "github.com/wenlng/go-captcha-service/internal/helper" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha/config" "github.com/wenlng/go-captcha/v2/rotate" ) -func setupRotate() (capt rotate.Captcha, err error) { - builder := rotate.NewBuilder( - rotate.WithRangeAnglePos([]option.RangeVal{ - {Min: 20, Max: 330}, - }), - ) +// RotateCaptInstance . +type RotateCaptInstance struct { + ResourcesVersion string + Version string + Instance rotate.Captcha +} + +// genRotateOptions . +func genRotateOptions(conf config.RotateConfig) ([]rotate.Option, error) { + options := make([]rotate.Option, 0) + + // Master image + if conf.Master.ImageSquareSize != 0 { + options = append(options, rotate.WithImageSquareSize(conf.Master.ImageSquareSize)) + } + + // Thumb image + if conf.Thumb.RangeAngles != nil && len(conf.Thumb.RangeAngles) > 0 { + options = append(options, rotate.WithRangeAnglePos(conf.Thumb.RangeAngles)) + } + + if conf.Thumb.RangeImageSquareSizes != nil && len(conf.Thumb.RangeImageSquareSizes) > 0 { + options = append(options, rotate.WithRangeThumbImageSquareSize(conf.Thumb.RangeImageSquareSizes)) + } + + if conf.Thumb.ImageAlpha > 0 { + options = append(options, rotate.WithThumbImageAlpha(conf.Thumb.ImageAlpha)) + } + + return options, nil +} + +// genRotateResources. +func genRotateResources(conf config.RotateConfig, resources config.ResourceConfig) ([]rotate.Resource, error) { + newResources := make([]rotate.Resource, 0) + + // Set Background images resources + if len(resources.MasterImage.FileMaps) > 0 { + var newImages = make([]image.Image, 0) + for _, file := range resources.MasterImage.FileMaps { + resourcesPath := helper.GetResourceDirAbsPath() + rootDir := resources.MasterImage.FileDir + filepath := path.Join(resourcesPath, rootDir, file) + + img, err := helper.LoadImageData(filepath) + if err != nil { + return nil, err + } + newImages = append(newImages, img) + } + newResources = append(newResources, rotate.WithImages(newImages)) + } else { + imgs, err := images.GetImages() + if err != nil { + return nil, err + } + newResources = append(newResources, rotate.WithImages(imgs)) + } + + return newResources, nil +} + +// setupRotateCapt +func setupRotateCapt(conf config.RotateConfig, resources config.ResourceConfig) (capt rotate.Captcha, err error) { + newOptions, err := genRotateOptions(conf) + if err != nil { + return nil, err + } - // background images - imgs, err := images.GetImages() + newResources, err := genRotateResources(conf, resources) if err != nil { return nil, err } - // set resources - builder.SetResources( - rotate.WithImages(imgs), - ) + // builder + builder := rotate.NewBuilder(newOptions...) + builder.SetResources(newResources...) return builder.Make(), nil } diff --git a/internal/pkg/gocaptcha/slide.go b/internal/pkg/gocaptcha/slide.go index 9e7cfa9..7174f13 100644 --- a/internal/pkg/gocaptcha/slide.go +++ b/internal/pkg/gocaptcha/slide.go @@ -1,78 +1,221 @@ package gocaptcha import ( - "log" + "image" + "path" "github.com/wenlng/go-captcha-assets/resources/images_v2" "github.com/wenlng/go-captcha-assets/resources/tiles" + "github.com/wenlng/go-captcha-service/internal/helper" + "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha/config" + "github.com/wenlng/go-captcha/v2/base/codec" "github.com/wenlng/go-captcha/v2/slide" ) -func setupSlide() (capt slide.Captcha, err error) { - builder := slide.NewBuilder() +// SlideCaptInstance . +type SlideCaptInstance struct { + ResourcesVersion string + Version string + Instance slide.Captcha +} - // background images - imgs, err := images.GetImages() - if err != nil { - return nil, err +// genSlideOptions . +func genSlideOptions(conf config.SlideConfig) ([]slide.Option, error) { + options := make([]slide.Option, 0) + + // Master image + if conf.Master.ImageSize.Height != 0 && conf.Master.ImageSize.Width != 0 { + options = append(options, slide.WithImageSize(conf.Master.ImageSize)) } - graphs, err := tiles.GetTiles() - if err != nil { - log.Fatalln(err) + if conf.Master.ImageAlpha > 0 { + options = append(options, slide.WithImageAlpha(conf.Master.ImageAlpha)) + } + + // Thumb image + if conf.Thumb.RangeGraphSizes.Min != 0 && conf.Thumb.RangeGraphSizes.Max != 0 { + options = append(options, slide.WithRangeGraphSize(conf.Thumb.RangeGraphSizes)) } - var newGraphs = make([]*slide.GraphImage, 0, len(graphs)) - for i := 0; i < len(graphs); i++ { - graph := graphs[i] - newGraphs = append(newGraphs, &slide.GraphImage{ - OverlayImage: graph.OverlayImage, - MaskImage: graph.MaskImage, - ShadowImage: graph.ShadowImage, - }) + if conf.Thumb.RangeGraphAngles != nil && len(conf.Thumb.RangeGraphAngles) > 0 { + options = append(options, slide.WithRangeGraphAnglePos(conf.Thumb.RangeGraphAngles)) } - // set resources - builder.SetResources( - slide.WithGraphImages(newGraphs), - slide.WithBackgrounds(imgs), - ) + if conf.Thumb.GenerateGraphNumber > 0 { + options = append(options, slide.WithGenGraphNumber(conf.Thumb.GenerateGraphNumber)) + } - return builder.Make(), nil + if conf.Thumb.EnableGraphVerticalRandom != false { + options = append(options, slide.WithEnableGraphVerticalRandom(conf.Thumb.EnableGraphVerticalRandom)) + } + + if conf.Thumb.RangeDeadZoneDirections != nil && len(conf.Thumb.RangeDeadZoneDirections) > 0 { + var list = make([]slide.DeadZoneDirectionType, 0, len(conf.Thumb.RangeDeadZoneDirections)) + + for _, direction := range conf.Thumb.RangeDeadZoneDirections { + if direction == "left" { + list = append(list, slide.DeadZoneDirectionTypeLeft) + } else if direction == "right" { + list = append(list, slide.DeadZoneDirectionTypeRight) + } else if direction == "top" { + list = append(list, slide.DeadZoneDirectionTypeTop) + } else if direction == "bottom" { + list = append(list, slide.DeadZoneDirectionTypeBottom) + } + } + + options = append(options, slide.WithRangeDeadZoneDirections(list)) + } + + return options, nil } -func setupDrag() (capt slide.Captcha, err error) { - builder := slide.NewBuilder( - slide.WithGenGraphNumber(2), - slide.WithEnableGraphVerticalRandom(true), - ) +// genSlideResources. +func genSlideResources(conf config.SlideConfig, resources config.ResourceConfig) ([]slide.Resource, error) { + newResources := make([]slide.Resource, 0) + + // Set Background images resources + if len(resources.MasterImage.FileMaps) > 0 { + var newImages = make([]image.Image, 0) + for _, file := range resources.MasterImage.FileMaps { + resourcesPath := helper.GetResourceDirAbsPath() + rootDir := resources.MasterImage.FileDir + filepath := path.Join(resourcesPath, rootDir, file) + stream, err := helper.ReadFileStream(filepath) + if err != nil { + return nil, err + } + + if path.Ext(file) == ".png" { + png, err := codec.DecodeByteToPng(stream) + if err != nil { + return nil, err + } + newImages = append(newImages, png) + } else { + jpeg, err := codec.DecodeByteToJpeg(stream) + if err != nil { + return nil, err + } + newImages = append(newImages, jpeg) + } + } + newResources = append(newResources, slide.WithBackgrounds(newImages)) + } else { + imgs, err := images.GetImages() + if err != nil { + return nil, err + } + newResources = append(newResources, slide.WithBackgrounds(imgs)) + } + + // Set Tile images resources + var isSetGraphsDonne bool + if len(resources.TileImage.FileMaps) > 0 { + var newGraphs = make([]*slide.GraphImage, 0, len(resources.TileImage.FileMaps)) + for name, file := range resources.TileImage.FileMaps { + resourcesPath := helper.GetResourceDirAbsPath() + rootDir := resources.TileImage.FileDir + overlayImageFilepath := path.Join(resourcesPath, rootDir, file) + + shadowImageFilepath, ok := resources.TileImage.FileMaps02[name] + if !ok { + break + } + shadowImageFilepath = path.Join(resourcesPath, rootDir, shadowImageFilepath) + + maskImageFilepath, ok := resources.TileImage.FileMaps03[name] + if !ok { + break + } + maskImageFilepath = path.Join(resourcesPath, rootDir, maskImageFilepath) + + graph := &slide.GraphImage{} + + // OverlayImage + overlayImage, err := helper.LoadImageData(overlayImageFilepath) + if err != nil { + return nil, err + } + graph.OverlayImage = overlayImage - // background images - imgs, err := images.GetImages() + // ShadowImage + shadowImage, err := helper.LoadImageData(shadowImageFilepath) + if err != nil { + return nil, err + } + graph.ShadowImage = shadowImage + + // MaskImage + maskImage, err := helper.LoadImageData(maskImageFilepath) + if err != nil { + return nil, err + } + graph.MaskImage = maskImage + + newGraphs = append(newGraphs, graph) + } + + if len(newGraphs) > 0 { + isSetGraphsDonne = true + newResources = append(newResources, slide.WithGraphImages(newGraphs)) + } + } + + if !isSetGraphsDonne { + graphs, err := tiles.GetTiles() + if err != nil { + return nil, err + } + + var newGraphs = make([]*slide.GraphImage, 0, len(graphs)) + for i := 0; i < len(graphs); i++ { + graph := graphs[i] + newGraphs = append(newGraphs, &slide.GraphImage{ + OverlayImage: graph.OverlayImage, + MaskImage: graph.MaskImage, + ShadowImage: graph.ShadowImage, + }) + } + newResources = append(newResources, slide.WithGraphImages(newGraphs)) + } + + return newResources, nil +} + +// setupSlideCapt +func setupSlideCapt(conf config.SlideConfig, resources config.ResourceConfig) (capt slide.Captcha, err error) { + newOptions, err := genSlideOptions(conf) + if err != nil { + return nil, err + } + + newResources, err := genSlideResources(conf, resources) if err != nil { return nil, err } - graphs, err := tiles.GetTiles() + // builder + builder := slide.NewBuilder(newOptions...) + builder.SetResources(newResources...) + + return builder.Make(), nil +} + +func setupDragCapt(conf config.SlideConfig, resources config.ResourceConfig) (capt slide.Captcha, err error) { + newOptions, err := genSlideOptions(conf) if err != nil { - log.Fatalln(err) + return nil, err } - var newGraphs = make([]*slide.GraphImage, 0, len(graphs)) - for i := 0; i < len(graphs); i++ { - graph := graphs[i] - newGraphs = append(newGraphs, &slide.GraphImage{ - OverlayImage: graph.OverlayImage, - MaskImage: graph.MaskImage, - ShadowImage: graph.ShadowImage, - }) + newResources, err := genSlideResources(conf, resources) + if err != nil { + return nil, err } - // set resources - builder.SetResources( - slide.WithGraphImages(newGraphs), - slide.WithBackgrounds(imgs), - ) + // builder + builder := slide.NewBuilder(newOptions...) + builder.SetResources(newResources...) - return builder.Make(), nil + return builder.MakeWithRegion(), nil } diff --git a/internal/server/grpc_server.go b/internal/server/grpc_server.go index e618a8c..f7316c5 100644 --- a/internal/server/grpc_server.go +++ b/internal/server/grpc_server.go @@ -9,6 +9,7 @@ import ( "github.com/wenlng/go-captcha-service/internal/adapt" "github.com/wenlng/go-captcha-service/internal/common" "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/consts" "github.com/wenlng/go-captcha-service/internal/logic" "github.com/wenlng/go-captcha-service/proto" "go.uber.org/zap" @@ -16,9 +17,10 @@ import ( // GrpcServer implements the gRPC cache service type GrpcServer struct { + svcCtx *common.SvcContext proto.UnimplementedGoCaptchaServiceServer - config *config.Config - logger *zap.Logger + dynamicCfg *config.DynamicConfig + logger *zap.Logger // Initialize logic clickCaptLogic *logic.ClickCaptLogic @@ -30,7 +32,8 @@ type GrpcServer struct { // NewGoCaptchaServer creates a new gRPC cache server func NewGoCaptchaServer(svcCtx *common.SvcContext) *GrpcServer { return &GrpcServer{ - config: svcCtx.Config, + svcCtx: svcCtx, + dynamicCfg: svcCtx.DynamicConfig, logger: svcCtx.Logger, clickCaptLogic: logic.NewClickCaptLogic(svcCtx), slideCaptLogic: logic.NewSlideCaptLogic(svcCtx), @@ -46,25 +49,26 @@ func (s *GrpcServer) GetData(ctx context.Context, req *proto.GetDataRequest) (*p var data = &adapt.CaptData{} - ctype := int(req.GetType()) - theme := int(req.GetTheme()) - lang := int(req.GetLang()) + id := req.GetId() + if id == "" { + return &proto.GetDataResponse{Code: 0, Message: "missing id parameter"}, nil + } - switch req.GetType() { - case proto.GoCaptchaType_GoCaptchaTypeClick: - data, err = s.clickCaptLogic.GetData(ctx, ctype, theme, lang) + switch s.svcCtx.Captcha.GetCaptTypeWithKey(id) { + case consts.GoCaptchaTypeClick: + data, err = s.clickCaptLogic.GetData(ctx, id) break - case proto.GoCaptchaType_GoCaptchaTypeClickShape: - data, err = s.clickCaptLogic.GetData(ctx, ctype, theme, lang) + case consts.GoCaptchaTypeClickShape: + data, err = s.clickCaptLogic.GetData(ctx, id) break - case proto.GoCaptchaType_GoCaptchaTypeSlide: - data, err = s.slideCaptLogic.GetData(ctx, ctype, theme, lang) + case consts.GoCaptchaTypeSlide: + data, err = s.slideCaptLogic.GetData(ctx, id) break - case proto.GoCaptchaType_GoCaptchaTypeDrag: - data, err = s.slideCaptLogic.GetData(ctx, ctype, theme, lang) + case consts.GoCaptchaTypeDrag: + data, err = s.slideCaptLogic.GetData(ctx, id) break - case proto.GoCaptchaType_GoCaptchaTypeRotate: - data, err = s.rotateCaptLogic.GetData(ctx, ctype, theme, lang) + case consts.GoCaptchaTypeRotate: + data, err = s.rotateCaptLogic.GetData(ctx, id) break default: // @@ -72,10 +76,10 @@ func (s *GrpcServer) GetData(ctx context.Context, req *proto.GetDataRequest) (*p if err != nil || data == nil { s.logger.Error("failed to get captcha data, err: ", zap.Error(err)) - return &proto.GetDataResponse{Code: 0, Message: "failed to get captcha data"}, nil + return &proto.GetDataResponse{Code: 0, Message: "captcha type not found"}, nil } - resp.Type = req.GetType() + resp.Id = req.GetId() return resp, nil } @@ -87,22 +91,27 @@ func (s *GrpcServer) CheckData(ctx context.Context, req *proto.CheckDataRequest) return &proto.CheckDataResponse{Code: 1, Message: "captchaKey and value are required"}, nil } + id := req.GetId() + if id == "" { + return &proto.CheckDataResponse{Code: 0, Message: "missing id parameter"}, nil + } + var err error var ok bool - switch req.GetType() { - case proto.GoCaptchaType_GoCaptchaTypeClick: + switch s.svcCtx.Captcha.GetCaptTypeWithKey(id) { + case consts.GoCaptchaTypeClick: ok, err = s.clickCaptLogic.CheckData(ctx, req.GetCaptchaKey(), req.GetValue()) break - case proto.GoCaptchaType_GoCaptchaTypeClickShape: + case consts.GoCaptchaTypeClickShape: ok, err = s.clickCaptLogic.CheckData(ctx, req.GetCaptchaKey(), req.GetValue()) break - case proto.GoCaptchaType_GoCaptchaTypeSlide: + case consts.GoCaptchaTypeSlide: ok, err = s.slideCaptLogic.CheckData(ctx, req.GetCaptchaKey(), req.GetValue()) break - case proto.GoCaptchaType_GoCaptchaTypeDrag: + case consts.GoCaptchaTypeDrag: ok, err = s.slideCaptLogic.CheckData(ctx, req.GetCaptchaKey(), req.GetValue()) break - case proto.GoCaptchaType_GoCaptchaTypeRotate: + case consts.GoCaptchaTypeRotate: var angle int64 angle, err = strconv.ParseInt(req.GetValue(), 10, 64) if err == nil { diff --git a/internal/server/http_handler.go b/internal/server/http_handler.go index 43d7d46..0c6d4b4 100644 --- a/internal/server/http_handler.go +++ b/internal/server/http_handler.go @@ -8,33 +8,39 @@ import ( "github.com/wenlng/go-captcha-service/internal/adapt" "github.com/wenlng/go-captcha-service/internal/common" "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/consts" "github.com/wenlng/go-captcha-service/internal/helper" "github.com/wenlng/go-captcha-service/internal/logic" "github.com/wenlng/go-captcha-service/internal/middleware" + config2 "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha/config" "go.uber.org/zap" ) // HTTPHandlers manages HTTP request handlers type HTTPHandlers struct { - config *config.Config - logger *zap.Logger + svcCtx *common.SvcContext + dynamicCfg *config.DynamicConfig + logger *zap.Logger // Initialize logic clickCaptLogic *logic.ClickCaptLogic slideCaptLogic *logic.SlideCaptLogic rotateCaptLogic *logic.RotateCaptLogic commonLogic *logic.CommonLogic + resourceLogic *logic.ResourceLogic } // NewHTTPHandlers creates a new HTTP handlers instance func NewHTTPHandlers(svcCtx *common.SvcContext) *HTTPHandlers { return &HTTPHandlers{ - config: svcCtx.Config, + svcCtx: svcCtx, + dynamicCfg: svcCtx.DynamicConfig, logger: svcCtx.Logger, clickCaptLogic: logic.NewClickCaptLogic(svcCtx), slideCaptLogic: logic.NewSlideCaptLogic(svcCtx), rotateCaptLogic: logic.NewRotateCaptLogic(svcCtx), commonLogic: logic.NewCommonLogic(svcCtx), + resourceLogic: logic.NewResourceLogic(svcCtx), } } @@ -50,49 +56,29 @@ func (h *HTTPHandlers) GetDataHandler(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() - typeStr := query.Get("type") - ctype, err := strconv.Atoi(typeStr) - if err != nil { - middleware.WriteError(w, http.StatusBadRequest, "missing type parameter") + id := query.Get("id") + if id == "" { + middleware.WriteError(w, http.StatusBadRequest, "missing id parameter") return } - themeStr := query.Get("theme") - var theme int - if themeStr != "" { - theme, err = strconv.Atoi(themeStr) - if err != nil { - middleware.WriteError(w, http.StatusBadRequest, "missing theme parameter") - return - } - } - - langStr := query.Get("lang") - var lang int - if langStr != "" { - lang, err = strconv.Atoi(langStr) - if err != nil { - middleware.WriteError(w, http.StatusBadRequest, "missing lang parameter") - return - } - } - var data *adapt.CaptData - switch ctype { - case common.GoCaptchaTypeClick: - data, err = h.clickCaptLogic.GetData(r.Context(), ctype, theme, lang) + var err error + switch h.svcCtx.Captcha.GetCaptTypeWithKey(id) { + case consts.GoCaptchaTypeClick: + data, err = h.clickCaptLogic.GetData(r.Context(), id) break - case common.GoCaptchaTypeClickShape: - data, err = h.clickCaptLogic.GetData(r.Context(), ctype, theme, lang) + case consts.GoCaptchaTypeClickShape: + data, err = h.clickCaptLogic.GetData(r.Context(), id) break - case common.GoCaptchaTypeSlide: - data, err = h.slideCaptLogic.GetData(r.Context(), ctype, theme, lang) + case consts.GoCaptchaTypeSlide: + data, err = h.slideCaptLogic.GetData(r.Context(), id) break - case common.GoCaptchaTypeDrag: - data, err = h.slideCaptLogic.GetData(r.Context(), ctype, theme, lang) + case consts.GoCaptchaTypeDrag: + data, err = h.slideCaptLogic.GetData(r.Context(), id) break - case common.GoCaptchaTypeRotate: - data, err = h.rotateCaptLogic.GetData(r.Context(), ctype, theme, lang) + case consts.GoCaptchaTypeRotate: + data, err = h.rotateCaptLogic.GetData(r.Context(), id) break default: //... @@ -100,13 +86,13 @@ func (h *HTTPHandlers) GetDataHandler(w http.ResponseWriter, r *http.Request) { if err != nil || data == nil { h.logger.Error("failed to get captcha data, err: ", zap.Error(err)) - middleware.WriteError(w, http.StatusNotFound, "v") + middleware.WriteError(w, http.StatusNotFound, "captcha type not found") return } resp.Code = http.StatusOK resp.Message = "success" - resp.Type = int32(ctype) + resp.Id = id resp.CaptchaKey = data.CaptchaKey resp.MasterImageBase64 = data.MasterImageBase64 @@ -133,7 +119,7 @@ func (h *HTTPHandlers) CheckDataHandler(w http.ResponseWriter, r *http.Request) } var req struct { - Type int32 `json:"type"` + Id string `json:"id"` CaptchaKey string `json:"captchaKey"` Value string `json:"value"` } @@ -146,22 +132,28 @@ func (h *HTTPHandlers) CheckDataHandler(w http.ResponseWriter, r *http.Request) return } - var err error + if req.Id == "" { + middleware.WriteError(w, http.StatusBadRequest, "missing id parameter") + return + } + var ok bool - switch req.Type { - case common.GoCaptchaTypeClick: + var err error + + switch h.svcCtx.Captcha.GetCaptTypeWithKey(req.Id) { + case consts.GoCaptchaTypeClick: ok, err = h.clickCaptLogic.CheckData(r.Context(), req.CaptchaKey, req.Value) break - case common.GoCaptchaTypeClickShape: + case consts.GoCaptchaTypeClickShape: ok, err = h.clickCaptLogic.CheckData(r.Context(), req.CaptchaKey, req.Value) break - case common.GoCaptchaTypeSlide: + case consts.GoCaptchaTypeSlide: ok, err = h.slideCaptLogic.CheckData(r.Context(), req.CaptchaKey, req.Value) break - case common.GoCaptchaTypeDrag: + case consts.GoCaptchaTypeDrag: ok, err = h.slideCaptLogic.CheckData(r.Context(), req.CaptchaKey, req.Value) break - case common.GoCaptchaTypeRotate: + case consts.GoCaptchaTypeRotate: var angle int64 angle, err = strconv.ParseInt(req.Value, 10, 64) if err == nil { @@ -246,7 +238,10 @@ func (h *HTTPHandlers) GetStatusInfoHandler(w http.ResponseWriter, r *http.Reque resp.Code = http.StatusOK if data != nil { - resp.Data = data + resp.Data = &adapt.CaptStatusInfo{ + Info: data.Data, + Status: data.Status, + } } json.NewEncoder(w).Encode(helper.Marshal(resp)) @@ -275,9 +270,170 @@ func (h *HTTPHandlers) DelStatusInfoHandler(w http.ResponseWriter, r *http.Reque return } + if ret { + resp.Data = "ok" + } else { + resp.Data = "no-ops" + } + + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} + +// UploadResourceHandler . +func (h *HTTPHandlers) UploadResourceHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: "success"} + + if r.Method != http.MethodPost { + middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + dirname := r.FormValue("dirname") + if dirname == "" { + middleware.WriteError(w, http.StatusBadRequest, "dirname is required") + return + } + + if !helper.IsValidDirName(dirname) { + middleware.WriteError(w, http.StatusBadRequest, "invalid directory name") + return + } + + maxUploadSize := int64(10 << 20) // 10MB + r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize) + + // Parse multipart/form-data + if err := r.ParseMultipartForm(maxUploadSize); err != nil { + h.logger.Error("Failed to parse form: %v ", zap.Error(err)) + middleware.WriteError(w, http.StatusBadRequest, "parse form fail") + return + } + + files := r.MultipartForm.File["files"] + if len(files) == 0 { + middleware.WriteError(w, http.StatusBadRequest, "no files uploaded") + return + } + + ret, allDone, err := h.resourceLogic.SaveResource(r.Context(), dirname, files) + if !ret && err != nil { + h.logger.Error("Failed to save resource, err: ", zap.Error(err)) + middleware.WriteError(w, http.StatusBadRequest, "save resource fail") + return + } + if ret { resp.Data = "ok" } + if !allDone { + resp.Message = "some files failed to be uploaded. check if they already exist" + } + + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} + +// GetResourceListHandler . +func (h *HTTPHandlers) GetResourceListHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: "success"} + if r.Method != http.MethodGet { + middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + query := r.URL.Query() + resourcePath := query.Get("path") + if resourcePath == "" { + middleware.WriteError(w, http.StatusBadRequest, "path is required") + return + } + + fileList, err := h.resourceLogic.GetResourceList(r.Context(), resourcePath) + if err != nil { + h.logger.Error("failed to get resource, err: ", zap.Error(err)) + middleware.WriteError(w, http.StatusBadRequest, "get resource fail") + return + } + + if fileList != nil { + resp.Data = fileList + } + + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} + +// DeleteResourceHandler . +func (h *HTTPHandlers) DeleteResourceHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: "success"} + if r.Method != http.MethodDelete { + middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + query := r.URL.Query() + resourcePath := query.Get("path") + if resourcePath == "" { + middleware.WriteError(w, http.StatusBadRequest, "path is required") + return + } + + ret, err := h.resourceLogic.DelResource(r.Context(), resourcePath) + if err != nil { + h.logger.Error("failed to delete resource, err: ", zap.Error(err)) + middleware.WriteError(w, http.StatusBadRequest, "delete resource fail") + return + } + + if ret { + resp.Data = "ok" + } else { + resp.Data = "no-ops" + } + + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} + +// GetGoCaptchaConfigHandler . +func (h *HTTPHandlers) GetGoCaptchaConfigHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: "success"} + if r.Method != http.MethodGet { + middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + resp.Data = h.svcCtx.Captcha.DynamicCnf.Get() + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} + +// UpdateHotGoCaptchaConfigHandler . +func (h *HTTPHandlers) UpdateHotGoCaptchaConfigHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: ""} + + if r.Method != http.MethodPost { + middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + var conf config2.CaptchaConfig + if err := json.NewDecoder(r.Body).Decode(&conf); err != nil { + middleware.WriteError(w, http.StatusBadRequest, "invalid request body") + return + } + + err := h.svcCtx.Captcha.DynamicCnf.HotUpdate(conf) + if err != nil { + h.logger.Error("failed to hot update config, err: ", zap.Error(err)) + middleware.WriteError(w, http.StatusBadRequest, "hot update config fail") + return + } + + resp.Data = "ok" + resp.Code = http.StatusOK + json.NewEncoder(w).Encode(helper.Marshal(resp)) } diff --git a/proto/api.pb.go b/proto/api.pb.go index 3454d45..315ee55 100644 --- a/proto/api.pb.go +++ b/proto/api.pb.go @@ -20,164 +20,12 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// Type -type GoCaptchaType int32 - -const ( - GoCaptchaType_GoCaptchaTypeClick GoCaptchaType = 0 - GoCaptchaType_GoCaptchaTypeClickShape GoCaptchaType = 1 - GoCaptchaType_GoCaptchaTypeSlide GoCaptchaType = 2 - GoCaptchaType_GoCaptchaTypeDrag GoCaptchaType = 3 - GoCaptchaType_GoCaptchaTypeRotate GoCaptchaType = 4 -) - -// Enum value maps for GoCaptchaType. -var ( - GoCaptchaType_name = map[int32]string{ - 0: "GoCaptchaTypeClick", - 1: "GoCaptchaTypeClickShape", - 2: "GoCaptchaTypeSlide", - 3: "GoCaptchaTypeDrag", - 4: "GoCaptchaTypeRotate", - } - GoCaptchaType_value = map[string]int32{ - "GoCaptchaTypeClick": 0, - "GoCaptchaTypeClickShape": 1, - "GoCaptchaTypeSlide": 2, - "GoCaptchaTypeDrag": 3, - "GoCaptchaTypeRotate": 4, - } -) - -func (x GoCaptchaType) Enum() *GoCaptchaType { - p := new(GoCaptchaType) - *p = x - return p -} - -func (x GoCaptchaType) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (GoCaptchaType) Descriptor() protoreflect.EnumDescriptor { - return file_proto_api_proto_enumTypes[0].Descriptor() -} - -func (GoCaptchaType) Type() protoreflect.EnumType { - return &file_proto_api_proto_enumTypes[0] -} - -func (x GoCaptchaType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use GoCaptchaType.Descriptor instead. -func (GoCaptchaType) EnumDescriptor() ([]byte, []int) { - return file_proto_api_proto_rawDescGZIP(), []int{0} -} - -// Theme -type GoCaptchaTheme int32 - -const ( - GoCaptchaTheme_GoCaptchaThemeDefault GoCaptchaTheme = 0 - GoCaptchaTheme_GoCaptchaThemeDark GoCaptchaTheme = 1 -) - -// Enum value maps for GoCaptchaTheme. -var ( - GoCaptchaTheme_name = map[int32]string{ - 0: "GoCaptchaThemeDefault", - 1: "GoCaptchaThemeDark", - } - GoCaptchaTheme_value = map[string]int32{ - "GoCaptchaThemeDefault": 0, - "GoCaptchaThemeDark": 1, - } -) - -func (x GoCaptchaTheme) Enum() *GoCaptchaTheme { - p := new(GoCaptchaTheme) - *p = x - return p -} - -func (x GoCaptchaTheme) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (GoCaptchaTheme) Descriptor() protoreflect.EnumDescriptor { - return file_proto_api_proto_enumTypes[1].Descriptor() -} - -func (GoCaptchaTheme) Type() protoreflect.EnumType { - return &file_proto_api_proto_enumTypes[1] -} - -func (x GoCaptchaTheme) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use GoCaptchaTheme.Descriptor instead. -func (GoCaptchaTheme) EnumDescriptor() ([]byte, []int) { - return file_proto_api_proto_rawDescGZIP(), []int{1} -} - -// Lang -type GoCaptchaLang int32 - -const ( - GoCaptchaLang_GoCaptchaLangDefault GoCaptchaLang = 0 - GoCaptchaLang_GoCaptchaLangEnglish GoCaptchaLang = 1 -) - -// Enum value maps for GoCaptchaLang. -var ( - GoCaptchaLang_name = map[int32]string{ - 0: "GoCaptchaLangDefault", - 1: "GoCaptchaLangEnglish", - } - GoCaptchaLang_value = map[string]int32{ - "GoCaptchaLangDefault": 0, - "GoCaptchaLangEnglish": 1, - } -) - -func (x GoCaptchaLang) Enum() *GoCaptchaLang { - p := new(GoCaptchaLang) - *p = x - return p -} - -func (x GoCaptchaLang) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (GoCaptchaLang) Descriptor() protoreflect.EnumDescriptor { - return file_proto_api_proto_enumTypes[2].Descriptor() -} - -func (GoCaptchaLang) Type() protoreflect.EnumType { - return &file_proto_api_proto_enumTypes[2] -} - -func (x GoCaptchaLang) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use GoCaptchaLang.Descriptor instead. -func (GoCaptchaLang) EnumDescriptor() ([]byte, []int) { - return file_proto_api_proto_rawDescGZIP(), []int{2} -} - type GetDataRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Type GoCaptchaType `protobuf:"varint,1,opt,name=type,proto3,enum=gocaptcha.GoCaptchaType" json:"type,omitempty"` - Theme *GoCaptchaTheme `protobuf:"varint,2,opt,name=theme,proto3,enum=gocaptcha.GoCaptchaTheme,oneof" json:"theme,omitempty"` - Lang *GoCaptchaLang `protobuf:"varint,3,opt,name=lang,proto3,enum=gocaptcha.GoCaptchaLang,oneof" json:"lang,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` } func (x *GetDataRequest) Reset() { @@ -212,25 +60,11 @@ func (*GetDataRequest) Descriptor() ([]byte, []int) { return file_proto_api_proto_rawDescGZIP(), []int{0} } -func (x *GetDataRequest) GetType() GoCaptchaType { +func (x *GetDataRequest) GetId() string { if x != nil { - return x.Type + return x.Id } - return GoCaptchaType_GoCaptchaTypeClick -} - -func (x *GetDataRequest) GetTheme() GoCaptchaTheme { - if x != nil && x.Theme != nil { - return *x.Theme - } - return GoCaptchaTheme_GoCaptchaThemeDefault -} - -func (x *GetDataRequest) GetLang() GoCaptchaLang { - if x != nil && x.Lang != nil { - return *x.Lang - } - return GoCaptchaLang_GoCaptchaLangDefault + return "" } type GetDataResponse struct { @@ -238,19 +72,19 @@ type GetDataResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - Type GoCaptchaType `protobuf:"varint,3,opt,name=type,proto3,enum=gocaptcha.GoCaptchaType" json:"type,omitempty"` - CaptchaKey string `protobuf:"bytes,4,opt,name=captchaKey,proto3" json:"captchaKey,omitempty"` - MasterImageBase64 string `protobuf:"bytes,5,opt,name=masterImageBase64,proto3" json:"masterImageBase64,omitempty"` - ThumbImageBase64 string `protobuf:"bytes,6,opt,name=thumbImageBase64,proto3" json:"thumbImageBase64,omitempty"` - MasterWidth int32 `protobuf:"varint,7,opt,name=masterWidth,proto3" json:"masterWidth,omitempty"` - MasterHeight int32 `protobuf:"varint,8,opt,name=masterHeight,proto3" json:"masterHeight,omitempty"` - ThumbWidth int32 `protobuf:"varint,9,opt,name=thumbWidth,proto3" json:"thumbWidth,omitempty"` - ThumbHeight int32 `protobuf:"varint,10,opt,name=thumbHeight,proto3" json:"thumbHeight,omitempty"` - ThumbSize int32 `protobuf:"varint,11,opt,name=thumbSize,proto3" json:"thumbSize,omitempty"` - DisplayX int32 `protobuf:"varint,12,opt,name=displayX,proto3" json:"displayX,omitempty"` - DisplayY int32 `protobuf:"varint,13,opt,name=displayY,proto3" json:"displayY,omitempty"` + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"` + CaptchaKey string `protobuf:"bytes,4,opt,name=captchaKey,proto3" json:"captchaKey,omitempty"` + MasterImageBase64 string `protobuf:"bytes,5,opt,name=masterImageBase64,proto3" json:"masterImageBase64,omitempty"` + ThumbImageBase64 string `protobuf:"bytes,6,opt,name=thumbImageBase64,proto3" json:"thumbImageBase64,omitempty"` + MasterWidth int32 `protobuf:"varint,7,opt,name=masterWidth,proto3" json:"masterWidth,omitempty"` + MasterHeight int32 `protobuf:"varint,8,opt,name=masterHeight,proto3" json:"masterHeight,omitempty"` + ThumbWidth int32 `protobuf:"varint,9,opt,name=thumbWidth,proto3" json:"thumbWidth,omitempty"` + ThumbHeight int32 `protobuf:"varint,10,opt,name=thumbHeight,proto3" json:"thumbHeight,omitempty"` + ThumbSize int32 `protobuf:"varint,11,opt,name=thumbSize,proto3" json:"thumbSize,omitempty"` + DisplayX int32 `protobuf:"varint,12,opt,name=displayX,proto3" json:"displayX,omitempty"` + DisplayY int32 `protobuf:"varint,13,opt,name=displayY,proto3" json:"displayY,omitempty"` } func (x *GetDataResponse) Reset() { @@ -299,11 +133,11 @@ func (x *GetDataResponse) GetMessage() string { return "" } -func (x *GetDataResponse) GetType() GoCaptchaType { +func (x *GetDataResponse) GetId() string { if x != nil { - return x.Type + return x.Id } - return GoCaptchaType_GoCaptchaTypeClick + return "" } func (x *GetDataResponse) GetCaptchaKey() string { @@ -381,9 +215,9 @@ type CheckDataRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Type GoCaptchaType `protobuf:"varint,1,opt,name=type,proto3,enum=gocaptcha.GoCaptchaType" json:"type,omitempty"` - CaptchaKey string `protobuf:"bytes,2,opt,name=captchaKey,proto3" json:"captchaKey,omitempty"` - Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + CaptchaKey string `protobuf:"bytes,2,opt,name=captchaKey,proto3" json:"captchaKey,omitempty"` + Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` } func (x *CheckDataRequest) Reset() { @@ -418,11 +252,11 @@ func (*CheckDataRequest) Descriptor() ([]byte, []int) { return file_proto_api_proto_rawDescGZIP(), []int{2} } -func (x *CheckDataRequest) GetType() GoCaptchaType { +func (x *CheckDataRequest) GetId() string { if x != nil { - return x.Type + return x.Id } - return GoCaptchaType_GoCaptchaTypeClick + return "" } func (x *CheckDataRequest) GetCaptchaKey() string { @@ -616,87 +450,56 @@ var File_proto_api_proto protoreflect.FileDescriptor var file_proto_api_proto_rawDesc = []byte{ 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x09, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x22, 0xba, 0x01, 0x0a, - 0x0e, 0x47, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x2c, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, - 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, - 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x34, 0x0a, - 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x67, - 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, - 0x68, 0x61, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x48, 0x00, 0x52, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, - 0x88, 0x01, 0x01, 0x12, 0x31, 0x0a, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x18, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x47, 0x6f, - 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4c, 0x61, 0x6e, 0x67, 0x48, 0x01, 0x52, 0x04, 0x6c, - 0x61, 0x6e, 0x67, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x74, 0x68, 0x65, 0x6d, 0x65, - 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6c, 0x61, 0x6e, 0x67, 0x22, 0xc5, 0x03, 0x0a, 0x0f, 0x47, 0x65, - 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x67, 0x6f, 0x63, 0x61, - 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, - 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x70, - 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, - 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x11, 0x6d, 0x61, 0x73, - 0x74, 0x65, 0x72, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6d, 0x61, 0x67, - 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x12, 0x2a, 0x0a, 0x10, 0x74, 0x68, 0x75, 0x6d, 0x62, - 0x49, 0x6d, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x10, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, - 0x65, 0x36, 0x34, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x57, 0x69, 0x64, - 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, - 0x57, 0x69, 0x64, 0x74, 0x68, 0x12, 0x22, 0x0a, 0x0c, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x48, - 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6d, 0x61, 0x73, - 0x74, 0x65, 0x72, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x68, 0x75, - 0x6d, 0x62, 0x57, 0x69, 0x64, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x74, - 0x68, 0x75, 0x6d, 0x62, 0x57, 0x69, 0x64, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x68, 0x75, - 0x6d, 0x62, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, - 0x74, 0x68, 0x75, 0x6d, 0x62, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, - 0x68, 0x75, 0x6d, 0x62, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, - 0x74, 0x68, 0x75, 0x6d, 0x62, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x58, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x64, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x58, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x59, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x59, 0x22, 0x76, 0x0a, 0x10, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, - 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, - 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x55, 0x0a, 0x11, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, - 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, - 0x22, 0x33, 0x0a, 0x11, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, - 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, - 0x68, 0x61, 0x4b, 0x65, 0x79, 0x22, 0x56, 0x0a, 0x12, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, - 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x2a, 0x8c, 0x01, - 0x0a, 0x0d, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x16, 0x0a, 0x12, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, - 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x47, 0x6f, 0x43, 0x61, 0x70, - 0x74, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x53, 0x68, 0x61, - 0x70, 0x65, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, - 0x61, 0x54, 0x79, 0x70, 0x65, 0x53, 0x6c, 0x69, 0x64, 0x65, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, - 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x79, 0x70, 0x65, 0x44, 0x72, 0x61, - 0x67, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, - 0x54, 0x79, 0x70, 0x65, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x10, 0x04, 0x2a, 0x43, 0x0a, 0x0e, - 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x19, - 0x0a, 0x15, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x68, 0x65, 0x6d, 0x65, - 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x47, 0x6f, 0x43, - 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x44, 0x61, 0x72, 0x6b, 0x10, - 0x01, 0x2a, 0x43, 0x0a, 0x0d, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4c, 0x61, - 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x14, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4c, - 0x61, 0x6e, 0x67, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, - 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4c, 0x61, 0x6e, 0x67, 0x45, 0x6e, 0x67, - 0x6c, 0x69, 0x73, 0x68, 0x10, 0x01, 0x32, 0x8e, 0x03, 0x0a, 0x10, 0x47, 0x6f, 0x43, 0x61, 0x70, + 0x6f, 0x12, 0x09, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x22, 0x20, 0x0a, 0x0e, + 0x47, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xa7, + 0x03, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, 0x79, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, 0x79, + 0x12, 0x2c, 0x0a, 0x11, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x42, + 0x61, 0x73, 0x65, 0x36, 0x34, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6d, 0x61, 0x73, + 0x74, 0x65, 0x72, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x12, 0x2a, + 0x0a, 0x10, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, 0x65, + 0x36, 0x34, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x49, + 0x6d, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, + 0x73, 0x74, 0x65, 0x72, 0x57, 0x69, 0x64, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x0b, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x57, 0x69, 0x64, 0x74, 0x68, 0x12, 0x22, 0x0a, 0x0c, + 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0c, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, + 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x57, 0x69, 0x64, 0x74, 0x68, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x57, 0x69, 0x64, 0x74, 0x68, + 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x48, 0x65, 0x69, 0x67, + 0x68, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x53, 0x69, 0x7a, 0x65, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x53, 0x69, 0x7a, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x58, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x08, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x58, 0x12, 0x1a, 0x0a, 0x08, + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x59, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x59, 0x22, 0x58, 0x0a, 0x10, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x0a, + 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x22, 0x55, 0x0a, 0x11, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x61, 0x74, 0x61, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x33, 0x0a, 0x11, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, + 0x0a, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x4b, 0x65, 0x79, 0x22, 0x56, + 0x0a, 0x12, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0x8e, 0x03, 0x0a, 0x10, 0x47, 0x6f, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x42, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x19, 0x2e, 0x67, 0x6f, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, @@ -737,40 +540,31 @@ func file_proto_api_proto_rawDescGZIP() []byte { return file_proto_api_proto_rawDescData } -var file_proto_api_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_proto_api_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_proto_api_proto_goTypes = []interface{}{ - (GoCaptchaType)(0), // 0: gocaptcha.GoCaptchaType - (GoCaptchaTheme)(0), // 1: gocaptcha.GoCaptchaTheme - (GoCaptchaLang)(0), // 2: gocaptcha.GoCaptchaLang - (*GetDataRequest)(nil), // 3: gocaptcha.GetDataRequest - (*GetDataResponse)(nil), // 4: gocaptcha.GetDataResponse - (*CheckDataRequest)(nil), // 5: gocaptcha.CheckDataRequest - (*CheckDataResponse)(nil), // 6: gocaptcha.CheckDataResponse - (*StatusInfoRequest)(nil), // 7: gocaptcha.StatusInfoRequest - (*StatusInfoResponse)(nil), // 8: gocaptcha.StatusInfoResponse + (*GetDataRequest)(nil), // 0: gocaptcha.GetDataRequest + (*GetDataResponse)(nil), // 1: gocaptcha.GetDataResponse + (*CheckDataRequest)(nil), // 2: gocaptcha.CheckDataRequest + (*CheckDataResponse)(nil), // 3: gocaptcha.CheckDataResponse + (*StatusInfoRequest)(nil), // 4: gocaptcha.StatusInfoRequest + (*StatusInfoResponse)(nil), // 5: gocaptcha.StatusInfoResponse } var file_proto_api_proto_depIdxs = []int32{ - 0, // 0: gocaptcha.GetDataRequest.type:type_name -> gocaptcha.GoCaptchaType - 1, // 1: gocaptcha.GetDataRequest.theme:type_name -> gocaptcha.GoCaptchaTheme - 2, // 2: gocaptcha.GetDataRequest.lang:type_name -> gocaptcha.GoCaptchaLang - 0, // 3: gocaptcha.GetDataResponse.type:type_name -> gocaptcha.GoCaptchaType - 0, // 4: gocaptcha.CheckDataRequest.type:type_name -> gocaptcha.GoCaptchaType - 3, // 5: gocaptcha.GoCaptchaService.GetData:input_type -> gocaptcha.GetDataRequest - 5, // 6: gocaptcha.GoCaptchaService.CheckData:input_type -> gocaptcha.CheckDataRequest - 7, // 7: gocaptcha.GoCaptchaService.CheckStatus:input_type -> gocaptcha.StatusInfoRequest - 7, // 8: gocaptcha.GoCaptchaService.GetStatusInfo:input_type -> gocaptcha.StatusInfoRequest - 7, // 9: gocaptcha.GoCaptchaService.DelStatusInfo:input_type -> gocaptcha.StatusInfoRequest - 4, // 10: gocaptcha.GoCaptchaService.GetData:output_type -> gocaptcha.GetDataResponse - 6, // 11: gocaptcha.GoCaptchaService.CheckData:output_type -> gocaptcha.CheckDataResponse - 8, // 12: gocaptcha.GoCaptchaService.CheckStatus:output_type -> gocaptcha.StatusInfoResponse - 8, // 13: gocaptcha.GoCaptchaService.GetStatusInfo:output_type -> gocaptcha.StatusInfoResponse - 8, // 14: gocaptcha.GoCaptchaService.DelStatusInfo:output_type -> gocaptcha.StatusInfoResponse - 10, // [10:15] is the sub-list for method output_type - 5, // [5:10] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 0, // 0: gocaptcha.GoCaptchaService.GetData:input_type -> gocaptcha.GetDataRequest + 2, // 1: gocaptcha.GoCaptchaService.CheckData:input_type -> gocaptcha.CheckDataRequest + 4, // 2: gocaptcha.GoCaptchaService.CheckStatus:input_type -> gocaptcha.StatusInfoRequest + 4, // 3: gocaptcha.GoCaptchaService.GetStatusInfo:input_type -> gocaptcha.StatusInfoRequest + 4, // 4: gocaptcha.GoCaptchaService.DelStatusInfo:input_type -> gocaptcha.StatusInfoRequest + 1, // 5: gocaptcha.GoCaptchaService.GetData:output_type -> gocaptcha.GetDataResponse + 3, // 6: gocaptcha.GoCaptchaService.CheckData:output_type -> gocaptcha.CheckDataResponse + 5, // 7: gocaptcha.GoCaptchaService.CheckStatus:output_type -> gocaptcha.StatusInfoResponse + 5, // 8: gocaptcha.GoCaptchaService.GetStatusInfo:output_type -> gocaptcha.StatusInfoResponse + 5, // 9: gocaptcha.GoCaptchaService.DelStatusInfo:output_type -> gocaptcha.StatusInfoResponse + 5, // [5:10] is the sub-list for method output_type + 0, // [0:5] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name } func init() { file_proto_api_proto_init() } @@ -852,20 +646,18 @@ func file_proto_api_proto_init() { } } } - file_proto_api_proto_msgTypes[0].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_api_proto_rawDesc, - NumEnums: 3, + NumEnums: 0, NumMessages: 6, NumExtensions: 0, NumServices: 1, }, GoTypes: file_proto_api_proto_goTypes, DependencyIndexes: file_proto_api_proto_depIdxs, - EnumInfos: file_proto_api_proto_enumTypes, MessageInfos: file_proto_api_proto_msgTypes, }.Build() File_proto_api_proto = out.File diff --git a/proto/api.proto b/proto/api.proto index d1851ee..1db6a83 100644 --- a/proto/api.proto +++ b/proto/api.proto @@ -13,37 +13,14 @@ service GoCaptchaService { rpc DelStatusInfo(StatusInfoRequest) returns (StatusInfoResponse) {} } -// Type -enum GoCaptchaType { - GoCaptchaTypeClick = 0; - GoCaptchaTypeClickShape = 1; - GoCaptchaTypeSlide = 2; - GoCaptchaTypeDrag = 3; - GoCaptchaTypeRotate = 4; -} - -// Theme -enum GoCaptchaTheme { - GoCaptchaThemeDefault = 0; - GoCaptchaThemeDark = 1; -} - -// Lang -enum GoCaptchaLang { - GoCaptchaLangDefault = 0; - GoCaptchaLangEnglish = 1; -} - message GetDataRequest { - GoCaptchaType type = 1; - optional GoCaptchaTheme theme = 2; - optional GoCaptchaLang lang = 3; + string id = 1; } message GetDataResponse { int32 code = 1; string message = 2; - GoCaptchaType type = 3; + string id = 3; string captchaKey = 4; string masterImageBase64 = 5; string thumbImageBase64 = 6; @@ -57,7 +34,7 @@ message GetDataResponse { } message CheckDataRequest { - GoCaptchaType type = 1; + string id = 1; string captchaKey = 2; string value = 3; } From 1d9b5ef09a150b49bf87fb4119c7a23c1824da68 Mon Sep 17 00:00:00 2001 From: Awen Date: Tue, 22 Apr 2025 00:13:39 +0800 Subject: [PATCH 4/6] update logic --- .gitignore | 4 + LICENSE | 222 ++++++- Makefile | 12 +- README.md | 11 +- cmd/go-captcha-service/main.go | 13 +- config.dev.json | 32 + config.json | 23 +- docs/cache.md | 0 docs/openapi.yaml | 0 go.mod | 41 +- go.sum | 123 +++- gocaptcha.dev.json | 553 ++++++++++++++++++ gocaptcha.json | 56 +- internal/adapt/capt.go | 37 +- internal/app/app.go | 429 ++++++-------- internal/app/setup.go | 361 ++++++++++++ internal/cache/cache.go | 145 +++++ internal/cache/etcd_client.go | 12 +- internal/cache/memcache_client.go | 12 +- internal/cache/memory_cache.go | 14 +- internal/cache/redis_client.go | 13 +- internal/common/svc_context.go | 10 +- internal/config/config.go | 290 ++++++--- internal/config/config_test.go | 4 +- internal/consts/consts.go | 6 + internal/helper/helper.go | 6 + internal/helper/outlog.go | 12 + internal/helper/path.go | 6 + internal/load_balancer/consistent_hash.go | 24 - internal/load_balancer/load_balancer.go | 10 - internal/load_balancer/load_balancer_test.go | 43 -- internal/load_balancer/round_robin.go | 26 - internal/logic/click.go | 47 +- internal/logic/click_test.go | 25 +- internal/logic/common.go | 16 +- internal/logic/resource.go | 10 +- internal/logic/rotate.go | 49 +- internal/logic/slide.go | 47 +- internal/middleware/grpc_middleware.go | 18 +- ...{http_niddleware.go => http_middleware.go} | 24 +- internal/pkg/gocaptcha/click.go | 16 +- internal/pkg/gocaptcha/config/base.config.go | 6 + internal/pkg/gocaptcha/config/config.go | 197 +++++-- .../pkg/gocaptcha/config/resrouce.config.go | 6 + internal/pkg/gocaptcha/gocaptcha.go | 14 +- internal/pkg/gocaptcha/rotate.go | 8 +- internal/pkg/gocaptcha/slide.go | 10 +- internal/server/grpc_server.go | 28 +- internal/server/grpc_server_test.go | 3 +- internal/server/http_handler.go | 59 +- internal/server/http_handler_test.go | 3 +- .../service_discovery/consul_discovery.go | 76 --- internal/service_discovery/etcd_discovery.go | 100 ---- internal/service_discovery/nacos_discovery.go | 93 --- .../service_discovery/service_discovery.go | 22 - .../service_discovery_test.go | 26 - .../service_discovery/zookeeper_discovery.go | 95 --- modd.conf | 2 +- 58 files changed, 2427 insertions(+), 1123 deletions(-) create mode 100644 config.dev.json delete mode 100644 docs/cache.md delete mode 100644 docs/openapi.yaml create mode 100644 gocaptcha.dev.json create mode 100644 internal/app/setup.go create mode 100644 internal/helper/outlog.go delete mode 100644 internal/load_balancer/consistent_hash.go delete mode 100644 internal/load_balancer/load_balancer.go delete mode 100644 internal/load_balancer/load_balancer_test.go delete mode 100644 internal/load_balancer/round_robin.go rename internal/middleware/{http_niddleware.go => http_middleware.go} (90%) delete mode 100644 internal/service_discovery/consul_discovery.go delete mode 100644 internal/service_discovery/etcd_discovery.go delete mode 100644 internal/service_discovery/nacos_discovery.go delete mode 100644 internal/service_discovery/service_discovery.go delete mode 100644 internal/service_discovery/service_discovery_test.go delete mode 100644 internal/service_discovery/zookeeper_discovery.go diff --git a/.gitignore b/.gitignore index 4caf33d..5b109a7 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,11 @@ unpackage logs *.log +gocaptcha.dd.dev.json + # resources +resources/* +!resources/gocaptcha resources/gocaptcha/fonts/* resources/gocaptcha/master_images/* resources/gocaptcha/shape_images/* diff --git a/LICENSE b/LICENSE index ca108e4..2766fc1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2025 Awen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2025] [Awen ] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Makefile b/Makefile index 186e89a..01679c8 100644 --- a/Makefile +++ b/Makefile @@ -4,10 +4,10 @@ BINARY_NAME=go-captcha-service VERSION?=0.1.0 BUILD_DIR=build -PLATFORMS=linux/amd64 linux/arm64 linux/arm/v7 darwin/amd64 darwin/arm64 windows/amd64 +PLATFORMS=darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 linux/arm/v7 windows/amd64 DOCKER_IMAGE?=wenlng/go-captcha-service GO=go -GOFLAGS=-ldflags="-w -s" -v -a -gcflags=-trimpath=$(PWD) -asmflags=-trimpath=$(PWD) +GOFLAGS=-ldflags="-w -s" -v -a -trimpath COPY_BUILD_FILES=config.json ecosystem.config.js # Default Target @@ -47,6 +47,11 @@ start-dev: modd -f modd.conf @echo "Starting modd successfully" +.PHONY: start +start: + go run ./cmd/go-captcha-service/main.go -config config.dev.json -gocaptcha-config gocaptcha.dev.json + @echo "Starting service successfully" + # Build the application .PHONY: build build: proto @@ -143,7 +148,8 @@ help: @echo "Available targets:" @echo " deps : Install dependencies" @echo " proto : Generate Protobuf code" - @echo " start-dev : Opening the development environment" + @echo " start : Opening the development environment" + @echo " start-dev : Opening the hot reload development environment" @echo " build : Build binary for current platform" @echo " build-multi : Build binaries for all platforms" @echo " package : Package binaries with config.json" diff --git a/README.md b/README.md index bd63f80..5456348 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # go-captcha-service -# config 执重载有效字段 +# config 热重载有效的字段 +redis_addrs +etcd_addrs +memcache_addrs +cache_type +cache_ttl +cache_key_prefix api_keys +log_level +rate_limit_qps +rate_limit_burst diff --git a/cmd/go-captcha-service/main.go b/cmd/go-captcha-service/main.go index a7375d8..e0fb1e5 100644 --- a/cmd/go-captcha-service/main.go +++ b/cmd/go-captcha-service/main.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package main import ( @@ -13,15 +19,16 @@ import ( func main() { a, err := app.NewApp() if err != nil { - fmt.Fprintf(os.Stderr, "Failed to initialize app: %v\n", err) + fmt.Fprintf(os.Stderr, "[Main] Failed to initialize app: %v\n", err) os.Exit(1) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() + // Start app if err = a.Start(ctx); err != nil { - fmt.Fprintf(os.Stderr, "Failed to start app: %v\n", err) + fmt.Fprintf(os.Stderr, "[Main] Failed to start app: %v\n", err) } // Handle termination signals @@ -30,5 +37,5 @@ func main() { <-sigCh a.Shutdown() - fmt.Fprintf(os.Stderr, "App service exited") + fmt.Fprintf(os.Stderr, "[Main] App service exited") } diff --git a/config.dev.json b/config.dev.json new file mode 100644 index 0000000..d3626fb --- /dev/null +++ b/config.dev.json @@ -0,0 +1,32 @@ +{ + "config_version": 15, + "service_name": "go-captcha-service", + "http_port": "8081", + "grpc_port": "50052", + "redis_addrs": "localhost:6379", + "etcd_addrs": "localhost:2379", + "memcache_addrs": "localhost:11211", + "cache_type": "redis", + "cache_ttl": 1800, + "cache_key_prefix": "DEV_ENV_GO_CAPTCHA_DATA:", + "enable_dynamic_config": true, + "enable_service_discovery": true, + "service_discovery": "nacos", + "service_discovery_addrs": "localhost:8848", + "service_discovery_username": "nacos", + "service_discovery_password": "nacos", + "service_discovery_ttl": 10, + "service_discovery_keep_alive": 3, + "service_discovery_max_retries": 3, + "service_discovery_base_retry_delay": 1, + "service_discovery_tls_server_name": "", + "service_discovery_tls_address": "", + "service_discovery_tls_cert_file": "", + "service_discovery_tls_key_file": "", + "service_discovery_tls_ca_file": "", + "rate_limit_qps": 1000, + "rate_limit_burst": 1000, + "enable_cors": true, + "log_level": "info", + "api_keys": ["my-secret-key-123", "another-key-456", "another-key-000"] +} \ No newline at end of file diff --git a/config.json b/config.json index ae4e4b9..d574995 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,5 @@ { + "config_version": 1, "service_name": "go-captcha-service", "http_port": "8080", "grpc_port": "50051", @@ -6,14 +7,26 @@ "etcd_addrs": "localhost:2379", "memcache_addrs": "localhost:11211", "cache_type": "memory", - "cache_ttl": 60, - "cache_cleanup_interval": 10, + "cache_ttl": 1800, "cache_key_prefix": "GO_CAPTCHA_DATA:", - "service_discovery": "", + "enable_dynamic_config": false, + "enable_service_discovery": false, + "service_discovery": "etcd", "service_discovery_addrs": "localhost:2379", + "service_discovery_username": "", + "service_discovery_password": "", + "service_discovery_ttl": 10, + "service_discovery_keep_alive": 3, + "service_discovery_max_retries": 3, + "service_discovery_base_retry_delay": 500, + "service_discovery_tls_server_name": "", + "service_discovery_tls_address": "", + "service_discovery_tls_cert_file": "", + "service_discovery_tls_key_file": "", + "service_discovery_tls_ca_file": "", "rate_limit_qps": 1000, "rate_limit_burst": 1000, - "load_balancer": "round-robin", "enable_cors": true, - "api_keys": ["my-secret-key-123", "another-key-456"] + "log_level": "info", + "api_keys": ["my-secret-key-123", "another-key-456", "another-key-789"] } \ No newline at end of file diff --git a/docs/cache.md b/docs/cache.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/openapi.yaml b/docs/openapi.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/go.mod b/go.mod index 2c29832..fad8721 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,14 @@ require ( github.com/alicebob/miniredis/v2 v2.32.1 github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 github.com/fsnotify/fsnotify v1.9.0 - github.com/go-zookeeper/zk v1.0.3 + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/google/uuid v1.6.0 - github.com/hashicorp/consul/api v1.29.4 - github.com/nacos-group/nacos-sdk-go/v2 v2.2.5 github.com/redis/go-redis/v9 v9.6.1 github.com/sony/gobreaker v0.5.0 github.com/stretchr/testify v1.9.0 + github.com/wenlng/go-captcha-assets v1.0.6 + github.com/wenlng/go-captcha/v2 v2.0.3 + github.com/wenlng/go-service-link v0.0.2 go.etcd.io/etcd/client/v3 v3.5.21 go.etcd.io/etcd/server/v3 v3.5.21 go.uber.org/zap v1.27.0 @@ -22,18 +23,33 @@ require ( ) require ( - github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect - github.com/alibabacloud-go/tea v1.1.17 // indirect + github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 // indirect + github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect + github.com/alibabacloud-go/darabonba-array v0.1.0 // indirect + github.com/alibabacloud-go/darabonba-encode-util v0.0.2 // indirect + github.com/alibabacloud-go/darabonba-map v0.0.2 // indirect + github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 // indirect + github.com/alibabacloud-go/darabonba-signature-util v0.0.7 // indirect + github.com/alibabacloud-go/darabonba-string v1.0.2 // indirect + github.com/alibabacloud-go/debug v1.0.1 // indirect + github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect + github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 // indirect + github.com/alibabacloud-go/openapi-util v0.1.0 // indirect + github.com/alibabacloud-go/tea v1.2.2 // indirect github.com/alibabacloud-go/tea-utils v1.4.4 // indirect + github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect + github.com/alibabacloud-go/tea-xml v1.1.3 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 // indirect - github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.2.2 // indirect - github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.7 // indirect + github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 // indirect + github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 // indirect + github.com/aliyun/credentials-go v1.4.3 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.5.5 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -42,9 +58,9 @@ require ( github.com/fatih/color v1.16.0 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-zookeeper/zk v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.0.1 // indirect @@ -53,6 +69,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/hashicorp/consul/api v1.29.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect @@ -71,6 +88,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nacos-group/nacos-sdk-go/v2 v2.2.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.12.2 // indirect @@ -80,9 +98,8 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect - github.com/wenlng/go-captcha-assets v1.0.6 // indirect - github.com/wenlng/go-captcha/v2 v2.0.3 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect go.etcd.io/bbolt v1.3.11 // indirect @@ -110,7 +127,7 @@ require ( google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect - gopkg.in/ini.v1 v1.66.2 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index f1a38bf..976a9eb 100644 --- a/go.sum +++ b/go.sum @@ -44,23 +44,69 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= +github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY= +github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= +github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= +github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= +github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= +github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= +github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 h1:vamGcYQFwXVqR6RWcrVTTqlIXZVsYjaA7pZbx+Xw6zw= +github.com/alibabacloud-go/kms-20160120/v3 v3.2.3/go.mod h1:3rIyughsFDLie1ut9gQJXkWkMg/NfXBCk+OtXnPu3lw= +github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY= +github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= -github.com/alibabacloud-go/tea v1.1.17 h1:05R5DnaJXe9sCNIe8KUgWHC/z6w/VZIwczgUwzRnul8= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA= +github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU= +github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o= github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw= +github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= +github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= +github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0= +github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0= +github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoOR4JssBo= github.com/alicebob/miniredis/v2 v2.32.1/go.mod h1:AqkLNAfUm0K07J28hnAyyQKf/x0YkCY/g5DCtuL01Mw= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 h1:ie/8RxBOfKZWcrbYSJi2Z8uX8TcOlSMwPlEJh83OeOw= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU= -github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.2.2 h1:rWkH6D2XlXb/Y+tNAQROxBzp3a0p92ni+pXcaHBe/WI= -github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.2.2/go.mod h1:GDtq+Kw+v0fO+j5BrrWiUHbBq7L+hfpzpPfXKOZMFE0= -github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.7 h1:olLiPI2iM8Hqq6vKnSxpM3awCrm9/BeOgHpzQkOYnI4= -github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.7/go.mod h1:oDg1j4kFxnhgftaiLJABkGeSvuEvSF5Lo6UmRAMruX4= +github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 h1:nJYyoFP+aqGKgPs9JeZgS1rWQ4NndNR0Zfhh161ZltU= +github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1/go.mod h1:WzGOmFFTlUzXM03CJnHWMQ85UN6QGpOXZocCjwkiyOg= +github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 h1:QeUdR7JF7iNCvO/81EhxEr3wDwxk4YBoYZOq6E0AjHI= +github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8/go.mod h1:xP0KIZry6i7oGPF24vhAPr1Q8vLZRcMcxtft5xDKwCU= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= +github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= +github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= +github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/aliyun/credentials-go v1.4.3 h1:N3iHyvHRMyOwY1+0qBLSf3hb5JFiOujVSVuEpgeGttY= +github.com/aliyun/credentials-go v1.4.3/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -81,8 +127,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -93,6 +139,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E= +github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg= @@ -215,6 +263,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= @@ -287,6 +337,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -336,8 +387,9 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nacos-group/nacos-sdk-go/v2 v2.2.5 h1:r0wwT7PayEjvEHzWXwr1ROi/JSqzujM4w+1L5ikThzQ= -github.com/nacos-group/nacos-sdk-go/v2 v2.2.5/go.mod h1:OObBon0prVJVPoIbSZxpEkFiBfL0d1LcBtuAMiNn+8c= +github.com/nacos-group/nacos-sdk-go/v2 v2.2.8 h1:lyt2ynw2+G4JywFUAM0qXCJ/nEA9ORsweiLkgwYISUo= +github.com/nacos-group/nacos-sdk-go/v2 v2.2.8/go.mod h1:pIv1y5pMAjqFNU4jo9fmbuBjiZqpeGCddKkv0Y12ysY= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -390,6 +442,9 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= @@ -398,16 +453,21 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -415,10 +475,13 @@ github.com/wenlng/go-captcha-assets v1.0.6 h1:PSTTReE7QXsYdBnE/oZB91BllCZSvBKb4u github.com/wenlng/go-captcha-assets v1.0.6/go.mod h1:zinRACsdYcL/S6pHgI9Iv7FKTU41d00+43pNX+b9+MM= github.com/wenlng/go-captcha/v2 v2.0.3 h1:QTZ39/gVDisPSgvL9O2X2HbTuj5P/z8QsdGB/aayg9c= github.com/wenlng/go-captcha/v2 v2.0.3/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34= +github.com/wenlng/go-service-link v0.0.2 h1:5nB/W1UD5EkH+JEtVA71tzBZX/FXZRyXHg8Ovo4Oggs= +github.com/wenlng/go-service-link v0.0.2/go.mod h1:1d4570TMBbMPv/w0pwgjT7Ev6Ajxv2/u0UVL1mIh6Y0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -477,9 +540,16 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -548,6 +618,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -556,6 +627,13 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -613,6 +691,7 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -639,11 +718,23 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -652,6 +743,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= @@ -666,6 +761,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -695,6 +791,7 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -796,17 +893,21 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/gocaptcha.dev.json b/gocaptcha.dev.json new file mode 100644 index 0000000..f826a49 --- /dev/null +++ b/gocaptcha.dev.json @@ -0,0 +1,553 @@ +{ + "config_version": 1, + "resources": { + "version": "0.0.1", + "char": { + "languages": { + "chinese": [], + "english": [] + } + }, + "font": { + "type": "load", + "file_dir": "./gocaptcha/fonts/", + "file_maps": { + "yrdzst_bold": "yrdzst-bold.ttf" + } + }, + "shape_image": { + "type": "load", + "file_dir": "./gocaptcha/shape_images/", + "file_maps": { + "shape_01": "shape_01.png", + "shape_01.png":"c.png" + } + }, + "master_image": { + "type": "load", + "file_dir": "./gocaptcha/master_images/", + "file_maps": { + "image_01": "image_01.jpg", + "image_02":"image_02.jpg" + } + }, + "thumb_image": { + "type": "load", + "file_dir": "./gocaptcha/thumb_images/", + "file_maps": { + + } + }, + "tile_image": { + "type": "load", + "file_dir": "./gocaptcha/tile_images/", + "file_maps": { + "tile_01": "tile_01.png", + "tile_02": "tile_02.png" + }, + "file_maps_02": { + "tile_mask_01": "tile_mask_01.png", + "tile_mask_02": "tile_mask_02.png" + }, + "file_maps_03": { + "tile_shadow_01": "tile_shadow_01.png", + "tile_shadow_02": "tile_shadow_02.png" + } + } + }, + "builder": { + "click_config_maps": { + "click-default-ch": { + "version": "0.0.1", + "language": "chinese", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click-dark-ch": { + "version": "0.0.1", + "language": "chinese", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#4a85fb", + "#d93ffb", + "#56be01", + "#ee2b2b", + "#cd6904", + "#b49b03", + "#01ad90" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click-default-en": { + "version": "0.0.1", + "language": "english", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 34, + "max": 48 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 34, + "max": 48 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click-dark-en": { + "version": "0.0.1", + "language": "english", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#4a85fb", + "#d93ffb", + "#56be01", + "#ee2b2b", + "#cd6904", + "#b49b03", + "#01ad90" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + } + }, + "click_shape_config_maps": { + "click-shape-default": { + "version": "0.0.1", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + } + }, + "slide_config_maps": { + "slide-default": { + "version": "0.0.1" + } + }, + "drag_config_maps": { + "drag-default": { + "version": "0.0.1" + } + }, + "rotate_config_maps": { + "rotate-default": { + "version": "0.0.1" + } + } + } +} \ No newline at end of file diff --git a/gocaptcha.json b/gocaptcha.json index 3489045..f826a49 100644 --- a/gocaptcha.json +++ b/gocaptcha.json @@ -1,4 +1,5 @@ { + "config_version": 1, "resources": { "version": "0.0.1", "char": { @@ -56,7 +57,8 @@ }, "builder": { "click_config_maps": { - "click_default_ch": { + "click-default-ch": { + "version": "0.0.1", "language": "chinese", "master": { "image_size": { @@ -150,7 +152,8 @@ "background_slim_line_num": 2 } }, - "click_dark_ch": { + "click-dark-ch": { + "version": "0.0.1", "language": "chinese", "master": { "image_size": { @@ -220,13 +223,13 @@ "max": 28 }, "range_text_colors": [ - "#1f55c4", - "#780592", - "#2f6b00", - "#910000", - "#864401", - "#675901", - "#016e5c" + "#4a85fb", + "#d93ffb", + "#56be01", + "#ee2b2b", + "#cd6904", + "#b49b03", + "#01ad90" ], "range_background_colors": [ "#1f55c4", @@ -244,7 +247,8 @@ "background_slim_line_num": 2 } }, - "click_default_en": { + "click-default-en": { + "version": "0.0.1", "language": "english", "master": { "image_size": { @@ -338,7 +342,8 @@ "background_slim_line_num": 2 } }, - "click_dark_en": { + "click-dark-en": { + "version": "0.0.1", "language": "english", "master": { "image_size": { @@ -408,13 +413,13 @@ "max": 28 }, "range_text_colors": [ - "#1f55c4", - "#780592", - "#2f6b00", - "#910000", - "#864401", - "#675901", - "#016e5c" + "#4a85fb", + "#d93ffb", + "#56be01", + "#ee2b2b", + "#cd6904", + "#b49b03", + "#01ad90" ], "range_background_colors": [ "#1f55c4", @@ -434,7 +439,8 @@ } }, "click_shape_config_maps": { - "click_shape_default": { + "click-shape-default": { + "version": "0.0.1", "master": { "image_size": { "width": 300, @@ -529,13 +535,19 @@ } }, "slide_config_maps": { - "slide_default": {} + "slide-default": { + "version": "0.0.1" + } }, "drag_config_maps": { - "drag_default": {} + "drag-default": { + "version": "0.0.1" + } }, "rotate_config_maps": { - "rotate_default": {} + "rotate-default": { + "version": "0.0.1" + } } } } \ No newline at end of file diff --git a/internal/adapt/capt.go b/internal/adapt/capt.go index 5401f80..8c24765 100644 --- a/internal/adapt/capt.go +++ b/internal/adapt/capt.go @@ -1,29 +1,20 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package adapt type CaptData struct { CaptchaKey string `json:"captcha_key,omitempty"` MasterImageBase64 string `json:"master_image_base64,omitempty"` ThumbImageBase64 string `json:"thumb_image_base64,omitempty"` - MasterImageWidth int32 `json:"master_width,omitempty"` - MasterImageHeight int32 `json:"master_height,omitempty"` - ThumbImageWidth int32 `json:"thumb_width,omitempty"` - ThumbImageHeight int32 `json:"thumb_height,omitempty"` - ThumbImageSize int32 `json:"thumb_size,omitempty"` - DisplayX int32 `json:"display_x,omitempty"` - DisplayY int32 `json:"display_y,omitempty"` -} - -type CaptDataResponse struct { - Code int32 `json:"code" default:"200"` - Message string `json:"message"` - CaptchaKey string `json:"captcha_key,omitempty"` - MasterImageBase64 string `json:"master_image_base64,omitempty"` - ThumbImageBase64 string `json:"thumb_image_base64,omitempty"` - MasterImageWidth int32 `json:"master_width,omitempty"` - MasterImageHeight int32 `json:"master_height,omitempty"` - ThumbImageWidth int32 `json:"thumb_width,omitempty"` - ThumbImageHeight int32 `json:"thumb_height,omitempty"` - ThumbImageSize int32 `json:"thumb_size,omitempty"` + MasterWidth int32 `json:"master_width,omitempty"` + MasterHeight int32 `json:"master_height,omitempty"` + ThumbWidth int32 `json:"thumb_width,omitempty"` + ThumbHeight int32 `json:"thumb_height,omitempty"` + ThumbSize int32 `json:"thumb_size,omitempty"` DisplayX int32 `json:"display_x,omitempty"` DisplayY int32 `json:"display_y,omitempty"` Id string `json:"id,omitempty"` @@ -35,12 +26,6 @@ type CaptNormalDataResponse struct { Data interface{} `json:"data"` } -type CaptStatusDataResponse struct { - Code int32 `json:"code" default:"200"` - Message string `json:"message" default:""` - Data string `json:"status" default:""` -} - type CaptStatusInfo struct { Info interface{} `json:"info"` Status int `json:"status"` diff --git a/internal/app/app.go b/internal/app/app.go index 30c9a7e..6375312 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,14 +1,18 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package app import ( "context" - "errors" "flag" "fmt" "net" "net/http" "os" - "strconv" "time" "github.com/google/uuid" @@ -16,12 +20,14 @@ import ( "github.com/wenlng/go-captcha-service/internal/cache" "github.com/wenlng/go-captcha-service/internal/common" "github.com/wenlng/go-captcha-service/internal/config" + "github.com/wenlng/go-captcha-service/internal/helper" "github.com/wenlng/go-captcha-service/internal/middleware" "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" config2 "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha/config" "github.com/wenlng/go-captcha-service/internal/server" - "github.com/wenlng/go-captcha-service/internal/service_discovery" "github.com/wenlng/go-captcha-service/proto" + "github.com/wenlng/go-service-link/dynaconfig" + "github.com/wenlng/go-service-link/servicediscovery" "go.uber.org/zap" "google.golang.org/grpc" ) @@ -31,28 +37,23 @@ type App struct { logger *zap.Logger dynamicCfg *config.DynamicConfig dynamicCaptCfg *config2.DynamicCaptchaConfig - cache cache.Cache - discovery service_discovery.ServiceDiscovery + cacheMgr *cache.CacheManager + discovery servicediscovery.ServiceDiscovery + configManager *dynaconfig.ConfigManager httpServer *http.Server grpcServer *grpc.Server cacheBreaker *gobreaker.CircuitBreaker limiter *middleware.DynamicLimiter + captcha *gocaptcha.GoCaptcha } -// CacheType . -const ( - CacheTypeRedis string = "redis" - CacheTypeMemory = "memory" - CacheTypeEtcd = "etcd" - CacheTypeMemcache = "memcache" -) - -// ServiceDiscoveryType . +// ServiceDiscovery . const ( ServiceDiscoveryTypeEtcd string = "etcd" ServiceDiscoveryTypeZookeeper = "zookeeper" ServiceDiscoveryTypeConsul = "consul" ServiceDiscoveryTypeNacos = "nacos" + ServiceDiscoveryTypeNone = "none" ) // NewApp initializes the application @@ -60,24 +61,35 @@ func NewApp() (*App, error) { // Parse command-line flags configFile := flag.String("config", "config.json", "Path to config file") - gocaptchaConfigFile := flag.String("gocaptcha.json", "gocaptcha.json", "Path to gocaptcha config file") + gocaptchaConfigFile := flag.String("gocaptcha-config", "gocaptcha.json", "Path to gocaptcha config file") serviceName := flag.String("service-name", "", "Name for service") httpPort := flag.String("http-port", "", "Port for HTTP server") grpcPort := flag.String("grpc-port", "", "Port for gRPC server") redisAddrs := flag.String("redis-addrs", "", "Comma-separated Redis cluster addresses") etcdAddrs := flag.String("etcd-addrs", "", "Comma-separated etcd addresses") memcacheAddrs := flag.String("memcache-addrs", "", "Comma-separated Memcached addresses") - cacheType := flag.String("cache-type", "", "Cache type: redis, memory, etcd, memcache") - cacheTTL := flag.Int("cache-ttl", 0, "Cache TTL in seconds") - cacheCleanupInt := flag.Int("cache-cleanup-interval", 0, "Cache cleanup interval in seconds") - cacheKeyPrefix := flag.Int("cache-key-prefix", 0, "Key prefix for cache") + cacheType := flag.String("cache-type", "", "CacheManager type: redis, memory, etcd, memcache") + cacheTTL := flag.Int("cache-ttl", 0, "CacheManager TTL in seconds") + cacheKeyPrefix := flag.String("cache-key-prefix", "GO_CAPTCHA_DATA:", "Key prefix for cache") serviceDiscovery := flag.String("service-discovery", "", "Service discovery: etcd, zookeeper, consul, nacos") - serviceDiscoveryAddrs := flag.String("service-discovery-addrs", "", "Service discovery addresses") + serviceDiscoveryAddrs := flag.String("service-discovery-addrs", "", "Comma-separated list of service discovery server addresses") + serviceDiscoveryTTL := flag.Int("service-discovery-ttl", 10, "Time-to-live in seconds for service discovery registrations") + serviceDiscoveryKeepAlive := flag.Int("service-discovery-keep-alive", 3, "Duration in seconds for service discovery keep-alive interval") + serviceDiscoveryMaxRetries := flag.Int("service-discovery-max-retries", 3, "Maximum number of retries for service discovery operations") + serviceDiscoveryBaseRetryDelay := flag.Int("service-discovery-base-retry-delay", 3, "Base delay in milliseconds for service discovery retry attempts") + serviceDiscoveryUsername := flag.String("service-discovery-username", "", "Username for service discovery authentication") + serviceDiscoveryPassword := flag.String("service-discovery-password", "", "Password for service discovery authentication") + serviceDiscoveryTlsServerName := flag.String("service-discovery-tls-server-name", "", "TLS server name for service discovery connection") + serviceDiscoveryTlsAddress := flag.String("service-discovery-tls-address", "", "TLS address for service discovery server") + serviceDiscoveryTlsCertFile := flag.String("service-discovery-tls-cert-file", "", "Path to TLS certificate file for service discovery") + serviceDiscoveryTlsKeyFile := flag.String("service-discovery-tls-key-file", "", "Path to TLS key file for service discovery") + serviceDiscoveryTlsCaFile := flag.String("service-discovery-tls-ca-file", "", "Path to TLS CA file for service discovery") rateLimitQPS := flag.Int("rate-limit-qps", 0, "Rate limit QPS") rateLimitBurst := flag.Int("rate-limit-burst", 0, "Rate limit burst") - loadBalancer := flag.String("load-balancer", "", "Load balancer: round-robin, consistent-hash") apiKeys := flag.String("api-keys", "", "Comma-separated API keys") logLevel := flag.String("log-level", "", "Set log level: error, debug, warn, info") + enableServiceDiscovery := flag.Bool("enable-service-discovery", false, "Enable service discovery") + enableDynamicConfig := flag.Bool("enable-dynamic-config", false, "Enable dynamic config") healthCheckFlag := flag.Bool("health-check", false, "Run health check and exit") enableCorsFlag := flag.Bool("enable-cors", false, "Enable cross-domain resources") flag.Parse() @@ -87,61 +99,75 @@ func NewApp() (*App, error) { if err != nil { return nil, fmt.Errorf("failed to initialize logger: %v", err) } - setLoggerLevel(logger, *logLevel) + setupLoggerLevel(logger, *logLevel) // Load configuration - dc, err := config.NewDynamicConfig(*configFile) + dc, err := config.NewDynamicConfig(*configFile, true) if err != nil { - logger.Warn("Failed to load config, using defaults", zap.Error(err)) - dc = &config.DynamicConfig{Config: config.DefaultConfig()} + if helper.FileExists(*configFile) { + logger.Warn("[App] Failed to load of the config.json file, using defaults", zap.Error(err)) + } else { + logger.Warn("[App] No configuration file 'config.json' was provided for the application. Use the defaults configuration") + } + dc = config.DefaultDynamicConfig() } // Register hot update callback - dc.RegisterHotCallbackHook("UPDATE_LOG_LEVEL", func(dnCfg *config.DynamicConfig) { - setLoggerLevel(logger, dnCfg.Get().LogLevel) + dc.RegisterHotCallback("UPDATE_LOG_LEVEL", func(dnCfg *config.DynamicConfig, hotType config.HotCallbackType) { + setupLoggerLevel(logger, dnCfg.Get().LogLevel) }) // Load configuration - dgc, err := config2.NewDynamicConfig(*gocaptchaConfigFile) + dgc, err := config2.NewDynamicConfig(*gocaptchaConfigFile, true) if err != nil { - logger.Warn("Failed to load gocaptcha config, using defaults", zap.Error(err)) - dgc = &config2.DynamicCaptchaConfig{Config: config2.DefaultConfig()} + if helper.FileExists(*gocaptchaConfigFile) { + logger.Warn("[App] Failed to load of the gocaptcha.json file, using defaults", zap.Error(err)) + } else { + logger.Warn("[App] No configuration file 'gocaptcha.json' was provided for the application. Use the defaults configuration") + } + dgc = config2.DefaultDynamicConfig() } // Merge command-line flags cfg := dc.Get() cfg = config.MergeWithFlags(cfg, map[string]interface{}{ - "service-name": *serviceName, - "http-port": *httpPort, - "grpc-port": *grpcPort, - "redis-addrs": *redisAddrs, - "etcd-addrs": *etcdAddrs, - "memcache-addrs": *memcacheAddrs, - "cache-type": *cacheType, - "cache-ttl": *cacheTTL, - "cache-cleanup-interval": *cacheCleanupInt, - "cache-key-prefix": *cacheKeyPrefix, - "service-discovery": *serviceDiscovery, - "service-discovery-addrs": *serviceDiscoveryAddrs, - "rate-limit-qps": *rateLimitQPS, - "rate-limit-burst": *rateLimitBurst, - "load-balancer": *loadBalancer, - "enable-cors": *enableCorsFlag, - "api-keys": *apiKeys, + "service-name": *serviceName, + "http-port": *httpPort, + "grpc-port": *grpcPort, + "redis-addrs": *redisAddrs, + "etcd-addrs": *etcdAddrs, + "memcache-addrs": *memcacheAddrs, + "cache-type": *cacheType, + "cache-ttl": *cacheTTL, + "cache-key-prefix": *cacheKeyPrefix, + "enable-service-discovery": *enableServiceDiscovery, + "service-discovery": *serviceDiscovery, + "service-discovery-addrs": *serviceDiscoveryAddrs, + "service-discovery-ttl": serviceDiscoveryTTL, + "service-discovery-keep-alive": serviceDiscoveryKeepAlive, + "service-discovery-max-retries": serviceDiscoveryMaxRetries, + "service-discovery-base-retry-delay": serviceDiscoveryBaseRetryDelay, + "service-discovery-username": serviceDiscoveryUsername, + "service-discovery-password": serviceDiscoveryPassword, + "service-discovery-tls-server-name": serviceDiscoveryTlsServerName, + "service-discovery-tls-address": serviceDiscoveryTlsAddress, + "service-discovery-tls-cert-file": serviceDiscoveryTlsCertFile, + "service-discovery-tls-key-file": serviceDiscoveryTlsKeyFile, + "service-discovery-tls-ca-file": serviceDiscoveryTlsCaFile, + "rate-limit-qps": *rateLimitQPS, + "rate-limit-burst": *rateLimitBurst, + "enable-cors": *enableCorsFlag, + "enable-dynamic-config": *enableDynamicConfig, + "api-keys": *apiKeys, }) if err = dc.Update(cfg); err != nil { - logger.Fatal("Configuration validation failed", zap.Error(err)) + logger.Fatal("[App] Configuration validation failed", zap.Error(err)) } // Initialize rate limiter limiter := middleware.NewDynamicLimiter(cfg.RateLimitQPS, cfg.RateLimitBurst) - go func() { - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - for range ticker.C { - newCfg := dc.Get() - limiter.Update(newCfg.RateLimitQPS, newCfg.RateLimitBurst) - } - }() + dc.RegisterHotCallback("UPDATE_LIMITER", func(dnCfg *config.DynamicConfig, hotType config.HotCallbackType) { + limiter.Update(dnCfg.Get().RateLimitQPS, dnCfg.Get().RateLimitBurst) + }) // Initialize circuit breaker cacheBreaker := gobreaker.NewCircuitBreaker(gobreaker.Settings{ @@ -154,56 +180,40 @@ func NewApp() (*App, error) { }, }) - // Initialize cache - var curCache cache.Cache - ttl := time.Duration(cfg.CacheTTL) * time.Second - cleanInt := time.Duration(cfg.CacheCleanupInt) * time.Second - switch cfg.CacheType { - case CacheTypeRedis: - curCache, err = cache.NewRedisClient(cfg.RedisAddrs, cfg.CacheKeyPrefix, ttl) - if err != nil { - logger.Fatal("Failed to initialize Redis", zap.Error(err)) - } - case CacheTypeMemory: - curCache = cache.NewMemoryCache(cfg.CacheKeyPrefix, ttl, cleanInt) - case CacheTypeEtcd: - curCache, err = cache.NewEtcdClient(cfg.EtcdAddrs, cfg.CacheKeyPrefix, ttl) - if err != nil { - logger.Fatal("Failed to initialize etcd", zap.Error(err)) - } - case CacheTypeMemcache: - curCache, err = cache.NewMemcacheClient(cfg.MemcacheAddrs, cfg.CacheKeyPrefix, ttl) - if err != nil { - logger.Fatal("Failed to initialize Memcached", zap.Error(err)) - } - default: - logger.Fatal("Invalid curCache type", zap.String("type", cfg.CacheType)) + // Setup cache + cacheMgr, err := setupCacheManager(dc, logger) + if err != nil { + logger.Fatal("[App] Create cache manager", zap.Error(err)) } - // Initialize service discovery - var discovery service_discovery.ServiceDiscovery - if cfg.ServiceDiscovery != "" { - switch cfg.ServiceDiscovery { - case ServiceDiscoveryTypeEtcd: - discovery, err = service_discovery.NewEtcdDiscovery(cfg.ServiceDiscoveryAddrs, 10) - case ServiceDiscoveryTypeZookeeper: - discovery, err = service_discovery.NewZookeeperDiscovery(cfg.ServiceDiscoveryAddrs, 10) - case ServiceDiscoveryTypeConsul: - discovery, err = service_discovery.NewConsulDiscovery(cfg.ServiceDiscoveryAddrs, 10) - case ServiceDiscoveryTypeNacos: - discovery, err = service_discovery.NewNacosDiscovery(cfg.ServiceDiscoveryAddrs, 10) - default: - logger.Fatal("Invalid service discovery type", zap.String("type", cfg.ServiceDiscovery)) - } + // Setup service discovery + discovery, err := setupServiceDiscovery(dc, logger) + if err != nil { + logger.Fatal("[App] Setup service discovery", zap.Error(err)) + } + + // Setup dynamic config + configManager, err := setupDynamicConfig(dc, dgc, logger) + if err != nil { + logger.Fatal("[App] Setup dynamic config manager", zap.Error(err)) + } + + // Setup captcha + captcha, err := gocaptcha.Setup(dgc) + if err != nil { + logger.Fatal("[App] Failed to setup gocaptcha: ", zap.Error(err)) + } + dgc.RegisterHotCallback("GENERATE_CAPTCHA", func(captchaConfig *config2.DynamicCaptchaConfig, callbackType config2.HotCallbackType) { + err = captcha.HotSetup(captchaConfig) if err != nil { - logger.Fatal("Failed to initialize service discovery", zap.Error(err)) + logger.Error("[App] Failed to hot update gocaptcha, without any change: ", zap.Error(err)) } - } + }) // Perform health check if requested if *healthCheckFlag { - if err = healthCheck(":"+cfg.HTTPPort, ":"+cfg.GRPCPort); err != nil { - logger.Error("Health check failed", zap.Error(err)) + if err = setupHealthCheck(":"+cfg.HTTPPort, ":"+cfg.GRPCPort); err != nil { + logger.Error("[App] Filed to health check", zap.Error(err)) os.Exit(1) } os.Exit(0) @@ -213,80 +223,42 @@ func NewApp() (*App, error) { logger: logger, dynamicCfg: dc, dynamicCaptCfg: dgc, - cache: curCache, + cacheMgr: cacheMgr, discovery: discovery, + configManager: configManager, cacheBreaker: cacheBreaker, limiter: limiter, + captcha: captcha, }, nil } -// setLoggerLevel setting the log Level -func setLoggerLevel(logger *zap.Logger, level string) { - switch level { - case "error": - logger.WithOptions(zap.IncreaseLevel(zap.ErrorLevel)) - break - case "debug": - logger.WithOptions(zap.IncreaseLevel(zap.DebugLevel)) - break - case "warn": - logger.WithOptions(zap.IncreaseLevel(zap.WarnLevel)) - break - case "info": - logger.WithOptions(zap.IncreaseLevel(zap.InfoLevel)) - break - } -} - -// Start launches the HTTP and gRPC servers +// Start starting the Application func (a *App) Start(ctx context.Context) error { cfg := a.dynamicCfg.Get() - // Setup captcha - captcha, err := gocaptcha.Setup(a.dynamicCaptCfg) - if err != nil { - a.logger.Fatal("Failed to setup gocaptcha: ", zap.Error(err)) - return errors.New("setup gocaptcha failed") - } - captcha.DynamicCnf = a.dynamicCaptCfg - - // Register hot update callback - a.dynamicCaptCfg.RegisterHotCallbackHook("GENERATE_CAPTCHA", func(dnCfg *config2.DynamicCaptchaConfig) { - err = captcha.HotUpdate(dnCfg) - if err != nil { - a.logger.Fatal("Failed to hot update gocaptcha, without any change: ", zap.Error(err)) - } - }) - // Register service with discovery - var instanceID string - if a.discovery != nil { - instanceID = uuid.New().String() - httpPortInt, _ := strconv.Atoi(cfg.HTTPPort) - grpcPortInt, _ := strconv.Atoi(cfg.GRPCPort) - if err = a.discovery.Register(ctx, cfg.ServiceName, instanceID, "127.0.0.1", httpPortInt, grpcPortInt); err != nil { - return fmt.Errorf("failed to register service: %v", err) - } - go a.updateInstances(ctx, instanceID) + err := a.startDiscoveryRegister(ctx, &cfg) + if err != nil { + return err } // Service context svcCtx := common.NewSvcContext() - svcCtx.Cache = a.cache + svcCtx.CacheMgr = a.cacheMgr svcCtx.DynamicConfig = a.dynamicCfg svcCtx.Logger = a.logger - svcCtx.Captcha = captcha + svcCtx.Captcha = a.captcha // Start HTTP server if cfg.HTTPPort != "" && cfg.HTTPPort != "0" { - if err = a.setupHTTPServer(svcCtx, &cfg); err != nil { + if err = a.startHTTPServer(svcCtx, &cfg); err != nil { return err } } // Start gRPC server if cfg.GRPCPort != "" && cfg.GRPCPort != "0" { - if err = a.setupGRPCServer(svcCtx, &cfg); err != nil { + if err = a.startGRPCServer(svcCtx, &cfg); err != nil { return err } } @@ -294,11 +266,22 @@ func (a *App) Start(ctx context.Context) error { return nil } -// setupHttpServer start HTTP server -func (a *App) setupHTTPServer(svcCtx *common.SvcContext, cfg *config.Config) error { - // Register HTTP routes - handlers := server.NewHTTPHandlers(svcCtx) +// startDiscoveryRegister start service discovery register +func (a *App) startDiscoveryRegister(ctx context.Context, cfg *config.Config) error { + var instanceID string + if a.discovery != nil { + instanceID = uuid.New().String() + if err := a.discovery.Register(ctx, cfg.ServiceName, instanceID, "localhost", cfg.HTTPPort, cfg.GRPCPort); err != nil { + return fmt.Errorf("failed to register service: %v", err) + } + go a.watchServiceDiscoveryInstances(ctx, instanceID) + } + return nil +} +// startHTTPServer start HTTP server +func (a *App) startHTTPServer(svcCtx *common.SvcContext, cfg *config.Config) error { + handlers := server.NewHTTPHandlers(svcCtx) var middlewares = make([]middleware.HTTPMiddleware, 0) // Enable cross-domain resource @@ -315,85 +298,86 @@ func (a *App) setupHTTPServer(svcCtx *common.SvcContext, cfg *config.Config) err mwChain := middleware.NewChainHTTP(middlewares...) - // Logic Routes - http.Handle("/get-data", mwChain.Then(handlers.GetDataHandler)) - http.Handle("/check-data", mwChain.Then(handlers.CheckDataHandler)) - http.Handle("/check-status", mwChain.Then(handlers.CheckStatusHandler)) - http.Handle("/get-status-info", mwChain.Then(handlers.GetStatusInfoHandler)) - http.Handle("/del-status-data", mwChain.Then(handlers.DelStatusInfoHandler)) + http.Handle("/v1/get-data", mwChain.Then(handlers.GetDataHandler)) + http.Handle("/v1/check-data", mwChain.Then(handlers.CheckDataHandler)) + http.Handle("/v1/check-status", mwChain.Then(handlers.CheckStatusHandler)) + http.Handle("/v1/get-status-info", mwChain.Then(handlers.GetStatusInfoHandler)) + http.Handle("/v1/del-status-info", mwChain.Then(handlers.DelStatusInfoHandler)) http.Handle("/rate-limit", mwChain.Then(middleware.RateLimitHandler(a.limiter, a.logger))) - http.Handle("/manage/upload-resource", mwChain.Then(handlers.UploadResourceHandler)) - http.Handle("/manage/delete-resource", mwChain.Then(handlers.DeleteResourceHandler)) - http.Handle("/manage/get-resource-list", mwChain.Then(handlers.GetResourceListHandler)) - http.Handle("/manage/get-config", mwChain.Then(handlers.GetGoCaptchaConfigHandler)) - http.Handle("/manage/update-hot-config", mwChain.Then(handlers.UpdateHotGoCaptchaConfigHandler)) + http.Handle("/v1/manage/upload-resource", mwChain.Then(handlers.UploadResourceHandler)) + http.Handle("/v1/manage/delete-resource", mwChain.Then(handlers.DeleteResourceHandler)) + http.Handle("/v1/manage/get-resource-list", mwChain.Then(handlers.GetResourceListHandler)) + http.Handle("/v1/manage/get-config", mwChain.Then(handlers.GetGoCaptchaConfigHandler)) + http.Handle("/v1/manage/update-hot-config", mwChain.Then(handlers.UpdateHotGoCaptchaConfigHandler)) - // Start HTTP server a.httpServer = &http.Server{ Addr: ":" + cfg.HTTPPort, } + go func() { - a.logger.Info("Starting HTTP server", zap.String("port", cfg.HTTPPort)) + a.logger.Info("[App] Starting HTTP server", zap.String("port", cfg.HTTPPort)) if err := a.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - a.logger.Fatal("HTTP server failed", zap.Error(err)) + a.logger.Fatal("[App] HTTP server failed", zap.Error(err)) } }() return nil } -// setupGRPCServer start gRPC server -func (a *App) setupGRPCServer(svcCtx *common.SvcContext, cfg *config.Config) error { +// startGRPCServer start gRPC server +func (a *App) startGRPCServer(svcCtx *common.SvcContext, cfg *config.Config) error { lis, err := net.Listen("tcp", ":"+cfg.GRPCPort) if err != nil { return fmt.Errorf("failed to listen: %v", err) } - a.grpcServer = grpc.NewServer( - grpc.UnaryInterceptor(middleware.UnaryServerInterceptor(a.dynamicCfg, a.logger, a.cacheBreaker)), - ) + + interceptor := middleware.UnaryServerInterceptor(a.dynamicCfg, a.logger, a.cacheBreaker) + a.grpcServer = grpc.NewServer(grpc.UnaryInterceptor(interceptor)) proto.RegisterGoCaptchaServiceServer(a.grpcServer, server.NewGoCaptchaServer(svcCtx)) + go func() { - a.logger.Info("Starting gRPC server", zap.String("port", cfg.GRPCPort)) + a.logger.Info("[App] Starting gRPC server", zap.String("port", cfg.GRPCPort)) if err := a.grpcServer.Serve(lis); err != nil && err != grpc.ErrServerStopped { - a.logger.Fatal("gRPC server failed", zap.Error(err)) + a.logger.Fatal("[App] gRPC server failed", zap.Error(err)) } }() return nil } -// updateInstances periodically updates service instances -func (a *App) updateInstances(ctx context.Context, instanceID string) { - ticker := time.NewTicker(10 * time.Second) +// watchServiceDiscoveryInstances periodically updates service instances +func (a *App) watchServiceDiscoveryInstances(ctx context.Context, instanceID string) { + if a.discovery == nil { + return + } + cfg := a.dynamicCfg.Get() + ch, err := a.discovery.Watch(ctx, cfg.ServiceName) + if err != nil { + a.logger.Fatal("[App] Failed to service discovery watch", zap.Error(err)) + } - defer ticker.Stop() for { select { case <-ctx.Done(): if a.discovery != nil { - if err := a.discovery.Deregister(ctx, instanceID); err != nil { - a.logger.Error("Failed to deregister service", zap.Error(err)) + if err = a.discovery.Deregister(ctx, cfg.ServiceName, instanceID); err != nil { + a.logger.Error("[App] Failed to deregister service", zap.Error(err)) } } return - case <-ticker.C: - if a.discovery == nil { - continue - } - instances, err := a.discovery.Discover(ctx, cfg.ServiceName) - if err != nil { - a.logger.Error("Failed to discover instances", zap.Error(err)) - continue + case instances, ok := <-ch: + if !ok { + return } - a.logger.Info("Discovered instances", zap.Int("count", len(instances))) + a.logger.Info("[App] Discovered service instances", zap.Int("count", len(instances))) } } } // Shutdown gracefully stops the application func (a *App) Shutdown() { - a.logger.Info("Received shutdown signal, shutting down gracefully") + a.logger.Info("[App] Received shutdown signal, shutting down gracefully") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -403,70 +387,43 @@ func (a *App) Shutdown() { // Stop HTTP server if a.httpServer != nil { if err := a.httpServer.Shutdown(ctx); err != nil { - a.logger.Error("HTTP server shutdown error", zap.Error(err)) + a.logger.Error("[App] HTTP server shutdown error", zap.Error(err)) } else { - a.logger.Info("HTTP server shut down successfully") + a.logger.Info("[App] HTTP server shut down successfully") } } // Stop gRPC server if a.grpcServer != nil { a.grpcServer.GracefulStop() - a.logger.Info("gRPC server shut down successfully") + a.logger.Info("[App] gRPC server shut down successfully") } - // Close cache - if redisClient, ok := a.cache.(*cache.RedisClient); ok { - if err := redisClient.Close(); err != nil { - a.logger.Error("Redis client close error", zap.Error(err)) - } else { - a.logger.Info("Redis client closed successfully") - } - } - if memoryCache, ok := a.cache.(*cache.MemoryCache); ok { - memoryCache.Stop() - a.logger.Info("Memory cache stopped successfully") - } - if etcdClient, ok := a.cache.(*cache.EtcdClient); ok { - if err := etcdClient.Close(); err != nil { - a.logger.Error("etcd client close error", zap.Error(err)) - } else { - a.logger.Info("etcd client closed successfully") - } - } - if memcacheClient, ok := a.cache.(*cache.MemcacheClient); ok { - if err := memcacheClient.Close(); err != nil { - a.logger.Error("Memcached client close error", zap.Error(err)) - } else { - a.logger.Info("Memcached client closed successfully") - } + // Stop cache + err := a.cacheMgr.Close() + if err != nil { + a.logger.Error("[App] CacheManager client close error", zap.Error(err)) + } else { + a.logger.Info("[App] CacheManager client stopped successfully", zap.Error(err)) } - // Close service discovery + // Stop service discovery if a.discovery != nil { if err := a.discovery.Close(); err != nil { - a.logger.Error("Service discovery close error", zap.Error(err)) + a.logger.Error("[App] Service discovery close error", zap.Error(err)) } else { - a.logger.Info("Service discovery closed successfully") + a.logger.Info("[App] Service discovery closed successfully") } } - a.logger.Info("App service shutdown") -} - -// healthCheck performs a health check on HTTP and gRPC servers -func healthCheck(httpAddr, grpcAddr string) error { - resp, err := http.Get("http://localhost" + httpAddr + "/read?key=test") - if err != nil || resp.StatusCode != http.StatusNotFound { - return fmt.Errorf("HTTP health check failed: %v", err) - } - resp.Body.Close() - - conn, err := net.DialTimeout("tcp", "localhost"+grpcAddr, 1*time.Second) - if err != nil { - return fmt.Errorf("gRPC health check failed: %v", err) + // Stop config manager + if a.configManager != nil { + if err = a.configManager.Close(); err != nil { + a.logger.Error("[App] Config manager close error", zap.Error(err)) + } else { + a.logger.Info("[App] Config manager closed successfully") + } } - conn.Close() - return nil + a.logger.Info("[App] App service shutdown") } diff --git a/internal/app/setup.go b/internal/app/setup.go new file mode 100644 index 0000000..7c1bee3 --- /dev/null +++ b/internal/app/setup.go @@ -0,0 +1,361 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + "time" + + "github.com/wenlng/go-captcha-service/internal/cache" + "github.com/wenlng/go-captcha-service/internal/config" + config2 "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha/config" + "github.com/wenlng/go-service-link/dynaconfig" + "github.com/wenlng/go-service-link/dynaconfig/provider" + "github.com/wenlng/go-service-link/foundation/common" + "github.com/wenlng/go-service-link/servicediscovery" + "go.uber.org/zap" +) + +// setupServiceDiscovery .. +func setupServiceDiscovery(dCfg *config.DynamicConfig, logger *zap.Logger) (servicediscovery.ServiceDiscovery, error) { + cfg := dCfg.Get() + + var discovery servicediscovery.ServiceDiscovery + if !cfg.EnableServiceDiscovery { + return nil, nil + } + + var sdType servicediscovery.ServiceDiscoveryType = servicediscovery.ServiceDiscoveryTypeNone + switch cfg.ServiceDiscovery { + case ServiceDiscoveryTypeEtcd: + sdType = servicediscovery.ServiceDiscoveryTypeEtcd + break + case ServiceDiscoveryTypeConsul: + sdType = servicediscovery.ServiceDiscoveryTypeConsul + break + case ServiceDiscoveryTypeNacos: + sdType = servicediscovery.ServiceDiscoveryTypeNacos + break + case ServiceDiscoveryTypeZookeeper: + sdType = servicediscovery.ServiceDiscoveryTypeZookeeper + break + } + + sdCfg := servicediscovery.Config{ + Type: sdType, + Addrs: cfg.ServiceDiscoveryAddrs, + TTL: time.Duration(cfg.ServiceDiscoveryTTL) * time.Second, + KeepAlive: time.Duration(cfg.ServiceDiscoveryKeepAlive) * time.Second, + ServiceName: cfg.ServiceName, + MaxRetries: cfg.ServiceDiscoveryMaxRetries, + BaseRetryDelay: time.Duration(cfg.ServiceDiscoveryBaseRetryDelay) * time.Millisecond, + Username: cfg.ServiceDiscoveryUsername, + Password: cfg.ServiceDiscoveryPassword, + } + if cfg.ServiceDiscoveryTlsCertFile != "" && cfg.ServiceDiscoveryTlsKeyFile != "" && cfg.ServiceDiscoveryTlsCaFile != "" { + sdCfg.TlsConfig = &common.TLSConfig{ + Address: cfg.ServiceDiscoveryTlsAddress, + CertFile: cfg.ServiceDiscoveryTlsCertFile, + KeyFile: cfg.ServiceDiscoveryTlsKeyFile, + CAFile: cfg.ServiceDiscoveryTlsCaFile, + ServerName: cfg.ServiceDiscoveryTlsServerName, + } + } + + var err error + discovery, err = servicediscovery.NewServiceDiscovery(sdCfg) + if err != nil { + return nil, fmt.Errorf("failed to initialize service discovery: %v", err) + } + discovery.SetOutputLogCallback(func(logType servicediscovery.OutputLogType, message string) { + if logType == servicediscovery.OutputLogTypeError { + logger.Error("[AppSetup] Service discovery error: ", zap.String("message", message)) + } else if logType == servicediscovery.OutputLogTypeWarn { + logger.Warn("[AppSetup] Service discovery warn: ", zap.String("message", message)) + } else { + logger.Info("[AppSetup] Service discovery info: ", zap.String("message", message)) + } + }) + return discovery, err +} + +// setupDynamicConfig .. +func setupDynamicConfig(appDynaCfg *config.DynamicConfig, captDynaCfg *config2.DynamicCaptchaConfig, logger *zap.Logger) (*dynaconfig.ConfigManager, error) { + appCfg := appDynaCfg.Get() + captCfg := appDynaCfg.Get() + + if !appCfg.EnableDynamicConfig { + return nil, nil + } + + appConfigKey := "/config/go-captcha-service/app-config" + appConfigName := "go-captcha-service-app-config" + + captConfigKey := "/config/go-captcha-service/captcha-config" + captConfigName := "go-captcha-service-captcha-config" + + configs := make(map[string]*provider.Config) + configs[appConfigKey] = &provider.Config{ + Name: appConfigName, + Version: appDynaCfg.Get().ConfigVersion, + Content: appCfg, + ValidateCallback: func(config *provider.Config) (skip bool, err error) { + if config.Content == "" { + return false, fmt.Errorf("contnet must be not empty") + } + return true, nil + }, + } + + configs[captConfigKey] = &provider.Config{ + Name: captConfigName, + Version: captDynaCfg.Get().ConfigVersion, + Content: captCfg, + ValidateCallback: func(config *provider.Config) (skip bool, err error) { + if config.Content == "" { + return false, fmt.Errorf("contnet must be not empty") + } + return true, nil + }, + } + + keys := make([]string, 0) + for key, _ := range configs { + keys = append(keys, key) + } + + var sdType provider.ProviderType + switch appCfg.ServiceDiscovery { + case ServiceDiscoveryTypeEtcd: + sdType = provider.ProviderTypeEtcd + break + case ServiceDiscoveryTypeConsul: + sdType = provider.ProviderTypeConsul + break + case ServiceDiscoveryTypeNacos: + sdType = provider.ProviderTypeNacos + break + case ServiceDiscoveryTypeZookeeper: + sdType = provider.ProviderTypeZookeeper + break + } + + providerCfg := provider.ProviderConfig{ + Type: sdType, + Endpoints: strings.Split(appCfg.ServiceDiscoveryAddrs, ","), + Username: appCfg.ServiceDiscoveryUsername, + Password: appCfg.ServiceDiscoveryPassword, + } + if appCfg.ServiceDiscoveryTlsCertFile != "" && appCfg.ServiceDiscoveryTlsKeyFile != "" && appCfg.ServiceDiscoveryTlsCaFile != "" { + providerCfg.TlsConfig = &common.TLSConfig{ + Address: appCfg.ServiceDiscoveryTlsAddress, + CertFile: appCfg.ServiceDiscoveryTlsCertFile, + KeyFile: appCfg.ServiceDiscoveryTlsKeyFile, + CAFile: appCfg.ServiceDiscoveryTlsCaFile, + ServerName: appCfg.ServiceDiscoveryTlsServerName, + } + } + + manager, err := dynaconfig.NewConfigManager(dynaconfig.ConfigManagerParams{ + ProviderConfig: providerCfg, + Configs: configs, + }) + if err != nil { + return nil, fmt.Errorf("failed to create config manager, err: %v ", err) + } + + manager.SetOutputLogCallback(func(logType dynaconfig.OutputLogType, message string) { + if logType == dynaconfig.OutputLogTypeError { + logger.Error("[AppSetup] " + message) + } else if logType == dynaconfig.OutputLogTypeWarn { + logger.Warn("[AppSetup] " + message) + } else if logType == dynaconfig.OutputLogTypeDebug { + logger.Debug("[AppSetup] " + message) + } else { + logger.Info("[AppSetup] " + message) + } + }) + + manager.Subscribe(func(key string, pConf *provider.Config) error { + if pConf.Content == "" { + return nil + } + + if key == appConfigKey { + cConf := pConf.Content + var newConf config.Config + + captCnfStr, err := json.Marshal(cConf) + if err != nil { + logger.Error("[AppSetup.ConfigManager] Filed to json marshal app config: ", zap.Any("config", cConf)) + return nil + } + + if err = json.Unmarshal(captCnfStr, &newConf); err != nil { + logger.Error("[AppSetup.ConfigManager] Filed to json unmarshal app config: ", zap.Any("config", cConf)) + return nil + } + + err = appDynaCfg.Update(newConf) + if err != nil { + logger.Error("[AppSetup.ConfigManager] Filed to update app config", zap.Error(err)) + return nil + } + appDynaCfg.HandleHotCallback(config.HotCallbackTypeRemoteConfig) + } else if key == captConfigKey { + cConf := pConf.Content + var newConf config2.CaptchaConfig + + captCnfStr, err := json.Marshal(cConf) + if err != nil { + logger.Error("[AppSetup.ConfigManager] Filed to json marshal app config: ", zap.Any("config", cConf)) + return nil + } + + if err = json.Unmarshal(captCnfStr, &newConf); err != nil { + logger.Error("[AppSetup.ConfigManager] Filed to json unmarshal app config: ", zap.Any("config", cConf)) + return nil + } + + err = captDynaCfg.Update(newConf) + if err != nil { + logger.Error("[AppSetup.ConfigManager] Filed to update captcha config", zap.Error(err)) + } + captDynaCfg.HandleHotCallback(config2.HotCallbackTypeRemoteConfig) + } + + return nil + }) + + appDynaCfg.RegisterHotCallback("ASYNC_APP_CONFIG", func(dynamicConfig *config.DynamicConfig, callbackType config.HotCallbackType) { + if callbackType == config.HotCallbackTypeLocalConfigFile { + manager.RefreshConfig(context.Background(), appConfigKey, &provider.Config{ + Name: appConfigName, + Version: appDynaCfg.Get().ConfigVersion, + Content: appDynaCfg.Get(), + ValidateCallback: func(config *provider.Config) (skip bool, err error) { + if config.Content == "" { + return false, fmt.Errorf("contnet must be not empty") + } + return true, nil + }, + }) + } + }) + + captDynaCfg.RegisterHotCallback("ASYNC_CAPTCHA_CONFIG", func(captchaConfig *config2.DynamicCaptchaConfig, callbackType config2.HotCallbackType) { + if callbackType == config2.HotCallbackTypeLocalConfigFile { + manager.RefreshConfig(context.Background(), captConfigKey, &provider.Config{ + Name: captConfigName, + Version: captDynaCfg.Get().ConfigVersion, + Content: captDynaCfg.Get(), + ValidateCallback: func(config *provider.Config) (skip bool, err error) { + if config.Content == "" { + return false, fmt.Errorf("contnet must be not empty") + } + return true, nil + }, + }) + } + }) + + manager.ASyncConfig(context.Background()) + + if err = manager.Watch(); err != nil { + return nil, fmt.Errorf("failed to start watch: %v ", err) + } + + //////////////////////// testing ///////////////////////// + // Testing read the configuration content in real time + //go func() { + // for { + // time.Sleep(10 * time.Second) + // for _, key := range keys { + // c := manager.GetLocalConfig(key) + // fmt.Printf("+++++++ >>> Current config manager -> config for %s: %+v\n\n", key, c) + // dc := appDynaCfg.Get() + // fmt.Printf("------- >>> Current app local -> config for %s: %+v\n\n\n\n", key, dc) + // } + // } + //}() + ///////////////////////////////////////////////// + + return manager, nil +} + +// setupLoggerLevel setting the log Level +func setupLoggerLevel(logger *zap.Logger, level string) { + switch level { + case "error": + logger.WithOptions(zap.IncreaseLevel(zap.ErrorLevel)) + break + case "debug": + logger.WithOptions(zap.IncreaseLevel(zap.DebugLevel)) + break + case "warn": + logger.WithOptions(zap.IncreaseLevel(zap.WarnLevel)) + break + case "info": + logger.WithOptions(zap.IncreaseLevel(zap.InfoLevel)) + break + } +} + +// setupHealthCheck performs a health check on HTTP and gRPC servers +func setupHealthCheck(httpAddr, grpcAddr string) error { + resp, err := http.Get("http://localhost" + httpAddr + "/read?key=test") + if err != nil || resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("HTTP health check failed: %v", err) + } + resp.Body.Close() + + conn, err := net.DialTimeout("tcp", "localhost"+grpcAddr, 1*time.Second) + if err != nil { + return fmt.Errorf("gRPC health check failed: %v", err) + } + conn.Close() + + return nil +} + +// cfg ... +func setupCacheManager(dcfg *config.DynamicConfig, logger *zap.Logger) (*cache.CacheManager, error) { + cfg := dcfg.Get() + // Initialize cache + ttl := time.Duration(cfg.CacheTTL) * time.Second + cleanInt := time.Duration(10) * time.Second // MemoryCache cleanupInterval + + cacheMgr, err := cache.NewCacheManager(&cache.CacheMgrParams{ + Type: cache.CacheType(cfg.CacheType), + RedisAddrs: cfg.RedisAddrs, + MemCacheAddrs: cfg.MemcacheAddrs, + EtcdAddrs: cfg.EtcdAddrs, + KeyPrefix: cfg.CacheKeyPrefix, + Ttl: ttl, + CleanInt: cleanInt, + }) + + if err != nil { + return nil, err + } + dcfg.RegisterHotCallback("UPDATE_SETUP_CACHE", func(dnCfg *config.DynamicConfig, hotType config.HotCallbackType) { + newCfg := dnCfg.Get() + err = cacheMgr.Setup(&cache.CacheMgrParams{ + Type: cache.CacheType(newCfg.CacheType), + RedisAddrs: newCfg.RedisAddrs, + MemCacheAddrs: newCfg.MemcacheAddrs, + EtcdAddrs: newCfg.EtcdAddrs, + KeyPrefix: newCfg.CacheKeyPrefix, + Ttl: ttl, + CleanInt: cleanInt, + }) + if err != nil { + logger.Error("[AppSetup] Setup cache manager", zap.Error(err)) + } + }) + + return cacheMgr, err +} diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 95c6117..9b7d9b7 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -1,7 +1,26 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package cache import ( "context" + "fmt" + "sync" + "time" +) + +type CacheType string + +// CacheType . +const ( + CacheTypeRedis CacheType = "redis" + CacheTypeMemory = "memory" + CacheTypeEtcd = "etcd" + CacheTypeMemcache = "memcache" ) // Cache defines the interface for cache operations @@ -12,7 +31,133 @@ type Cache interface { Close() error } +// CaptCacheData .. type CaptCacheData struct { Data interface{} `json:"data"` + Type int `json:"type"` Status int `json:"status"` } + +// CacheManager .. +type CacheManager struct { + cache Cache + mu sync.RWMutex + cType CacheType + cAddress string + cKeyPrefix string + cTtl time.Duration + cCleanInt time.Duration +} + +// CacheMgrParams .. +type CacheMgrParams struct { + Type CacheType + RedisAddrs string + EtcdAddrs string + MemCacheAddrs string + KeyPrefix string + Ttl time.Duration + CleanInt time.Duration +} + +// NewCacheManager .. +func NewCacheManager(arg *CacheMgrParams) (*CacheManager, error) { + cm := &CacheManager{} + err := cm.Setup(arg) + return cm, err +} + +// GetCache .. +func (cm *CacheManager) GetCache() Cache { + cm.mu.RLock() + defer cm.mu.RUnlock() + return cm.cache +} + +// Setup initialize the cache +func (cm *CacheManager) Setup(arg *CacheMgrParams) error { + var curCache Cache + var err error + var curAddrs string + switch arg.Type { + case CacheTypeRedis: + curAddrs = arg.RedisAddrs + if cm.cAddress == curAddrs && cm.cKeyPrefix == arg.KeyPrefix && cm.cTtl == arg.Ttl { + return nil + } + curCache, err = NewRedisClient(arg.RedisAddrs, arg.KeyPrefix, arg.Ttl) + if err != nil { + return fmt.Errorf("failed to initialize Redis: %v", err) + } + case CacheTypeMemory: + if cm.cKeyPrefix == arg.KeyPrefix && cm.cTtl == arg.Ttl && cm.cCleanInt == arg.CleanInt { + return nil + } + curCache = NewMemoryCache(arg.KeyPrefix, arg.Ttl, arg.CleanInt) + case CacheTypeEtcd: + curAddrs = arg.EtcdAddrs + if cm.cAddress == curAddrs && cm.cKeyPrefix == arg.KeyPrefix && cm.cTtl == arg.Ttl { + return nil + } + curCache, err = NewEtcdClient(arg.EtcdAddrs, arg.KeyPrefix, arg.Ttl) + if err != nil { + return fmt.Errorf("failed to initialize Etcd: %v", err) + } + case CacheTypeMemcache: + if cm.cAddress == curAddrs && cm.cKeyPrefix == arg.KeyPrefix && cm.cTtl == arg.Ttl { + return nil + } + curAddrs = arg.MemCacheAddrs + curCache, err = NewMemcacheClient(arg.MemCacheAddrs, arg.KeyPrefix, arg.Ttl) + if err != nil { + return fmt.Errorf("failed to initialize Memcached: %v", err) + } + default: + return fmt.Errorf("invalid cache type: %v", arg.Type) + } + + cm.cType = arg.Type + cm.cAddress = curAddrs + cm.cKeyPrefix = arg.KeyPrefix + cm.cTtl = arg.Ttl + cm.cCleanInt = arg.CleanInt + + cm.mu.Lock() + cm.cache = curCache + cm.mu.Unlock() + + return nil +} + +// Close .. +func (cm *CacheManager) Close() error { + cm.mu.RLock() + defer cm.mu.RUnlock() + + if redisClient, ok := cm.cache.(*RedisClient); ok { + if err := redisClient.Close(); err != nil { + return fmt.Errorf("redis client close error: %v", err) + } + return nil + } + + if memoryCache, ok := cm.cache.(*MemoryCache); ok { + memoryCache.Stop() + return nil + } + + if etcdClient, ok := cm.cache.(*EtcdClient); ok { + if err := etcdClient.Close(); err != nil { + return fmt.Errorf("etcd client close error: %v", err) + } + return nil + } + + if memcacheClient, ok := cm.cache.(*MemcacheClient); ok { + if err := memcacheClient.Close(); err != nil { + return fmt.Errorf("memcached client close error: %v", err) + } + } + + return nil +} diff --git a/internal/cache/etcd_client.go b/internal/cache/etcd_client.go index bda05e9..95c819b 100644 --- a/internal/cache/etcd_client.go +++ b/internal/cache/etcd_client.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package cache import ( @@ -17,7 +23,7 @@ type EtcdClient struct { ttl time.Duration } -// NewEtcdClient creates a new etcd client +// NewEtcdClient .. func NewEtcdClient(addrs, prefix string, ttl time.Duration) (*EtcdClient, error) { client, err := clientv3.New(clientv3.Config{ Endpoints: []string{addrs}, @@ -68,7 +74,9 @@ func (c *EtcdClient) SetCache(ctx context.Context, key, value string) error { return nil } +// DeleteCache .. func (c *EtcdClient) DeleteCache(ctx context.Context, key string) error { + key = c.prefix + key _, err := c.client.Delete(ctx, key) if err != nil { return fmt.Errorf("etcd delete error: %v", err) @@ -76,7 +84,7 @@ func (c *EtcdClient) DeleteCache(ctx context.Context, key string) error { return nil } -// Close closes the etcd client +// Close .. func (c *EtcdClient) Close() error { return c.client.Close() } diff --git a/internal/cache/memcache_client.go b/internal/cache/memcache_client.go index 875d0ef..cb55dab 100644 --- a/internal/cache/memcache_client.go +++ b/internal/cache/memcache_client.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package cache import ( @@ -15,7 +21,7 @@ type MemcacheClient struct { ttl time.Duration } -// NewMemcacheClient creates a new Memcached client +// NewMemcacheClient .. func NewMemcacheClient(addrs, prefix string, ttl time.Duration) (*MemcacheClient, error) { client := memcache.New(addrs) return &MemcacheClient{client: client, prefix: prefix, ttl: ttl}, nil @@ -45,7 +51,9 @@ func (c *MemcacheClient) SetCache(ctx context.Context, key, value string) error return c.client.Set(item) } +// DeleteCache .. func (c *MemcacheClient) DeleteCache(ctx context.Context, key string) error { + key = c.prefix + key err := c.client.Delete(key) if err != nil && err != memcache.ErrCacheMiss { return fmt.Errorf("memcache delete error: %v", err) @@ -53,7 +61,7 @@ func (c *MemcacheClient) DeleteCache(ctx context.Context, key string) error { return nil } -// Close closes the Memcached client +// Close .. func (c *MemcacheClient) Close() error { return nil } diff --git a/internal/cache/memory_cache.go b/internal/cache/memory_cache.go index 5cb6987..8c19410 100644 --- a/internal/cache/memory_cache.go +++ b/internal/cache/memory_cache.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package cache import ( @@ -21,7 +27,7 @@ type cacheItem struct { expiration int64 } -// NewMemoryCache creates a new memory cache +// NewMemoryCache .. func NewMemoryCache(prefix string, ttl, cleanupInterval time.Duration) *MemoryCache { cache := &MemoryCache{ items: make(map[string]cacheItem), @@ -78,20 +84,22 @@ func (c *MemoryCache) SetCache(ctx context.Context, key, value string) error { return nil } +// DeleteCache delete a value in memory cache func (c *MemoryCache) DeleteCache(ctx context.Context, key string) error { + key = c.prefix + key c.mu.Lock() defer c.mu.Unlock() delete(c.items, key) return nil } -// Close stops the memory cache +// Close .. func (c *MemoryCache) Close() error { c.Stop() return nil } -// Stop stops the memory cache cleanup routine +// Stop .. func (c *MemoryCache) Stop() { close(c.stop) } diff --git a/internal/cache/redis_client.go b/internal/cache/redis_client.go index 3041f42..7bd5260 100644 --- a/internal/cache/redis_client.go +++ b/internal/cache/redis_client.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package cache import ( @@ -15,7 +21,7 @@ type RedisClient struct { ttl time.Duration } -// NewRedisClient creates a new Redis client +// NewRedisClient .. func NewRedisClient(addrs, prefix string, ttl time.Duration) (*RedisClient, error) { client := redis.NewClient(&redis.Options{ Addr: addrs, @@ -43,8 +49,9 @@ func (c *RedisClient) SetCache(ctx context.Context, key, value string) error { return c.client.Set(ctx, key, value, c.ttl).Err() } -// DeleteCache stores a value in Redis +// DeleteCache delete a value in Redis func (c *RedisClient) DeleteCache(ctx context.Context, key string) error { + key = c.prefix + key err := c.client.Del(ctx, key).Err() if err != nil && err != redis.Nil { return fmt.Errorf("redis delete error: %v", err) @@ -52,7 +59,7 @@ func (c *RedisClient) DeleteCache(ctx context.Context, key string) error { return nil } -// Close closes the Redis client +// Close .. func (c *RedisClient) Close() error { return c.client.Close() } diff --git a/internal/common/svc_context.go b/internal/common/svc_context.go index 76aba3a..05611de 100644 --- a/internal/common/svc_context.go +++ b/internal/common/svc_context.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package common import ( @@ -7,13 +13,15 @@ import ( "go.uber.org/zap" ) +// SvcContext service context type SvcContext struct { - Cache cache.Cache + CacheMgr *cache.CacheManager DynamicConfig *config.DynamicConfig Logger *zap.Logger Captcha *gocaptcha.GoCaptcha } +// NewSvcContext .. func NewSvcContext() *SvcContext { return &SvcContext{} } diff --git a/internal/config/config.go b/internal/config/config.go index 6c1cb5e..1343c58 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,50 +11,101 @@ import ( "sync" "github.com/fsnotify/fsnotify" + "github.com/wenlng/go-captcha-service/internal/helper" ) // Config defines the configuration structure for the application type Config struct { - ServiceName string `json:"service_name"` - HTTPPort string `json:"http_port"` - GRPCPort string `json:"grpc_port"` - RedisAddrs string `json:"redis_addrs"` - EtcdAddrs string `json:"etcd_addrs"` - MemcacheAddrs string `json:"memcache_addrs"` - CacheType string `json:"cache_type"` // redis, memory, etcd, memcache - CacheTTL int `json:"cache_ttl"` // seconds - CacheCleanupInt int `json:"cache_cleanup_interval"` // seconds - CacheKeyPrefix string `json:"cache_key_prefix"` - ServiceDiscovery string `json:"service_discovery"` // etcd, zookeeper, consul, nacos - ServiceDiscoveryAddrs string `json:"service_discovery_addrs"` - RateLimitQPS int `json:"rate_limit_qps"` - RateLimitBurst int `json:"rate_limit_burst"` - LoadBalancer string `json:"load_balancer"` // round-robin, consistent-hash - EnableCors bool `json:"enable_cors"` - APIKeys []string `json:"api_keys"` - LogLevel string `json:"log_level"` // error, debug, info, none + ConfigVersion int64 `json:"config_version"` + ServiceName string `json:"service_name"` + HTTPPort string `json:"http_port"` + GRPCPort string `json:"grpc_port"` + RedisAddrs string `json:"redis_addrs"` + EtcdAddrs string `json:"etcd_addrs"` + MemcacheAddrs string `json:"memcache_addrs"` + CacheType string `json:"cache_type"` // redis, memory, etcd, memcache + CacheTTL int `json:"cache_ttl"` // seconds + CacheKeyPrefix string `json:"cache_key_prefix"` + RateLimitQPS int `json:"rate_limit_qps"` + RateLimitBurst int `json:"rate_limit_burst"` + EnableCors bool `json:"enable_cors"` + APIKeys []string `json:"api_keys"` + LogLevel string `json:"log_level"` // error, debug, info, none + + EnableDynamicConfig bool `json:"enable_dynamic_config"` + EnableServiceDiscovery bool `json:"enable_service_discovery"` + ServiceDiscovery string `json:"service_discovery"` // etcd, zookeeper, consul, nacos + ServiceDiscoveryAddrs string `json:"service_discovery_addrs"` + ServiceDiscoveryTTL int `json:"service_discovery_ttl"` + ServiceDiscoveryKeepAlive int `json:"service_discovery_keep_alive"` + ServiceDiscoveryMaxRetries int `json:"service_discovery_max_retries"` + ServiceDiscoveryBaseRetryDelay int `json:"service_discovery_base_retry_delay"` + ServiceDiscoveryUsername string `json:"service_discovery_username"` + ServiceDiscoveryPassword string `json:"service_discovery_password"` + ServiceDiscoveryTlsServerName string `json:"service_discovery_tls_server_name"` + ServiceDiscoveryTlsAddress string `json:"service_discovery_tls_address"` + ServiceDiscoveryTlsCertFile string `json:"service_discovery_tls_cert_file"` + ServiceDiscoveryTlsKeyFile string `json:"service_discovery_tls_key_file"` + ServiceDiscoveryTlsCaFile string `json:"service_discovery_tls_ca_file"` } // DynamicConfig . type DynamicConfig struct { - Config Config - mu sync.RWMutex - hotCbsHooks map[string]HandleHotCallbackHookFnc + Config Config + mu sync.RWMutex + hotCbsHooks map[string]HandleHotCallbackFunc + outputLogCbs helper.OutputLogCallback } -type HandleHotCallbackHookFnc = func(*DynamicConfig) +// HotCallbackType .. +type HotCallbackType int + +const ( + HotCallbackTypeLocalConfigFile HotCallbackType = 1 + HotCallbackTypeRemoteConfig = 2 +) + +// HandleHotCallbackFunc .. +type HandleHotCallbackFunc = func(*DynamicConfig, HotCallbackType) // NewDynamicConfig . -func NewDynamicConfig(file string) (*DynamicConfig, error) { - cfg, err := Load(file) - if err != nil { - return nil, err +func NewDynamicConfig(file string, isWatchFile bool) (*DynamicConfig, error) { + cfg := DefaultConfig() + var err error + if file != "" { + cfg, err = Load(file) + if err != nil { + return nil, err + } + } + + dc := &DynamicConfig{Config: cfg, hotCbsHooks: make(map[string]HandleHotCallbackFunc)} + + if isWatchFile { + go dc.watchFile(file) } - dc := &DynamicConfig{Config: cfg, hotCbsHooks: make(map[string]HandleHotCallbackHookFnc)} - go dc.watchFile(file) + return dc, nil } +// DefaultDynamicConfig . +func DefaultDynamicConfig() *DynamicConfig { + cfg := DefaultConfig() + return &DynamicConfig{Config: cfg, hotCbsHooks: make(map[string]HandleHotCallbackFunc)} +} + +// SetOutputLogCallback Set the log out hook function +func (dc *DynamicConfig) SetOutputLogCallback(outputLogCbs helper.OutputLogCallback) { + dc.outputLogCbs = outputLogCbs +} + +// outLog .. +func (dc *DynamicConfig) outLog(logType helper.OutputLogType, message string) { + if dc.outputLogCbs != nil { + dc.outputLogCbs(logType, message) + } +} + // Get retrieves the current configuration func (dc *DynamicConfig) Get() Config { dc.mu.RLock() @@ -62,6 +113,33 @@ func (dc *DynamicConfig) Get() Config { return dc.Config } +// MarshalConfig .. +func (dc *DynamicConfig) MarshalConfig() (string, error) { + dc.mu.RLock() + cByte, err := json.Marshal(dc.Config) + if err != nil { + return "", err + } + dc.mu.RUnlock() + + return string(cByte), nil +} + +// UnMarshalConfig .. +func (dc *DynamicConfig) UnMarshalConfig(str string) error { + var config Config + err := json.Unmarshal([]byte(str), &config) + if err != nil { + return err + } + + dc.mu.Lock() + dc.Config = config + dc.mu.Unlock() + + return nil +} + // Update updates the configuration func (dc *DynamicConfig) Update(cfg Config) error { if err := Validate(cfg); err != nil { @@ -73,47 +151,76 @@ func (dc *DynamicConfig) Update(cfg Config) error { return nil } -// RegisterHotCallbackHook callback when updating configuration -func (dc *DynamicConfig) RegisterHotCallbackHook(key string, callback HandleHotCallbackHookFnc) { +// RegisterHotCallback callback when updating configuration +func (dc *DynamicConfig) RegisterHotCallback(key string, callback HandleHotCallbackFunc) { if _, ok := dc.hotCbsHooks[key]; !ok { dc.hotCbsHooks[key] = callback } } -// UnRegisterHotCallbackHook callback when updating configuration -func (dc *DynamicConfig) UnRegisterHotCallbackHook(key string) { +// UnRegisterHotCallback callback when updating configuration +func (dc *DynamicConfig) UnRegisterHotCallback(key string) { if _, ok := dc.hotCbsHooks[key]; !ok { delete(dc.hotCbsHooks, key) } } -// HandleHotCallbackHook . -func (dc *DynamicConfig) HandleHotCallbackHook() { +// HandleHotCallback . +func (dc *DynamicConfig) HandleHotCallback(hotType HotCallbackType) { for _, fnc := range dc.hotCbsHooks { if fnc != nil { - fnc(dc) + fnc(dc, hotType) } } } +// HotUpdate .. +func (dc *DynamicConfig) HotUpdate(cfg Config) error { + if err := Validate(cfg); err != nil { + return err + } + dc.mu.Lock() + defer dc.mu.Unlock() + + // Update config fields + dc.Config.ConfigVersion = cfg.ConfigVersion + dc.Config.APIKeys = cfg.APIKeys + dc.Config.LogLevel = cfg.LogLevel + dc.Config.RedisAddrs = cfg.RedisAddrs + dc.Config.EtcdAddrs = cfg.EtcdAddrs + dc.Config.MemcacheAddrs = cfg.MemcacheAddrs + dc.Config.CacheType = cfg.CacheType + dc.Config.CacheTTL = cfg.CacheTTL + dc.Config.CacheKeyPrefix = cfg.CacheKeyPrefix + + if cfg.RateLimitQPS > 0 { + dc.Config.RateLimitQPS = cfg.RateLimitQPS + } + if cfg.RateLimitBurst > 0 { + dc.Config.RateLimitBurst = cfg.RateLimitBurst + } + + return nil +} + // watchFile monitors the Config file for changes func (dc *DynamicConfig) watchFile(file string) { watcher, err := fsnotify.NewWatcher() if err != nil { - fmt.Fprintf(os.Stderr, "Failed to create watcher: %v\n", err) + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[Config] Failed to create watcher, err: %v", err)) return } defer watcher.Close() absPath, err := filepath.Abs(file) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to get absolute path: %v\n", err) + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[Config] Failed to get absolute path, err: %v", err)) return } dir := filepath.Dir(absPath) if err := watcher.Add(dir); err != nil { - fmt.Fprintf(os.Stderr, "Failed to watch directory: %v\n", err) + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[Config] Failed to watch directory, err: %v", err)) return } @@ -126,22 +233,22 @@ func (dc *DynamicConfig) watchFile(file string) { if event.Name == absPath && (event.Op&fsnotify.Write == fsnotify.Write) { cfg, err := Load(file) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to reload Config: %v\n", err) + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[Config] Failed to reload Config, err: %v", err)) continue } - if err := dc.Update(cfg); err != nil { - fmt.Fprintf(os.Stderr, "Failed to update Config: %v\n", err) + if err = dc.HotUpdate(cfg); err != nil { + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[Config] Failed to update Config, err: %v", err)) continue } - dc.HandleHotCallbackHook() - fmt.Printf("Configuration reloaded successfully\n") + dc.HandleHotCallback(HotCallbackTypeLocalConfigFile) + dc.outLog(helper.OutputLogTypeInfo, "[Config] Configuration reloaded successfully") } case err, ok := <-watcher.Errors: if !ok { return } - fmt.Fprintf(os.Stderr, "Watcher error: %v\n", err) + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[Config] Failed to watcher, err: %v", err)) } } } @@ -196,9 +303,6 @@ func Validate(config Config) error { if config.CacheTTL <= 0 { return fmt.Errorf("cache_ttl must be positive: %d", config.CacheTTL) } - if config.CacheCleanupInt <= 0 && config.CacheType == "memory" { - return fmt.Errorf("cache_cleanup_interval must be positive for memory cache: %d", config.CacheCleanupInt) - } validDiscoveryTypes := map[string]bool{ "etcd": true, @@ -229,14 +333,6 @@ func Validate(config Config) error { } } - validBalancerTypes := map[string]bool{ - "round-robin": true, - "consistent-hash": true, - } - if config.LoadBalancer != "" && !validBalancerTypes[config.LoadBalancer] { - return fmt.Errorf("invalid load_balancer: %s, must be round-robin or consistent-hash", config.LoadBalancer) - } - return nil } @@ -281,9 +377,6 @@ func MergeWithFlags(config Config, flags map[string]interface{}) Config { if v, ok := flags["cache-ttl"].(int); ok && v != 0 { config.CacheTTL = v } - if v, ok := flags["cache-cleanup-interval"].(int); ok && v != 0 { - config.CacheCleanupInt = v - } if v, ok := flags["cache-key-prefix"].(string); ok && v != "" { config.CacheKeyPrefix = v } @@ -293,43 +386,82 @@ func MergeWithFlags(config Config, flags map[string]interface{}) Config { if v, ok := flags["service-discovery-addrs"].(string); ok && v != "" { config.ServiceDiscoveryAddrs = v } + if v, ok := flags["service-discovery-ttl"].(int); ok && v != 0 { + config.ServiceDiscoveryTTL = v + } + if v, ok := flags["service-discovery-keep-alive"].(int); ok && v != 0 { + config.ServiceDiscoveryKeepAlive = v + } + if v, ok := flags["service-discovery-max-retries"].(int); ok && v != 0 { + config.ServiceDiscoveryMaxRetries = v + } + if v, ok := flags["service-discovery-base-retry-delay"].(int); ok && v != 0 { + config.ServiceDiscoveryBaseRetryDelay = v + } + if v, ok := flags["service-discovery-username"].(string); ok && v != "" { + config.ServiceDiscoveryUsername = v + } + if v, ok := flags["service-discovery-password"].(string); ok && v != "" { + config.ServiceDiscoveryPassword = v + } + if v, ok := flags["service-discovery-tls-server-name"].(string); ok && v != "" { + config.ServiceDiscoveryTlsServerName = v + } + if v, ok := flags["service-discovery-tls-address"].(string); ok && v != "" { + config.ServiceDiscoveryTlsAddress = v + } + if v, ok := flags["service-discovery-tls-cert-file"].(string); ok && v != "" { + config.ServiceDiscoveryTlsCertFile = v + } + if v, ok := flags["service-discovery-tls-key-file"].(string); ok && v != "" { + config.ServiceDiscoveryTlsKeyFile = v + } + if v, ok := flags["service-discovery-tls-ca-file"].(string); ok && v != "" { + config.ServiceDiscoveryTlsCaFile = v + } if v, ok := flags["rate-limit-qps"].(int); ok && v != 0 { config.RateLimitQPS = v } if v, ok := flags["rate-limit-burst"].(int); ok && v != 0 { config.RateLimitBurst = v } - if v, ok := flags["load-balancer"].(string); ok && v != "" { - config.LoadBalancer = v - } if v, ok := flags["api-keys"].(string); ok && v != "" { config.APIKeys = strings.Split(v, ",") } if v, ok := flags["log-level"].(string); ok && v != "" { config.LogLevel = v } + if v, ok := flags["enable-dynamic-config"].(bool); ok && !config.EnableDynamicConfig { + config.EnableDynamicConfig = v + } + if v, ok := flags["enable-service-discovery"].(bool); ok && !config.EnableServiceDiscovery { + config.EnableServiceDiscovery = v + } + if v, ok := flags["enable-cors"].(bool); ok && !config.EnableCors { + config.EnableCors = v + } return config } func DefaultConfig() Config { return Config{ - ServiceName: "go-captcha-service", - HTTPPort: "8080", - GRPCPort: "50051", - RedisAddrs: "localhost:6379", - EtcdAddrs: "localhost:2379", - MemcacheAddrs: "localhost:11211", - CacheType: "memory", - CacheTTL: 60, - CacheCleanupInt: 10, - CacheKeyPrefix: "GO_CAPTCHA_DATA", - ServiceDiscovery: "", - ServiceDiscoveryAddrs: "localhost:2379", - RateLimitQPS: 1000, - RateLimitBurst: 1000, - LoadBalancer: "round-robin", - EnableCors: false, - APIKeys: []string{"my-secret-key-123"}, - LogLevel: "info", + ServiceName: "go-captcha-service", + HTTPPort: "8080", + GRPCPort: "50051", + RedisAddrs: "localhost:6379", + EtcdAddrs: "localhost:2379", + MemcacheAddrs: "localhost:11211", + CacheType: "memory", + CacheTTL: 60, + CacheKeyPrefix: "GO_CAPTCHA_DATA:", + EnableDynamicConfig: false, + EnableServiceDiscovery: false, + ServiceDiscovery: "", + ServiceDiscoveryAddrs: "localhost:2379", + RateLimitQPS: 1000, + RateLimitBurst: 1000, + EnableCors: false, + APIKeys: []string{"my-secret-key-123"}, + LogLevel: "info", } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index e7fa1a7..57ca9e9 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -48,12 +48,10 @@ func TestValidate(t *testing.T) { RedisAddrs: "localhost:6379", CacheType: "redis", CacheTTL: 60, - CacheCleanupInt: 10, ServiceDiscovery: "etcd", ServiceDiscoveryAddrs: "localhost:2379", RateLimitQPS: 1000, RateLimitBurst: 1000, - LoadBalancer: "round-robin", APIKeys: []string{"key1"}, } assert.NoError(t, Validate(config)) @@ -91,7 +89,7 @@ func TestDynamicConfig(t *testing.T) { err = os.WriteFile(configPath, []byte(configContent), 0644) assert.NoError(t, err) - dc, err := NewDynamicConfig(configPath) + dc, err := NewDynamicConfig(configPath, false) assert.NoError(t, err) cfg := dc.Get() diff --git a/internal/consts/consts.go b/internal/consts/consts.go index b938369..51350aa 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package consts // Type diff --git a/internal/helper/helper.go b/internal/helper/helper.go index 302fd5d..65cba2b 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package helper import ( diff --git a/internal/helper/outlog.go b/internal/helper/outlog.go new file mode 100644 index 0000000..7f28644 --- /dev/null +++ b/internal/helper/outlog.go @@ -0,0 +1,12 @@ +package helper + +type OutputLogType string + +const ( + OutputLogTypeWarn OutputLogType = "warn" + OutputLogTypeInfo = "info" + OutputLogTypeError = "error" + OutputLogTypeDebug = "debug" +) + +type OutputLogCallback = func(logType OutputLogType, message string) diff --git a/internal/helper/path.go b/internal/helper/path.go index e01eff1..df20cb8 100644 --- a/internal/helper/path.go +++ b/internal/helper/path.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package helper import "path" diff --git a/internal/load_balancer/consistent_hash.go b/internal/load_balancer/consistent_hash.go deleted file mode 100644 index 26ef55c..0000000 --- a/internal/load_balancer/consistent_hash.go +++ /dev/null @@ -1,24 +0,0 @@ -package load_balancer - -import ( - "fmt" - - "github.com/wenlng/go-captcha-service/internal/service_discovery" -) - -// ConsistentHash . -type ConsistentHash struct{} - -// NewConsistentHash . -func NewConsistentHash() *ConsistentHash { - return &ConsistentHash{} -} - -// Select selects an instance using consistent hashing -func (lb *ConsistentHash) Select(instances []service_discovery.Instance) (service_discovery.Instance, error) { - if len(instances) == 0 { - return service_discovery.Instance{}, fmt.Errorf("no instances available") - } - // select first instance - return instances[0], nil -} diff --git a/internal/load_balancer/load_balancer.go b/internal/load_balancer/load_balancer.go deleted file mode 100644 index 2570ae1..0000000 --- a/internal/load_balancer/load_balancer.go +++ /dev/null @@ -1,10 +0,0 @@ -package load_balancer - -import ( - "github.com/wenlng/go-captcha-service/internal/service_discovery" -) - -// LoadBalancer . -type LoadBalancer interface { - Select(instances []service_discovery.Instance) (service_discovery.Instance, error) -} diff --git a/internal/load_balancer/load_balancer_test.go b/internal/load_balancer/load_balancer_test.go deleted file mode 100644 index a17bec5..0000000 --- a/internal/load_balancer/load_balancer_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package load_balancer - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/wenlng/go-captcha-service/internal/service_discovery" -) - -func TestRoundRobin(t *testing.T) { - lb := NewRoundRobin() - instances := []service_discovery.Instance{ - {InstanceID: "1", Host: "127.0.0.1", HTTPPort: 8080, GRPCPort: 50051}, - {InstanceID: "2", Host: "127.0.0.2", HTTPPort: 8081, GRPCPort: 50052}, - } - - instance, err := lb.Select(instances) - assert.NoError(t, err) - assert.Equal(t, "1", instance.InstanceID) - - instance, err = lb.Select(instances) - assert.NoError(t, err) - assert.Equal(t, "2", instance.InstanceID) - - instance, err = lb.Select(instances) - assert.NoError(t, err) - assert.Equal(t, "1", instance.InstanceID) -} - -func TestConsistentHash(t *testing.T) { - lb := NewConsistentHash() - instances := []service_discovery.Instance{ - {InstanceID: "1", Host: "127.0.0.1", HTTPPort: 8080, GRPCPort: 50051}, - } - - instance, err := lb.Select(instances) - assert.NoError(t, err) - assert.Equal(t, "1", instance.InstanceID) - - _, err = lb.Select([]service_discovery.Instance{}) - assert.Error(t, err) -} diff --git a/internal/load_balancer/round_robin.go b/internal/load_balancer/round_robin.go deleted file mode 100644 index 10d03e2..0000000 --- a/internal/load_balancer/round_robin.go +++ /dev/null @@ -1,26 +0,0 @@ -package load_balancer - -import ( - "fmt" - - "github.com/wenlng/go-captcha-service/internal/service_discovery" -) - -// RoundRobin implements round-robin load balancing -type RoundRobin struct { - index int -} - -// NewRoundRobin . -func NewRoundRobin() *RoundRobin { - return &RoundRobin{} -} - -// Select selects an instance using round-robin -func (lb *RoundRobin) Select(instances []service_discovery.Instance) (service_discovery.Instance, error) { - if len(instances) == 0 { - return service_discovery.Instance{}, fmt.Errorf("no instances available") - } - lb.index = (lb.index + 1) % len(instances) - return instances[lb.index], nil -} diff --git a/internal/logic/click.go b/internal/logic/click.go index d811ae8..6ea24fa 100644 --- a/internal/logic/click.go +++ b/internal/logic/click.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package logic import ( @@ -22,7 +28,7 @@ import ( type ClickCaptLogic struct { svcCtx *common.SvcContext - cache cache.Cache + cacheMgr *cache.CacheManager dynamicCfg *config.DynamicConfig logger *zap.Logger captcha *gocaptcha.GoCaptcha @@ -32,7 +38,7 @@ type ClickCaptLogic struct { func NewClickCaptLogic(svcCtx *common.SvcContext) *ClickCaptLogic { return &ClickCaptLogic{ svcCtx: svcCtx, - cache: svcCtx.Cache, + cacheMgr: svcCtx.CacheMgr, dynamicCfg: svcCtx.DynamicConfig, logger: svcCtx.Logger, captcha: svcCtx.Captcha, @@ -48,7 +54,8 @@ func (cl *ClickCaptLogic) GetData(ctx context.Context, id string) (res *adapt.Ca } var capt *gocaptcha.ClickCaptInstance - switch cl.svcCtx.Captcha.GetCaptTypeWithKey(id) { + ttype := cl.svcCtx.Captcha.GetCaptTypeWithKey(id) + switch ttype { case consts.GoCaptchaTypeClick: capt = cl.svcCtx.Captcha.GetClickInstanceWithKey(id) break @@ -82,6 +89,7 @@ func (cl *ClickCaptLogic) GetData(ctx context.Context, id string) (res *adapt.Ca cacheData := &cache.CaptCacheData{ Data: data, + Type: ttype, Status: 0, } cacheDataByte, err := json.Marshal(cacheData) @@ -94,16 +102,16 @@ func (cl *ClickCaptLogic) GetData(ctx context.Context, id string) (res *adapt.Ca return nil, fmt.Errorf("failed to generate uuid: %v", err) } - err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) if err != nil { return res, fmt.Errorf("failed to write cache:: %v", err) } opts := capt.Instance.GetOptions() - res.MasterImageWidth = int32(opts.GetImageSize().Width) - res.MasterImageHeight = int32(opts.GetImageSize().Height) - res.ThumbImageWidth = int32(opts.GetThumbImageSize().Width) - res.ThumbImageHeight = int32(opts.GetThumbImageSize().Height) + res.MasterWidth = int32(opts.GetImageSize().Width) + res.MasterHeight = int32(opts.GetImageSize().Height) + res.ThumbWidth = int32(opts.GetThumbImageSize().Width) + res.ThumbHeight = int32(opts.GetThumbImageSize().Height) res.CaptchaKey = key return res, nil } @@ -114,7 +122,7 @@ func (cl *ClickCaptLogic) CheckData(ctx context.Context, key string, dots string return false, fmt.Errorf("invalid key") } - cacheData, err := cl.cache.GetCache(ctx, key) + cacheData, err := cl.cacheMgr.GetCache().GetCache(ctx, key) if err != nil { return false, fmt.Errorf("failed to get cache: %v", err) } @@ -125,15 +133,20 @@ func (cl *ClickCaptLogic) CheckData(ctx context.Context, key string, dots string src := strings.Split(dots, ",") - var captData *cache.CaptCacheData - err = json.Unmarshal([]byte(cacheData), &captData) + var cacheCaptData *cache.CaptCacheData + err = json.Unmarshal([]byte(cacheData), &cacheCaptData) if err != nil { return false, fmt.Errorf("failed to json unmarshal: %v", err) } - dct, ok := captData.Data.(map[int]*click.Dot) - if !ok { - return false, fmt.Errorf("cache data invalid: %v", err) + var dct map[int]*click.Dot + captDataStr, err := json.Marshal(cacheCaptData.Data) + if err != nil { + return false, fmt.Errorf("failed to json marshal: %v", err) + } + err = json.Unmarshal(captDataStr, &dct) + if err != nil { + return false, fmt.Errorf("failed to json unmarshal: %v", err) } ret := false @@ -153,13 +166,13 @@ func (cl *ClickCaptLogic) CheckData(ctx context.Context, key string, dots string } if ret { - captData.Status = 1 - cacheDataByte, err := json.Marshal(captData) + cacheCaptData.Status = 1 + cacheDataByte, err := json.Marshal(cacheCaptData) if err != nil { return ret, fmt.Errorf("failed to json marshal: %v", err) } - err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) if err != nil { return ret, fmt.Errorf("failed to update cache:: %v", err) } diff --git a/internal/logic/click_test.go b/internal/logic/click_test.go index 9d3159b..8cf0d1b 100644 --- a/internal/logic/click_test.go +++ b/internal/logic/click_test.go @@ -14,6 +14,7 @@ import ( "github.com/wenlng/go-captcha-service/internal/common" "github.com/wenlng/go-captcha-service/internal/config" "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" + config2 "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha/config" "github.com/wenlng/go-captcha/v2/click" "go.uber.org/zap" ) @@ -29,37 +30,37 @@ func TestCacheLogic(t *testing.T) { defer cacheClient.Close() dc := &config.DynamicConfig{Config: config.DefaultConfig()} - cnf := dc.Get() + cdc := &config2.DynamicCaptchaConfig{Config: config2.DefaultConfig()} logger, err := zap.NewProduction() assert.NoError(t, err) - captcha, err := gocaptcha.Setup() + captcha, err := gocaptcha.Setup(cdc) assert.NoError(t, err) svcCtx := &common.SvcContext{ - Cache: cacheClient, - Config: &cnf, - Logger: logger, - Captcha: captcha, + CacheMgr: cacheClient, + DynamicConfig: dc, + Logger: logger, + Captcha: captcha, } logic := NewClickCaptLogic(svcCtx) t.Run("GetData", func(t *testing.T) { - _, err := logic.GetData(context.Background(), 0, 1, 1) + _, err := logic.GetData(context.Background(), "dd") assert.NoError(t, err) }) t.Run("GetData_Miss", func(t *testing.T) { - _, err := logic.GetData(context.Background(), -1, 1, 1) + _, err := logic.GetData(context.Background(), "dd") assert.Error(t, err) }) t.Run("CheckData", func(t *testing.T) { - data, err := logic.GetData(context.Background(), 1, 1, 1) + data, err := logic.GetData(context.Background(), "dd") assert.NoError(t, err) - cacheData, err := svcCtx.Cache.GetCache(context.Background(), data.CaptchaKey) + cacheData, err := svcCtx.CacheMgr.GetCache(context.Background(), data.CaptchaKey) assert.NoError(t, err) var dct map[int]*click.Dot @@ -79,10 +80,10 @@ func TestCacheLogic(t *testing.T) { }) t.Run("CheckData_MISS", func(t *testing.T) { - data, err := logic.GetData(context.Background(), 1, 1, 1) + data, err := logic.GetData(context.Background(), "dd") assert.NoError(t, err) - cacheData, err := svcCtx.Cache.GetCache(context.Background(), data.CaptchaKey) + cacheData, err := svcCtx.CacheMgr.GetCache(context.Background(), data.CaptchaKey) assert.NoError(t, err) var dct map[int]*click.Dot diff --git a/internal/logic/common.go b/internal/logic/common.go index 239e393..883d288 100644 --- a/internal/logic/common.go +++ b/internal/logic/common.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package logic import ( @@ -16,7 +22,7 @@ import ( type CommonLogic struct { svcCtx *common.SvcContext - cache cache.Cache + cacheMgr *cache.CacheManager dynamicCfg *config.DynamicConfig logger *zap.Logger captcha *gocaptcha.GoCaptcha @@ -26,7 +32,7 @@ type CommonLogic struct { func NewCommonLogic(svcCtx *common.SvcContext) *CommonLogic { return &CommonLogic{ svcCtx: svcCtx, - cache: svcCtx.Cache, + cacheMgr: svcCtx.CacheMgr, dynamicCfg: svcCtx.DynamicConfig, logger: svcCtx.Logger, captcha: svcCtx.Captcha, @@ -39,7 +45,7 @@ func (cl *CommonLogic) CheckStatus(ctx context.Context, key string) (ret bool, e return false, fmt.Errorf("invalid key") } - cacheData, err := cl.cache.GetCache(ctx, key) + cacheData, err := cl.cacheMgr.GetCache().GetCache(ctx, key) if err != nil { return false, fmt.Errorf("failed to get cache: %v", err) } @@ -65,7 +71,7 @@ func (cl *CommonLogic) GetStatusInfo(ctx context.Context, key string) (data *cac captData := &cache.CaptCacheData{} - cacheData, err := cl.cache.GetCache(ctx, key) + cacheData, err := cl.cacheMgr.GetCache().GetCache(ctx, key) if err != nil { return nil, fmt.Errorf("failed to get cache: %v", err) } @@ -89,7 +95,7 @@ func (cl *CommonLogic) DelStatusInfo(ctx context.Context, key string) (ret bool, return false, fmt.Errorf("invalid key") } - err = cl.cache.DeleteCache(ctx, key) + err = cl.cacheMgr.GetCache().DeleteCache(ctx, key) if err != nil { return false, fmt.Errorf("failed to delete cache: %v", err) } diff --git a/internal/logic/resource.go b/internal/logic/resource.go index c2b1487..84f1835 100644 --- a/internal/logic/resource.go +++ b/internal/logic/resource.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package logic import ( @@ -21,7 +27,7 @@ import ( type ResourceLogic struct { svcCtx *common.SvcContext - cache cache.Cache + cacheMgr *cache.CacheManager dynamicCfg *config.DynamicConfig logger *zap.Logger captcha *gocaptcha.GoCaptcha @@ -31,7 +37,7 @@ type ResourceLogic struct { func NewResourceLogic(svcCtx *common.SvcContext) *ResourceLogic { return &ResourceLogic{ svcCtx: svcCtx, - cache: svcCtx.Cache, + cacheMgr: svcCtx.CacheMgr, dynamicCfg: svcCtx.DynamicConfig, logger: svcCtx.Logger, captcha: svcCtx.Captcha, diff --git a/internal/logic/rotate.go b/internal/logic/rotate.go index 601ce6b..0e0434a 100644 --- a/internal/logic/rotate.go +++ b/internal/logic/rotate.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package logic import ( @@ -20,7 +26,7 @@ import ( type RotateCaptLogic struct { svcCtx *common.SvcContext - cache cache.Cache + cacheMgr *cache.CacheManager dynamicCfg *config.DynamicConfig logger *zap.Logger captcha *gocaptcha.GoCaptcha @@ -30,7 +36,7 @@ type RotateCaptLogic struct { func NewRotateCaptLogic(svcCtx *common.SvcContext) *RotateCaptLogic { return &RotateCaptLogic{ svcCtx: svcCtx, - cache: svcCtx.Cache, + cacheMgr: svcCtx.CacheMgr, dynamicCfg: svcCtx.DynamicConfig, logger: svcCtx.Logger, captcha: svcCtx.Captcha, @@ -46,7 +52,8 @@ func (cl *RotateCaptLogic) GetData(ctx context.Context, id string) (res *adapt.C } var capt *gocaptcha.RotateCaptInstance - switch cl.svcCtx.Captcha.GetCaptTypeWithKey(id) { + ttype := cl.svcCtx.Captcha.GetCaptTypeWithKey(id) + switch ttype { case consts.GoCaptchaTypeRotate: capt = cl.svcCtx.Captcha.GetRotateInstanceWithKey(id) break @@ -77,6 +84,7 @@ func (cl *RotateCaptLogic) GetData(ctx context.Context, id string) (res *adapt.C cacheData := &cache.CaptCacheData{ Data: data, + Type: ttype, Status: 0, } cacheDataByte, err := json.Marshal(cacheData) @@ -89,17 +97,17 @@ func (cl *RotateCaptLogic) GetData(ctx context.Context, id string) (res *adapt.C return nil, fmt.Errorf("failed to generate uuid: %v", err) } - err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) if err != nil { return res, fmt.Errorf("failed to write cache:: %v", err) } opts := capt.Instance.GetOptions() - res.MasterImageWidth = int32(opts.GetImageSize()) - res.MasterImageHeight = int32(opts.GetImageSize()) - res.ThumbImageWidth = int32(data.Width) - res.ThumbImageHeight = int32(data.Height) - res.ThumbImageSize = int32(data.Width) + res.MasterWidth = int32(opts.GetImageSize()) + res.MasterHeight = int32(opts.GetImageSize()) + res.ThumbWidth = int32(data.Width) + res.ThumbHeight = int32(data.Height) + res.ThumbSize = int32(data.Width) res.CaptchaKey = key return res, nil } @@ -110,7 +118,7 @@ func (cl *RotateCaptLogic) CheckData(ctx context.Context, key string, angle int) return false, fmt.Errorf("invalid key") } - cacheData, err := cl.cache.GetCache(ctx, key) + cacheData, err := cl.cacheMgr.GetCache().GetCache(ctx, key) if err != nil { return false, fmt.Errorf("failed to get cache: %v", err) } @@ -119,27 +127,32 @@ func (cl *RotateCaptLogic) CheckData(ctx context.Context, key string, angle int) return false, nil } - var captData *cache.CaptCacheData - err = json.Unmarshal([]byte(cacheData), &captData) + var cacheCaptData *cache.CaptCacheData + err = json.Unmarshal([]byte(cacheData), &cacheCaptData) if err != nil { return false, fmt.Errorf("failed to json unmarshal: %v", err) } - dct, ok := captData.Data.(*rotate.Block) - if !ok { - return false, fmt.Errorf("cache data invalid: %v", err) + var dct *rotate.Block + captDataStr, err := json.Marshal(cacheCaptData.Data) + if err != nil { + return false, fmt.Errorf("failed to json marshal: %v", err) + } + err = json.Unmarshal(captDataStr, &dct) + if err != nil { + return false, fmt.Errorf("failed to json unmarshal: %v", err) } ret := rotate.CheckAngle(int64(angle), int64(dct.Angle), 2) if ret { - captData.Status = 1 - cacheDataByte, err := json.Marshal(captData) + cacheCaptData.Status = 1 + cacheDataByte, err := json.Marshal(cacheCaptData) if err != nil { return ret, fmt.Errorf("failed to json marshal: %v", err) } - err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) if err != nil { return ret, fmt.Errorf("failed to update cache:: %v", err) } diff --git a/internal/logic/slide.go b/internal/logic/slide.go index 18a4937..63f5687 100644 --- a/internal/logic/slide.go +++ b/internal/logic/slide.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package logic import ( @@ -22,7 +28,7 @@ import ( type SlideCaptLogic struct { svcCtx *common.SvcContext - cache cache.Cache + cacheMgr *cache.CacheManager dynamicCfg *config.DynamicConfig logger *zap.Logger captcha *gocaptcha.GoCaptcha @@ -32,7 +38,7 @@ type SlideCaptLogic struct { func NewSlideCaptLogic(svcCtx *common.SvcContext) *SlideCaptLogic { return &SlideCaptLogic{ svcCtx: svcCtx, - cache: svcCtx.Cache, + cacheMgr: svcCtx.CacheMgr, dynamicCfg: svcCtx.DynamicConfig, logger: svcCtx.Logger, captcha: svcCtx.Captcha, @@ -48,7 +54,8 @@ func (cl *SlideCaptLogic) GetData(ctx context.Context, id string) (res *adapt.Ca } var capt *gocaptcha.SlideCaptInstance - switch cl.svcCtx.Captcha.GetCaptTypeWithKey(id) { + ttype := cl.svcCtx.Captcha.GetCaptTypeWithKey(id) + switch ttype { case consts.GoCaptchaTypeSlide: capt = cl.svcCtx.Captcha.GetSlideInstanceWithKey(id) break @@ -82,6 +89,7 @@ func (cl *SlideCaptLogic) GetData(ctx context.Context, id string) (res *adapt.Ca cacheData := &cache.CaptCacheData{ Data: data, + Type: ttype, Status: 0, } cacheDataByte, err := json.Marshal(cacheData) @@ -94,16 +102,16 @@ func (cl *SlideCaptLogic) GetData(ctx context.Context, id string) (res *adapt.Ca return nil, fmt.Errorf("failed to generate uuid: %v", err) } - err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) if err != nil { return res, fmt.Errorf("failed to write cache:: %v", err) } opts := capt.Instance.GetOptions() - res.MasterImageWidth = int32(opts.GetImageSize().Width) - res.MasterImageHeight = int32(opts.GetImageSize().Height) - res.ThumbImageWidth = int32(data.Width) - res.ThumbImageHeight = int32(data.Height) + res.MasterWidth = int32(opts.GetImageSize().Width) + res.MasterHeight = int32(opts.GetImageSize().Height) + res.ThumbWidth = int32(data.Width) + res.ThumbHeight = int32(data.Height) res.DisplayX = int32(data.TileX) res.DisplayY = int32(data.TileY) res.CaptchaKey = key @@ -116,7 +124,7 @@ func (cl *SlideCaptLogic) CheckData(ctx context.Context, key string, dots string return false, fmt.Errorf("invalid key") } - cacheData, err := cl.cache.GetCache(ctx, key) + cacheData, err := cl.cacheMgr.GetCache().GetCache(ctx, key) if err != nil { return false, fmt.Errorf("failed to get cache: %v", err) } @@ -127,15 +135,20 @@ func (cl *SlideCaptLogic) CheckData(ctx context.Context, key string, dots string src := strings.Split(dots, ",") - var captData *cache.CaptCacheData - err = json.Unmarshal([]byte(cacheData), &captData) + var cacheCaptData *cache.CaptCacheData + err = json.Unmarshal([]byte(cacheData), &cacheCaptData) if err != nil { return false, fmt.Errorf("failed to json unmarshal: %v", err) } - dct, ok := captData.Data.(*slide.Block) - if !ok { - return false, fmt.Errorf("cache data invalid: %v", err) + var dct *slide.Block + captDataStr, err := json.Marshal(cacheCaptData.Data) + if err != nil { + return false, fmt.Errorf("failed to json marshal: %v", err) + } + err = json.Unmarshal(captDataStr, &dct) + if err != nil { + return false, fmt.Errorf("failed to json unmarshal: %v", err) } ret := false @@ -146,13 +159,13 @@ func (cl *SlideCaptLogic) CheckData(ctx context.Context, key string, dots string } if ret { - captData.Status = 1 - cacheDataByte, err := json.Marshal(captData) + cacheCaptData.Status = 1 + cacheDataByte, err := json.Marshal(cacheCaptData) if err != nil { return ret, fmt.Errorf("failed to json marshal: %v", err) } - err = cl.cache.SetCache(ctx, key, string(cacheDataByte)) + err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) if err != nil { return ret, fmt.Errorf("failed to update cache:: %v", err) } diff --git a/internal/middleware/grpc_middleware.go b/internal/middleware/grpc_middleware.go index b41c148..5d9ee5f 100644 --- a/internal/middleware/grpc_middleware.go +++ b/internal/middleware/grpc_middleware.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package middleware import ( @@ -28,16 +34,16 @@ func UnaryServerInterceptor(dc *config.DynamicConfig, logger *zap.Logger, breake md, ok := metadata.FromIncomingContext(ctx) if !ok { - logger.Warn("Missing metadata") + logger.Warn("[GrpcMiddleware] Missing metadata") return nil, status.Error(codes.Unauthenticated, "missing API Key") } apiKeys := md.Get("x-api-key") if len(apiKeys) == 0 { - logger.Warn("Missing API Key") + logger.Warn("[GrpcMiddleware] Missing API Key") return nil, status.Error(codes.Unauthenticated, "missing API Key") } if _, exists := apiKeyMap[apiKeys[0]]; !exists { - logger.Warn("Invalid API Key", zap.String("key", apiKeys[0])) + logger.Warn("[GrpcMiddleware] Invalid API Key", zap.String("key", apiKeys[0])) return nil, status.Error(codes.Unauthenticated, "invalid API Key") } @@ -49,16 +55,16 @@ func UnaryServerInterceptor(dc *config.DynamicConfig, logger *zap.Logger, breake return nil, nil }) if cbErr == gobreaker.ErrOpenState || cbErr == gobreaker.ErrTooManyRequests { - logger.Warn("gRPC circuit breaker tripped", zap.Error(cbErr)) + logger.Warn("[GrpcMiddleware] gRPC circuit breaker tripped", zap.Error(cbErr)) return nil, status.Error(codes.Unavailable, "service unavailable") } if cbErr != nil { - logger.Error("gRPC circuit breaker error", zap.Error(cbErr)) + logger.Error("[GrpcMiddleware] gRPC circuit breaker error", zap.Error(cbErr)) return nil, status.Error(codes.Internal, "internal server error") } // Log request - logger.Info("gRPC request", + logger.Info("[GrpcMiddleware] gRPC request", zap.String("method", info.FullMethod), zap.Duration("duration", time.Since(start)), zap.Error(err), diff --git a/internal/middleware/http_niddleware.go b/internal/middleware/http_middleware.go similarity index 90% rename from internal/middleware/http_niddleware.go rename to internal/middleware/http_middleware.go index 45981b5..cf315b0 100644 --- a/internal/middleware/http_niddleware.go +++ b/internal/middleware/http_middleware.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package middleware import ( @@ -72,12 +78,12 @@ func APIKeyMiddleware(dc *config.DynamicConfig, logger *zap.Logger) HTTPMiddlewa apiKey := r.Header.Get("X-API-Key") if apiKey == "" { - logger.Warn("Missing API Key") + logger.Warn("[HttpMiddleware] Missing API Key") WriteError(w, http.StatusUnauthorized, "missing API Key") return } if _, exists := apiKeyMap[apiKey]; !exists { - logger.Warn("Invalid API Key", zap.String("key", apiKey)) + logger.Warn("[HttpMiddleware] Invalid API Key", zap.String("key", apiKey)) WriteError(w, http.StatusUnauthorized, "invalid API Key") return } @@ -91,7 +97,7 @@ func RateLimitMiddleware(limiter *DynamicLimiter, logger *zap.Logger) HTTPMiddle return func(next HandlerFunc) HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := limiter.Wait(r.Context()); err != nil { - logger.Warn("Rate limit exceeded", zap.String("client", r.RemoteAddr)) + logger.Warn("[HttpMiddleware] Rate limit exceeded", zap.String("client", r.RemoteAddr)) WriteError(w, http.StatusTooManyRequests, "rate limit exceeded") return } @@ -106,7 +112,7 @@ func LoggingMiddleware(logger *zap.Logger) HTTPMiddleware { return func(w http.ResponseWriter, r *http.Request) { start := time.Now() next(w, r) - logger.Info("HTTP request", + logger.Info("[HttpMiddleware] HTTP request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("client", r.RemoteAddr), @@ -125,12 +131,12 @@ func CircuitBreakerMiddleware(breaker *gobreaker.CircuitBreaker, logger *zap.Log return nil, nil }) if err == gobreaker.ErrOpenState || err == gobreaker.ErrTooManyRequests { - logger.Warn("Circuit breaker tripped", zap.Error(err)) + logger.Warn("[HttpMiddleware] Circuit breaker tripped", zap.Error(err)) WriteError(w, http.StatusServiceUnavailable, "service unavailable") return } if err != nil { - logger.Error("Circuit breaker error", zap.Error(err)) + logger.Error("[HttpMiddleware] Circuit breaker error", zap.Error(err)) WriteError(w, http.StatusInternalServerError, "internal server error") return } @@ -162,7 +168,7 @@ func CORSMiddleware(logger *zap.Logger) HTTPMiddleware { w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Custom-Header") } - w.Header().Set("Access-Control-Max-Age", "86400") // Cache preflight response for 24 hours + w.Header().Set("Access-Control-Max-Age", "86400") // CacheMgr preflight response for 24 hours w.Header().Set("Access-Control-Allow-Credentials", "true") // Allow credentials if needed // Handle preflight (OPTIONS) requests @@ -224,7 +230,7 @@ func RateLimitHandler(limiter *DynamicLimiter, logger *zap.Logger) HandlerFunc { Burst int `json:"burst"` } if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - logger.Warn("Invalid rate limit params", zap.Error(err)) + logger.Warn("[HttpMiddleware] Invalid rate limit params", zap.Error(err)) WriteError(w, http.StatusBadRequest, "invalid parameters") return } @@ -233,7 +239,7 @@ func RateLimitHandler(limiter *DynamicLimiter, logger *zap.Logger) HandlerFunc { return } limiter.Update(params.QPS, params.Burst) - logger.Info("Rate limit updated", + logger.Info("[HttpMiddleware] Rate limit updated", zap.Int("qps", params.QPS), zap.Int("burst", params.Burst), ) diff --git a/internal/pkg/gocaptcha/click.go b/internal/pkg/gocaptcha/click.go index bc771fe..75cca9d 100644 --- a/internal/pkg/gocaptcha/click.go +++ b/internal/pkg/gocaptcha/click.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package gocaptcha import ( @@ -28,11 +34,11 @@ func genClickOptions(conf config.ClickConfig) ([]click.Option, error) { options := make([]click.Option, 0) // Master image - if conf.Master.ImageSize.Height != 0 && conf.Master.ImageSize.Width != 0 { + if conf.Master.ImageSize.Height > 0 && conf.Master.ImageSize.Width > 0 { options = append(options, click.WithImageSize(conf.Master.ImageSize)) } - if conf.Master.RangeLength.Min >= 0 && conf.Master.RangeLength.Max >= 0 { + if conf.Master.RangeLength.Min >= 0 && conf.Master.RangeLength.Max > 0 { options = append(options, click.WithRangeLen(conf.Master.RangeLength)) } @@ -40,7 +46,7 @@ func genClickOptions(conf config.ClickConfig) ([]click.Option, error) { options = append(options, click.WithRangeAnglePos(conf.Master.RangeAngles)) } - if conf.Master.RangeSize.Min >= 0 && conf.Master.RangeSize.Max >= 0 { + if conf.Master.RangeSize.Min >= 0 && conf.Master.RangeSize.Max > 0 { options = append(options, click.WithRangeSize(conf.Master.RangeSize)) } @@ -84,7 +90,7 @@ func genClickOptions(conf config.ClickConfig) ([]click.Option, error) { options = append(options, click.WithDisabledRangeVerifyLen(conf.Thumb.DisabledRangeVerifyLength)) } - if conf.Thumb.RangeTextSize.Min != 0 && conf.Thumb.RangeTextSize.Max != 0 { + if conf.Thumb.RangeTextSize.Min > 0 && conf.Thumb.RangeTextSize.Max > 0 { options = append(options, click.WithRangeThumbSize(conf.Thumb.RangeTextSize)) } @@ -119,7 +125,7 @@ func genClickOptions(conf config.ClickConfig) ([]click.Option, error) { return options, nil } -// GetMixinAlphaChars 数字+字母组合(双组合) +// GetMixinAlphaChars . func GetMixinAlphaChars() []string { var ret = make([]string, 0) letterArr := strings.Split("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "") diff --git a/internal/pkg/gocaptcha/config/base.config.go b/internal/pkg/gocaptcha/config/base.config.go index 3892306..8877eb2 100644 --- a/internal/pkg/gocaptcha/config/base.config.go +++ b/internal/pkg/gocaptcha/config/base.config.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package config import ( diff --git a/internal/pkg/gocaptcha/config/config.go b/internal/pkg/gocaptcha/config/config.go index 2d7cd0d..06e81fb 100644 --- a/internal/pkg/gocaptcha/config/config.go +++ b/internal/pkg/gocaptcha/config/config.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package config import ( @@ -23,31 +29,69 @@ type BuilderConfig struct { // CaptchaConfig defines the configuration structure for the gocaptcha type CaptchaConfig struct { - Resources ResourceConfig `json:"resources"` - Builder BuilderConfig `json:"builder"` + ConfigVersion int64 `json:"config_version"` + Resources ResourceConfig `json:"resources"` + Builder BuilderConfig `json:"builder"` } // DynamicCaptchaConfig . type DynamicCaptchaConfig struct { Config CaptchaConfig mu sync.RWMutex - hotCbsHooks map[string]HandleHotCallbackHookFnc + hotCbsHooks map[string]HandleHotCallbackFnc + + outputLogCbs helper.OutputLogCallback } -type HandleHotCallbackHookFnc = func(*DynamicCaptchaConfig) +type HotCallbackType int + +const ( + HotCallbackTypeLocalConfigFile HotCallbackType = 1 + HotCallbackTypeRemoteConfig = 2 +) + +type HandleHotCallbackFnc = func(*DynamicCaptchaConfig, HotCallbackType) // NewDynamicConfig . -func NewDynamicConfig(file string) (*DynamicCaptchaConfig, error) { - cfg, err := Load(file) - if err != nil { - return nil, err +func NewDynamicConfig(file string, hasWatchFile bool) (*DynamicCaptchaConfig, error) { + cfg := DefaultConfig() + var err error + + if file != "" { + cfg, err = Load(file) + if err != nil { + return nil, err + } + } + + dc := &DynamicCaptchaConfig{Config: cfg, hotCbsHooks: make(map[string]HandleHotCallbackFnc)} + + if hasWatchFile { + go dc.watchFile(file) } - dc := &DynamicCaptchaConfig{Config: cfg, hotCbsHooks: make(map[string]HandleHotCallbackHookFnc)} - go dc.watchFile(file) + return dc, nil } -// Get retrieves the current configuration +// DefaultDynamicConfig . +func DefaultDynamicConfig() *DynamicCaptchaConfig { + cfg := DefaultConfig() + return &DynamicCaptchaConfig{Config: cfg, hotCbsHooks: make(map[string]HandleHotCallbackFnc)} +} + +// SetOutputLogCallback Set the log out hook function +func (dc *DynamicCaptchaConfig) SetOutputLogCallback(outputLogCbs helper.OutputLogCallback) { + dc.outputLogCbs = outputLogCbs +} + +// outLog .. +func (dc *DynamicCaptchaConfig) outLog(logType helper.OutputLogType, message string) { + if dc.outputLogCbs != nil { + dc.outputLogCbs(logType, message) + } +} + +// Get .. func (dc *DynamicCaptchaConfig) Get() CaptchaConfig { dc.mu.RLock() defer dc.mu.RUnlock() @@ -65,25 +109,52 @@ func (dc *DynamicCaptchaConfig) Update(cfg CaptchaConfig) error { return nil } -// RegisterHotCallbackHook callback when updating configuration -func (dc *DynamicCaptchaConfig) RegisterHotCallbackHook(key string, callback HandleHotCallbackHookFnc) { +// MarshalConfig .. +func (dc *DynamicCaptchaConfig) MarshalConfig() (string, error) { + dc.mu.RLock() + cByte, err := json.Marshal(dc.Config) + if err != nil { + return "", err + } + dc.mu.RUnlock() + + return string(cByte), nil +} + +// UnMarshalConfig .. +func (dc *DynamicCaptchaConfig) UnMarshalConfig(str string) error { + var config CaptchaConfig + err := json.Unmarshal([]byte(str), &config) + if err != nil { + return err + } + + dc.mu.Lock() + dc.Config = config + dc.mu.Unlock() + + return nil +} + +// RegisterHotCallback callback when updating configuration +func (dc *DynamicCaptchaConfig) RegisterHotCallback(key string, callback HandleHotCallbackFnc) { if _, ok := dc.hotCbsHooks[key]; !ok { dc.hotCbsHooks[key] = callback } } -// UnRegisterHotCallbackHook callback when updating configuration -func (dc *DynamicCaptchaConfig) UnRegisterHotCallbackHook(key string) { +// UnRegisterHotCallback callback when updating configuration +func (dc *DynamicCaptchaConfig) UnRegisterHotCallback(key string) { if _, ok := dc.hotCbsHooks[key]; !ok { delete(dc.hotCbsHooks, key) } } -// HandleHotCallbackHook . -func (dc *DynamicCaptchaConfig) HandleHotCallbackHook() { +// HandleHotCallback . +func (dc *DynamicCaptchaConfig) HandleHotCallback(hostType HotCallbackType) { for _, fnc := range dc.hotCbsHooks { if fnc != nil { - fnc(dc) + fnc(dc, hostType) } } } @@ -92,20 +163,20 @@ func (dc *DynamicCaptchaConfig) HandleHotCallbackHook() { func (dc *DynamicCaptchaConfig) watchFile(file string) { watcher, err := fsnotify.NewWatcher() if err != nil { - fmt.Fprintf(os.Stderr, "Failed to create watcher: %v\n", err) + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[CaptchaConfig] Failed to create watcher, err: %v", err)) return } defer watcher.Close() absPath, err := filepath.Abs(file) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to get absolute path: %v\n", err) + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[CaptchaConfig] Failed to get absolute path, err: %v", err)) return } dir := filepath.Dir(absPath) if err := watcher.Add(dir); err != nil { - fmt.Fprintf(os.Stderr, "Failed to watch directory: %v\n", err) + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[CaptchaConfig] Failed to watch directory, err: %v", err)) return } @@ -118,24 +189,23 @@ func (dc *DynamicCaptchaConfig) watchFile(file string) { if event.Name == absPath && (event.Op&fsnotify.Write == fsnotify.Write) { cfg, err := Load(file) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to reload CaptchaConfig: %v\n", err) + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[CaptchaConfig]Failed to reload Config, err: %v", err)) continue } - if err := dc.Update(cfg); err != nil { - fmt.Fprintf(os.Stderr, "Failed to update CaptchaConfig: %v\n", err) + if err = dc.Update(cfg); err != nil { + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[CaptchaConfig] Failed to update Config, err: %v", err)) continue } // Instance update gocaptcha - dc.HandleHotCallbackHook() - - fmt.Printf("GoCaptcha Configuration reloaded successfully\n") + dc.HandleHotCallback(HotCallbackTypeLocalConfigFile) + dc.outLog(helper.OutputLogTypeInfo, "[CaptchaConfig] Configuration reloaded successfully") } case err, ok := <-watcher.Errors: if !ok { return } - fmt.Fprintf(os.Stderr, "Watcher error: %v\n", err) + dc.outLog(helper.OutputLogTypeError, fmt.Sprintf("[CaptchaConfig] Failed to watcher, err: %v", err)) } } } @@ -143,12 +213,11 @@ func (dc *DynamicCaptchaConfig) watchFile(file string) { // HotUpdate hot update configuration func (dc *DynamicCaptchaConfig) HotUpdate(cfg CaptchaConfig) error { if err := dc.Update(cfg); err != nil { - fmt.Fprintf(os.Stderr, "Failed to update CaptchaConfig: %v\n", err) return err } // Instance update gocaptcha - dc.HandleHotCallbackHook() + dc.HandleHotCallback(HotCallbackTypeLocalConfigFile) return nil } @@ -224,7 +293,13 @@ func isValidFileExist(filePaths []string) error { func DefaultConfig() CaptchaConfig { return CaptchaConfig{ Resources: ResourceConfig{ - Char: ResourceChar{}, + Version: "0.0.1", + Char: ResourceChar{ + Languages: map[string][]string{ + "chinese": make([]string, 0), + "english": make([]string, 0), + }, + }, Font: ResourceFileConfig{}, ShapeImage: ResourceFileConfig{}, MasterImage: ResourceFileConfig{}, @@ -233,40 +308,76 @@ func DefaultConfig() CaptchaConfig { }, Builder: BuilderConfig{ ClickConfigMaps: map[string]ClickConfig{ - "click_default_ch": { + "click-default-ch": { + Version: "0.0.1", Language: "chinese", Master: ClickMasterOption{}, Thumb: ClickThumbOption{}, }, - "click_dark_ch": { + "click-dark-ch": { + Version: "0.0.1", Language: "chinese", Master: ClickMasterOption{}, - Thumb: ClickThumbOption{}, + Thumb: ClickThumbOption{ + RangeTextColors: []string{ + "#4a85fb", + "#d93ffb", + "#56be01", + "#ee2b2b", + "#cd6904", + "#b49b03", + "#01ad90", + }, + }, }, - "click_default_en": { + "click-default-en": { + Version: "0.0.1", Language: "english", Master: ClickMasterOption{}, Thumb: ClickThumbOption{}, }, - "click_dark_en": { + "click-dark-en": { + Version: "0.0.1", Language: "english", Master: ClickMasterOption{}, - Thumb: ClickThumbOption{}, + Thumb: ClickThumbOption{ + RangeTextColors: []string{ + "#4a85fb", + "#d93ffb", + "#56be01", + "#ee2b2b", + "#cd6904", + "#b49b03", + "#01ad90", + }, + }, + }, + "click-shape-light-default": { + Version: "0.0.1", + }, + "click-shape-dark-default": { + Version: "0.0.1", }, - "click_shape_light_default": {}, - "click_shape_dark_default": {}, }, ClickShapeConfigMaps: map[string]ClickConfig{ - "click_shape_default": {}, + "click-shape-default": { + Version: "0.0.1", + }, }, SlideConfigMaps: map[string]SlideConfig{ - "slide_default": {}, + "slide-default": { + Version: "0.0.1", + }, }, DragConfigMaps: map[string]SlideConfig{ - "drag_default": {}, + "drag-default": { + Version: "0.0.1", + }, }, RotateConfigMaps: map[string]RotateConfig{ - "rotate_default": {}, + "rotate-default": { + Version: "0.0.1", + }, }, }, } diff --git a/internal/pkg/gocaptcha/config/resrouce.config.go b/internal/pkg/gocaptcha/config/resrouce.config.go index 95ff902..73f0f74 100644 --- a/internal/pkg/gocaptcha/config/resrouce.config.go +++ b/internal/pkg/gocaptcha/config/resrouce.config.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package config // ResourceChar . diff --git a/internal/pkg/gocaptcha/gocaptcha.go b/internal/pkg/gocaptcha/gocaptcha.go index 0d69f90..c47b33c 100644 --- a/internal/pkg/gocaptcha/gocaptcha.go +++ b/internal/pkg/gocaptcha/gocaptcha.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package gocaptcha import ( @@ -207,8 +213,8 @@ func (gc *GoCaptcha) UpdateRotateInstance(configMaps map[string]config.RotateCon return nil } -// HotUpdate . -func (gc *GoCaptcha) HotUpdate(dyCnf *config.DynamicCaptchaConfig) error { +// HotSetup . +func (gc *GoCaptcha) HotSetup(dyCnf *config.DynamicCaptchaConfig) error { cnf := dyCnf.Get() var err error @@ -243,7 +249,9 @@ func (gc *GoCaptcha) HotUpdate(dyCnf *config.DynamicCaptchaConfig) error { // Setup initializes the captcha func Setup(dyCnf *config.DynamicCaptchaConfig) (*GoCaptcha, error) { gc := newGoCaptcha() - err := gc.HotUpdate(dyCnf) + gc.DynamicCnf = dyCnf + + err := gc.HotSetup(dyCnf) if err != nil { return nil, err } diff --git a/internal/pkg/gocaptcha/rotate.go b/internal/pkg/gocaptcha/rotate.go index 76dae83..5d5a279 100644 --- a/internal/pkg/gocaptcha/rotate.go +++ b/internal/pkg/gocaptcha/rotate.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package gocaptcha import ( @@ -22,7 +28,7 @@ func genRotateOptions(conf config.RotateConfig) ([]rotate.Option, error) { options := make([]rotate.Option, 0) // Master image - if conf.Master.ImageSquareSize != 0 { + if conf.Master.ImageSquareSize > 0 { options = append(options, rotate.WithImageSquareSize(conf.Master.ImageSquareSize)) } diff --git a/internal/pkg/gocaptcha/slide.go b/internal/pkg/gocaptcha/slide.go index 7174f13..cb3d8f2 100644 --- a/internal/pkg/gocaptcha/slide.go +++ b/internal/pkg/gocaptcha/slide.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package gocaptcha import ( @@ -24,7 +30,7 @@ func genSlideOptions(conf config.SlideConfig) ([]slide.Option, error) { options := make([]slide.Option, 0) // Master image - if conf.Master.ImageSize.Height != 0 && conf.Master.ImageSize.Width != 0 { + if conf.Master.ImageSize.Height > 0 && conf.Master.ImageSize.Width > 0 { options = append(options, slide.WithImageSize(conf.Master.ImageSize)) } @@ -33,7 +39,7 @@ func genSlideOptions(conf config.SlideConfig) ([]slide.Option, error) { } // Thumb image - if conf.Thumb.RangeGraphSizes.Min != 0 && conf.Thumb.RangeGraphSizes.Max != 0 { + if conf.Thumb.RangeGraphSizes.Min >= 0 && conf.Thumb.RangeGraphSizes.Max > 0 { options = append(options, slide.WithRangeGraphSize(conf.Thumb.RangeGraphSizes)) } diff --git a/internal/server/grpc_server.go b/internal/server/grpc_server.go index f7316c5..5d4c472 100644 --- a/internal/server/grpc_server.go +++ b/internal/server/grpc_server.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package server import ( @@ -75,11 +81,23 @@ func (s *GrpcServer) GetData(ctx context.Context, req *proto.GetDataRequest) (*p } if err != nil || data == nil { - s.logger.Error("failed to get captcha data, err: ", zap.Error(err)) + s.logger.Warn("[GrpcServer] Failed to get captcha data, err: ", zap.Error(err)) return &proto.GetDataResponse{Code: 0, Message: "captcha type not found"}, nil } resp.Id = req.GetId() + + resp.CaptchaKey = data.CaptchaKey + resp.MasterImageBase64 = data.MasterImageBase64 + resp.ThumbImageBase64 = data.ThumbImageBase64 + resp.MasterWidth = data.MasterWidth + resp.MasterHeight = data.MasterHeight + resp.ThumbWidth = data.ThumbWidth + resp.ThumbHeight = data.ThumbHeight + resp.ThumbSize = data.ThumbSize + resp.DisplayX = data.DisplayX + resp.DisplayY = data.DisplayY + return resp, nil } @@ -123,7 +141,7 @@ func (s *GrpcServer) CheckData(ctx context.Context, req *proto.CheckDataRequest) } if err != nil { - s.logger.Error("failed to check captcha data, err: ", zap.Error(err)) + s.logger.Warn("[GrpcServer] Failed to check captcha data, err: ", zap.Error(err)) return &proto.CheckDataResponse{Code: 1, Message: "failed to check captcha data"}, nil } @@ -146,7 +164,7 @@ func (s *GrpcServer) CheckStatus(ctx context.Context, req *proto.StatusInfoReque data, err := s.commonLogic.GetStatusInfo(ctx, req.GetCaptchaKey()) if err != nil { - s.logger.Error("failed to check status, err: ", zap.Error(err)) + s.logger.Warn("[GrpcServer] Failed to check status, err: ", zap.Error(err)) return &proto.StatusInfoResponse{Code: 1}, nil } @@ -169,7 +187,7 @@ func (s *GrpcServer) GetStatusInfo(ctx context.Context, req *proto.StatusInfoReq data, err := s.commonLogic.GetStatusInfo(ctx, req.GetCaptchaKey()) if err != nil { - s.logger.Error("failed to check status, err: ", zap.Error(err)) + s.logger.Warn("[GrpcServer] Failed to check status, err: ", zap.Error(err)) return &proto.StatusInfoResponse{Code: 1}, nil } @@ -195,7 +213,7 @@ func (s *GrpcServer) DelStatusInfo(ctx context.Context, req *proto.StatusInfoReq ret, err := s.commonLogic.DelStatusInfo(ctx, req.GetCaptchaKey()) if err != nil { - s.logger.Error("failed to delete status info, err: ", zap.Error(err)) + s.logger.Warn("[GrpcServer] Failed to delete status info, err: ", zap.Error(err)) return &proto.StatusInfoResponse{Code: 1}, nil } diff --git a/internal/server/grpc_server_test.go b/internal/server/grpc_server_test.go index 8fdeeef..fb0d678 100644 --- a/internal/server/grpc_server_test.go +++ b/internal/server/grpc_server_test.go @@ -30,11 +30,12 @@ func TestCacheServer(t *testing.T) { dc := &config.DynamicConfig{Config: config.DefaultConfig()} cnf := dc.Get() + captDCfg := &config2.DynamicCaptchaConfig{Config: config2.DefaultConfig()} logger, err := zap.NewProduction() assert.NoError(t, err) - captcha, err := gocaptcha.Setup() + captcha, err := gocaptcha.Setup(captDCfg) assert.NoError(t, err) svcCtx := &common.SvcContext{ diff --git a/internal/server/http_handler.go b/internal/server/http_handler.go index 0c6d4b4..6cd1829 100644 --- a/internal/server/http_handler.go +++ b/internal/server/http_handler.go @@ -1,3 +1,9 @@ +/** + * @Author Awen + * @Date 2025/04/04 + * @Email wengaolng@gmail.com + **/ + package server import ( @@ -47,7 +53,7 @@ func NewHTTPHandlers(svcCtx *common.SvcContext) *HTTPHandlers { // GetDataHandler . func (h *HTTPHandlers) GetDataHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - resp := &adapt.CaptDataResponse{Code: http.StatusOK, Message: ""} + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: ""} if r.Method != http.MethodGet { middleware.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") @@ -85,25 +91,27 @@ func (h *HTTPHandlers) GetDataHandler(w http.ResponseWriter, r *http.Request) { } if err != nil || data == nil { - h.logger.Error("failed to get captcha data, err: ", zap.Error(err)) + h.logger.Warn("[HttpHandler] Failed to get captcha data, err: ", zap.Error(err)) middleware.WriteError(w, http.StatusNotFound, "captcha type not found") return } resp.Code = http.StatusOK resp.Message = "success" - resp.Id = id - - resp.CaptchaKey = data.CaptchaKey - resp.MasterImageBase64 = data.MasterImageBase64 - resp.ThumbImageBase64 = data.ThumbImageBase64 - resp.MasterImageWidth = data.MasterImageWidth - resp.MasterImageHeight = data.MasterImageHeight - resp.ThumbImageWidth = data.ThumbImageWidth - resp.ThumbImageHeight = data.ThumbImageHeight - resp.ThumbImageSize = data.ThumbImageSize - resp.DisplayX = data.DisplayX - resp.DisplayY = data.DisplayY + + resp.Data = &adapt.CaptData{ + Id: id, + CaptchaKey: data.CaptchaKey, + MasterImageBase64: data.MasterImageBase64, + ThumbImageBase64: data.ThumbImageBase64, + MasterWidth: data.MasterWidth, + MasterHeight: data.MasterHeight, + ThumbWidth: data.ThumbWidth, + ThumbHeight: data.ThumbHeight, + ThumbSize: data.ThumbSize, + DisplayX: data.DisplayX, + DisplayY: data.DisplayY, + } json.NewEncoder(w).Encode(helper.Marshal(resp)) } @@ -165,6 +173,7 @@ func (h *HTTPHandlers) CheckDataHandler(w http.ResponseWriter, r *http.Request) } if err != nil { + h.logger.Warn("[HttpHandler] Failed to check data, err: ", zap.Error(err)) middleware.WriteError(w, http.StatusBadRequest, "failed to check captcha data") return } @@ -198,7 +207,7 @@ func (h *HTTPHandlers) CheckStatusHandler(w http.ResponseWriter, r *http.Request data, err := h.commonLogic.GetStatusInfo(r.Context(), captchaKey) if err != nil { - h.logger.Error("failed to check status, err: ", zap.Error(err)) + h.logger.Warn("[HttpHandler] Failed to check status, err: ", zap.Error(err)) middleware.WriteError(w, http.StatusBadRequest, "failed to check status") return } @@ -231,17 +240,14 @@ func (h *HTTPHandlers) GetStatusInfoHandler(w http.ResponseWriter, r *http.Reque data, err := h.commonLogic.GetStatusInfo(r.Context(), captchaKey) if err != nil { - h.logger.Error("failed to get status info, err: ", zap.Error(err)) + h.logger.Warn("[HttpHandler] Failed to get status info, err: ", zap.Error(err)) middleware.WriteError(w, http.StatusNotFound, "not found status info") return } resp.Code = http.StatusOK if data != nil { - resp.Data = &adapt.CaptStatusInfo{ - Info: data.Data, - Status: data.Status, - } + resp.Data = data } json.NewEncoder(w).Encode(helper.Marshal(resp)) @@ -265,7 +271,7 @@ func (h *HTTPHandlers) DelStatusInfoHandler(w http.ResponseWriter, r *http.Reque ret, err := h.commonLogic.DelStatusInfo(r.Context(), captchaKey) if err != nil { - h.logger.Error("failed to del status data, err: ", zap.Error(err)) + h.logger.Warn("[HttpHandler] Failed to del status data, err: ", zap.Error(err)) middleware.WriteError(w, http.StatusBadRequest, "not found status info") return } @@ -305,7 +311,7 @@ func (h *HTTPHandlers) UploadResourceHandler(w http.ResponseWriter, r *http.Requ // Parse multipart/form-data if err := r.ParseMultipartForm(maxUploadSize); err != nil { - h.logger.Error("Failed to parse form: %v ", zap.Error(err)) + h.logger.Warn("[HttpHandler] Failed to parse form: %v ", zap.Error(err)) middleware.WriteError(w, http.StatusBadRequest, "parse form fail") return } @@ -318,7 +324,7 @@ func (h *HTTPHandlers) UploadResourceHandler(w http.ResponseWriter, r *http.Requ ret, allDone, err := h.resourceLogic.SaveResource(r.Context(), dirname, files) if !ret && err != nil { - h.logger.Error("Failed to save resource, err: ", zap.Error(err)) + h.logger.Warn("[HttpHandler] Failed to save resource, err: ", zap.Error(err)) middleware.WriteError(w, http.StatusBadRequest, "save resource fail") return } @@ -328,6 +334,7 @@ func (h *HTTPHandlers) UploadResourceHandler(w http.ResponseWriter, r *http.Requ } if !allDone { + resp.Data = "some-files-ok" resp.Message = "some files failed to be uploaded. check if they already exist" } @@ -352,7 +359,7 @@ func (h *HTTPHandlers) GetResourceListHandler(w http.ResponseWriter, r *http.Req fileList, err := h.resourceLogic.GetResourceList(r.Context(), resourcePath) if err != nil { - h.logger.Error("failed to get resource, err: ", zap.Error(err)) + h.logger.Warn("[HttpHandler] Failed to get resource, err: ", zap.Error(err)) middleware.WriteError(w, http.StatusBadRequest, "get resource fail") return } @@ -382,7 +389,7 @@ func (h *HTTPHandlers) DeleteResourceHandler(w http.ResponseWriter, r *http.Requ ret, err := h.resourceLogic.DelResource(r.Context(), resourcePath) if err != nil { - h.logger.Error("failed to delete resource, err: ", zap.Error(err)) + h.logger.Warn("[HttpHandler] Failed to delete resource, err: ", zap.Error(err)) middleware.WriteError(w, http.StatusBadRequest, "delete resource fail") return } @@ -427,7 +434,7 @@ func (h *HTTPHandlers) UpdateHotGoCaptchaConfigHandler(w http.ResponseWriter, r err := h.svcCtx.Captcha.DynamicCnf.HotUpdate(conf) if err != nil { - h.logger.Error("failed to hot update config, err: ", zap.Error(err)) + h.logger.Warn("[HttpHandler] Failed to hot update config, err: ", zap.Error(err)) middleware.WriteError(w, http.StatusBadRequest, "hot update config fail") return } diff --git a/internal/server/http_handler_test.go b/internal/server/http_handler_test.go index 8047868..6c67d1f 100644 --- a/internal/server/http_handler_test.go +++ b/internal/server/http_handler_test.go @@ -12,7 +12,6 @@ import ( "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" - "github.com/wenlng/go-captcha-service/internal/common" "github.com/wenlng/go-captcha-service/internal/config" "github.com/wenlng/go-captcha-service/internal/middleware" "github.com/wenlng/go-captcha-service/internal/pkg/gocaptcha" @@ -40,7 +39,7 @@ func TestHTTPHandlers(t *testing.T) { captcha, err := gocaptcha.Setup() assert.NoError(t, err) - svcCtx := &common.SvcContext{ + svcCtx := &base.SvcContext{ Cache: cacheClient, Config: &cnf, Logger: logger, diff --git a/internal/service_discovery/consul_discovery.go b/internal/service_discovery/consul_discovery.go deleted file mode 100644 index 096fd9f..0000000 --- a/internal/service_discovery/consul_discovery.go +++ /dev/null @@ -1,76 +0,0 @@ -package service_discovery - -import ( - "context" - "fmt" - "strconv" - "strings" - - "github.com/hashicorp/consul/api" -) - -type ConsulDiscovery struct { - client *api.Client - ttl int -} - -func NewConsulDiscovery(addrs string, ttl int) (*ConsulDiscovery, error) { - cfg := api.DefaultConfig() - cfg.Address = strings.Split(addrs, ",")[0] - client, err := api.NewClient(cfg) - if err != nil { - return nil, fmt.Errorf("failed to connect to Consul: %v", err) - } - _, err = client.Status().Leader() - if err != nil { - return nil, fmt.Errorf("Consul health check failed: %v", err) - } - return &ConsulDiscovery{client: client, ttl: ttl}, nil -} - -func (d *ConsulDiscovery) Register(ctx context.Context, serviceName, instanceID, host string, httpPort, grpcPort int) error { - reg := &api.AgentServiceRegistration{ - ID: instanceID, - Name: serviceName, - Address: host, - Port: httpPort, - Tags: []string{"http", "grpc"}, - Meta: map[string]string{ - "grpc_port": fmt.Sprintf("%d", grpcPort), - }, - Check: &api.AgentServiceCheck{ - HTTP: fmt.Sprintf("http://%s:%d/hello", host, httpPort), - Interval: fmt.Sprintf("%ds", d.ttl), - Timeout: "5s", - TTL: fmt.Sprintf("%ds", d.ttl), - }, - } - return d.client.Agent().ServiceRegister(reg) -} - -func (d *ConsulDiscovery) Deregister(ctx context.Context, instanceID string) error { - return d.client.Agent().ServiceDeregister(instanceID) -} - -func (d *ConsulDiscovery) Discover(ctx context.Context, serviceName string) ([]Instance, error) { - services, _, err := d.client.Health().Service(serviceName, "", true, nil) - if err != nil { - return nil, fmt.Errorf("failed to discover instances: %v", err) - } - var instances []Instance - for _, entry := range services { - grpcPort, _ := strconv.Atoi(entry.Service.Meta["grpc_port"]) - instances = append(instances, Instance{ - InstanceID: entry.Service.ID, - Host: entry.Service.Address, - HTTPPort: entry.Service.Port, - GRPCPort: grpcPort, - Metadata: entry.Service.Meta, - }) - } - return instances, nil -} - -func (d *ConsulDiscovery) Close() error { - return nil -} diff --git a/internal/service_discovery/etcd_discovery.go b/internal/service_discovery/etcd_discovery.go deleted file mode 100644 index 1ede2f2..0000000 --- a/internal/service_discovery/etcd_discovery.go +++ /dev/null @@ -1,100 +0,0 @@ -package service_discovery - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - clientv3 "go.etcd.io/etcd/client/v3" -) - -type EtcdDiscovery struct { - client *clientv3.Client - ttl int64 // seconds -} - -func NewEtcdDiscovery(addrs string, ttl int64) (*EtcdDiscovery, error) { - client, err := clientv3.New(clientv3.Config{ - Endpoints: strings.Split(addrs, ","), - DialTimeout: 5 * time.Second, - }) - if err != nil { - return nil, fmt.Errorf("failed to connect to etcd: %v", err) - } - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - _, err = client.Status(ctx, addrs) - if err != nil { - client.Close() - return nil, fmt.Errorf("etcd health check failed: %v", err) - } - return &EtcdDiscovery{client: client, ttl: ttl}, nil -} - -func (d *EtcdDiscovery) Register(ctx context.Context, serviceName, instanceID, host string, httpPort, grpcPort int) error { - lease, err := d.client.Grant(ctx, d.ttl) - if err != nil { - return fmt.Errorf("failed to create lease: %v", err) - } - - instance := Instance{ - InstanceID: instanceID, - Host: host, - HTTPPort: httpPort, - GRPCPort: grpcPort, - } - data, err := json.Marshal(instance) - if err != nil { - return fmt.Errorf("failed to marshal instance: %v", err) - } - - key := fmt.Sprintf("/services/%s/%s", serviceName, instanceID) - _, err = d.client.Put(ctx, key, string(data), clientv3.WithLease(lease.ID)) - if err != nil { - return fmt.Errorf("failed to register instance: %v", err) - } - - go func() { - for { - select { - case <-ctx.Done(): - return - default: - _, err := d.client.KeepAliveOnce(context.Background(), lease.ID) - if err != nil { - return - } - time.Sleep(time.Duration(d.ttl/2) * time.Second) - } - } - }() - return nil -} - -func (d *EtcdDiscovery) Deregister(ctx context.Context, instanceID string) error { - key := fmt.Sprintf("/services/%s/%s", "go-captcha-service", instanceID) - _, err := d.client.Delete(ctx, key) - return err -} - -func (d *EtcdDiscovery) Discover(ctx context.Context, serviceName string) ([]Instance, error) { - resp, err := d.client.Get(ctx, fmt.Sprintf("/services/%s/", serviceName), clientv3.WithPrefix()) - if err != nil { - return nil, fmt.Errorf("failed to discover instances: %v", err) - } - var instances []Instance - for _, kv := range resp.Kvs { - var instance Instance - if err := json.Unmarshal(kv.Value, &instance); err != nil { - continue - } - instances = append(instances, instance) - } - return instances, nil -} - -func (d *EtcdDiscovery) Close() error { - return d.client.Close() -} diff --git a/internal/service_discovery/nacos_discovery.go b/internal/service_discovery/nacos_discovery.go deleted file mode 100644 index 963b812..0000000 --- a/internal/service_discovery/nacos_discovery.go +++ /dev/null @@ -1,93 +0,0 @@ -package service_discovery - -import ( - "context" - "fmt" - "strconv" - "strings" - - "github.com/nacos-group/nacos-sdk-go/v2/clients" - "github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client" - "github.com/nacos-group/nacos-sdk-go/v2/common/constant" - "github.com/nacos-group/nacos-sdk-go/v2/vo" -) - -type NacosDiscovery struct { - client naming_client.INamingClient -} - -func NewNacosDiscovery(addrs string, ttl int64) (*NacosDiscovery, error) { - clientConfig := *constant.NewClientConfig( - constant.WithNamespaceId(""), - constant.WithTimeoutMs(5000), - constant.WithNotLoadCacheAtStart(true), - ) - serverConfigs := []constant.ServerConfig{} - for _, addr := range strings.Split(addrs, ",") { - hostPort := strings.Split(addr, ":") - host := hostPort[0] - port, _ := strconv.Atoi(hostPort[1]) - serverConfigs = append(serverConfigs, *constant.NewServerConfig(host, uint64(port))) - } - namingClient, err := clients.NewNamingClient( - vo.NacosClientParam{ - ClientConfig: &clientConfig, - ServerConfigs: serverConfigs, - }, - ) - if err != nil { - return nil, fmt.Errorf("failed to connect to Nacos: %v", err) - } - return &NacosDiscovery{client: namingClient}, nil -} - -func (d *NacosDiscovery) Register(ctx context.Context, serviceName, instanceID, host string, httpPort, grpcPort int) error { - _, err := d.client.RegisterInstance(vo.RegisterInstanceParam{ - Ip: host, - Port: uint64(httpPort), - ServiceName: serviceName, - GroupName: instanceID, - Weight: 1, - Enable: true, - Healthy: true, - Ephemeral: true, - Metadata: map[string]string{ - "grpc_port": fmt.Sprintf("%d", grpcPort), - }, - }) - - return err -} - -func (d *NacosDiscovery) Deregister(ctx context.Context, instanceID string) error { - _, err := d.client.DeregisterInstance(vo.DeregisterInstanceParam{ - GroupName: instanceID, - ServiceName: "go-captcha-service", - }) - return err -} - -func (d *NacosDiscovery) Discover(ctx context.Context, serviceName string) ([]Instance, error) { - instances, err := d.client.GetService(vo.GetServiceParam{ - ServiceName: serviceName, - }) - if err != nil { - return nil, fmt.Errorf("failed to discover instances: %v", err) - } - var result []Instance - for _, inst := range instances.Hosts { - grpcPort, _ := strconv.Atoi(inst.Metadata["grpc_port"]) - result = append(result, Instance{ - InstanceID: inst.InstanceId, - Host: inst.Ip, - HTTPPort: int(inst.Port), - GRPCPort: grpcPort, - Metadata: inst.Metadata, - }) - } - return result, nil -} - -func (d *NacosDiscovery) Close() error { - return nil -} diff --git a/internal/service_discovery/service_discovery.go b/internal/service_discovery/service_discovery.go deleted file mode 100644 index da214ae..0000000 --- a/internal/service_discovery/service_discovery.go +++ /dev/null @@ -1,22 +0,0 @@ -package service_discovery - -import ( - "context" -) - -// ServiceDiscovery defines the interface for service discovery -type ServiceDiscovery interface { - Register(ctx context.Context, serviceName, instanceID, host string, httpPort, grpcPort int) error - Deregister(ctx context.Context, instanceID string) error - Discover(ctx context.Context, serviceName string) ([]Instance, error) - Close() error -} - -// Instance represents a service instance -type Instance struct { - InstanceID string - Host string - HTTPPort int - GRPCPort int - Metadata map[string]string -} diff --git a/internal/service_discovery/service_discovery_test.go b/internal/service_discovery/service_discovery_test.go deleted file mode 100644 index a9b8f35..0000000 --- a/internal/service_discovery/service_discovery_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package service_discovery - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEtcdDiscovery(t *testing.T) { - discovery, err := NewEtcdDiscovery("localhost:2379", 10) - assert.NoError(t, err) - - err = discovery.Register(context.Background(), "go-captcha-service", "id1", "127.0.0.1", 8080, 50051) - assert.NoError(t, err) - - instances, err := discovery.Discover(context.Background(), "go-captcha-service") - assert.NoError(t, err) - assert.Empty(t, instances) - - err = discovery.Deregister(context.Background(), "id1") - assert.NoError(t, err) - - err = discovery.Close() - assert.NoError(t, err) -} diff --git a/internal/service_discovery/zookeeper_discovery.go b/internal/service_discovery/zookeeper_discovery.go deleted file mode 100644 index a97e568..0000000 --- a/internal/service_discovery/zookeeper_discovery.go +++ /dev/null @@ -1,95 +0,0 @@ -package service_discovery - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/go-zookeeper/zk" -) - -type ZookeeperDiscovery struct { - conn *zk.Conn - ttl int64 // seconds -} - -func NewZookeeperDiscovery(addrs string, ttl int64) (*ZookeeperDiscovery, error) { - conn, _, err := zk.Connect(strings.Split(addrs, ","), 5*time.Second) - if err != nil { - return nil, fmt.Errorf("failed to connect to ZooKeeper: %v", err) - } - _, _, err = conn.Children("/") - if err != nil { - conn.Close() - return nil, fmt.Errorf("ZooKeeper health check failed: %v", err) - } - return &ZookeeperDiscovery{conn: conn, ttl: ttl}, nil -} - -func (d *ZookeeperDiscovery) Register(ctx context.Context, serviceName, instanceID, host string, httpPort, grpcPort int) error { - instance := Instance{ - InstanceID: instanceID, - Host: host, - HTTPPort: httpPort, - GRPCPort: grpcPort, - } - data, err := json.Marshal(instance) - if err != nil { - return fmt.Errorf("failed to marshal instance: %v", err) - } - - path := fmt.Sprintf("/services/%s/%s", serviceName, instanceID) - _, err = d.conn.Create(path, data, zk.FlagEphemeral, zk.WorldACL(zk.PermAll)) - if err != nil && err != zk.ErrNodeExists { - return fmt.Errorf("failed to register instance: %v", err) - } - - go func() { - for { - select { - case <-ctx.Done(): - return - default: - exists, _, err := d.conn.Exists(path) - if err != nil || !exists { - d.conn.Create(path, data, zk.FlagEphemeral, zk.WorldACL(zk.PermAll)) - } - time.Sleep(time.Duration(d.ttl/2) * time.Second) - } - } - }() - return nil -} - -func (d *ZookeeperDiscovery) Deregister(ctx context.Context, instanceID string) error { - path := fmt.Sprintf("/services/%s/%s", "go-captcha-service", instanceID) - return d.conn.Delete(path, -1) -} - -func (d *ZookeeperDiscovery) Discover(ctx context.Context, serviceName string) ([]Instance, error) { - path := fmt.Sprintf("/services/%s", serviceName) - children, _, err := d.conn.Children(path) - if err != nil { - return nil, fmt.Errorf("failed to discover instances: %v", err) - } - var instances []Instance - for _, child := range children { - data, _, err := d.conn.Get(path + "/" + child) - if err != nil { - continue - } - var instance Instance - if err := json.Unmarshal(data, &instance); err != nil { - continue - } - instances = append(instances, instance) - } - return instances, nil -} - -func (d *ZookeeperDiscovery) Close() error { - d.conn.Close() - return nil -} diff --git a/modd.conf b/modd.conf index b503538..efac005 100644 --- a/modd.conf +++ b/modd.conf @@ -1,4 +1,4 @@ **/*.go { prep: go build -o ./.cache/go-captcha-service -v cmd/go-captcha-service/main.go - daemon +sigkill: ./.cache/go-captcha-service -config config.json + daemon +sigkill: ./.cache/go-captcha-service -config config.dev.json -gocaptcha-config gocaptcha.dev.json } \ No newline at end of file From c0ae89faa9db2e7224482080175bc6ec433aabc9 Mon Sep 17 00:00:00 2001 From: Awen Date: Tue, 22 Apr 2025 17:22:16 +0800 Subject: [PATCH 5/6] add README_zh.md --- Dockerfile | 4 +- Makefile | 25 +- README.md | 24 +- README_zh.md | 874 ++++++++++++++++++++++++++++++++ cmd/go-captcha-service/main.go | 2 + internal/app/app.go | 21 +- internal/server/http_handler.go | 7 + 7 files changed, 926 insertions(+), 31 deletions(-) create mode 100644 README_zh.md diff --git a/Dockerfile b/Dockerfile index 8129b20..b339223 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build phase -FROM --platform=$BUILDPLATFORM golang:1.21 AS builder +FROM --platform=$BUILDPLATFORM golang:1.23 AS builder WORKDIR /app @@ -10,7 +10,7 @@ COPY . . ARG TARGETOS ARG TARGETARCH -RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s" -o go-captcha-service ./cmd/go-captcha-service +RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s" -v -a -trimpath -o go-captcha-service ./cmd/go-captcha-service # Run phase (default binary) FROM scratch AS binary diff --git a/Makefile b/Makefile index 01679c8..1c239f3 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # Variables BINARY_NAME=go-captcha-service -VERSION?=0.1.0 +VERSION?=0.0.1 BUILD_DIR=build PLATFORMS=darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 linux/arm/v7 windows/amd64 DOCKER_IMAGE?=wenlng/go-captcha-service @@ -116,11 +116,24 @@ docker-build: .PHONY: docker-build-multi docker-build-multi: docker buildx build \ - --platform linux/amd64,linux/arm64,linux/arm/v7 \ - -t $(DOCKER_IMAGE):latest \ - -t $(DOCKER_IMAGE):amd64 \ - -t $(DOCKER_IMAGE):arm64 \ - -t $(DOCKER_IMAGE):armv7 \ + --platform linux/amd64,linux/arm64,linux/arm/v7,windows/amd64 \ + -t $(DOCKER_IMAGE):$(VERSION) \ + -t $(DOCKER_IMAGE):amd64-$(VERSION) \ + -t $(DOCKER_IMAGE):arm64-$(VERSION) \ + -t $(DOCKER_IMAGE):armv7-$(VERSION) \ + --push . + +# Multi-architecture Docker build and push (binary) +.PHONY: docker-proxy-build-multi +docker-proxy-build-multi: + export http_proxy=http://127.0.0.1:7890 + export https_proxy=http://127.0.0.1:7890 + docker buildx build \ + --platform linux/amd64,linux/arm64,linux/arm/v7,windows/amd64 \ + -t $(DOCKER_IMAGE):$(VERSION) \ + -t $(DOCKER_IMAGE):amd64-$(VERSION) \ + -t $(DOCKER_IMAGE):arm64-$(VERSION) \ + -t $(DOCKER_IMAGE):armv7-$(VERSION) \ --push . # Run a local Docker container (binary) diff --git a/README.md b/README.md index 5456348..49ca850 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ -# go-captcha-service +
+

GoCaptcha Service

+
+ + + + + + +
- -# config 热重载有效的字段 -redis_addrs -etcd_addrs -memcache_addrs -cache_type -cache_ttl -cache_key_prefix -api_keys -log_level -rate_limit_qps -rate_limit_burst +
diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000..18ea042 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,874 @@ +
+

GoCaptcha Service

+
+ + + + + + +
+ +
+ +`GoCaptcha Service` 是一个基于 Go 语言开发的高性能行为验证码服务,基于 **[go-captcha](https://github.com/wenlng/go-captcha)** 行为验证码基本库,支持点击、滑动、拖拽和旋转等多种验证码模式。它提供 HTTP 和 gRPC 接口,集成多种服务发现(Etcd、Nacos、Zookeeper、Consul)、分布式缓存(Memory、Redis、Etcd、Memcache)和分布式动态配置,支持单机和分布式部署,旨在为 Web 应用提供安全、灵活的验证码解决方案。 + +
+ +> English | [中文](README_zh.md) +

⭐️ 如果能帮助到你,请随手给点一个star

+

QQ交流群:178498936

+ +
+Poster +
+ +
+
+
+ +## 项目生态 + +| 名称 | 描述 | +|----------------------------------------------------------------------------|---------------------------------------------------------------------------------------------| +| [document](http://gocaptcha.wencodes.com) | GoCaptcha 文档 | +| [online demo](http://gocaptcha.wencodes.com/demo/) | GoCaptcha 在线演示 | +| [go-captcha-example](https://github.com/wenlng/go-captcha-example) | Golang + 前端 + APP实例 | +| [go-captcha-assets](https://github.com/wenlng/go-captcha-assets) | Golang 内嵌素材资源 | +| [go-captcha](https://github.com/wenlng/go-captcha) | Golang 验证码 | +| [go-captcha-jslib](https://github.com/wenlng/go-captcha-jslib) | Javascript 验证码 | +| [go-captcha-vue](https://github.com/wenlng/go-captcha-vue) | Vue 验证码 | +| [go-captcha-react](https://github.com/wenlng/go-captcha-react) | React 验证码 | +| [go-captcha-angular](https://github.com/wenlng/go-captcha-angular) | Angular 验证码 | +| [go-captcha-svelte](https://github.com/wenlng/go-captcha-svelte) | Svelte 验证码 | +| [go-captcha-solid](https://github.com/wenlng/go-captcha-solid) | Solid 验证码 | +| [go-captcha-uni](https://github.com/wenlng/go-captcha-uni) | UniApp 验证码,兼容 Android、IOS、小程序、快应用等 | +| [go-captcha-service](https://github.com/wenlng/go-captcha-service) | GoCaptcha 服务,支持二进制、Docker镜像等方式部署,
提供 HTTP/GRPC 方式访问接口,
可用单机模式和分布式(服务发现、负载均衡、动态配置等) | +| [go-captcha-service-sdk](https://github.com/wenlng/go-captcha-service-sdk) | GoCaptcha 服务SDK工具包,包含 HTTP/GRPC 请求服务接口,
支持静态模式、服务发现、负载均衡 | +| ... | | + +
+
+ +## 功能特性 + +- **多种验证码模式**:支持文本/图形点击、滑动、拖拽和旋转验证码。 +- **双协议支持**:提供 RESTful HTTP 和 gRPC 接口。 +- **服务发现**:支持 Etcd、Nacos、Zookeeper 和 Consul,实现分布式服务注册与发现。 +- **分布式缓存**:支持 Memory、Redis、Etcd 和 Memcache,优化验证码数据存储。 +- **分布式动态配置**:通过 Etcd、Nacos、Zookeeper 或 Consul 实时更新配置。 +- **高可配置性**:支持自定义文本、字体、图片资源、验证码尺寸、生成规则等配置。 +- **高性能**:基于 Go 的并发模型,适合高流量场景,同时结合分布式架构部署,确保服务处于高可用、高性能、高响应的状态。 +- **跨平台**:支持二进制、命令行、PM2、Docker 和 Docker Compose 部署。 + +
+
+ +## 安装与部署 +`GoCaptcha Service` 支持多种部署方式,包括单机部署(二进制、命令行、PM2、Docker)和分布式部署(结合服务发现和分布式缓存,分布式动态配置可选)。 + +### 前置条件 +- 可选:Docker(用于容器化部署) +- 可选:服务发现/动态配置中间件(Etcd、Nacos、Zookeeper、Consul) +- 可选:缓存服务(Redis、Etcd、Memcache) +- 可选:Node.js 和 PM2(用于 PM2 部署) +- 可选:gRPC 客户端工具(如 `grpcurl`) + +### 单机部署 + +#### 二进制方式 + +1. 从 [Github Releases](https://github.com/wenlng/go-captcha-service/releases) 或 [Gitee Releases](https://gitee.com/wenlng/go-captcha-service/releases) 发布页下载最新版本相对应平台的二进制可执行文件。 + + ```bash + ./go-captcha-service-[xxx] + ``` + +2. 可选:配置应用:可复制仓库下的 config.json 应用配置和 gocaptcha.json 验证码配置文件放在同级目录下,在启动时指定配置文件。 + + ```bash + ./go-captcha-service-[xxx] -config config.json -gocaptcha-config gocaptcha.json + ``` + +3. 访问 HTTP 接口(如 `http://localhost:8080/api/v1/get-data?id=click-default-ch`)或 gRPC 接口(`localhost:50051`)。 + + +
+
+ +#### PM2 部署 +PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守护和日志管理。 +1. 安装 Node.js 和 PM2: + + ```bash + npm install -g pm2 + ``` + +2. 创建 PM2 配置文件 `ecosystem.config.js`: + + ```javascript + module.exports = { + apps: [{ + name: 'go-captcha-service', + script: './go-captcha-service-[xxx]', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + CAPTCHA_HTTP_PORT: '8080', + CAPTCHA_GRPC_PORT: '50051', + CAPTCHA_CACHE_TYPE: 'memory' + } + }] + }; + ``` + +3. 启动服务: + + ```bash + pm2 start ecosystem.config.js + ``` + +4. 查看日志和状态: + + ```bash + pm2 logs go-captcha-service + pm2 status + ``` + +5. 设置开机自启: + + ```bash + pm2 startup + pm2 save + ``` + +
+
+ +#### Docker 部署 + +1. 创建 `Dockerfile`: + + ```dockerfile + FROM golang:1.18 + + WORKDIR /app + + COPY . . + + RUN go mod download + RUN go build -o go-captcha-service + + EXPOSE 8080 50051 + + CMD ["./go-captcha-service"] + ``` + +2. 构建镜像: + + ```bash + docker build -t go-captcha-service:1.0.0 . + ``` + +3. 运行容器,挂载配置文件: + + ```bash + docker run -d -p 8080:8080 -p 50051:50051 \ + -v $(pwd)/config.json:/app/config.json \ + -v $(pwd)/gocaptcha.json:/app/gocaptcha.json \ + -v $(pwd)/gocaptcha:/app/gocaptcha \ + --name go-captcha-service go-captcha-service:latest + ``` + +
+
+ + +#### 使用官方 Docker 镜像 + +1. 拉取官方镜像: + + ```bash + docker pull wenlng/go-captcha-service + ``` + +2. 运行容器: + + ```bash + docker run -d -p 8080:8080 -p 50051:50051 \ + -v $(pwd)/config.json:/app/config.json \ + -v $(pwd)/gocaptcha.json:/app/gocaptcha.json \ + -v $(pwd)/gocaptcha:/app/gocaptcha \ + --name go-captcha-service wenlng/go-captcha-service:latest + ``` + +
+
+ +#### 配置分布式缓存 + +1. 在 `config.json` 中选择分布式缓存(如 Redis): + + ```json + { + "cache_type": "redis", + "redis_addrs": "localhost:6379", + "cache_ttl": 1800, + "cache_key_prefix": "GO_CAPTCHA_DATA:" + } + ``` + +2. 启动 Redis: + + ```bash + docker run -d -p 6379:6379 --name redis redis:latest + ``` + +
+ +#### 分布式动态配置 +注意:当开启分布式动态配置功能后,`config.json` 和 `gocaptcha.json` 会同时作用 + +1. 在 `config.json` 中启用动态配置,选择中间件(如 Etcd): + + ```json + { + "enable_dynamic_config": true, + "service_discovery": "etcd", + "service_discovery_addrs": "localhost:2379" + } + ``` + +2. 启动 Etcd: + + ```bash + docker run -d -p 8848:8848 --name etcd bitnami/etcd::latest + ``` + +3. 例如在 gocaptcha.json 配置文件中,修改配置: + + ```json + { + "builder": { + + } + } + ``` + +4. 配置文件同步与拉取 +* 服务在启动时会根据 `config_version` 版本决定推送与拉取,当本地版本大于远程(如 Etcd)的配置版本时会将本地配置推送覆盖,反之自动拉取更新到本地(非文件式更新)。 +* 在服务启动后,动态配置管理器会实时监听远程(如 Etcd)的配置,当远程配置发生变更后,会摘取到本地进行版本比较,当大于本地版本时都会覆盖本地的配置。 + + +
+
+ + +#### 分布式服务发现 +1. 在 `config.json` 中启用动态配置,选择中间件(如 Etcd): + + ```json + { + "enable_service_discovery": true, + "service_discovery": "etcd", + "service_discovery_addrs": "localhost:2379" + } + ``` + +2. 启动 Etcd: + + ```bash + docker run -d -p 8848:8848 --name etcd bitnami/etcd::latest + ``` +3. 服务注册与发现 +* 服务在启动时会自动向(Etcd | xxx)的中心注册服务实例。 +* 在服务启动后,同时会进行服务实例的变化监听,可参考在 [go-captcha-service-sdk](https://github.com/wenlng/go-captcha-service-sdk) 中的负载均衡应用。 + +
+
+ +#### Docker Compose 分布式部署 + +创建 `docker-compose.yml`,包含多个服务实例、Consul、Redis、ZooKeeper 和 Nacos: + +```yaml +version: '3' +services: + captcha-service-1: + image: wenlng/go-captcha-service:latest + ports: + - "8080:8080" + - "50051:50051" + volumes: + - ./config.json:/app/config.json + - ./gocaptcha.json:/app/gocaptcha.json + - ./resources/gocaptcha:/app/resources/gocaptcha + depends_on: + - consul + - redis + restart: unless-stopped + + captcha-service-2: + image: wenlng/go-captcha-service:latest + ports: + - "8081:8080" + - "50052:50051" + volumes: + - ./config.json:/app/config.json + - ./gocaptcha.json:/app/gocaptcha.json + - ./resources/gocaptcha:/app/resources/gocaptcha + depends_on: + - consul + - redis + restart: unless-stopped + + consul: + image: consul:latest + ports: + - "8500:8500" + command: agent -server -bootstrap -ui -client=0.0.0.0 + restart: unless-stopped + + redis: + image: redis:latest + ports: + - "6379:6379" + restart: unless-stopped +``` + +运行: + +```bash +docker-compose up -d +``` + +
+
+ + +## 配置说明 + +### 启动参数 +注:启动参数与 `config.json` 文件中有相对应,注意名称格式(**推荐使用配置文件方式**) +* config:指定配置文件路径,默认 "config.json"。 +* gocaptcha-config:指定 GoCaptcha 配置文件路径,默认 "gocaptcha.json"。 +* service-name:设置服务名称。 +* http-port:设置 HTTP 服务器端口。 +* grpc-port:设置 gRPC 服务器端口。 +* redis-addrs:设置 Redis 集群地址,逗号分隔。 +* etcd-addrs:设置 etcd 地址,逗号分隔。 +* memcache-addrs:设置 Memcached 地址,逗号分隔。 +* cache-type:设置缓存类型,支持 redis、memory、etcd、memcache。 +* cache-ttl:设置缓存 TTL,单位秒。 +* cache-key-prefix:设置缓存键前缀,默认 "GO_CAPTCHA_DATA:"。 +* service-discovery:设置服务发现类型,支持 etcd、zookeeper、consul、nacos。 +* service-discovery-addrs:设置服务发现服务器地址,逗号分隔。 +* service-discovery-ttl:设置服务发现注册存活时间,单位秒,默认 10。 +* service-discovery-keep-alive:设置服务发现保活间隔,单位秒,默认 3。 +* service-discovery-max-retries:设置服务发现操作最大重试次数,默认 3。 +* service-discovery-base-retry-delay:设置服务发现重试基础延迟,单位毫秒,默认 3。 +* service-discovery-username:设置服务发现认证用户名。 +* service-discovery-password:设置服务发现认证密码。 +* service-discovery-tls-server-name:设置服务发现 TLS 服务器名称。 +* service-discovery-tls-address:设置服务发现 TLS 服务器地址。 +* service-discovery-tls-cert-file:设置服务发现 TLS 证书文件路径。 +* service-discovery-tls-key-file:设置服务发现 TLS 密钥文件路径。 +* service-discovery-tls-ca-file:设置服务发现 TLS CA 文件路径。 +* rate-limit-qps:设置速率限制 QPS。 +* rate-limit-burst:设置速率限制突发量。 +* api-keys:设置 API 密钥,逗号分隔。 +* log-level:设置日志级别,支持 error、debug、warn、info。 +* enable-service-discovery:启用服务发现,默认 false。 +* enable-dynamic-config:启用动态配置,默认 false。 +* health-check:运行健康检查并退出,默认 false。 +* enable-cors:启用跨域资源共享,默认 false。 + + +### 配置文件 +服务使用两个配置文件:`config.json` 和 `gocaptcha.json`,分别定义服务运行参数和验证码生成的配置. + +### config.json + +`config.json` 定义服务的基础配置。 + +```json +{ + "config_version": 1, + "service_name": "go-captcha-service", + "http_port": "8080", + "grpc_port": "50051", + "redis_addrs": "localhost:6379", + "etcd_addrs": "localhost:2379", + "memcache_addrs": "localhost:11211", + "cache_type": "memory", + "cache_ttl": 1800, + "cache_key_prefix": "GO_CAPTCHA_DATA:", + "enable_dynamic_config": false, + "enable_service_discovery": false, + "service_discovery": "etcd", + "service_discovery_addrs": "localhost:2379", + "service_discovery_username": "", + "service_discovery_password": "", + "service_discovery_ttl": 10, + "service_discovery_keep_alive": 3, + "service_discovery_max_retries": 3, + "service_discovery_base_retry_delay": 500, + "service_discovery_tls_server_name": "", + "service_discovery_tls_address": "", + "service_discovery_tls_cert_file": "", + "service_discovery_tls_key_file": "", + "service_discovery_tls_ca_file": "", + "rate_limit_qps": 1000, + "rate_limit_burst": 1000, + "enable_cors": true, + "log_level": "info", + "api_keys": ["my-secret-key-123", "another-key-456", "another-key-789"] +} +``` + +#### 参数说明 + +- `config_version` (整数):配置文件版本号,用于分布式动态配置控制,默认 `1`。 +- `service_name` (字符串):服务名称,默认 `go-captcha-service`。 +- `http_port` (字符串):HTTP 端口,默认 `8080`。 +- `grpc_port` (字符串):gRPC 端口,默认 `50051`。 +- `redis_addrs` (字符串):Redis 地址,默认 `localhost:6379`。用于 `cache_type: redis`。 +- `etcd_addrs` (字符串):Etcd 地址,默认 `localhost:2379`。用于 `cache_type: etcd` 或 `service_discovery: etcd`. +- `memcache_addrs` (字符串):Memcache 地址,默认 `localhost:11211`。用于 `cache_type: memcache`. +- `cache_type` (字符串):缓存类型,默认 `memory`: + - `memory`:内存缓存,适合单机部署。 + - `redis`:分布式键值存储,适合高可用场景。 + - `etcd`:分布式键值存储,适合与服务发现共用 Etcd。 + - `memcache`:高性能分布式缓存,适合高并发。 +- `cache_ttl` (整数):缓存有效期(秒),默认 `1800`. +- `cache_key_prefix` (字符串):缓存键前缀,默认 `GO_CAPTCHA_DATA:`。 +- `enable_dynamic_config` (布尔):启用动态配置,默认 `false`。 +- `enable_service_discovery` (布尔):启用服务发现,默认 `false`。 +- `service_discovery` (字符串):服务发现类型,默认 `etcd`: + - `etcd`:适合一致性要求高的场景。 + - `nacos`:适合云原生环境。 + - `zookeeper`:适合复杂分布式系统。 + - `consul`:轻量级,支持健康检查。 +- `service_discovery_addrs` (字符串):服务发现地址,如 Etcd 为 `localhost:2379`,Nacos 为 `localhost:8848`。 +- `service_discovery_username` (字符串):用户名,例如 Nacos 的默认用户名为`nacos`,默认空。 +- `service_discovery_password` (字符串):密码,例如 Nacos 的默认用户密码为`nacos`,默认空。 +- `service_discovery_ttl` (整数):服务注册租约时间(秒),默认 `10`。 +- `service_discovery_keep_alive` (整数):心跳间隔(秒),默认 `3`。 +- `service_discovery_max_retries` (整数):重试次数,默认 `3`。 +- `service_discovery_base_retry_delay` (整数):重试延迟(毫秒),默认 `500`。 +- `service_discovery_tls_server_name` (字符串):TLS 服务器名称,默认空。 +- `service_discovery_tls_address` (字符串):TLS 地址,默认空。 +- `service_discovery_tls_cert_file` (字符串):TLS 证书文件,默认空。 +- `service_discovery_tls_key_file` (字符串):TLS 密钥文件,默认空。 +- `service_discovery_tls_ca_file` (字符串):TLS CA 证书文件,默认空。 +- `rate_limit_qps` (整数):API 每秒请求限流,默认 `1000`。 +- `rate_limit_burst` (整数):API 限流突发容量,默认 `1000`。 +- `enable_cors` (布尔):启用 CORS,默认 `true`。 +- `log_level` (字符串):日志级别(`debug`、`info`、`warn`、`error`),默认 `info`。 +- `api_keys` (字符串数组):API 认证密钥,默认包含示例密钥。 + +### gocaptcha.json + +`gocaptcha.json` 定义验证码的资源和生成配置。 + +```json +{ + "config_version": 1, + "resources": { + "version": "0.0.1", + "char": { + "languages": { + "chinese": [], + "english": [] + } + }, + "font": { + "type": "load", + "file_dir": "./gocaptcha/fonts/", + "file_maps": { + "yrdzst_bold": "yrdzst-bold.ttf" + } + }, + "shape_image": { + "type": "load", + "file_dir": "./gocaptcha/shape_images/", + "file_maps": { + "shape_01": "shape_01.png", + "shape_01.png":"c.png" + } + }, + "master_image": { + "type": "load", + "file_dir": "./gocaptcha/master_images/", + "file_maps": { + "image_01": "image_01.jpg", + "image_02":"image_02.jpg" + } + }, + "thumb_image": { + "type": "load", + "file_dir": "./gocaptcha/thumb_images/", + "file_maps": { + + } + }, + "tile_image": { + "type": "load", + "file_dir": "./gocaptcha/tile_images/", + "file_maps": { + "tile_01": "tile_01.png", + "tile_02": "tile_02.png" + }, + "file_maps_02": { + "tile_mask_01": "tile_mask_01.png", + "tile_mask_02": "tile_mask_02.png" + }, + "file_maps_03": { + "tile_shadow_01": "tile_shadow_01.png", + "tile_shadow_02": "tile_shadow_02.png" + } + } + }, + "builder": { + "click_config_maps": { + "click-default-ch": { + "version": "0.0.1", + "language": "chinese", + "master": { + "image_size": { "width": 300, "height": 200 }, + "range_length": { "min": 6, "max": 7 }, + "range_angles": [ + { "min": 20, "max": 35 }, + { "min": 35, "max": 45 }, + { "min": 290, "max": 305 }, + { "min": 305, "max": 325 }, + { "min": 325, "max": 330 } + ], + "range_size": { "min": 26, "max": 32 }, + "range_colors": [ "#fde98e", "#60c1ff", "#fcb08e", "#fb88ff", "#b4fed4", "#cbfaa9", "#78d6f8"], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { "x": -1, "y": -1 }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { "width": 150, "height": 40 }, + "range_verify_length": { "min": 2, "max": 4 }, + "disabled_range_verify_length": false, + "range_text_size": { "min": 22, "max": 28 }, + "range_text_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c"], + "range_background_colors": ["#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c"], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click-dark-ch": { + "version": "0.0.1", + "language": "chinese", + // ... + }, + "click-default-en": { + "version": "0.0.1", + "language": "english", + // ... + }, + "click-dark-en": { + "version": "0.0.1", + "language": "english", + // ... + } + }, + "click_shape_config_maps": { + "click-shape-default": { + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "range_length": { "min": 6, "max": 7 }, + "range_angles": [ + { "min": 20, "max": 35 }, + { "min": 35, "max": 45 }, + { "min": 290, "max": 305 }, + { "min": 305, "max": 325 }, + { "min": 325, "max": 330 } + ], + "range_size": { "min": 26, "max": 32 }, + "range_colors": [ "#fde98e", "#60c1ff", "#fcb08e", "#fb88ff", "#b4fed4", "#cbfaa9", "#78d6f8"], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { "x": -1, "y": -1 }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { "width": 150, "height": 40}, + "range_verify_length": { "min": 2, "max": 4 }, + "disabled_range_verify_length": false, + "range_text_size": { "min": 22, "max": 28}, + "range_text_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c"], + "range_background_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c" ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + } + }, + "slide_config_maps": { + "slide-default": { + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { "min": 60, "max": 70 }, + "range_graph_angles": [ + { "min": 20, "max": 35 }, + ], + "generate_graph_number": 1, + "enable_graph_vertical_random": false, + "range_dead_zone_directions": ["left", "right"] + } + } + }, + "drag_config_maps": { + "drag-default": { + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { "min": 60, "max": 70 }, + "range_graph_angles": [ + { "min": 0, "max": 0 }, + ], + "generate_graph_number": 2, + "enable_graph_vertical_random": true, + "range_dead_zone_directions": ["left", "right", "top", "bottom"] + } + } + }, + "rotate_config_maps": { + "rotate-default": { + "version": "0.0.1", + "master": { + "image_square_size": 220, + }, + "thumb": { + "range_angles": [{ "min": 30, "max": 330 }], + "range_image_square_sizes": [140, 150, 160, 170], + "image_alpha": 1 + } + } + } + } +} +``` +
+ +##### 顶级字段 + +- `config_version` (整数):配置文件版本号,用于分布动态配置管理,默认 `1`。 + + +##### resources 字段 + +- `version` (字符串):资源配置版本号,用于控制重新创建新的验证码实例,默认 `0.0.1`。 +- `char.languages.chinese` (字符串数组):中文字符集,用于点击验证码的文本内容,默认空(默认取内置的资源)。 +- `char.languages.english` (字符串数组):英文字符集,默认空(默认取内置的资源)。 +- `font.type` (字符串):字体加载方式,固定为 `load`(从文件加载)。 +- `font.file_dir` (字符串):字体文件目录,默认 `./gocaptcha/fonts/`。 +- `font.file_maps` (对象):字体文件映射,键为字体名称,值为文件名。 + - 示例:`"yrdzst_bold": "yrdzst-bold.ttf"` 表示使用 `yrdzst-bold.ttf` 字体。 +- `shape_image.type` (字符串):形状图片加载方式,固定为 `load`。 +- `shape_image.file_dir` (字符串):形状图片目录,默认 `./gocaptcha/shape_images/`。 +- `shape_image.file_maps` (对象):形状图片映射。 + - 示例:`"shape_01": "shape_01.png"` 表示使用 `shape_01.png` 作为形状。 +- `master_image.type` (字符串):主图片加载方式,固定为 `load`。 +- `master_image.file_dir` (字符串):主图片目录,默认 `./gocaptcha/master_images/`。 +- `master_image.file_maps` (对象):主图片映射。 + - 示例:`"image_01": "image_01.jpg"` 表示使用 `image_01.jpg` 作为背景。 +- `thumb_image.type` (字符串):缩略图加载方式,固定为 `load`。 +- `thumb_image.file_dir` (字符串):缩略图目录,默认 `./gocaptcha/thumb_images/`。 +- `thumb_image.file_maps` (对象):缩略图映射,默认空。 +- `tile_image.type` (字符串):拼图图片加载方式,固定为 `load`。 +- `tile_image.file_dir` (字符串):拼图图片目录,默认 `./gocaptcha/tile_images/`。 +- `tile_image.file_maps` (对象):拼图图片映射。 + - 示例:`"tile_01": "tile_01.png"`。 +- `tile_image.file_maps_02` (对象):拼图蒙版映射。 + - 示例:`"tile_mask_01": "tile_mask_01.png"`。 +- `tile_image.file_maps_03` (对象):拼图阴影映射。 + - 示例:`"tile_shadow_01": "tile_shadow_01.png"`。 + +
+ +##### builder 字段 + +定义验证码生成样式,包含点击、形状点击、滑动、拖拽和旋转验证码的配置。 + + +###### click_config_maps + +定义文本点击验证码的配置,支持中英文和明暗主题,key为ID,在请求时传递,例如:`api/v1/get-data?id=click-default-ch`。 + +- `click-default-ch` (对象):中文默认主题配置。 + - `version` (字符串):配置版本号,用于控制重新创建新的验证码实例,默认 `0.0.1`。 + - `language` (字符串):语言,可配置 `char.languages` 中定义的语言名称,例如中文: `chinese`。 + - `master` (对象):主验证码图片配置。 + - `image_size.width` (整数):主图片宽度,默认 `300`。 + - `image_size.height` (整数):主图片高度,默认 `200`。 + - `range_length.min` (整数):验证码点数最小值,默认 `6`。 + - `range_length.max` (整数):验证码点数最大值,默认 `7`。 + - `range_angles` (对象数组):文本旋转角度范围(度)。 + - 示例:`{"min": 20, "max": 35}` 表示角度范围 20°-35°。 + - `range_size.min` (整数):文本大小最小值(像素),默认 `26`。 + - `range_size.max` (整数):文本大小最大值,默认 `32`。 + - `range_colors` (字符串数组):文本颜色列表(十六进制)。 + - 示例:`"#fde98e"`。 + - `display_shadow` (布尔):是否显示文本阴影,默认 `true`。 + - `shadow_color` (字符串):阴影颜色,默认 `#101010`。 + - `shadow_point.x` (整数):阴影偏移 X 坐标,默认 `-1`(自动计算)。 + - `shadow_point.y` (整数):阴影偏移 Y 坐标,默认 `-1`。 + - `image_alpha` (浮点数):图片透明度(0-1),默认 `1`。 + - `use_shape_original_color` (布尔):是否使用形状原始颜色,默认 `true`。 + - `thumb` (对象):缩略图(提示文本)配置。 + - `image_size.width` (整数):缩略图宽度,默认 `150`。 + - `image_size.height` (整数):缩略图高度,默认 `40`。 + - `range_verify_length.min` (整数):验证点数最小值,默认 `2`。 + - `range_verify_length.max` (整数):验证点数最大值,默认 `4`。 + - `disabled_range_verify_length` (布尔):是否禁用验证点数限制,默认 `false`。 + - `range_text_size.min` (整数):文本大小最小值,默认 `22`。 + - `range_text_size.max` (整数):文本大小最大值,默认 `28`。 + - `range_text_colors` (字符串数组):文本颜色列表。 + - `range_background_colors` (字符串数组):背景颜色列表。 + - `is_non_deform_ability` (布尔):是否禁用变形效果,默认 `false`。 + - `background_distort` (整数):背景扭曲程度,默认 `4`。 + - `background_distort_alpha` (浮点数):背景扭曲透明度,默认 `1`。 + - `background_circles_num` (整数):背景圆形干扰点数量,默认 `24`。 + - `background_slim_line_num` (整数):背景细线干扰数量,默认 `2`。 + +- `click-dark-ch` (对象):中文暗色主题配置,参数与 `click-default-ch` 类似,区别在于 `thumb.range_text_colors` 使用更亮的颜色以适配暗色背景。 + +- `click-default-en` (对象):英文默认主题配置,`language: english` 、`master.range_size` 和 `thumb.range_text_size` 更大(`34-48`),适配英文字符。 + +- `click-dark-en` (对象):英文暗色主题配置,类似 `click-dark-ch`, 注意区别字段 `language: english`。 + +
+ +###### click_shape_config_maps + +定义形状点击验证码的配置。 + +- `click-shape-default` (对象):默认形状点击配置,参数与 `click_config_maps` 的 `master` 和 `thumb` 类似,但针对形状图片而非文本。 + +
+ +###### slide_config_maps + +定义滑动验证码配置。 + +- `slide-default` (对象): + - `version` (字符串):配置版本号,用于控制重新创建新的验证码实例,默认 `0.0.1`。 + - `master` (对象):主验证码图片配置。 + - `image_size.width` (整数):主图片宽度,默认 `300`。 + - `image_size.height` (整数):主图片高度,默认 `200`。 + - `image_alpha` (浮点数):图片透明度(0-1),默认 `1`。 + - `thumb` (对象):滑块配置。 + - `range_graph_size.min` (整数):滑块图形大小最小值(像素),默认 `60`。 + - `range_graph_size.max` (整数):滑块图形大小最大值,默认 `70`。 + - `range_graph_angles` (对象数组):滑块图形旋转角度范围(度)。 + - 示例:`{"min": 20, "max": 35}`。 + - `generate_graph_number` (整数):生成滑块图形数量,默认 `1`。 + - `enable_graph_vertical_random` (布尔):是否启用垂直方向随机偏移,默认 `false`。 + - `range_dead_zone_directions` (字符串数组):滑块禁区方向,默认 `["left", "right"]`。 + +
+ +###### drag_config_maps + +定义拖拽验证码配置。 + +- `drag-default` (对象): + - `version` (字符串):配置版本号,用于控制重新创建新的验证码实例,默认 `0.0.1`。 + - `master` (对象):主验证码图片配置。 + - `image_size.width` (整数):主图片宽度,默认 `300`。 + - `image_size.height` (整数):主图片高度,默认 `200`。 + - `image_alpha` (浮点数):图片透明度(0-1),默认 `1`。 + - `thumb` (对象):拖拽图形配置。 + - `range_graph_size.min` (整数):拖拽图形大小最小值(像素),默认 `60`。 + - `range_graph_size.max` (整数):拖拽图形大小最大值,默认 `70`。 + - `range_graph_angles` (对象数组):拖拽图形旋转角度范围(度)。 + - 示例:`{"min": 0, "max": 0}` 表示无旋转。 + - `generate_graph_number` (整数):生成拖拽图形数量,默认 `2`。 + - `enable_graph_vertical_random` (布尔):是否启用垂直方向随机偏移,默认 `true`。 + - `range_dead_zone_directions` (字符串数组):拖拽禁区方向,默认 `["left", "right", "top", "bottom"]`。 + +
+ +###### rotate_config_maps + +定义旋转验证码配置。 + +- `rotate-default` (对象): + - `version` (字符串):配置版本号,用于控制重新创建新的验证码实例,默认 `0.0.1`。 + - `master` (对象):主验证码图片配置。 + - `image_square_size` (整数):主图片正方形边长(像素),默认 `220`。 + - `thumb` (对象):旋转图形配置。 + - `range_angles` (对象数组):旋转角度范围(度)。 + - 示例:`{"min": 30, "max": 330}` 表示旋转范围 30°-330°。 + - `range_image_square_sizes` (整数数组):旋转图片正方形边长列表,默认 `[140, 150, 160, 170]`。 + - `image_alpha` (浮点数):图片透明度(0-1),默认 `1`。 + + + +
+
+ + +### 配置热重载说明 +`gocaptcha.json` 热重载以每个配置顶的 version 字段决定是否生效。 + +`config.json` 热重载有效的字段如下: +* redis_addrs +* etcd_addrs +* memcache_addrs +* cache_type +* cache_ttl +* cache_key_prefix +* api_keys +* log_level +* rate_limit_qps +* rate_limit_burst + + + +### 测试: + +- 验证码生成:验证图片、形状和密钥有效性。 +- 验证逻辑:测试不同输入的处理。 +- 服务发现:模拟 Etcd/Nacos/Zookeeper/Consul。 +- 缓存:测试 Memory/Redis/Etcd/Memcache。 +- 动态配置:测试 Nacos 配置更新。 + + +### 压力测试 + +测试 HTTP 接口: + +```bash +wrk -t12 -c400 -d30s http://127.0.0.1:8080/api/v1/get-data?id=click-default-ch +``` + +测试 gRPC 接口: + +```bash +grpcurl -plaintext -d '{"id":"click-default-ch"}' localhost:50051 gocaptcha.GoCaptchaService/GetData +``` diff --git a/cmd/go-captcha-service/main.go b/cmd/go-captcha-service/main.go index e0fb1e5..e64aa6a 100644 --- a/cmd/go-captcha-service/main.go +++ b/cmd/go-captcha-service/main.go @@ -17,6 +17,7 @@ import ( ) func main() { + fmt.Fprintf(os.Stdout, "[Main] Starting the application ...") a, err := app.NewApp() if err != nil { fmt.Fprintf(os.Stderr, "[Main] Failed to initialize app: %v\n", err) @@ -34,6 +35,7 @@ func main() { // Handle termination signals sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + fmt.Fprintf(os.Stdout, "[Main] The application start successfully") <-sigCh a.Shutdown() diff --git a/internal/app/app.go b/internal/app/app.go index 6375312..1fbc301 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -298,18 +298,19 @@ func (a *App) startHTTPServer(svcCtx *common.SvcContext, cfg *config.Config) err mwChain := middleware.NewChainHTTP(middlewares...) - http.Handle("/v1/get-data", mwChain.Then(handlers.GetDataHandler)) - http.Handle("/v1/check-data", mwChain.Then(handlers.CheckDataHandler)) - http.Handle("/v1/check-status", mwChain.Then(handlers.CheckStatusHandler)) - http.Handle("/v1/get-status-info", mwChain.Then(handlers.GetStatusInfoHandler)) - http.Handle("/v1/del-status-info", mwChain.Then(handlers.DelStatusInfoHandler)) + http.Handle("/api/v1/get-data", mwChain.Then(handlers.GetDataHandler)) + http.Handle("/api/v1/check-data", mwChain.Then(handlers.CheckDataHandler)) + http.Handle("/api/v1/check-status", mwChain.Then(handlers.CheckStatusHandler)) + http.Handle("/api/v1/get-status-info", mwChain.Then(handlers.GetStatusInfoHandler)) + http.Handle("/api/v1/del-status-info", mwChain.Then(handlers.DelStatusInfoHandler)) + http.Handle("/api/v1/status/health", mwChain.Then(handlers.HealthStatusHandler)) http.Handle("/rate-limit", mwChain.Then(middleware.RateLimitHandler(a.limiter, a.logger))) - http.Handle("/v1/manage/upload-resource", mwChain.Then(handlers.UploadResourceHandler)) - http.Handle("/v1/manage/delete-resource", mwChain.Then(handlers.DeleteResourceHandler)) - http.Handle("/v1/manage/get-resource-list", mwChain.Then(handlers.GetResourceListHandler)) - http.Handle("/v1/manage/get-config", mwChain.Then(handlers.GetGoCaptchaConfigHandler)) - http.Handle("/v1/manage/update-hot-config", mwChain.Then(handlers.UpdateHotGoCaptchaConfigHandler)) + http.Handle("/api/v1/manage/upload-resource", mwChain.Then(handlers.UploadResourceHandler)) + http.Handle("/api/v1/manage/delete-resource", mwChain.Then(handlers.DeleteResourceHandler)) + http.Handle("/api/v1/manage/get-resource-list", mwChain.Then(handlers.GetResourceListHandler)) + http.Handle("/api/v1/manage/get-config", mwChain.Then(handlers.GetGoCaptchaConfigHandler)) + http.Handle("/api/v1/manage/update-hot-config", mwChain.Then(handlers.UpdateHotGoCaptchaConfigHandler)) a.httpServer = &http.Server{ Addr: ":" + cfg.HTTPPort, diff --git a/internal/server/http_handler.go b/internal/server/http_handler.go index 6cd1829..08e5076 100644 --- a/internal/server/http_handler.go +++ b/internal/server/http_handler.go @@ -50,6 +50,13 @@ func NewHTTPHandlers(svcCtx *common.SvcContext) *HTTPHandlers { } } +// HealthStatusHandler . +func (h *HTTPHandlers) HealthStatusHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + resp := &adapt.CaptNormalDataResponse{Code: http.StatusOK, Message: "success"} + json.NewEncoder(w).Encode(helper.Marshal(resp)) +} + // GetDataHandler . func (h *HTTPHandlers) GetDataHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") From 73355416b95f1e77c16375dcfdfd1fe6d6d5f6fd Mon Sep 17 00:00:00 2001 From: Awen Date: Wed, 23 Apr 2025 00:35:36 +0800 Subject: [PATCH 6/6] add README_zh.md --- .github/.github/docker.yml | 10 +- README.md | 1051 +++++++++++++++++++++++++++++ README_zh.md | 386 ++++++++--- config.dev.json | 43 +- config.json | 27 +- ecosystem.config.js | 14 +- go.mod | 2 +- go.sum | 4 +- gocaptcha.dev.json | 130 ++-- gocaptcha.json | 130 ++-- internal/app/app.go | 179 +++-- internal/app/setup.go | 36 +- internal/cache/cache.go | 17 +- internal/cache/etcd_client.go | 4 +- internal/cache/memcache_client.go | 26 +- internal/cache/redis_client.go | 6 +- internal/config/config.go | 153 +++-- internal/logic/click.go | 23 +- internal/logic/rotate.go | 25 +- internal/logic/slide.go | 25 +- testing/config.json | 49 ++ testing/docker-compose.yml | 68 ++ testing/gocaptcha.json | 527 +++++++++++++++ 23 files changed, 2498 insertions(+), 437 deletions(-) create mode 100644 testing/config.json create mode 100644 testing/docker-compose.yml create mode 100644 testing/gocaptcha.json diff --git a/.github/.github/docker.yml b/.github/.github/docker.yml index 01ef381..c15c9ff 100644 --- a/.github/.github/docker.yml +++ b/.github/.github/docker.yml @@ -65,11 +65,11 @@ jobs: uses: docker/build-push-action@v6 with: context: . - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64,linux/arm/v7,windows/amd64 push: true tags: | - ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:latest - ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:amd64 - ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:arm64 - ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:armv7 + ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:0.0.1 + ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:amd64-0.0.1 + ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:arm64-0.0.1 + ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:armv7-0.0.1 ${{ secrets.DOCKER_USERNAME }}/go-captcha-service:${{ github.ref_name }} diff --git a/README.md b/README.md index 49ca850..7916a1b 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,1054 @@
+ +`GoCaptcha Service` is a high-performance behavioral CAPTCHA service developed in Go, based on the **[go-captcha](https://github.com/wenlng/go-captcha)** core library. It supports multiple CAPTCHA modes including click, slide, drag, and rotate. The service provides HTTP and gRPC interfaces, integrates with various service discovery mechanisms (Etcd, Nacos, Zookeeper, Consul), distributed caching (Memory, Redis, Etcd, Memcache), and dynamic configuration. It supports both standalone and distributed deployments, aiming to provide a secure and flexible CAPTCHA solution for web applications. + +
+ +> English | [中文](README_zh.md) +

⭐️ If this project is helpful, please give it a star!

+ +
+Poster +
+ +
+
+
+ +## Related Projects + +| Name | Description | +|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| +| [go-captcha](https://github.com/wenlng/go-captcha) | Golang CAPTCHA core library | +| [document](http://gocaptcha.wencodes.com) | GoCaptcha documentation | +| [online demo](http://gocaptcha.wencodes.com/demo/) | GoCaptcha online demo | +| [go-captcha-service](https://github.com/wenlng/go-captcha-service) | GoCaptcha service providing HTTP/gRPC interfaces,
supporting standalone and distributed modes (service discovery, load balancing, dynamic configuration),
deployable via binary or Docker images | +| [go-captcha-service-sdk](https://github.com/wenlng/go-captcha-service-sdk) | GoCaptcha service SDK toolkit, including HTTP/gRPC request interfaces,
supporting static mode, service discovery, and load balancing | +| [go-captcha-jslib](https://github.com/wenlng/go-captcha-jslib) | JavaScript CAPTCHA library | +| [go-captcha-vue](https://github.com/wenlng/go-captcha-vue) | Vue CAPTCHA | +| [go-captcha-react](https://github.com/wenlng/go-captcha-react) | React CAPTCHA | +| [go-captcha-angular](https://github.com/wenlng/go-captcha-angular) | Angular CAPTCHA | +| [go-captcha-svelte](https://github.com/wenlng/go-captcha-svelte) | Svelte CAPTCHA | +| [go-captcha-solid](https://github.com/wenlng/go-captcha-solid) | Solid CAPTCHA | +| [go-captcha-uni](https://github.com/wenlng/go-captcha-uni) | UniApp CAPTCHA, compatible with APP, mini-programs, and quick apps | +| ... | | + +
+
+ +## Features + +- **Multiple CAPTCHA Modes**: Supports text/image click, slide, drag, and rotate CAPTCHAs. +- **Dual Protocol Support**: Provides RESTful HTTP and gRPC interfaces. +- **Service Discovery**: Integrates with Etcd, Nacos, Zookeeper, and Consul for distributed service registration and discovery. +- **Distributed Caching**: Supports Memory, Redis, Etcd, and Memcache for optimized CAPTCHA data storage. +- **Dynamic Configuration**: Enables real-time configuration updates via Etcd, Nacos, Zookeeper, or Consul. +- **Highly Configurable**: Supports customization of text, fonts, image resources, CAPTCHA dimensions, and generation rules. +- **High Performance**: Built on Go’s concurrency model, suitable for high-traffic scenarios, with distributed architecture ensuring high availability, performance, and responsiveness. +- **Cross-Platform**: Supports deployment via binary, command line, PM2, Docker, and Docker Compose. + +
+
+ +## Installation and Deployment +`GoCaptcha Service` supports multiple deployment methods, including standalone (binary, command line, PM2, Docker) and distributed deployments (with service discovery, distributed caching, and optional dynamic configuration). + +### Prerequisites +- Optional: Docker (for containerized deployment) +- Optional: Service discovery/dynamic configuration middleware (Etcd, Nacos, Zookeeper, Consul) +- Optional: Caching services (Redis, Etcd, Memcache) +- Optional: Node.js and PM2 (for PM2 deployment) +- Optional: gRPC client tools (e.g., `grpcurl`) + +### Standalone Deployment + +#### Binary Deployment + +1. Download the latest binary executable for your platform from [Github Releases](https://github.com/wenlng/go-captcha-service/releases). + + ```bash + ./go-captcha-service-[xxx] + ``` + +2. Optional: Configure the application by copying the `config.json` and `gocaptcha.json` files from the repository to the same directory and specifying them at startup. + + ```bash + ./go-captcha-service-[xxx] -config config.json -gocaptcha-config gocaptcha.json + ``` + +3. Access the HTTP interface (e.g., `http://localhost:8080/api/v1/public/get-data?id=click-default-ch`) or gRPC interface (`localhost:50051`). + +
+
+ +#### PM2 Deployment +PM2 is a Node.js process manager that can manage Go services, providing process monitoring and log management. + +1. Install Node.js and PM2: + + ```bash + npm install -g pm2 + ``` + +2. Create a PM2 configuration file `ecosystem.config.js`: + + ```javascript + module.exports = { + apps: [{ + name: 'go-captcha-service', + script: './go-captcha-service-[xxx]', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + CONFIG: 'config.json', + GO_CAPTCHA_CONFIG: 'gocaptcha.json', + SERVICE_NAME: 'go-captcha-service', + CACHE_TYPE: 'redis', + CACHE_ADDRS: 'localhost:6379', + }, + env_production: { + CONFIG: 'config.json', + GO_CAPTCHA_CONFIG: 'gocaptcha.json', + SERVICE_NAME: 'go-captcha-service', + CACHE_TYPE: 'redis', + CACHE_ADDRS: 'localhost:6379', + } + }] + }; + ``` + +3. Start the service: + + ```bash + pm2 start ecosystem.config.js + ``` + +4. View logs and status: + + ```bash + pm2 logs go-captcha-service + pm2 status + ``` + +5. Enable auto-start on boot: + + ```bash + pm2 startup + pm2 save + ``` + +
+
+ +#### Golang Source Code + Docker Deployment + +1. Create a `Dockerfile` for building from source: + + ```dockerfile + FROM --platform=$BUILDPLATFORM golang:1.23 AS builder + WORKDIR /app + + COPY go.mod go.sum ./ + RUN go mod download + + COPY . . + + ARG TARGETOS + ARG TARGETARCH + RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s" -v -a -trimpath -o go-captcha-service ./cmd/go-captcha-service + + FROM scratch AS binary + WORKDIR /app + + COPY --from=builder /app/go-captcha-service . + COPY config.json . + COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + + EXPOSE 8080 50051 + + HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD ["/app/go-captcha-service", "--health-check"] || exit 1 + + CMD ["/app/go-captcha-service"] + ``` + +2. Build the image: + + ```bash + docker build -t go-captcha-service:1.0.0 . + ``` + +3. Run the container with mounted configuration files: + + ```bash + docker run -d -p 8080:8080 -p 50051:50051 \ + -v $(pwd)/config.json:/app/config.json \ + -v $(pwd_MAIN)/gocaptcha.json:/app/gocaptcha.json \ + -v $(pwd)/resource/gocaptcha:/app/resource/gocaptcha \ + --name go-captcha-service go-captcha-service:latest + ``` + +
+
+ +#### Official Docker Image + +1. Pull the official image: + + ```bash + docker pull wenlng/go-captcha-service@latest + ``` + +2. Run the container: + + ```bash + docker run -d -p 8080:8080 -p 50051:50051 \ + -v $(pwd)/config.json:/app/config.json \ + -v $(pwd)/gocaptcha.json:/app/gocaptcha.json \ + -v $(pwd)/resource/gocaptcha:/app/resource/gocaptcha \ + --name go-captcha-service wenlng/go-captcha-service:latest + ``` + +
+
+ +### Distributed Deployment + +#### Distributed Caching + +1. Configure distributed caching (e.g., Redis) in `config.json`: + + ```json + { + "cache_type": "redis", + "cache_ttl": 1800, + "cache_key_prefix": "GO_CAPTCHA_DATA:", + "redis_addrs": "localhost:6379" + } + ``` + +2. Start Redis: + + ```bash + docker run -d -p 6379:6379 --name redis redis:latest + ``` + +
+
+ +#### Dynamic Configuration +Note: When dynamic configuration is enabled, both `config.json` and `gocaptcha.json` are applied simultaneously. + +1. Enable dynamic configuration in `config.json` and select middleware (e.g., Etcd): + + ```json + { + "enable_dynamic_config": true, + "dynamic_config": "etcd", + "dynamic_config_addrs": "localhost:2379" + } + ``` + +2. Start Etcd: + + ```bash + docker run -d -p 8848:8848 --name etcd bitnami/etcd::latest + ``` + +3. Configuration Synchronization and Retrieval + - At startup, the service decides whether to push or pull configurations based on the `config_version`. If the local version is higher than the remote (e.g., Etcd) version, the local configuration is pushed to override the remote one; otherwise, the remote configuration is pulled to update the local one (non-file-based update). + - After startup, the dynamic configuration manager continuously monitors remote configuration changes (e.g., in Etcd). When a remote configuration change occurs, it is fetched and compared with the local version, overriding the local configuration if the remote version is higher. + +
+
+ +#### Service Discovery + +1. Enable service discovery in `config.json` and select middleware (e.g., Etcd): + + ```json + { + "enable_service_discovery": true, + "service_discovery": "etcd", + "service_discovery_addrs": "localhost:2379" + } + ``` + +2. Start Etcd: + + ```bash + docker run -d -p 8848:8848 --name etcd bitnami/etcd::latest + ``` + +3. Service Registration and Discovery + - At startup, the service automatically registers its instance with the service discovery center (e.g., Etcd). + - After startup, the service monitors changes in service instances. Refer to [go-captcha-service-sdk](https://github.com/wenlng/go-captcha-service-sdk) for load balancing implementations. + +
+
+ +#### Docker Compose Multi-Instance Deployment + +Create a `docker-compose.yml` file including multiple service instances, Consul, Redis, ZooKeeper, and Nacos: + +```yaml +version: '3' +services: + captcha-service-1: + image: wenlng/go-captcha-service:latest + ports: + - "8080:8080" + - "50051:50051" + volumes: + - ./config.json:/app/config.json + - ./gocaptcha.json:/app/gocaptcha.json + - ./resources/gocaptcha:/app/resources/gocaptcha + environment: + - CONFIG=config.json + - GO_CAPTCHA_CONFIG=gocaptcha.json + - SERVICE_NAME=go-captcha-service + - CACHE_TYPE=redis + - CACHE_ADDRS=localhost:6379 + - ENABLE_DYNAMIC_CONFIG=true + - DYNAMIC_CONFIG_TYPE=etcd + - DYNAMIC_CONFIG_ADDRS=localhost:2379 + - ENABLE_SERVICE_DISCOVERY=true + - SERVICE_DISCOVERY_TYPE=etcd + - SERVICE_DISCOVERY_ADDRS=localhost:2379 + depends_on: + - etcd + - redis + restart: unless-stopped + + captcha-service-2: + image: wenlng/go-captcha-service:latest + ports: + - "8081:8080" + - "50052:50051" + volumes: + - ./config.json:/app/config.json + - ./gocaptcha.json:/app/gocaptcha.json + - ./resources/gocaptcha:/app/resources/gocaptcha + environment: + - CONFIG=config.json + - GO_CAPTCHA_CONFIG=gocaptcha.json + - SERVICE_NAME=go-captcha-service + - CACHE_TYPE=redis + - CACHE_ADDRS=localhost:6379 + - ENABLE_DYNAMIC_CONFIG=true + - DYNAMIC_CONFIG_TYPE=etcd + - DYNAMIC_CONFIG_ADDRS=localhost:2379 + - ENABLE_SERVICE_DISCOVERY=true + - SERVICE_DISCOVERY_TYPE=etcd + - SERVICE_DISCOVERY_ADDRS=localhost:2379 + depends_on: + - etcd + - redis + restart: unless-stopped + + etcd: + image: bitnami/etcd:latest + ports: + - "2379:2379" + environment: + - ALLOW_NONE_AUTHENTICATION=yes + privileged: true + restart: unless-stopped + + redis: + image: redis:latest + ports: + - "6379:6379" + restart: unless-stopped +``` + +Run: + +```bash +docker-compose up -d +``` + +
+
+ +## Predefined APIs + +* Get CAPTCHA + ```shell + curl -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/public/get-data\?id\=click-default-ch + ``` + +* Verify CAPTCHA + ```shell + curl -X POST -H "X-API-Key:my-secret-key-123" -H "Content-Type:application/json" -d '{"id":"click-default-ch","captchaKey":"xxxx-xxxxx","value": "x1,y1,x2,y2"}' http://127.0.0.1:8181/api/v1/public/check-data + ``` + +* Check Verification Status (`data == "ok"` indicates success) + ```shell + curl -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/public/check-status\?captchaKey\=xxxx-xxxx + ``` + +* Get Status Info (not exposed to public networks) + ```shell + curl -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/manage/get-status-info\?captchaKey\=xxxx-xxxx + ``` + +* Upload Resources (not exposed to public networks) + ```shell + curl -X POST -H "X-API-Key:my-secret-key-123" -F "dirname=imagesdir" -F "files=@/path/to/file1.jpg" -F "files=@/path/to/file2.jpg" http://127.0.0.1:8080/api/v1/manage/upload-resource + ``` + +* Delete Resources (not exposed to public networks) + ```shell + curl -X DELETE -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/manage/delete-resource?path=xxxxx.jpg + ``` + +* Get Resource File List (not exposed to public networks) + ```shell + curl -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/manage/get-resource-list?path=imagesdir + ``` + +* Get CAPTCHA Configuration (not exposed to public networks) + ```shell + curl -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/manage/get-config + ``` + +* Update CAPTCHA Configuration (non-file update, not exposed to public networks) + ```shell + curl -X POST -H "X-API-Key:my-secret-key-123" -H "Content-Type:application/json" -d '{"config_version":3,"resources":{ ... },"builder": { ... }}' http://127.0.0.1:8080/api/v1/manage/update-hot-config + ``` + +For more details and gRPC APIs, refer to [go-captcha-service-sdk](https://github.com/wenlng/go-captcha-service-sdk). + +
+
+ +## API Authentication Configuration +If `api-keys` are configured in `config.json`, all HTTP and gRPC APIs require the `X-API-Key` header for authentication. + +The `/api/v1/manage` APIs are not allowed to be exposed to public networks due to security concerns. Only the `/api/v1/public` routes should be publicly accessible. These can be proxied through web servers, reverse proxy servers, or gateway software such as Kong, Envoy, Tomcat, or Nginx. + +Example Nginx reverse proxy configuration for public routes: + +```text +server { + listen 80; + server_name example.com; + + # Proxy requests matching /api/v1/public to the backend + location ^~ /api/v1/public { + proxy_pass http://localhost:8080; # Assuming the service runs on port 8080 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # Deny requests matching /api/v1/manage + location ^~ /api/v1/manage { + deny all; # Deny all requests, return 403 + } +} +``` + +
+
+ +## Configuration Details + +### Startup Parameters +Note: Startup parameters correspond to fields in `config.json`. It is recommended to use the configuration file. + +* `config`: Specifies the configuration file path, default `config.json`. +* `gocaptcha-config`: Specifies the GoCaptcha configuration file path, default `gocaptcha.json`. +* `service-name`: Sets the service name. +* `http-port`: Sets the HTTP server port. +* `grpc-port`: Sets the gRPC server port. +* `redis-addrs`: Sets Redis cluster addresses, comma-separated. +* `etcd-addrs`: Sets Etcd addresses, comma-separated. +* `memcache-addrs`: Sets Memcached addresses, comma-separated. +* `cache-type`: Sets the cache type, supports `redis`, `memory`, `etcd`, `memcache`. +* `cache-ttl`: Sets cache TTL in seconds. +* `cache-key-prefix`: Sets the cache key prefix, default `GO_CAPTCHA_DATA:`. + +* `enable-dynamic-config`: Enables dynamic configuration service, default `false`. +* `dynamic-config-type`: Sets the dynamic configuration service type, supports `etcd`, `zookeeper`, `consul`, `nacos`. +* `dynamic-config-addrs`: Sets the dynamic configuration server addresses, comma-separated. +* `dynamic-config-ttl`: Sets the dynamic configuration service registration TTL in seconds, default `10`. +* `dynamic-config-keep-alive`: Sets the dynamic configuration service keep-alive interval in seconds, default `3`. +* `dynamic-config-max-retries`: Sets the maximum retry attempts for dynamic configuration operations, default `3`. +* `dynamic-config-base-retry-delay`: Sets the base retry delay for dynamic configuration in milliseconds, default `3`. +* `dynamic-config-username`: Sets the dynamic configuration service authentication username. +* `dynamic-config-password`: Sets the dynamic configuration service authentication password. +* `dynamic-config-tls-server-name`: Sets the dynamic configuration service TLS server name. +* `dynamic-config-tls-address`: Sets the dynamic configuration service TLS server address. +* `dynamic-config-tls-cert-file`: Sets the dynamic configuration service TLS certificate file path. +* `dynamic-config-tls-key-file`: Sets the dynamic configuration service TLS key file path. +* `dynamic-config-tls-ca-file`: Sets the dynamic configuration service TLS CA file path. + +* `enable-service-discovery`: Enables service discovery, default `false`. +* `service-discovery-type`: Sets the service discovery type, supports `etcd`, `zookeeper`, `consul`, `nacos`. +* `service-discovery-addrs`: Sets the service discovery server addresses, comma-separated. +* `service-discovery-ttl`: Sets the service discovery registration TTL in seconds, default `10`. +* `service-discovery-keep-alive`: Sets the service discovery keep-alive interval in seconds, default `3`. +* `service-discovery-max-retries`: Sets the maximum retry attempts for service discovery operations, default `3`. +* `service-discovery-base-retry-delay`: Sets the base retry delay for service discovery in milliseconds, default `3`. +* `service-discovery-username`: Sets the service discovery authentication username. +* `service-discovery-password`: Sets the service discovery authentication password. +* `service-discovery-tls-server-name`: Sets the service discovery TLS server name. +* `service-discovery-tls-address`: Sets the service discovery TLS server address. +* `service-discovery-tls-cert-file`: Sets the service discovery TLS certificate file path. +* `service-discovery-tls-key-file`: Sets the service discovery TLS key file path. +* `service-discovery-tls-ca-file`: Sets the service discovery TLS CA file path. + +* `rate-limit-qps`: Sets the rate limit QPS. +* `rate-limit-burst`: Sets the rate limit burst capacity. +* `api-keys`: Sets the API keys, comma-separated. +* `log-level`: Sets the log level, supports `error`, `debug`, `warn`, `info`. +* `health-check`: Runs a health check and exits, default `false`. +* `enable-cors`: Enables Cross-Origin Resource Sharing, default `false`. + +
+ +### Environment Variables +Basic Configuration: + +* `CONFIG`: Main configuration file path for loading application settings. +* `GO_CAPTCHA_CONFIG`: CAPTCHA service configuration file path. +* `SERVICE_NAME`: Service name to identify the service instance. +* `HTTP_PORT`: HTTP service listening port. +* `GRPC_PORT`: gRPC service listening port. +* `API_KEYS`: API keys for authentication or authorization. + +Cache Configuration: +* `CACHE_TYPE`: Cache type (e.g., `redis`, `memcached`, `memory`, `etcd`). +* `CACHE_ADDRS`: Cache service address list. +* `CACHE_USERNAME`: Cache service authentication username. +* `CACHE_PASSWORD`: Cache service authentication password. + +Dynamic Configuration Service: +* `ENABLE_DYNAMIC_CONFIG`: Enables dynamic configuration (`true` to enable). +* `DYNAMIC_CONFIG_TYPE`: Dynamic configuration type (e.g., `consul`, `zookeeper`, `nacos`, `etcd`). +* `DYNAMIC_CONFIG_ADDRS`: Dynamic configuration service address list. +* `DYNAMIC_CONFIG_USERNAME`: Dynamic configuration service authentication username. +* `DYNAMIC_CONFIG_PASSWORD`: Dynamic configuration service authentication password. + +Service Discovery: +* `ENABLE_SERVICE_DISCOVERY`: Enables service discovery (`true` to enable). +* `SERVICE_DISCOVERY_TYPE`: Service discovery type (e.g., `consul`, `zookeeper`, `nacos`, `etcd`). +* `SERVICE_DISCOVERY_ADDRS`: Service discovery service address list. +* `SERVICE_DISCOVERY_USERNAME`: Service discovery service authentication username. +* `SERVICE_DISCOVERY_PASSWORD`: Service discovery service authentication password. + +
+ +### Configuration Files +The service uses two configuration files: `config.json` for service runtime parameters and `gocaptcha.json` for CAPTCHA generation settings. + +### config.json + +`config.json` defines the basic service configuration. + +```json +{ + "config_version": 1, + "service_name": "go-captcha-service", + "http_port": "8080", + "grpc_port": "50051", + "redis_addrs": "localhost:6379", + "etcd_addrs": "localhost:2379", + "memcache_addrs": "localhost:11211", + "cache_type": "memory", + "cache_ttl": 1800, + "cache_key_prefix": "GO_CAPTCHA_DATA:", + + "enable_dynamic_config": false, + "dynamic_config_type": "etcd", + "dynamic_config_addrs": "localhost:2379", + "dynamic_config_username": "", + "dynamic_config_password": "", + "dynamic_config_ttl": 10, + "dynamic_config_keep_alive": 3, + "dynamic_config_max_retries": 3, + "dynamic_config_base_retry_delay": 500, + "dynamic_config_tls_server_name": "", + "dynamic_config_tls_address": "", + "dynamic_config_tls_cert_file": "", + "dynamic_config_tls_key_file": "", + "dynamic_config_tls_ca_file": "", + + "enable_service_discovery": false, + "service_discovery_type": "etcd", + "service_discovery_addrs": "localhost:2379", + "service_discovery_username": "", + "service_discovery_password": "", + "service_discovery_ttl": 10, + "service_discovery_keep_alive": 3, + "service_discovery_max_retries": 3, + "service_discovery_base_retry_delay": 500, + "service_discovery_tls_server_name": "", + "service_discovery_tls_address": "", + "service_discovery_tls_cert_file": "", + "service_discovery_tls_key_file": "", + "service_discovery_tls_ca_file": "", + + "rate_limit_qps": 1000, + "rate_limit_burst": 1000, + "enable_cors": true, + "log_level": "info", + "api_keys": ["my-secret-key-123", "another-key-456", "another-key-789"] +} +``` + +#### Parameter Descriptions + +- `config_version` (integer): Configuration file version for distributed dynamic configuration, default `1`. +- `service_name` (string): Service name, default `go-captcha-service`. +- `http_port` (string): HTTP port, default `8080`. +- `grpc_port` (string): gRPC port, default `50051`. +- `redis_addrs` (string): Redis address, default `localhost:6379`. Used when `cache_type: redis`. +- `etcd_addrs` (string): Etcd address, default `localhost:2379`. Used when `cache_type: etcd` or `service_discovery: etcd`. +- `memcache_addrs` (string): Memcache address, default `localhost:11211`. Used when `cache_type: memcache`. +- `cache_type` (string): Cache type, default `memory`: + - `memory`: In-memory cache, suitable for standalone deployment. + - `redis`: Distributed key-value store, suitable for high-availability scenarios. + - `etcd`: Distributed key-value store, suitable for sharing with service discovery. + - `memcache`: High-performance distributed cache, suitable for high concurrency. +- `cache_ttl` (integer): Cache expiration time in seconds, default `1800`. +- `cache_key_prefix` (string): Cache key prefix, default `GO_CAPTCHA_DATA:`. + +- `enable_dynamic_config` (boolean): Enables dynamic configuration service, default `false`. +- `dynamic_config_type` (string): Dynamic configuration service type, default `etcd`: + - `etcd`: Suitable for high-consistency scenarios. + - `nacos`: Suitable for cloud-native environments. + - `zookeeper`: Suitable for complex distributed systems. + - `consul`: Lightweight, supports health checks. +- `dynamic_config_addrs` (string): Dynamic configuration service addresses, e.g., Etcd: `localhost:2379`, Nacos: `localhost:8848`. +- `dynamic_config_username` (string): Username, e.g., Nacos default username is `nacos`, default empty. +- `dynamic_config_password` (string): Password, e.g., Nacos default password is `nacos`, default empty. +- `dynamic_config_ttl` (integer): Service lease time in seconds, default `10`. +- `dynamic_config_keep_alive` (integer): Heartbeat interval in seconds, default `3`. +- `dynamic_config_max_retries` (integer): Retry attempts, default `3`. +- `dynamic_config_base_retry_delay` (integer): Retry delay in milliseconds, default `500`. +- `dynamic_config_tls_server_name` (string): TLS server name, default empty. +- `dynamic_config_tls_address` (string): TLS server's address, default empty. +- `dynamic_config_tls_cert_file` (string): TLS certificate file, default empty. +- `dynamic_config_tls_key_file` (string): TLS key file, default empty. +- `dynamic_config_tls_ca_file` (string): TLS CA certificate file, default empty. + +- `enable_service_discovery` (boolean): Enables service discovery, default `false`. +- `service_discovery_type` (string): Service discovery type, default `etcd`: + - `etcd`: Suitable for high-consistency scenarios. + - `nacos`: Suitable for cloud-native environments. + - `zookeeper`: Suitable for complex distributed systems. + - `consul`: Lightweight, supports health checks. +- `service_discovery_addrs` (string): Service discovery addresses, e.g., Etcd: `localhost:2379`, Nacos: `localhost:8848`. +- `service_discovery_username` (string): Username, e.g., Nacos default username is `nacos`, default empty. +- `service_discovery_password` (string): Password, e.g., Nacos default password is `nacos`, default empty. +- `service_discovery_ttl` (integer): Service registration lease time in seconds, default `10`. +- `service_discovery_keep_alive` (integer): Heartbeat interval in seconds, default `3`. +- `service_discovery_max_retries` (integer): Retry attempts, default `3`. +- `service_discovery_base_retry_delay` (integer): Retry delay in milliseconds, default `500`. +- `service_discovery_tls_server_name` (string): TLS server name, default empty. +- `service_discovery_tls_address` (string): TLS server's address, default empty. +- `service_discovery_tls_cert_file` (string): TLS certificate file, default empty. +- `service_discovery_tls_key_file` (string): TLS key file, default empty. +- `service_discovery_tls_ca_file` (string): TLS CA certificate file, default empty. + +- `rate_limit_qps` (integer): API requests per second limit, default `1000`. +- `rate_limit_burst` (integer): API burst capacity limit, default `1000`. +- `enable_cors` (boolean): Enables CORS, default `true`. +- `log_level` (string): Log level (`debug`, `info`, `warn`, `error`), default `info`. +- `api_keys` (string array): API authentication keys. + +### gocaptcha.json + +`gocaptcha.json` defines resources and generation settings for CAPTCHAs. + +```json +{ + "config_version": 1, + "resources": { + "version": "0.0.1", + "char": { + "languages": { + "chinese": [], + "english": [] + } + }, + "font": { + "type": "load", + "file_dir": "./gocaptcha/fonts/", + "file_maps": { + "yrdzst_bold": "yrdzst-bold.ttf" + } + }, + "shape_image": { + "type": "load", + "file_dir": "./gocaptcha/shape_images/", + "file_maps": { + "shape_01": "shape_01.png", + "shape_01.png":"c.png" + } + }, + "master_image": { + "type": "load", + "file_dir": "./gocaptcha/master_images/", + "file_maps": { + "image_01": "image_01.jpg", + "image_02":"image_02.jpg" + } + }, + "thumb_image": { + "type": "load", + "file_dir": "./gocaptcha/thumb_images/", + "file_maps": { + + } + }, + "tile_image": { + "type": "load", + "file_dir": "./gocaptcha/tile_images/", + "file_maps": { + "tile_01": "tile_01.png", + "tile_02": "tile_02.png" + }, + "file_maps_02": { + "tile_mask_01": "tile_mask_01.png", + "tile_mask_02": "tile_mask_02.png" + }, + "file_maps_03": { + "tile_shadow_01": "tile_shadow_01.png", + "tile_shadow_02": "tile_shadow_02.png" + } + } + }, + "builder": { + "click_config_maps": { + "click-default-ch": { + "version": "0.0.1", + "language": "chinese", + "master": { + "image_size": { "width": 300, "height": 200 }, + "range_length": { "min": 6, "max": 7 }, + "range_angles": [ + { "min": 20, "max": 35 }, + { "min": 35, "max": 45 }, + { "min": 290, "max": 305 }, + { "min": 305, "max": 325 }, + { "min": 325, "max": 330 } + ], + "range_size": { "min": 26, "max": 32 }, + "range_colors": [ "#fde98e", "#60c1ff", "#fcb08e", "#fb88ff", "#b4fed4", "#cbfaa9", "#78d6f8"], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { "x": -1, "y": -1 }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { "width": 150, "height": 40 }, + "range_verify_length": { "min": 2, "max": 4 }, + "disabled_range_verify_length": false, + "range_text_size": { "min": 22, "max": 28 }, + "range_text_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c"], + "range_background_colors": ["#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c"], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click-dark-ch": { + "version": "0.0.1", + "language": "chinese", + // Same as above... + }, + "click-default-en": { + "version": "0.0.1", + "language": "english", + // Same as above... + }, + "click-dark-en": { + "version": "0.0.1", + "language": "english", + // Same as above... + } + }, + "click_shape_config_maps": { + "click-shape-default": { + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "range_length": { "min": 6, "max": 7 }, + "range_angles": [ + { "min": 20, "max": 35 }, + { "min": 35, "max": 45 }, + { "min": 290, "max": 305 }, + { "min": 305, "max": 325 }, + { "min": 325, "max": 330 } + ], + "range_size": { "min": 26, "max": 32 }, + "range_colors": [ "#fde98e", "#60c1ff", "#fcb08e", "#fb88ff", "#b4fed4", "#cbfaa9", "#78d6f8"], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { "x": -1, "y": -1 }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { "width": 150, "height": 40}, + "range_verify_length": { "min": 2, "max": 4 }, + "disabled_range_verify_length": false, + "range_text_size": { "min": 22, "max": 28}, + "range_text_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c"], + "range_background_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c" ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + } + }, + "slide_config_maps": { + "slide-default": { + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { "min": 60, "max": 70 }, + "range_graph_angles": [ + { "min": 20, "max": 35 }, + ], + "generate_graph_number": 1, + "enable_graph_vertical_random": false, + "range_dead_zone_directions": ["left", "right"] + } + } + }, + "drag_config_maps": { + "drag-default": { + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { "min": 60, "max": 70 }, + "range_graph_angles": [ + { "min": 0, "max": 0 }, + ], + "generate_graph_number": 2, + "enable_graph_vertical_random": true, + "range_dead_zone_directions": ["left", "right", "top", "bottom"] + } + } + }, + "rotate_config_maps": { + "rotate-default": { + "version": "0.0.1", + "master": { + "image_square_size": 220, + }, + "thumb": { + "range_angles": [{ "min": 30, "max": 330 }], + "range_image_square_sizes": [140, 150, 160, 170], + "image_alpha": 1 + } + } + } + } +} +``` + +
+ +##### Top-Level Fields + +- `config_version` (integer): Configuration file version for distributed dynamic configuration management, default `1`. + +##### resources Field + +- `version` (string): Resource configuration version to control CAPTCHA instance recreation, default `0.0.1`. +- `char.languages.chinese` (string array): Chinese character set for click CAPTCHA text, default empty (uses built-in resources). +- `char.languages.english` (string array): English character set, default empty (uses built-in resources). +- `font.type` (string): Font loading method, fixed as `load` (load from file). +- `font.file_dir` (string): Font file directory, default `./gocaptcha/fonts/`. +- `font.file_maps` (object): Font file mappings, key is the font name, value is the file name. + - Example: `"yrdzst_bold": "yrdzst-bold.ttf"` uses `yrdzst-bold.ttf` font. +- `shape_image.type` (string): Shape image loading method, fixed as `load`. +- `shape_image.file_dir` (string): Shape image directory, default `./gocaptcha/shape_images/`. +- `shape_image.file_maps` (object): Shape image mappings. + - Example: `"shape_01": "shape_01.png"` uses `shape_01.png` as a shape. +- `master_image.type` (string): Main image loading method, fixed as `load`. +- `master_image.file_dir` (string): Main image directory, default `./gocaptcha/master_images/`. +- `master_image.file_maps` (object): Main image mappings. + - Example: `"image_01": "image_01.jpg"` uses `image_01.jpg` as the background. +- `thumb_image.type` (string): Thumbnail image loading method, fixed as `load`. +- `thumb_image.file_dir` (string): Thumbnail image directory, default `./gocaptcha/thumb_images/`. +- `thumb_image.file_maps` (object): Thumbnail image mappings, default empty. +- `tile_image.type` (string): Tile image loading method, fixed as `load`. +- `tile_image.file_dir` (string): Tile image directory, default `./gocaptcha/tile_images/`. +- `tile_image.file_maps` (object): Tile image mappings. + - Example: `"tile_01": "tile_01.png"`. +- `tile_image.file_maps_02` (object): Tile mask mappings. + - Example: `"tile_mask_01": "tile_mask_01.png"`. +- `tile_image.file_maps_03` (object): Tile shadow mappings. + - Example: `"tile_shadow_01": "tile_shadow_01.png"`. + +
+ +##### builder Field + +Defines CAPTCHA generation styles, including configurations for click, shape click, slide, drag, and rotate CAPTCHAs. + +###### click_config_maps + +Defines text click CAPTCHA configurations, supporting Chinese and English with light and dark themes. The key is the ID passed in the CAPTCHA API request, e.g., `api/v1/public/get-data?id=click-default-ch`. + +- `click-default-ch` (object): Default Chinese theme configuration. + - `version` (string): Configuration version to control CAPTCHA instance recreation, default `0.0.1`. + $ + - `language` (string): Language, matches defined `char.languages`, e.g., `chinese` for Chinese. + - `master` (object): Main CAPTCHA image configuration. + - `image_size.width` (integer): Main image width, default `300`. + - `image_size.height` (integer): Main image height, default `200`. + - `range_length.min` (integer): Minimum number of CAPTCHA points, default `6`. + - `range_length.max` (integer): Maximum number of CAPTCHA points, default `7`. + - `range_angles` (object array): Text rotation angle ranges (degrees). + - Example: `{"min": 20, "max": 35}` for 20°-35°. + - `range_size.min` (integer): Minimum text size (pixels), default `26`. + - `range_size.max` (integer): Maximum text size, default `32`. + - `range_colors` (string array): Text color list (hexadecimal). + - Example: `"#fde98e"`. + - `display_shadow` (boolean): Display text shadow, default `true`. + - `shadow_color` (string): Shadow color, default `#101010`. + - `shadow_point.x` (integer): Shadow offset X coordinate, default `-1` (auto-calculated). + - `shadow_point.y` (integer): Shadow offset Y coordinate, default `-1`. + - `image_alpha` (float): Image opacity (0-1), default `1`. + - `use_shape_original_color` (boolean): Use original shape color, default `true`. + - `thumb` (object): Thumbnail (prompt text) configuration. + - `image_size.width` (integer): Thumbnail width, default `150`. + - `image_size.height` (integer): Thumbnail height, default `40`. + - `range_verify_length.min` (integer): Minimum verification points, default `2`. + - `range_verify_length.max` (integer): Maximum verification points, default `4`. + - `disabled_range_verify_length` (boolean): Disable verification point limit, default `false`. + - `range_text_size.min` (integer): Minimum text size, default `22`. + - `range_text_size.max` (integer): Maximum text size, default `28`. + - `range_text_colors` (string array): Text color list. + - `range_background_colors` (string array): Background color list. + - `is_non_deform_ability` (boolean): Disable deformation effect, default `false`. + - `background_distort` (integer): Background distortion level, default `4`. + - `background_distort_alpha` (float): Background distortion opacity, default `1`. + - `background_circles_num` (integer): Number of background circle interference points, default `24`. + - `background_slim_line_num` (integer): Number of background slim line interferences, default `2`. + +- `click-dark-ch` (object): Chinese dark theme configuration, similar to `click-default-ch`, but `thumb.range_text_colors` uses brighter colors for dark backgrounds. + +- `click-default-en` (object): Default English theme configuration, with `language: english`, larger `master.range_size` and `thumb.range_text_size` (`34-48`) for English characters. + +- `click-dark-en` (object): English dark theme configuration, similar to `click-dark-ch`, with `language: english`. + +
+ +###### click_shape_config_maps + +Defines shape click CAPTCHA configurations. + +- `click-shape-default` (object): Default shape click configuration, similar to `click_config_maps` `master` and `thumb` but for shape images instead of text. + +
+ +###### slide_config_maps + +Defines slide CAPTCHA configurations. + +- `slide-default` (object): + - `version` (string): Configuration version to control CAPTCHA instance recreation, default `0.0.1`. + - `master` (object): Main CAPTCHA image configuration. + - `image_size.width` (integer): Main image width, default `300`. + - `image_size.height` (integer): Main image height, default `200`. + - `image_alpha` (float): Image opacity (0-1), default `1`. + - `thumb` (object): Slider configuration. + - `range_graph_size.min` (integer): Minimum slider graphic size (pixels), default `60`. + - `range_graph_size.max` (integer): Maximum slider graphic size, default `70`. + - `range_graph_angles` (object array): Slider graphic rotation angle ranges (degrees). + - Example: `{"min": 20, "max": 35}`. + - `generate_graph_number` (integer): Number of slider graphics to generate, default `1`. + - `enable_graph_vertical_random` (boolean): Enable vertical random offset, default `false`. + - `range_dead_zone_directions` (string array): Slider dead zone directions, default `["left", "right"]`. + +
+ +###### drag_config_maps + +Defines drag CAPTCHA configurations. + +- `drag-default` (object): + - `version` (string): Configuration version to control CAPTCHA instance recreation, default `0.0.1`. + - `master` (object): Main CAPTCHA image configuration. + - `image_size.width` (integer): Main image width, default `300`. + - `image_size.height` (integer): Main image height, default `200`. + - `image_alpha` (float): Image opacity (0-1), default `1`. + - `thumb` (object): Drag graphic configuration. + - `range_graph_size.min` (integer): Minimum drag graphic size (pixels), default `60`. + - `range_graph_size.max` (integer): Maximum drag graphic size, default `70`. + - `range_graph_angles` (object array): Drag graphic rotation angle ranges (degrees). + - Example: `{"min": 0, "max": 0}` for no rotation. + - `generate_graph_number` (integer): Number of drag graphics to generate, default `2`. + - `enable_graph_vertical_random` (boolean): Enable vertical random offset, default `true`. + - `range_dead_zone_directions` (string array): Drag dead zone directions, default `["left", "right", "top", "bottom"]`. + +
+ +###### rotate_config_maps + +Defines rotate CAPTCHA configurations. + +- `rotate-default` (object): + - `version` (string): Configuration version to control CAPTCHA instance recreation, default `0.0.1`. + - `master` (object): Main CAPTCHA image configuration. + - `image_square_size` (integer): Main image square side length (pixels), default `220`. + - `thumb` (object): Rotate graphic configuration. + - `range_angles` (object array): Rotation angle ranges (degrees). + - Example: `{"min": 30, "max": 330}` for 30°-330°. + - `range_image_square_sizes` (integer array): Rotate image square side length list, default `[140, 150, 160, 170]`. + - `image_alpha` (float): Image opacity (0-1), default `1`. + +
+
+ +### Configuration Hot Reloading +Hot reloading for `gocaptcha.json` is determined by the `version` field of each configuration item. + +Hot-reloadable fields in `config.json` include: +* `cache_type` +* `cache_addrs` +* `cache_ttl` +* `cache_key_prefix` +* `api_keys` +* `log_level` +* `rate_limit_qps` +* `rate_limit_burst` + +### Testing + +- CAPTCHA Generation: Verify image, shape, and key validity. +- Verification Logic: Test handling of different inputs. +- Service Discovery: Simulate Etcd/Nacos/Zookeeper/Consul. +- Caching: Test Memory/Redis/Etcd/Memcache. +- Dynamic Configuration: Test Nacos configuration updates. + +
+
+ + +## LICENSE +Go Captcha Service source code is licensed under the Apache License, Version 2.0 [http://www.apache.org/licenses/LICENSE-2.0.html](http://www.apache.org/licenses/LICENSE-2.0.html) \ No newline at end of file diff --git a/README_zh.md b/README_zh.md index 18ea042..0b191d6 100644 --- a/README_zh.md +++ b/README_zh.md @@ -27,25 +27,23 @@

-## 项目生态 - -| 名称 | 描述 | -|----------------------------------------------------------------------------|---------------------------------------------------------------------------------------------| -| [document](http://gocaptcha.wencodes.com) | GoCaptcha 文档 | -| [online demo](http://gocaptcha.wencodes.com/demo/) | GoCaptcha 在线演示 | -| [go-captcha-example](https://github.com/wenlng/go-captcha-example) | Golang + 前端 + APP实例 | -| [go-captcha-assets](https://github.com/wenlng/go-captcha-assets) | Golang 内嵌素材资源 | -| [go-captcha](https://github.com/wenlng/go-captcha) | Golang 验证码 | -| [go-captcha-jslib](https://github.com/wenlng/go-captcha-jslib) | Javascript 验证码 | -| [go-captcha-vue](https://github.com/wenlng/go-captcha-vue) | Vue 验证码 | -| [go-captcha-react](https://github.com/wenlng/go-captcha-react) | React 验证码 | -| [go-captcha-angular](https://github.com/wenlng/go-captcha-angular) | Angular 验证码 | -| [go-captcha-svelte](https://github.com/wenlng/go-captcha-svelte) | Svelte 验证码 | -| [go-captcha-solid](https://github.com/wenlng/go-captcha-solid) | Solid 验证码 | -| [go-captcha-uni](https://github.com/wenlng/go-captcha-uni) | UniApp 验证码,兼容 Android、IOS、小程序、快应用等 | -| [go-captcha-service](https://github.com/wenlng/go-captcha-service) | GoCaptcha 服务,支持二进制、Docker镜像等方式部署,
提供 HTTP/GRPC 方式访问接口,
可用单机模式和分布式(服务发现、负载均衡、动态配置等) | -| [go-captcha-service-sdk](https://github.com/wenlng/go-captcha-service-sdk) | GoCaptcha 服务SDK工具包,包含 HTTP/GRPC 请求服务接口,
支持静态模式、服务发现、负载均衡 | -| ... | | +## 周边项目 + +| 名称 | 描述 | +|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| +| [go-captcha](https://github.com/wenlng/go-captcha) | Golang 验证码基本库 | +| [document](http://gocaptcha.wencodes.com) | GoCaptcha 文档 | +| [online demo](http://gocaptcha.wencodes.com/demo/) | GoCaptcha 在线演示 | +| [go-captcha-service](https://github.com/wenlng/go-captcha-service) | GoCaptcha 服务,提供 HTTP/GRPC 方式访问接口,
支持单机模式和分布式(服务发现、负载均衡、动态配置等),
可用二进制、Docker镜像等方式部署 | +| [go-captcha-service-sdk](https://github.com/wenlng/go-captcha-service-sdk) | GoCaptcha 服务SDK工具包,包含 HTTP/GRPC 请求服务接口,
支持静态模式、服务发现、负载均衡 | +| [go-captcha-jslib](https://github.com/wenlng/go-captcha-jslib) | Javascript 验证码 | +| [go-captcha-vue](https://github.com/wenlng/go-captcha-vue) | Vue 验证码 | +| [go-captcha-react](https://github.com/wenlng/go-captcha-react) | React 验证码 | +| [go-captcha-angular](https://github.com/wenlng/go-captcha-angular) | Angular 验证码 | +| [go-captcha-svelte](https://github.com/wenlng/go-captcha-svelte) | Svelte 验证码 | +| [go-captcha-solid](https://github.com/wenlng/go-captcha-solid) | Solid 验证码 | +| [go-captcha-uni](https://github.com/wenlng/go-captcha-uni) | UniApp 验证码,兼容 APP、小程序、快应用等 | +| ... | |

@@ -90,14 +88,14 @@ ./go-captcha-service-[xxx] -config config.json -gocaptcha-config gocaptcha.json ``` -3. 访问 HTTP 接口(如 `http://localhost:8080/api/v1/get-data?id=click-default-ch`)或 gRPC 接口(`localhost:50051`)。 +3. 访问 HTTP 接口(如 `http://localhost:8080/api/v1/public/get-data?id=click-default-ch`)或 gRPC 接口(`localhost:50051`)。

#### PM2 部署 -PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守护和日志管理。 +PM2 是 Node.js 进程守护管理工具,可用于管理 Go 服务,提供进程守护和日志管理。 1. 安装 Node.js 和 PM2: ```bash @@ -116,9 +114,18 @@ PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守 watch: false, max_memory_restart: '1G', env: { - CAPTCHA_HTTP_PORT: '8080', - CAPTCHA_GRPC_PORT: '50051', - CAPTCHA_CACHE_TYPE: 'memory' + CONFIG: 'config.json', + GO_CAPTCHA_CONFIG: 'gocaptcha.json', + SERVICE_NAME: 'go-captcha-service', + CACHE_TYPE: 'redis', + CACHE_ADDRS: 'localhost:6379', + }, + env_production: { + CONFIG: 'config.json', + GO_CAPTCHA_CONFIG: 'gocaptcha.json', + SERVICE_NAME: 'go-captcha-service', + CACHE_TYPE: 'redis', + CACHE_ADDRS: 'localhost:6379', } }] }; @@ -147,23 +154,36 @@ PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守

-#### Docker 部署 +#### 使用源码 + Docker 部署 -1. 创建 `Dockerfile`: +1. 创建 `Dockerfile` + 源码方式: ```dockerfile - FROM golang:1.18 - - WORKDIR /app - - COPY . . - - RUN go mod download - RUN go build -o go-captcha-service - - EXPOSE 8080 50051 - - CMD ["./go-captcha-service"] + FROM --platform=$BUILDPLATFORM golang:1.23 AS builder + WORKDIR /app + + COPY go.mod go.sum ./ + RUN go mod download + + COPY . . + + ARG TARGETOS + ARG TARGETARCH + RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s" -v -a -trimpath -o go-captcha-service ./cmd/go-captcha-service + + FROM scratch AS binary + WORKDIR /app + + COPY --from=builder /app/go-captcha-service . + COPY config.json . + COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + + EXPOSE 8080 50051 + + HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD ["/app/go-captcha-service", "--health-check"] || exit 1 + + CMD ["/app/go-captcha-service"] ``` 2. 构建镜像: @@ -178,7 +198,7 @@ PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守 docker run -d -p 8080:8080 -p 50051:50051 \ -v $(pwd)/config.json:/app/config.json \ -v $(pwd)/gocaptcha.json:/app/gocaptcha.json \ - -v $(pwd)/gocaptcha:/app/gocaptcha \ + -v $(pwd)/resource/gocaptcha:/app/resource/gocaptcha \ --name go-captcha-service go-captcha-service:latest ``` @@ -191,7 +211,7 @@ PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守 1. 拉取官方镜像: ```bash - docker pull wenlng/go-captcha-service + docker pull wenlng/go-captcha-service@latest ``` 2. 运行容器: @@ -200,13 +220,17 @@ PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守 docker run -d -p 8080:8080 -p 50051:50051 \ -v $(pwd)/config.json:/app/config.json \ -v $(pwd)/gocaptcha.json:/app/gocaptcha.json \ - -v $(pwd)/gocaptcha:/app/gocaptcha \ + -v $(pwd)/resource/gocaptcha:/app/resource/gocaptcha \ --name go-captcha-service wenlng/go-captcha-service:latest ```

+ +### 分布式部署 + + #### 配置分布式缓存 1. 在 `config.json` 中选择分布式缓存(如 Redis): @@ -214,9 +238,9 @@ PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守 ```json { "cache_type": "redis", - "redis_addrs": "localhost:6379", "cache_ttl": 1800, - "cache_key_prefix": "GO_CAPTCHA_DATA:" + "cache_key_prefix": "GO_CAPTCHA_DATA:", + "redis_addrs": "localhost:6379" } ``` @@ -227,6 +251,8 @@ PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守 ```
+
+ #### 分布式动态配置 注意:当开启分布式动态配置功能后,`config.json` 和 `gocaptcha.json` 会同时作用 @@ -236,8 +262,8 @@ PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守 ```json { "enable_dynamic_config": true, - "service_discovery": "etcd", - "service_discovery_addrs": "localhost:2379" + "dynamic_config": "etcd", + "dynamic_config_addrs": "localhost:2379" } ``` @@ -247,17 +273,7 @@ PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守 docker run -d -p 8848:8848 --name etcd bitnami/etcd::latest ``` -3. 例如在 gocaptcha.json 配置文件中,修改配置: - - ```json - { - "builder": { - - } - } - ``` - -4. 配置文件同步与拉取 +3. 配置文件同步与拉取 * 服务在启动时会根据 `config_version` 版本决定推送与拉取,当本地版本大于远程(如 Etcd)的配置版本时会将本地配置推送覆盖,反之自动拉取更新到本地(非文件式更新)。 * 在服务启动后,动态配置管理器会实时监听远程(如 Etcd)的配置,当远程配置发生变更后,会摘取到本地进行版本比较,当大于本地版本时都会覆盖本地的配置。 @@ -289,7 +305,7 @@ PM2 是 Node.js 进程管理工具,可用于管理 Go 服务,提供进程守

-#### Docker Compose 分布式部署 +#### Docker Compose 多实例部署 创建 `docker-compose.yml`,包含多个服务实例、Consul、Redis、ZooKeeper 和 Nacos: @@ -305,8 +321,20 @@ services: - ./config.json:/app/config.json - ./gocaptcha.json:/app/gocaptcha.json - ./resources/gocaptcha:/app/resources/gocaptcha + environment: + - CONFIG=config.json + - GO_CAPTCHA_CONFIG=gocaptcha.json + - SERVICE_NAME=go-captcha-service + - CACHE_TYPE=redis + - CACHE_ADDRS=localhost:6379 + - ENABLE_DYNAMIC_CONFIG=true + - DYNAMIC_CONFIG_TYPE=etcd + - DYNAMIC_CONFIG_ADDRS=localhost:2379 + - ENABLE_SERVICE_DISCOVERY=true + - SERVICE_DISCOVERY_TYPE=etcd + - SERVICE_DISCOVERY_ADDRS=localhost:2379 depends_on: - - consul + - etcd - redis restart: unless-stopped @@ -319,16 +347,30 @@ services: - ./config.json:/app/config.json - ./gocaptcha.json:/app/gocaptcha.json - ./resources/gocaptcha:/app/resources/gocaptcha + environment: + - CONFIG=config.json + - GO_CAPTCHA_CONFIG=gocaptcha.json + - SERVICE_NAME=go-captcha-service + - CACHE_TYPE=redis + - CACHE_ADDRS=localhost:6379 + - ENABLE_DYNAMIC_CONFIG=true + - DYNAMIC_CONFIG_TYPE=etcd + - DYNAMIC_CONFIG_ADDRS=localhost:2379 + - ENABLE_SERVICE_DISCOVERY=true + - SERVICE_DISCOVERY_TYPE=etcd + - SERVICE_DISCOVERY_ADDRS=localhost:2379 depends_on: - - consul + - etcd - redis restart: unless-stopped - consul: - image: consul:latest + etcd: + image: bitnami/etcd:latest ports: - - "8500:8500" - command: agent -server -bootstrap -ui -client=0.0.0.0 + - "2379:2379" + environment: + - ALLOW_NONE_AUTHENTICATION=yes + privileged: true restart: unless-stopped redis: @@ -347,6 +389,86 @@ docker-compose up -d

+## 预置 API +* 获取验证码 + ```shell + curl -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/public/get-data\?id\=click-default-ch + ``` + +* 验证码校验 + ```shell + curl -X POST -H "X-API-Key:my-secret-key-123" -H "Content-Type:application/json" -d '{"id":"click-default-ch","captchaKey":"xxxx-xxxxx","value": "x1,y1,x2,y2"}' http://127.0.0.1:8181/api/v1/public/check-data + ``` + +* 获取校验结果 data == "ok" 代表成功 + ```shell + curl -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/public/check-status\?captchaKey\=xxxx-xxxx + ``` + +* 获取状态信息(不允许暴露公网) + ```shell + curl -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/manage/get-status-info\?captchaKey\=xxxx-xxxx + ``` + +* 上传资源(不允许暴露公网) + ```shell + curl -X POST -H "X-API-Key:my-secret-key-123" -F "dirname=imagesdir" -F "files=@/path/to/file1.jpg" -F "files=@/path/to/file2.jpg" http://127.0.0.1:8080/api/v1/manage/upload-resource + ``` + +* 删除资源(不允许暴露公网) + ```shell + curl -X DELETE -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/manage/delete-resource?path=xxxxx.jpg + ``` + +* 获取资源文件列表(不允许暴露公网) + ```shell + curl -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/manage/get-resource-list?path=imagesdir + ``` + +* 获取验证码配置(不允许暴露公网) + ```shell + curl -H "X-API-Key:my-secret-key-123" http://127.0.0.1:8080/api/v1/manage/get-config + ``` + +* 更新验证码配置,非文件更新(不允许暴露公网) + ```shell + curl -X POST -H "X-API-Key:my-secret-key-123" -H "Content-Type:application/json" -d '{"config_version":3,"resources":{ ... },"builder": { ... }}' http://127.0.0.1:8080/api/v1/manage/update-hot-config + ``` + +更详情和 Grpc API 请转到 [go-captcha-service-sdk](https://github.com/wenlng/go-captcha-service-sdk) + +
+
+ + +## API 校验配置 +如果在 `config.json` 配置了 `api-keys`,则服务的 HTTP 和 gRPC 相关的 API 都需要通过请求头携带 X-API-Key 进行校验。 + +内置 API 的 `/api/v1/manage` 是不允许暴露公网,不安全,需要匹配路由规则为 `/api/v1/public` 开放到公开,可以通过相关WEB应用服务器、反向代理服务器或者网关软件代理到内部服务,例如:Kong、Envoy、Tomcat、Nginx 等。 + +以 Nginx 反向代理路由匹配规则公网路由规则示例 +```text +server { + listen 80; + server_name example.com; + + # 匹配 /api/v1/public 的请求,代理到后端 + location ^~ /api/v1/public { + proxy_pass http://localhost:8080; # 假设服务运行在 8080 端口 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # 匹配 /api/v1/manage 的请求,禁止访问 + location ^~ /api/v1/manage { + deny all; # 禁止所有请求,返回 403 + } +} +``` + +
+
## 配置说明 @@ -363,7 +485,24 @@ docker-compose up -d * cache-type:设置缓存类型,支持 redis、memory、etcd、memcache。 * cache-ttl:设置缓存 TTL,单位秒。 * cache-key-prefix:设置缓存键前缀,默认 "GO_CAPTCHA_DATA:"。 -* service-discovery:设置服务发现类型,支持 etcd、zookeeper、consul、nacos。 + +* enable-dynamic-config:启用动态配置服务,默认 false。 +* dynamic-config-type:设置动态配置服务类型,支持 etcd、zookeeper、consul、nacos。 +* dynamic-config-addrs:设置动态配置服务器地址,逗号分隔。 +* dynamic-config-ttl:设置动态配置服务注册存活时间,单位秒,默认 10。 +* dynamic-config-keep-alive:设置动态配置服务保活间隔,单位秒,默认 3。 +* dynamic-config-max-retries:设置动态配置服务操作最大重试次数,默认 3。 +* dynamic-config-base-retry-delay:设置动态配置服务重试基础延迟,单位毫秒,默认 3。 +* dynamic-config-username:设置动态配置服务认证用户名。 +* dynamic-config-password:设置动态配置服务认证密码。 +* dynamic-config-tls-server-name:设置动态配置服务 TLS 服务器名称。 +* dynamic-config-tls-address:设置动态配置服务 TLS 服务器地址。 +* dynamic-config-tls-cert-file:设置动态配置服务 TLS 证书文件路径。 +* dynamic-config-tls-key-file:设置动态配置服务 TLS 密钥文件路径。 +* dynamic-config-tls-ca-file:设置动态配置服务 TLS CA 文件路径。 + +* enable-service-discovery:启用服务发现,默认 false。 +* service-discovery-type:设置服务发现类型,支持 etcd、zookeeper、consul、nacos。 * service-discovery-addrs:设置服务发现服务器地址,逗号分隔。 * service-discovery-ttl:设置服务发现注册存活时间,单位秒,默认 10。 * service-discovery-keep-alive:设置服务发现保活间隔,单位秒,默认 3。 @@ -376,15 +515,47 @@ docker-compose up -d * service-discovery-tls-cert-file:设置服务发现 TLS 证书文件路径。 * service-discovery-tls-key-file:设置服务发现 TLS 密钥文件路径。 * service-discovery-tls-ca-file:设置服务发现 TLS CA 文件路径。 + * rate-limit-qps:设置速率限制 QPS。 * rate-limit-burst:设置速率限制突发量。 * api-keys:设置 API 密钥,逗号分隔。 * log-level:设置日志级别,支持 error、debug、warn、info。 -* enable-service-discovery:启用服务发现,默认 false。 -* enable-dynamic-config:启用动态配置,默认 false。 * health-check:运行健康检查并退出,默认 false。 * enable-cors:启用跨域资源共享,默认 false。 +
+ +### 环境变量 +基本配置: + +* CONFIG: 主配置文件路径,用于加载应用程序配置。 +* GO_CAPTCHA_CONFIG: CAPTCHA 服务的配置文件路径。 +* SERVICE_NAME: 服务名称,用于标识服务实例。 +* HTTP_PORT: HTTP 服务监听端口。 +* GRPC_PORT: gRPC 服务监听端口。 +* API_KEYS: API 密钥,用于认证或授权。 + +缓存配置: +* CACHE_TYPE: 缓存类型(如 redis、memcached、memory、etcd)。 +* CACHE_ADDRS: 缓存服务地址列表。 +* CACHE_USERNAME: 缓存服务认证用户名。 +* CACHE_PASSWORD: 缓存服务认证密码。 + +动态配置服务: +* ENABLE_DYNAMIC_CONFIG: 是否启用动态配置(值为 true 表示启用)。 +* DYNAMIC_CONFIG_TYPE: 动态配置类型(如 consul、zookeeper、nacos、etcd)。 +* DYNAMIC_CONFIG_ADDRS: 动态配置服务地址列表。 +* DYNAMIC_CONFIG_USERNAME: 动态配置服务认证用户名。 +* DYNAMIC_CONFIG_PASSWORD: 动态配置服务认证密码。 + +服务发现: +* ENABLE_SERVICE_DISCOVERY: 是否启用服务发现(值为 true 表示启用)。 +* SERVICE_DISCOVERY_TYPE: 服务发现类型(如 consul、zookeeper、nacos、etcd)。 +* SERVICE_DISCOVERY_ADDRS: 服务发现服务地址列表。 +* SERVICE_DISCOVERY_USERNAME: 服务发现服务认证用户名。 +* SERVICE_DISCOVERY_PASSWORD: 服务发现服务认证密码。 + +
### 配置文件 服务使用两个配置文件:`config.json` 和 `gocaptcha.json`,分别定义服务运行参数和验证码生成的配置. @@ -405,9 +576,24 @@ docker-compose up -d "cache_type": "memory", "cache_ttl": 1800, "cache_key_prefix": "GO_CAPTCHA_DATA:", + "enable_dynamic_config": false, + "dynamic_config_type": "etcd", + "dynamic_config_addrs": "localhost:2379", + "dynamic_config_username": "", + "dynamic_config_password": "", + "dynamic_config_ttl": 10, + "dynamic_config_keep_alive": 3, + "dynamic_config_max_retries": 3, + "dynamic_config_base_retry_delay": 500, + "dynamic_config_tls_server_name": "", + "dynamic_config_tls_address": "", + "dynamic_config_tls_cert_file": "", + "dynamic_config_tls_key_file": "", + "dynamic_config_tls_ca_file": "", + "enable_service_discovery": false, - "service_discovery": "etcd", + "service_discovery_type": "etcd", "service_discovery_addrs": "localhost:2379", "service_discovery_username": "", "service_discovery_password": "", @@ -420,6 +606,7 @@ docker-compose up -d "service_discovery_tls_cert_file": "", "service_discovery_tls_key_file": "", "service_discovery_tls_ca_file": "", + "rate_limit_qps": 1000, "rate_limit_burst": 1000, "enable_cors": true, @@ -444,9 +631,28 @@ docker-compose up -d - `memcache`:高性能分布式缓存,适合高并发。 - `cache_ttl` (整数):缓存有效期(秒),默认 `1800`. - `cache_key_prefix` (字符串):缓存键前缀,默认 `GO_CAPTCHA_DATA:`。 -- `enable_dynamic_config` (布尔):启用动态配置,默认 `false`。 + +- `enable_dynamic_config` (布尔):启用动态配置服务,默认 `false`。 +- `dynamic_config_type` (字符串):动态配置服务类型,默认 `etcd`: + - `etcd`:适合一致性要求高的场景。 + - `nacos`:适合云原生环境。 + - `zookeeper`:适合复杂分布式系统。 + - `consul`:轻量级,支持健康检查。 +- `dynamic_config_addrs` (字符串):动态配置服务地址,如 Etcd 为 `localhost:2379`,Nacos 为 `localhost:8848`。 +- `dynamic_config_username` (字符串):用户名,例如 Nacos 的默认用户名为`nacos`,默认空。 +- `dynamic_config_password` (字符串):密码,例如 Nacos 的默认用户密码为`nacos`,默认空。 +- `dynamic_config_ttl` (整数):服务租约时间(秒),默认 `10`。 +- `dynamic_config_keep_alive` (整数):心跳间隔(秒),默认 `3`。 +- `dynamic_config_max_retries` (整数):重试次数,默认 `3`。 +- `dynamic_config_base_retry_delay` (整数):重试延迟(毫秒),默认 `500`。 +- `dynamic_config_tls_server_name` (字符串):TLS 服务器名称,默认空。 +- `dynamic_config_tls_address` (字符串):TLS 地址,默认空。 +- `dynamic_config_tls_cert_file` (字符串):TLS 证书文件,默认空。 +- `dynamic_config_tls_key_file` (字符串):TLS 密钥文件,默认空。 +- `dynamic_config_tls_ca_file` (字符串):TLS CA 证书文件,默认空。 + - `enable_service_discovery` (布尔):启用服务发现,默认 `false`。 -- `service_discovery` (字符串):服务发现类型,默认 `etcd`: +- `service_discovery_type` (字符串):服务发现类型,默认 `etcd`: - `etcd`:适合一致性要求高的场景。 - `nacos`:适合云原生环境。 - `zookeeper`:适合复杂分布式系统。 @@ -463,11 +669,12 @@ docker-compose up -d - `service_discovery_tls_cert_file` (字符串):TLS 证书文件,默认空。 - `service_discovery_tls_key_file` (字符串):TLS 密钥文件,默认空。 - `service_discovery_tls_ca_file` (字符串):TLS CA 证书文件,默认空。 + - `rate_limit_qps` (整数):API 每秒请求限流,默认 `1000`。 - `rate_limit_burst` (整数):API 限流突发容量,默认 `1000`。 - `enable_cors` (布尔):启用 CORS,默认 `true`。 - `log_level` (字符串):日志级别(`debug`、`info`、`warn`、`error`),默认 `info`。 -- `api_keys` (字符串数组):API 认证密钥,默认包含示例密钥。 +- `api_keys` (字符串数组):API 认证密钥。 ### gocaptcha.json @@ -571,17 +778,17 @@ docker-compose up -d "click-dark-ch": { "version": "0.0.1", "language": "chinese", - // ... + // 同上... }, "click-default-en": { "version": "0.0.1", "language": "english", - // ... + // 同上... }, "click-dark-en": { "version": "0.0.1", "language": "english", - // ... + // 同上... } }, "click_shape_config_maps": { @@ -678,7 +885,6 @@ docker-compose up -d - `config_version` (整数):配置文件版本号,用于分布动态配置管理,默认 `1`。 - ##### resources 字段 - `version` (字符串):资源配置版本号,用于控制重新创建新的验证码实例,默认 `0.0.1`。 @@ -717,7 +923,7 @@ docker-compose up -d ###### click_config_maps -定义文本点击验证码的配置,支持中英文和明暗主题,key为ID,在请求时传递,例如:`api/v1/get-data?id=click-default-ch`。 +定义文本点击验证码的配置,支持中英文和明暗主题,key为ID,在请求验证码API时传递,例如:`api/v1/public/get-data?id=click-default-ch`。 - `click-default-ch` (对象):中文默认主题配置。 - `version` (字符串):配置版本号,用于控制重新创建新的验证码实例,默认 `0.0.1`。 @@ -834,13 +1040,11 @@ docker-compose up -d ### 配置热重载说明 -`gocaptcha.json` 热重载以每个配置顶的 version 字段决定是否生效。 +`gocaptcha.json` 热重载以每个配置项的 version 字段决定是否生效。 `config.json` 热重载有效的字段如下: -* redis_addrs -* etcd_addrs -* memcache_addrs * cache_type +* cahce_addrs * cache_ttl * cache_key_prefix * api_keys @@ -849,7 +1053,6 @@ docker-compose up -d * rate_limit_burst - ### 测试: - 验证码生成:验证图片、形状和密钥有效性。 @@ -859,16 +1062,21 @@ docker-compose up -d - 动态配置:测试 Nacos 配置更新。 -### 压力测试 +
+
+ +## 赞助一下 + +

如果觉得项目有帮助,可以请作者喝杯咖啡 🍹

+
+Buy Me A Coffee +Buy Me A Coffee +
-测试 HTTP 接口: +
-```bash -wrk -t12 -c400 -d30s http://127.0.0.1:8080/api/v1/get-data?id=click-default-ch -``` +## LICENSE +Go Captcha Service source code is licensed under the Apache Licence, Version 2.0 [http://www.apache.org/licenses/LICENSE-2.0.html](http://www.apache.org/licenses/LICENSE-2.0.html) -测试 gRPC 接口: +
-```bash -grpcurl -plaintext -d '{"id":"click-default-ch"}' localhost:50051 gocaptcha.GoCaptchaService/GetData -``` diff --git a/config.dev.json b/config.dev.json index d3626fb..d020972 100644 --- a/config.dev.json +++ b/config.dev.json @@ -1,32 +1,49 @@ { - "config_version": 15, + "config_version": 5, "service_name": "go-captcha-service", - "http_port": "8081", - "grpc_port": "50052", - "redis_addrs": "localhost:6379", - "etcd_addrs": "localhost:2379", - "memcache_addrs": "localhost:11211", + "http_port": "8181", + "grpc_port": "50058", + "cache_type": "redis", "cache_ttl": 1800, - "cache_key_prefix": "DEV_ENV_GO_CAPTCHA_DATA:", + "cache_key_prefix": "GO_CAPTCHA_DATA:", + "cache_addrs": "localhost:6379", + "cache_username": "", + "cache_password": "", + "enable_dynamic_config": true, + "dynamic_config_type": "etcd", + "dynamic_config_addrs": "localhost:2379", + "dynamic_config_username": "", + "dynamic_config_password": "", + "dynamic_config_ttl": 10, + "dynamic_config_keep_alive": 3, + "dynamic_config_max_retries": 3, + "dynamic_config_base_retry_delay": 500, + "dynamic_config_tls_server_name": "", + "dynamic_config_tls_address": "", + "dynamic_config_tls_cert_file": "", + "dynamic_config_tls_key_file": "", + "dynamic_config_tls_ca_file": "", + "enable_service_discovery": true, - "service_discovery": "nacos", - "service_discovery_addrs": "localhost:8848", - "service_discovery_username": "nacos", - "service_discovery_password": "nacos", + "service_discovery_type": "etcd", + "service_discovery_addrs": "localhost:2379", + "service_discovery_username": "", + "service_discovery_password": "", "service_discovery_ttl": 10, "service_discovery_keep_alive": 3, "service_discovery_max_retries": 3, - "service_discovery_base_retry_delay": 1, + "service_discovery_base_retry_delay": 500, "service_discovery_tls_server_name": "", "service_discovery_tls_address": "", "service_discovery_tls_cert_file": "", "service_discovery_tls_key_file": "", "service_discovery_tls_ca_file": "", + "rate_limit_qps": 1000, "rate_limit_burst": 1000, "enable_cors": true, "log_level": "info", - "api_keys": ["my-secret-key-123", "another-key-456", "another-key-000"] + "api_keys": ["my-secret-key-123", "another-key-456", "another-key-789"] } \ No newline at end of file diff --git a/config.json b/config.json index d574995..4afd3ce 100644 --- a/config.json +++ b/config.json @@ -3,15 +3,31 @@ "service_name": "go-captcha-service", "http_port": "8080", "grpc_port": "50051", - "redis_addrs": "localhost:6379", - "etcd_addrs": "localhost:2379", - "memcache_addrs": "localhost:11211", + "cache_type": "memory", - "cache_ttl": 1800, + "cache_addrs": "", + "cache_username": "", + "cache_password": "", "cache_key_prefix": "GO_CAPTCHA_DATA:", + "cache_ttl": 1800, + "enable_dynamic_config": false, + "dynamic_config_type": "etcd", + "dynamic_config_addrs": "localhost:2379", + "dynamic_config_username": "", + "dynamic_config_password": "", + "dynamic_config_ttl": 10, + "dynamic_config_keep_alive": 3, + "dynamic_config_max_retries": 3, + "dynamic_config_base_retry_delay": 500, + "dynamic_config_tls_server_name": "", + "dynamic_config_tls_address": "", + "dynamic_config_tls_cert_file": "", + "dynamic_config_tls_key_file": "", + "dynamic_config_tls_ca_file": "", + "enable_service_discovery": false, - "service_discovery": "etcd", + "service_discovery_type": "etcd", "service_discovery_addrs": "localhost:2379", "service_discovery_username": "", "service_discovery_password": "", @@ -24,6 +40,7 @@ "service_discovery_tls_cert_file": "", "service_discovery_tls_key_file": "", "service_discovery_tls_ca_file": "", + "rate_limit_qps": 1000, "rate_limit_burst": 1000, "enable_cors": true, diff --git a/ecosystem.config.js b/ecosystem.config.js index 925aac7..6e88c6f 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -8,15 +8,17 @@ module.exports = { max_memory_restart: '1G', env: { CONFIG: 'config.json', + GO_CAPTCHA_CONFIG: 'gocaptcha.json', + SERVICE_NAME: 'go-captcha-service', CACHE_TYPE: 'redis', - CACHE_TTL: '60', - CACHE_CLEANUP_INTERVAL: '10', + CACHE_ADDRS: 'localhost:6379', }, env_production: { - CONFIG: '/etc/go-captcha-service/config.json', - CACHE_TYPE: 'etcd', - CACHE_TTL: '30', - CACHE_CLEANUP_INTERVAL: '5', + CONFIG: 'config.json', + GO_CAPTCHA_CONFIG: 'gocaptcha.json', + SERVICE_NAME: 'go-captcha-service', + CACHE_TYPE: 'redis', + CACHE_ADDRS: 'localhost:6379', } }] }; \ No newline at end of file diff --git a/go.mod b/go.mod index fad8721..73312b8 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,10 @@ go 1.23.0 require ( github.com/alicebob/miniredis/v2 v2.32.1 - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 github.com/fsnotify/fsnotify v1.9.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/google/uuid v1.6.0 + github.com/memcachier/mc/v3 v3.0.3 github.com/redis/go-redis/v9 v9.6.1 github.com/sony/gobreaker v0.5.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 976a9eb..e8d6011 100644 --- a/go.sum +++ b/go.sum @@ -119,8 +119,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -369,6 +367,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4= +github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= diff --git a/gocaptcha.dev.json b/gocaptcha.dev.json index f826a49..6f352dd 100644 --- a/gocaptcha.dev.json +++ b/gocaptcha.dev.json @@ -442,90 +442,30 @@ "click-shape-default": { "version": "0.0.1", "master": { - "image_size": { - "width": 300, - "height": 200 - }, - "range_length": { - "min": 6, - "max": 7 - }, + "image_size": { "width": 300, "height": 200 }, + "range_length": { "min": 6, "max": 7 }, "range_angles": [ - { - "min": 20, - "max": 35 - }, - { - "min": 35, - "max": 45 - }, - { - "min": 290, - "max": 305 - }, - { - "min": 305, - "max": 325 - }, - { - "min": 325, - "max": 330 - } - ], - "range_size": { - "min": 26, - "max": 32 - }, - "range_colors": [ - "#fde98e", - "#60c1ff", - "#fcb08e", - "#fb88ff", - "#b4fed4", - "#cbfaa9", - "#78d6f8" + { "min": 20, "max": 35 }, + { "min": 35, "max": 45 }, + { "min": 290, "max": 305 }, + { "min": 305, "max": 325 }, + { "min": 325, "max": 330 } ], + "range_size": { "min": 26, "max": 32 }, + "range_colors": [ "#fde98e", "#60c1ff", "#fcb08e", "#fb88ff", "#b4fed4", "#cbfaa9", "#78d6f8"], "display_shadow": true, "shadow_color": "#101010", - "shadow_point": { - "x": -1, - "y": -1 - }, + "shadow_point": { "x": -1, "y": -1 }, "image_alpha": 1, "use_shape_original_color": true }, "thumb": { - "image_size": { - "width": 150, - "height": 40 - }, - "range_verify_length": { - "min": 2, - "max": 4 - }, + "image_size": { "width": 150, "height": 40}, + "range_verify_length": { "min": 2, "max": 4 }, "disabled_range_verify_length": false, - "range_text_size": { - "min": 22, - "max": 28 - }, - "range_text_colors": [ - "#1f55c4", - "#780592", - "#2f6b00", - "#910000", - "#864401", - "#675901", - "#016e5c" - ], - "range_background_colors": [ - "#1f55c4", - "#780592", - "#2f6b00", - "#910000", - "#864401", - "#675901", - "#016e5c" - ], + "range_text_size": { "min": 22, "max": 28}, + "range_text_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c"], + "range_background_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c" ], "is_non_deform_ability": false, "background_distort": 4, "background_distort_alpha": 1, @@ -536,17 +476,51 @@ }, "slide_config_maps": { "slide-default": { - "version": "0.0.1" + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { "min": 60, "max": 70 }, + "range_graph_angles": [ + { "min": 20, "max": 35 } + ], + "generate_graph_number": 1, + "enable_graph_vertical_random": false, + "range_dead_zone_directions": ["left", "right"] + } } }, "drag_config_maps": { "drag-default": { - "version": "0.0.1" + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { "min": 60, "max": 70 }, + "range_graph_angles": [ + { "min": 0, "max": 0 } + ], + "generate_graph_number": 2, + "enable_graph_vertical_random": true, + "range_dead_zone_directions": ["left", "right", "top", "bottom"] + } } }, "rotate_config_maps": { "rotate-default": { - "version": "0.0.1" + "version": "0.0.1", + "master": { + "image_square_size": 220 + }, + "thumb": { + "range_angles": [{ "min": 30, "max": 330 }], + "range_image_square_sizes": [140, 150, 160, 170], + "image_alpha": 1 + } } } } diff --git a/gocaptcha.json b/gocaptcha.json index f826a49..6f352dd 100644 --- a/gocaptcha.json +++ b/gocaptcha.json @@ -442,90 +442,30 @@ "click-shape-default": { "version": "0.0.1", "master": { - "image_size": { - "width": 300, - "height": 200 - }, - "range_length": { - "min": 6, - "max": 7 - }, + "image_size": { "width": 300, "height": 200 }, + "range_length": { "min": 6, "max": 7 }, "range_angles": [ - { - "min": 20, - "max": 35 - }, - { - "min": 35, - "max": 45 - }, - { - "min": 290, - "max": 305 - }, - { - "min": 305, - "max": 325 - }, - { - "min": 325, - "max": 330 - } - ], - "range_size": { - "min": 26, - "max": 32 - }, - "range_colors": [ - "#fde98e", - "#60c1ff", - "#fcb08e", - "#fb88ff", - "#b4fed4", - "#cbfaa9", - "#78d6f8" + { "min": 20, "max": 35 }, + { "min": 35, "max": 45 }, + { "min": 290, "max": 305 }, + { "min": 305, "max": 325 }, + { "min": 325, "max": 330 } ], + "range_size": { "min": 26, "max": 32 }, + "range_colors": [ "#fde98e", "#60c1ff", "#fcb08e", "#fb88ff", "#b4fed4", "#cbfaa9", "#78d6f8"], "display_shadow": true, "shadow_color": "#101010", - "shadow_point": { - "x": -1, - "y": -1 - }, + "shadow_point": { "x": -1, "y": -1 }, "image_alpha": 1, "use_shape_original_color": true }, "thumb": { - "image_size": { - "width": 150, - "height": 40 - }, - "range_verify_length": { - "min": 2, - "max": 4 - }, + "image_size": { "width": 150, "height": 40}, + "range_verify_length": { "min": 2, "max": 4 }, "disabled_range_verify_length": false, - "range_text_size": { - "min": 22, - "max": 28 - }, - "range_text_colors": [ - "#1f55c4", - "#780592", - "#2f6b00", - "#910000", - "#864401", - "#675901", - "#016e5c" - ], - "range_background_colors": [ - "#1f55c4", - "#780592", - "#2f6b00", - "#910000", - "#864401", - "#675901", - "#016e5c" - ], + "range_text_size": { "min": 22, "max": 28}, + "range_text_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c"], + "range_background_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c" ], "is_non_deform_ability": false, "background_distort": 4, "background_distort_alpha": 1, @@ -536,17 +476,51 @@ }, "slide_config_maps": { "slide-default": { - "version": "0.0.1" + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { "min": 60, "max": 70 }, + "range_graph_angles": [ + { "min": 20, "max": 35 } + ], + "generate_graph_number": 1, + "enable_graph_vertical_random": false, + "range_dead_zone_directions": ["left", "right"] + } } }, "drag_config_maps": { "drag-default": { - "version": "0.0.1" + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { "min": 60, "max": 70 }, + "range_graph_angles": [ + { "min": 0, "max": 0 } + ], + "generate_graph_number": 2, + "enable_graph_vertical_random": true, + "range_dead_zone_directions": ["left", "right", "top", "bottom"] + } } }, "rotate_config_maps": { "rotate-default": { - "version": "0.0.1" + "version": "0.0.1", + "master": { + "image_square_size": 220 + }, + "thumb": { + "range_angles": [{ "min": 30, "max": 330 }], + "range_image_square_sizes": [140, 150, 160, 170], + "image_alpha": 1 + } } } } diff --git a/internal/app/app.go b/internal/app/app.go index 1fbc301..171a9f0 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -65,13 +65,31 @@ func NewApp() (*App, error) { serviceName := flag.String("service-name", "", "Name for service") httpPort := flag.String("http-port", "", "Port for HTTP server") grpcPort := flag.String("grpc-port", "", "Port for gRPC server") - redisAddrs := flag.String("redis-addrs", "", "Comma-separated Redis cluster addresses") - etcdAddrs := flag.String("etcd-addrs", "", "Comma-separated etcd addresses") - memcacheAddrs := flag.String("memcache-addrs", "", "Comma-separated Memcached addresses") + cacheType := flag.String("cache-type", "", "CacheManager type: redis, memory, etcd, memcache") + cacheAddrs := flag.String("cache-addrs", "", "Comma-separated Cache cluster addresses") + cacheUsername := flag.String("cache-username", "", "Comma-separated cache cluster username") + cachePassword := flag.String("cache-password", "", "Comma-separated cache cluster password") cacheTTL := flag.Int("cache-ttl", 0, "CacheManager TTL in seconds") cacheKeyPrefix := flag.String("cache-key-prefix", "GO_CAPTCHA_DATA:", "Key prefix for cache") - serviceDiscovery := flag.String("service-discovery", "", "Service discovery: etcd, zookeeper, consul, nacos") + + enableDynamicConfig := flag.Bool("enable-dynamic-config", false, "Enable dynamic config") + dynamicConfigType := flag.String("dynamic-config-type", "", "Service discovery: etcd, zookeeper, consul, nacos") + dynamicConfigAddrs := flag.String("dynamic-config-addrs", "", "Comma-separated list of service dynamic config addresses") + dynamicConfigTTL := flag.Int("dynamic-config-ttl", 10, "Time-to-live in seconds for dynamic config registrations") + dynamicConfigKeepAlive := flag.Int("dynamic-config-keep-alive", 3, "Duration in seconds for dynamic config keep-alive interval") + dynamicConfigMaxRetries := flag.Int("dynamic-config-max-retries", 3, "Maximum number of retries for dynamic config operations") + dynamicConfigBaseRetryDelay := flag.Int("dynamic-config-base-retry-delay", 3, "Base delay in milliseconds for dynamic config retry attempts") + dynamicConfigUsername := flag.String("dynamic-config-username", "", "Username for dynamic config authentication") + dynamicConfigPassword := flag.String("dynamic-config-password", "", "Password for dynamic config authentication") + dynamicConfigTlsServerName := flag.String("dynamic-config-tls-server-name", "", "TLS server name for dynamic config connection") + dynamicConfigTlsAddress := flag.String("dynamic-config-tls-address", "", "TLS address for service dynamic config") + dynamicConfigTlsCertFile := flag.String("dynamic-config-tls-cert-file", "", "Path to TLS certificate file for dynamic config") + dynamicConfigTlsKeyFile := flag.String("dynamic-config-tls-key-file", "", "Path to TLS key file for dynamic config") + dynamicConfigTlsCaFile := flag.String("dynamic-config-tls-ca-file", "", "Path to TLS CA file for dynamic config") + + enableServiceDiscovery := flag.Bool("enable-service-discovery", false, "Enable service discovery") + serviceDiscoveryType := flag.String("service-discovery-type", "", "Service discovery: etcd, zookeeper, consul, nacos") serviceDiscoveryAddrs := flag.String("service-discovery-addrs", "", "Comma-separated list of service discovery server addresses") serviceDiscoveryTTL := flag.Int("service-discovery-ttl", 10, "Time-to-live in seconds for service discovery registrations") serviceDiscoveryKeepAlive := flag.Int("service-discovery-keep-alive", 3, "Duration in seconds for service discovery keep-alive interval") @@ -84,16 +102,83 @@ func NewApp() (*App, error) { serviceDiscoveryTlsCertFile := flag.String("service-discovery-tls-cert-file", "", "Path to TLS certificate file for service discovery") serviceDiscoveryTlsKeyFile := flag.String("service-discovery-tls-key-file", "", "Path to TLS key file for service discovery") serviceDiscoveryTlsCaFile := flag.String("service-discovery-tls-ca-file", "", "Path to TLS CA file for service discovery") + rateLimitQPS := flag.Int("rate-limit-qps", 0, "Rate limit QPS") rateLimitBurst := flag.Int("rate-limit-burst", 0, "Rate limit burst") apiKeys := flag.String("api-keys", "", "Comma-separated API keys") logLevel := flag.String("log-level", "", "Set log level: error, debug, warn, info") - enableServiceDiscovery := flag.Bool("enable-service-discovery", false, "Enable service discovery") - enableDynamicConfig := flag.Bool("enable-dynamic-config", false, "Enable dynamic config") healthCheckFlag := flag.Bool("health-check", false, "Run health check and exit") enableCorsFlag := flag.Bool("enable-cors", false, "Enable cross-domain resources") + flag.Parse() + // Read environment variables + if *serviceName == "" { + if v, exists := os.LookupEnv("CONFIG"); exists { + *configFile = v + } + if v, exists := os.LookupEnv("GO_CAPTCHA_CONFIG"); exists { + *gocaptchaConfigFile = v + } + + if v, exists := os.LookupEnv("SERVICE_NAME"); exists { + *serviceName = v + } + if v, exists := os.LookupEnv("HTTP_PORT"); exists { + *httpPort = v + } + if v, exists := os.LookupEnv("GRPC_PORT"); exists { + *grpcPort = v + } + if v, exists := os.LookupEnv("API_KEYS"); exists { + *apiKeys = v + } + if v, exists := os.LookupEnv("CACHE_TYPE"); exists { + *cacheType = v + } + if v, exists := os.LookupEnv("CACHE_ADDRS"); exists { + *cacheAddrs = v + } + if v, exists := os.LookupEnv("CACHE_USERNAME"); exists { + *cacheUsername = v + } + if v, exists := os.LookupEnv("CACHE_PASSWORD"); exists { + *cachePassword = v + } + + if v, exists := os.LookupEnv("ENABLE_DYNAMIC_CONFIG"); exists { + *enableDynamicConfig = v == "true" + } + if v, exists := os.LookupEnv("DYNAMIC_CONFIG_TYPE"); exists { + *dynamicConfigType = v + } + if v, exists := os.LookupEnv("DYNAMIC_CONFIG_ADDRS"); exists { + *dynamicConfigAddrs = v + } + if v, exists := os.LookupEnv("DYNAMIC_CONFIG_USERNAME"); exists { + *dynamicConfigUsername = v + } + if v, exists := os.LookupEnv("DYNAMIC_CONFIG_PASSWORD"); exists { + *dynamicConfigPassword = v + } + + if v, exists := os.LookupEnv("ENABLE_SERVICE_DISCOVERY"); exists { + *enableServiceDiscovery = v == "true" + } + if v, exists := os.LookupEnv("SERVICE_DISCOVERY_TYPE"); exists { + *serviceDiscoveryType = v + } + if v, exists := os.LookupEnv("SERVICE_DISCOVERY_ADDRS"); exists { + *serviceDiscoveryAddrs = v + } + if v, exists := os.LookupEnv("SERVICE_DISCOVERY_USERNAME"); exists { + *serviceDiscoveryUsername = v + } + if v, exists := os.LookupEnv("SERVICE_DISCOVERY_PASSWORD"); exists { + *serviceDiscoveryPassword = v + } + } + // Initialize logger logger, err := zap.NewProduction() if err != nil { @@ -130,34 +215,51 @@ func NewApp() (*App, error) { // Merge command-line flags cfg := dc.Get() cfg = config.MergeWithFlags(cfg, map[string]interface{}{ - "service-name": *serviceName, - "http-port": *httpPort, - "grpc-port": *grpcPort, - "redis-addrs": *redisAddrs, - "etcd-addrs": *etcdAddrs, - "memcache-addrs": *memcacheAddrs, - "cache-type": *cacheType, - "cache-ttl": *cacheTTL, - "cache-key-prefix": *cacheKeyPrefix, + "service-name": *serviceName, + "http-port": *httpPort, + "grpc-port": *grpcPort, + + "cache-type": *cacheType, + "cache-addrs": *cacheAddrs, + "cache-username": *cacheUsername, + "cache-password": *cachePassword, + "cache-ttl": *cacheTTL, + "cache-key-prefix": *cacheKeyPrefix, + + "enable-dynamic-config": *enableDynamicConfig, + "dynamic-config-type": *dynamicConfigType, + "dynamic-config-addrs": *dynamicConfigAddrs, + "dynamic-config-username": *dynamicConfigUsername, + "dynamic-config-password": *dynamicConfigPassword, + "dynamic-config-ttl": *dynamicConfigTTL, + "dynamic-config-keep-alive": *dynamicConfigKeepAlive, + "dynamic-config-max-retries": *dynamicConfigMaxRetries, + "dynamic-config-base-retry-delay": *dynamicConfigBaseRetryDelay, + "dynamic-config-tls-server-name": *dynamicConfigTlsServerName, + "dynamic-config-tls-address": *dynamicConfigTlsAddress, + "dynamic-config-tls-cert-file": *dynamicConfigTlsCertFile, + "dynamic-config-tls-key-file": *dynamicConfigTlsKeyFile, + "dynamic-config-tls-ca-file": *dynamicConfigTlsCaFile, + "enable-service-discovery": *enableServiceDiscovery, - "service-discovery": *serviceDiscovery, + "service-discovery-type": *serviceDiscoveryType, "service-discovery-addrs": *serviceDiscoveryAddrs, - "service-discovery-ttl": serviceDiscoveryTTL, - "service-discovery-keep-alive": serviceDiscoveryKeepAlive, - "service-discovery-max-retries": serviceDiscoveryMaxRetries, - "service-discovery-base-retry-delay": serviceDiscoveryBaseRetryDelay, - "service-discovery-username": serviceDiscoveryUsername, - "service-discovery-password": serviceDiscoveryPassword, - "service-discovery-tls-server-name": serviceDiscoveryTlsServerName, - "service-discovery-tls-address": serviceDiscoveryTlsAddress, - "service-discovery-tls-cert-file": serviceDiscoveryTlsCertFile, - "service-discovery-tls-key-file": serviceDiscoveryTlsKeyFile, - "service-discovery-tls-ca-file": serviceDiscoveryTlsCaFile, - "rate-limit-qps": *rateLimitQPS, - "rate-limit-burst": *rateLimitBurst, - "enable-cors": *enableCorsFlag, - "enable-dynamic-config": *enableDynamicConfig, - "api-keys": *apiKeys, + "service-discovery-username": *serviceDiscoveryUsername, + "service-discovery-password": *serviceDiscoveryPassword, + "service-discovery-ttl": *serviceDiscoveryTTL, + "service-discovery-keep-alive": *serviceDiscoveryKeepAlive, + "service-discovery-max-retries": *serviceDiscoveryMaxRetries, + "service-discovery-base-retry-delay": *serviceDiscoveryBaseRetryDelay, + "service-discovery-tls-server-name": *serviceDiscoveryTlsServerName, + "service-discovery-tls-address": *serviceDiscoveryTlsAddress, + "service-discovery-tls-cert-file": *serviceDiscoveryTlsCertFile, + "service-discovery-tls-key-file": *serviceDiscoveryTlsKeyFile, + "service-discovery-tls-ca-file": *serviceDiscoveryTlsCaFile, + + "rate-limit-qps": *rateLimitQPS, + "rate-limit-burst": *rateLimitBurst, + "enable-cors": *enableCorsFlag, + "api-keys": *apiKeys, }) if err = dc.Update(cfg); err != nil { logger.Fatal("[App] Configuration validation failed", zap.Error(err)) @@ -298,14 +400,15 @@ func (a *App) startHTTPServer(svcCtx *common.SvcContext, cfg *config.Config) err mwChain := middleware.NewChainHTTP(middlewares...) - http.Handle("/api/v1/get-data", mwChain.Then(handlers.GetDataHandler)) - http.Handle("/api/v1/check-data", mwChain.Then(handlers.CheckDataHandler)) - http.Handle("/api/v1/check-status", mwChain.Then(handlers.CheckStatusHandler)) - http.Handle("/api/v1/get-status-info", mwChain.Then(handlers.GetStatusInfoHandler)) - http.Handle("/api/v1/del-status-info", mwChain.Then(handlers.DelStatusInfoHandler)) - http.Handle("/api/v1/status/health", mwChain.Then(handlers.HealthStatusHandler)) + http.Handle("/status/health", mwChain.Then(handlers.HealthStatusHandler)) http.Handle("/rate-limit", mwChain.Then(middleware.RateLimitHandler(a.limiter, a.logger))) + + http.Handle("/api/v1/public/get-data", mwChain.Then(handlers.GetDataHandler)) + http.Handle("/api/v1/public/check-data", mwChain.Then(handlers.CheckDataHandler)) + http.Handle("/api/v1/public/check-status", mwChain.Then(handlers.CheckStatusHandler)) + http.Handle("/api/v1/manage/get-status-info", mwChain.Then(handlers.GetStatusInfoHandler)) + http.Handle("/api/v1/manage/del-status-info", mwChain.Then(handlers.DelStatusInfoHandler)) http.Handle("/api/v1/manage/upload-resource", mwChain.Then(handlers.UploadResourceHandler)) http.Handle("/api/v1/manage/delete-resource", mwChain.Then(handlers.DeleteResourceHandler)) http.Handle("/api/v1/manage/get-resource-list", mwChain.Then(handlers.GetResourceListHandler)) diff --git a/internal/app/setup.go b/internal/app/setup.go index 7c1bee3..db6c47f 100644 --- a/internal/app/setup.go +++ b/internal/app/setup.go @@ -29,7 +29,7 @@ func setupServiceDiscovery(dCfg *config.DynamicConfig, logger *zap.Logger) (serv } var sdType servicediscovery.ServiceDiscoveryType = servicediscovery.ServiceDiscoveryTypeNone - switch cfg.ServiceDiscovery { + switch cfg.ServiceDiscoveryType { case ServiceDiscoveryTypeEtcd: sdType = servicediscovery.ServiceDiscoveryTypeEtcd break @@ -128,7 +128,7 @@ func setupDynamicConfig(appDynaCfg *config.DynamicConfig, captDynaCfg *config2.D } var sdType provider.ProviderType - switch appCfg.ServiceDiscovery { + switch appCfg.ServiceDiscoveryType { case ServiceDiscoveryTypeEtcd: sdType = provider.ProviderTypeEtcd break @@ -145,17 +145,17 @@ func setupDynamicConfig(appDynaCfg *config.DynamicConfig, captDynaCfg *config2.D providerCfg := provider.ProviderConfig{ Type: sdType, - Endpoints: strings.Split(appCfg.ServiceDiscoveryAddrs, ","), - Username: appCfg.ServiceDiscoveryUsername, - Password: appCfg.ServiceDiscoveryPassword, + Endpoints: strings.Split(appCfg.DynamicConfigAddrs, ","), + Username: appCfg.DynamicConfigUsername, + Password: appCfg.DynamicConfigPassword, } - if appCfg.ServiceDiscoveryTlsCertFile != "" && appCfg.ServiceDiscoveryTlsKeyFile != "" && appCfg.ServiceDiscoveryTlsCaFile != "" { + if appCfg.DynamicConfigTlsCertFile != "" && appCfg.DynamicConfigTlsKeyFile != "" && appCfg.DynamicConfigTlsCaFile != "" { providerCfg.TlsConfig = &common.TLSConfig{ - Address: appCfg.ServiceDiscoveryTlsAddress, - CertFile: appCfg.ServiceDiscoveryTlsCertFile, - KeyFile: appCfg.ServiceDiscoveryTlsKeyFile, - CAFile: appCfg.ServiceDiscoveryTlsCaFile, - ServerName: appCfg.ServiceDiscoveryTlsServerName, + Address: appCfg.DynamicConfigTlsAddress, + CertFile: appCfg.DynamicConfigTlsCertFile, + KeyFile: appCfg.DynamicConfigTlsKeyFile, + CAFile: appCfg.DynamicConfigTlsCaFile, + ServerName: appCfg.DynamicConfigTlsServerName, } } @@ -306,7 +306,7 @@ func setupLoggerLevel(logger *zap.Logger, level string) { // setupHealthCheck performs a health check on HTTP and gRPC servers func setupHealthCheck(httpAddr, grpcAddr string) error { - resp, err := http.Get("http://localhost" + httpAddr + "/read?key=test") + resp, err := http.Get("http://localhost" + httpAddr + "/status/health") if err != nil || resp.StatusCode != http.StatusNotFound { return fmt.Errorf("HTTP health check failed: %v", err) } @@ -330,9 +330,9 @@ func setupCacheManager(dcfg *config.DynamicConfig, logger *zap.Logger) (*cache.C cacheMgr, err := cache.NewCacheManager(&cache.CacheMgrParams{ Type: cache.CacheType(cfg.CacheType), - RedisAddrs: cfg.RedisAddrs, - MemCacheAddrs: cfg.MemcacheAddrs, - EtcdAddrs: cfg.EtcdAddrs, + CacheAddrs: cfg.CacheAddrs, + CacheUsername: cfg.CacheUsername, + CachePassword: cfg.CachePassword, KeyPrefix: cfg.CacheKeyPrefix, Ttl: ttl, CleanInt: cleanInt, @@ -345,9 +345,9 @@ func setupCacheManager(dcfg *config.DynamicConfig, logger *zap.Logger) (*cache.C newCfg := dnCfg.Get() err = cacheMgr.Setup(&cache.CacheMgrParams{ Type: cache.CacheType(newCfg.CacheType), - RedisAddrs: newCfg.RedisAddrs, - MemCacheAddrs: newCfg.MemcacheAddrs, - EtcdAddrs: newCfg.EtcdAddrs, + CacheAddrs: cfg.CacheAddrs, + CacheUsername: cfg.CacheUsername, + CachePassword: cfg.CachePassword, KeyPrefix: newCfg.CacheKeyPrefix, Ttl: ttl, CleanInt: cleanInt, diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 9b7d9b7..aab4d9b 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -52,9 +52,9 @@ type CacheManager struct { // CacheMgrParams .. type CacheMgrParams struct { Type CacheType - RedisAddrs string - EtcdAddrs string - MemCacheAddrs string + CacheAddrs string + CacheUsername string + CachePassword string KeyPrefix string Ttl time.Duration CleanInt time.Duration @@ -78,14 +78,13 @@ func (cm *CacheManager) GetCache() Cache { func (cm *CacheManager) Setup(arg *CacheMgrParams) error { var curCache Cache var err error - var curAddrs string + curAddrs := arg.CacheAddrs switch arg.Type { case CacheTypeRedis: - curAddrs = arg.RedisAddrs if cm.cAddress == curAddrs && cm.cKeyPrefix == arg.KeyPrefix && cm.cTtl == arg.Ttl { return nil } - curCache, err = NewRedisClient(arg.RedisAddrs, arg.KeyPrefix, arg.Ttl) + curCache, err = NewRedisClient(arg.CacheAddrs, arg.KeyPrefix, arg.Ttl, arg.CacheUsername, arg.CachePassword) if err != nil { return fmt.Errorf("failed to initialize Redis: %v", err) } @@ -95,11 +94,10 @@ func (cm *CacheManager) Setup(arg *CacheMgrParams) error { } curCache = NewMemoryCache(arg.KeyPrefix, arg.Ttl, arg.CleanInt) case CacheTypeEtcd: - curAddrs = arg.EtcdAddrs if cm.cAddress == curAddrs && cm.cKeyPrefix == arg.KeyPrefix && cm.cTtl == arg.Ttl { return nil } - curCache, err = NewEtcdClient(arg.EtcdAddrs, arg.KeyPrefix, arg.Ttl) + curCache, err = NewEtcdClient(arg.CacheAddrs, arg.KeyPrefix, arg.Ttl, arg.CacheUsername, arg.CachePassword) if err != nil { return fmt.Errorf("failed to initialize Etcd: %v", err) } @@ -107,8 +105,7 @@ func (cm *CacheManager) Setup(arg *CacheMgrParams) error { if cm.cAddress == curAddrs && cm.cKeyPrefix == arg.KeyPrefix && cm.cTtl == arg.Ttl { return nil } - curAddrs = arg.MemCacheAddrs - curCache, err = NewMemcacheClient(arg.MemCacheAddrs, arg.KeyPrefix, arg.Ttl) + curCache, err = NewMemcacheClient(arg.CacheAddrs, arg.KeyPrefix, arg.Ttl, arg.CacheUsername, arg.CachePassword) if err != nil { return fmt.Errorf("failed to initialize Memcached: %v", err) } diff --git a/internal/cache/etcd_client.go b/internal/cache/etcd_client.go index 95c819b..103867a 100644 --- a/internal/cache/etcd_client.go +++ b/internal/cache/etcd_client.go @@ -24,10 +24,12 @@ type EtcdClient struct { } // NewEtcdClient .. -func NewEtcdClient(addrs, prefix string, ttl time.Duration) (*EtcdClient, error) { +func NewEtcdClient(addrs, prefix string, ttl time.Duration, username, password string) (*EtcdClient, error) { client, err := clientv3.New(clientv3.Config{ Endpoints: []string{addrs}, DialTimeout: 5 * time.Second, + Username: username, + Password: password, }) if err != nil { return nil, err diff --git a/internal/cache/memcache_client.go b/internal/cache/memcache_client.go index cb55dab..a01a80a 100644 --- a/internal/cache/memcache_client.go +++ b/internal/cache/memcache_client.go @@ -11,51 +11,47 @@ import ( "fmt" "time" - "github.com/bradfitz/gomemcache/memcache" + "github.com/memcachier/mc/v3" ) // MemcacheClient implements the Cache interface for Memcached type MemcacheClient struct { - client *memcache.Client + client *mc.Client prefix string ttl time.Duration } // NewMemcacheClient .. -func NewMemcacheClient(addrs, prefix string, ttl time.Duration) (*MemcacheClient, error) { - client := memcache.New(addrs) +func NewMemcacheClient(addrs, prefix string, ttl time.Duration, username, password string) (*MemcacheClient, error) { + client := mc.NewMC(addrs, username, password) return &MemcacheClient{client: client, prefix: prefix, ttl: ttl}, nil } // GetCache retrieves a value from Memcached func (c *MemcacheClient) GetCache(ctx context.Context, key string) (string, error) { key = c.prefix + key - item, err := c.client.Get(key) - if err == memcache.ErrCacheMiss { + item, _, _, err := c.client.Get(key) + if err == mc.ErrNotFound { return "", nil } if err != nil { return "", err } - return string(item.Value), nil + return item, nil } // SetCache stores a value in Memcached func (c *MemcacheClient) SetCache(ctx context.Context, key, value string) error { key = c.prefix + key - item := &memcache.Item{ - Key: key, - Value: []byte(value), - Expiration: int32(c.ttl / time.Second), - } - return c.client.Set(item) + _, err := c.client.Set(key, value, uint32(0), uint32(c.ttl/time.Second), uint64(0)) + return err } // DeleteCache .. func (c *MemcacheClient) DeleteCache(ctx context.Context, key string) error { key = c.prefix + key - err := c.client.Delete(key) - if err != nil && err != memcache.ErrCacheMiss { + err := c.client.Del(key) + if err != nil && err != mc.ErrNotFound { return fmt.Errorf("memcache delete error: %v", err) } return nil diff --git a/internal/cache/redis_client.go b/internal/cache/redis_client.go index 7bd5260..e92a827 100644 --- a/internal/cache/redis_client.go +++ b/internal/cache/redis_client.go @@ -22,9 +22,11 @@ type RedisClient struct { } // NewRedisClient .. -func NewRedisClient(addrs, prefix string, ttl time.Duration) (*RedisClient, error) { +func NewRedisClient(addrs, prefix string, ttl time.Duration, username, password string) (*RedisClient, error) { client := redis.NewClient(&redis.Options{ - Addr: addrs, + Addr: addrs, + Username: username, + Password: password, }) _, err := client.Ping(context.Background()).Result() if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index 1343c58..2715683 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,9 +20,9 @@ type Config struct { ServiceName string `json:"service_name"` HTTPPort string `json:"http_port"` GRPCPort string `json:"grpc_port"` - RedisAddrs string `json:"redis_addrs"` - EtcdAddrs string `json:"etcd_addrs"` - MemcacheAddrs string `json:"memcache_addrs"` + CacheAddrs string `json:"cache_addrs"` + CacheUsername string `json:"cache_username"` + CachePassword string `json:"cache_password"` CacheType string `json:"cache_type"` // redis, memory, etcd, memcache CacheTTL int `json:"cache_ttl"` // seconds CacheKeyPrefix string `json:"cache_key_prefix"` @@ -32,9 +32,23 @@ type Config struct { APIKeys []string `json:"api_keys"` LogLevel string `json:"log_level"` // error, debug, info, none - EnableDynamicConfig bool `json:"enable_dynamic_config"` + EnableDynamicConfig bool `json:"enable_dynamic_config"` + DynamicConfigType string `json:"dynamic_config_type"` // etcd, zookeeper, consul, nacos + DynamicConfigAddrs string `json:"dynamic_config_addrs"` + DynamicConfigTTL int `json:"dynamic_config_ttl"` + DynamicConfigKeepAlive int `json:"dynamic_config_keep_alive"` + DynamicConfigMaxRetries int `json:"dynamic_config_max_retries"` + DynamicConfigBaseRetryDelay int `json:"dynamic_config_base_retry_delay"` + DynamicConfigUsername string `json:"dynamic_config_username"` + DynamicConfigPassword string `json:"dynamic_config_password"` + DynamicConfigTlsServerName string `json:"dynamic_config_tls_server_name"` + DynamicConfigTlsAddress string `json:"dynamic_config_tls_address"` + DynamicConfigTlsCertFile string `json:"dynamic_config_tls_cert_file"` + DynamicConfigTlsKeyFile string `json:"dynamic_config_tls_key_file"` + DynamicConfigTlsCaFile string `json:"dynamic_config_tls_ca_file"` + EnableServiceDiscovery bool `json:"enable_service_discovery"` - ServiceDiscovery string `json:"service_discovery"` // etcd, zookeeper, consul, nacos + ServiceDiscoveryType string `json:"service_discovery_type"` // etcd, zookeeper, consul, nacos ServiceDiscoveryAddrs string `json:"service_discovery_addrs"` ServiceDiscoveryTTL int `json:"service_discovery_ttl"` ServiceDiscoveryKeepAlive int `json:"service_discovery_keep_alive"` @@ -186,9 +200,9 @@ func (dc *DynamicConfig) HotUpdate(cfg Config) error { dc.Config.ConfigVersion = cfg.ConfigVersion dc.Config.APIKeys = cfg.APIKeys dc.Config.LogLevel = cfg.LogLevel - dc.Config.RedisAddrs = cfg.RedisAddrs - dc.Config.EtcdAddrs = cfg.EtcdAddrs - dc.Config.MemcacheAddrs = cfg.MemcacheAddrs + dc.Config.CacheAddrs = cfg.CacheAddrs + dc.Config.CacheUsername = cfg.CacheUsername + dc.Config.CachePassword = cfg.CachePassword dc.Config.CacheType = cfg.CacheType dc.Config.CacheTTL = cfg.CacheTTL dc.Config.CacheKeyPrefix = cfg.CacheKeyPrefix @@ -285,18 +299,9 @@ func Validate(config Config) error { return fmt.Errorf("invalid cache_type: %s, must be redis, memory, etcd, or memcache", config.CacheType) } - switch config.CacheType { - case "redis": - if !isValidAddrs(config.RedisAddrs) { - return fmt.Errorf("invalid redis_addrs: %s", config.RedisAddrs) - } - case "etcd": - if !isValidAddrs(config.EtcdAddrs) { - return fmt.Errorf("invalid etcd_addrs: %s", config.EtcdAddrs) - } - case "memcache": - if !isValidAddrs(config.MemcacheAddrs) { - return fmt.Errorf("invalid memcache_addrs: %s", config.MemcacheAddrs) + if config.CacheType != "" && config.CacheType != "memory" { + if !isValidAddrs(config.CacheAddrs) { + return fmt.Errorf("invalid cache_addrs: %s", config.CacheAddrs) } } @@ -310,13 +315,20 @@ func Validate(config Config) error { "consul": true, "nacos": true, } - if config.ServiceDiscovery != "" && !validDiscoveryTypes[config.ServiceDiscovery] { - return fmt.Errorf("invalid service_discovery: %s, must be etcd, zookeeper, consul, or nacos", config.ServiceDiscovery) + if config.ServiceDiscoveryType != "" && !validDiscoveryTypes[config.ServiceDiscoveryType] { + return fmt.Errorf("invalid service_discovery_type: %s, must be etcd, zookeeper, consul, or nacos", config.ServiceDiscoveryType) } - if config.ServiceDiscovery != "" && !isValidAddrs(config.ServiceDiscoveryAddrs) { + if config.ServiceDiscoveryType != "" && !isValidAddrs(config.ServiceDiscoveryAddrs) { return fmt.Errorf("invalid service_discovery_addrs: %s", config.ServiceDiscoveryAddrs) } + if config.DynamicConfigType != "" && !validDiscoveryTypes[config.DynamicConfigType] { + return fmt.Errorf("invalid dynamic_config_type: %s, must be etcd, zookeeper, consul, or nacos", config.DynamicConfigType) + } + if config.DynamicConfigType != "" && !isValidAddrs(config.DynamicConfigAddrs) { + return fmt.Errorf("invalid dynamic_config_addrs: %s", config.DynamicConfigAddrs) + } + if config.RateLimitQPS <= 0 { return fmt.Errorf("rate_limit_qps must be positive: %d", config.RateLimitQPS) } @@ -324,12 +336,11 @@ func Validate(config Config) error { return fmt.Errorf("rate_limit_burst must be positive: %d", config.RateLimitBurst) } - if len(config.APIKeys) == 0 { - return fmt.Errorf("api_keys must not be empty") - } - for _, key := range config.APIKeys { - if key == "" { - return fmt.Errorf("api_keys contain empty key") + if len(config.APIKeys) > 0 { + for _, key := range config.APIKeys { + if key == "" { + return fmt.Errorf("api_keys contain empty key") + } } } @@ -362,14 +373,14 @@ func MergeWithFlags(config Config, flags map[string]interface{}) Config { if v, ok := flags["grpc-port"].(string); ok && v != "" { config.GRPCPort = v } - if v, ok := flags["redis-addrs"].(string); ok && v != "" { - config.RedisAddrs = v + if v, ok := flags["cache-addrs"].(string); ok && v != "" { + config.CacheAddrs = v } - if v, ok := flags["etcd-addrs"].(string); ok && v != "" { - config.EtcdAddrs = v + if v, ok := flags["cache-username"].(string); ok && v != "" { + config.CacheUsername = v } - if v, ok := flags["memcache-addrs"].(string); ok && v != "" { - config.MemcacheAddrs = v + if v, ok := flags["cache-password"].(string); ok && v != "" { + config.CachePassword = v } if v, ok := flags["cache-type"].(string); ok && v != "" { config.CacheType = v @@ -380,8 +391,57 @@ func MergeWithFlags(config Config, flags map[string]interface{}) Config { if v, ok := flags["cache-key-prefix"].(string); ok && v != "" { config.CacheKeyPrefix = v } - if v, ok := flags["service-discovery"].(string); ok && v != "" { - config.ServiceDiscovery = v + + ///// + if v, ok := flags["enable-dynamic-config"].(bool); ok && !config.EnableDynamicConfig { + config.EnableDynamicConfig = v + } + if v, ok := flags["dynamic-config-type"].(string); ok && v != "" { + config.DynamicConfigType = v + } + if v, ok := flags["dynamic-config-addrs"].(string); ok && v != "" { + config.DynamicConfigAddrs = v + } + if v, ok := flags["dynamic-config-ttl"].(int); ok && v != 0 { + config.DynamicConfigTTL = v + } + if v, ok := flags["dynamic-config-keep-alive"].(int); ok && v != 0 { + config.DynamicConfigKeepAlive = v + } + if v, ok := flags["dynamic-config-max-retries"].(int); ok && v != 0 { + config.DynamicConfigMaxRetries = v + } + if v, ok := flags["dynamic-config-base-retry-delay"].(int); ok && v != 0 { + config.DynamicConfigBaseRetryDelay = v + } + if v, ok := flags["dynamic-config-username"].(string); ok && v != "" { + config.DynamicConfigUsername = v + } + if v, ok := flags["dynamic-config-password"].(string); ok && v != "" { + config.DynamicConfigPassword = v + } + if v, ok := flags["dynamic-config-tls-server-name"].(string); ok && v != "" { + config.DynamicConfigTlsServerName = v + } + if v, ok := flags["dynamic-config-tls-address"].(string); ok && v != "" { + config.DynamicConfigTlsAddress = v + } + if v, ok := flags["dynamic-config-tls-cert-file"].(string); ok && v != "" { + config.DynamicConfigTlsCertFile = v + } + if v, ok := flags["dynamic-config-tls-key-file"].(string); ok && v != "" { + config.DynamicConfigTlsKeyFile = v + } + if v, ok := flags["dynamic-config-tls-ca-file"].(string); ok && v != "" { + config.DynamicConfigTlsCaFile = v + } + + ///// + if v, ok := flags["enable-service-discovery"].(bool); ok && !config.EnableServiceDiscovery { + config.EnableServiceDiscovery = v + } + if v, ok := flags["service-discovery-type"].(string); ok && v != "" { + config.ServiceDiscoveryType = v } if v, ok := flags["service-discovery-addrs"].(string); ok && v != "" { config.ServiceDiscoveryAddrs = v @@ -419,6 +479,8 @@ func MergeWithFlags(config Config, flags map[string]interface{}) Config { if v, ok := flags["service-discovery-tls-ca-file"].(string); ok && v != "" { config.ServiceDiscoveryTlsCaFile = v } + + /////// if v, ok := flags["rate-limit-qps"].(int); ok && v != 0 { config.RateLimitQPS = v } @@ -431,12 +493,7 @@ func MergeWithFlags(config Config, flags map[string]interface{}) Config { if v, ok := flags["log-level"].(string); ok && v != "" { config.LogLevel = v } - if v, ok := flags["enable-dynamic-config"].(bool); ok && !config.EnableDynamicConfig { - config.EnableDynamicConfig = v - } - if v, ok := flags["enable-service-discovery"].(bool); ok && !config.EnableServiceDiscovery { - config.EnableServiceDiscovery = v - } + if v, ok := flags["enable-cors"].(bool); ok && !config.EnableCors { config.EnableCors = v } @@ -448,20 +505,16 @@ func DefaultConfig() Config { ServiceName: "go-captcha-service", HTTPPort: "8080", GRPCPort: "50051", - RedisAddrs: "localhost:6379", - EtcdAddrs: "localhost:2379", - MemcacheAddrs: "localhost:11211", CacheType: "memory", + CacheAddrs: "", CacheTTL: 60, CacheKeyPrefix: "GO_CAPTCHA_DATA:", EnableDynamicConfig: false, EnableServiceDiscovery: false, - ServiceDiscovery: "", - ServiceDiscoveryAddrs: "localhost:2379", RateLimitQPS: 1000, RateLimitBurst: 1000, - EnableCors: false, - APIKeys: []string{"my-secret-key-123"}, + EnableCors: true, + APIKeys: make([]string, 0), LogLevel: "info", } } diff --git a/internal/logic/click.go b/internal/logic/click.go index 6ea24fa..7295c4e 100644 --- a/internal/logic/click.go +++ b/internal/logic/click.go @@ -149,6 +149,10 @@ func (cl *ClickCaptLogic) CheckData(ctx context.Context, key string, dots string return false, fmt.Errorf("failed to json unmarshal: %v", err) } + if cacheCaptData.Status == 2 { + return false, nil + } + ret := false if (len(dct) * 2) == len(src) { for i := 0; i < len(dct); i++ { @@ -167,15 +171,18 @@ func (cl *ClickCaptLogic) CheckData(ctx context.Context, key string, dots string if ret { cacheCaptData.Status = 1 - cacheDataByte, err := json.Marshal(cacheCaptData) - if err != nil { - return ret, fmt.Errorf("failed to json marshal: %v", err) - } + } else { + cacheCaptData.Status = 2 + } - err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) - if err != nil { - return ret, fmt.Errorf("failed to update cache:: %v", err) - } + cacheDataByte, err := json.Marshal(cacheCaptData) + if err != nil { + return ret, fmt.Errorf("failed to json marshal: %v", err) + } + + err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) + if err != nil { + return ret, fmt.Errorf("failed to update cache:: %v", err) } return ret, nil diff --git a/internal/logic/rotate.go b/internal/logic/rotate.go index 0e0434a..3a04d0f 100644 --- a/internal/logic/rotate.go +++ b/internal/logic/rotate.go @@ -143,19 +143,26 @@ func (cl *RotateCaptLogic) CheckData(ctx context.Context, key string, angle int) return false, fmt.Errorf("failed to json unmarshal: %v", err) } + if cacheCaptData.Status == 2 { + return false, nil + } + ret := rotate.CheckAngle(int64(angle), int64(dct.Angle), 2) if ret { cacheCaptData.Status = 1 - cacheDataByte, err := json.Marshal(cacheCaptData) - if err != nil { - return ret, fmt.Errorf("failed to json marshal: %v", err) - } - - err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) - if err != nil { - return ret, fmt.Errorf("failed to update cache:: %v", err) - } + } else { + cacheCaptData.Status = 2 + } + + cacheDataByte, err := json.Marshal(cacheCaptData) + if err != nil { + return ret, fmt.Errorf("failed to json marshal: %v", err) + } + + err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) + if err != nil { + return ret, fmt.Errorf("failed to update cache:: %v", err) } return ret, nil diff --git a/internal/logic/slide.go b/internal/logic/slide.go index 63f5687..c36c5cf 100644 --- a/internal/logic/slide.go +++ b/internal/logic/slide.go @@ -151,6 +151,10 @@ func (cl *SlideCaptLogic) CheckData(ctx context.Context, key string, dots string return false, fmt.Errorf("failed to json unmarshal: %v", err) } + if cacheCaptData.Status == 2 { + return false, nil + } + ret := false if 2 == len(src) { sx, _ := strconv.ParseInt(src[0], 10, 64) @@ -160,15 +164,18 @@ func (cl *SlideCaptLogic) CheckData(ctx context.Context, key string, dots string if ret { cacheCaptData.Status = 1 - cacheDataByte, err := json.Marshal(cacheCaptData) - if err != nil { - return ret, fmt.Errorf("failed to json marshal: %v", err) - } - - err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) - if err != nil { - return ret, fmt.Errorf("failed to update cache:: %v", err) - } + } else { + cacheCaptData.Status = 2 + } + + cacheDataByte, err := json.Marshal(cacheCaptData) + if err != nil { + return ret, fmt.Errorf("failed to json marshal: %v", err) + } + + err = cl.cacheMgr.GetCache().SetCache(ctx, key, string(cacheDataByte)) + if err != nil { + return ret, fmt.Errorf("failed to update cache:: %v", err) } return ret, nil diff --git a/testing/config.json b/testing/config.json new file mode 100644 index 0000000..6724ef7 --- /dev/null +++ b/testing/config.json @@ -0,0 +1,49 @@ +{ + "config_version": 1, + "service_name": "go-captcha-service", + "http_port": "8080", + "grpc_port": "50051", + + "cache_type": "memory", + "cache_key_prefix": "GO_CAPTCHA_DATA:", + "cache_ttl": 1800, + "cache_addrs": "localhost:6379", + "cache_username": "", + "cache_password": "", + + "enable_dynamic_config": false, + "dynamic_config_type": "etcd", + "dynamic_config_addrs": "localhost:2379", + "dynamic_config_username": "", + "dynamic_config_password": "", + "dynamic_config_ttl": 10, + "dynamic_config_keep_alive": 3, + "dynamic_config_max_retries": 3, + "dynamic_config_base_retry_delay": 500, + "dynamic_config_tls_server_name": "", + "dynamic_config_tls_address": "", + "dynamic_config_tls_cert_file": "", + "dynamic_config_tls_key_file": "", + "dynamic_config_tls_ca_file": "", + + "enable_service_discovery": false, + "service_discovery_type": "etcd", + "service_discovery_addrs": "localhost:2379", + "service_discovery_username": "", + "service_discovery_password": "", + "service_discovery_ttl": 10, + "service_discovery_keep_alive": 3, + "service_discovery_max_retries": 3, + "service_discovery_base_retry_delay": 500, + "service_discovery_tls_server_name": "", + "service_discovery_tls_address": "", + "service_discovery_tls_cert_file": "", + "service_discovery_tls_key_file": "", + "service_discovery_tls_ca_file": "", + + "rate_limit_qps": 1000, + "rate_limit_burst": 1000, + "enable_cors": true, + "log_level": "info", + "api_keys": ["my-secret-key-123", "another-key-456", "another-key-789"] +} \ No newline at end of file diff --git a/testing/docker-compose.yml b/testing/docker-compose.yml new file mode 100644 index 0000000..b459040 --- /dev/null +++ b/testing/docker-compose.yml @@ -0,0 +1,68 @@ +version: '3' +services: + captcha-service-1: + image: wenlng/go-captcha-service:latest + ports: + - "8080:8080" + - "50051:50051" + volumes: + - ./config.json:/app/config.json + - ./gocaptcha.json:/app/gocaptcha.json + - ./resources/gocaptcha:/app/resources/gocaptcha + environment: + - CONFIG=config.json + - GO_CAPTCHA_CONFIG=gocaptcha.json + - SERVICE_NAME=go-captcha-service + - CACHE_TYPE=redis + - CACHE_ADDRS=localhost:6379 + - ENABLE_DYNAMIC_CONFIG=true + - DYNAMIC_CONFIG_TYPE=etcd + - DYNAMIC_CONFIG_ADDRS=localhost:2379 + - ENABLE_SERVICE_DISCOVERY=true + - SERVICE_DISCOVERY_TYPE=etcd + - SERVICE_DISCOVERY_ADDRS=localhost:2379 + depends_on: + - etcd + - redis + restart: unless-stopped + + captcha-service-2: + image: wenlng/go-captcha-service:latest + ports: + - "8081:8080" + - "50052:50051" + volumes: + - ./config.json:/app/config.json + - ./gocaptcha.json:/app/gocaptcha.json + - ./resources/gocaptcha:/app/resources/gocaptcha + environment: + - CONFIG=config.json + - GO_CAPTCHA_CONFIG=gocaptcha.json + - SERVICE_NAME=go-captcha-service + - CACHE_TYPE=redis + - CACHE_ADDRS=localhost:6379 + - ENABLE_DYNAMIC_CONFIG=true + - DYNAMIC_CONFIG_TYPE=etcd + - DYNAMIC_CONFIG_ADDRS=localhost:2379 + - ENABLE_SERVICE_DISCOVERY=true + - SERVICE_DISCOVERY_TYPE=etcd + - SERVICE_DISCOVERY_ADDRS=localhost:2379 + depends_on: + - etcd + - redis + restart: unless-stopped + + etcd: + image: bitnami/etcd:latest + ports: + - "2379:2379" + environment: + - ALLOW_NONE_AUTHENTICATION=yes + privileged: true + restart: unless-stopped + + redis: + image: redis:latest + ports: + - "6379:6379" + restart: unless-stopped \ No newline at end of file diff --git a/testing/gocaptcha.json b/testing/gocaptcha.json new file mode 100644 index 0000000..6f352dd --- /dev/null +++ b/testing/gocaptcha.json @@ -0,0 +1,527 @@ +{ + "config_version": 1, + "resources": { + "version": "0.0.1", + "char": { + "languages": { + "chinese": [], + "english": [] + } + }, + "font": { + "type": "load", + "file_dir": "./gocaptcha/fonts/", + "file_maps": { + "yrdzst_bold": "yrdzst-bold.ttf" + } + }, + "shape_image": { + "type": "load", + "file_dir": "./gocaptcha/shape_images/", + "file_maps": { + "shape_01": "shape_01.png", + "shape_01.png":"c.png" + } + }, + "master_image": { + "type": "load", + "file_dir": "./gocaptcha/master_images/", + "file_maps": { + "image_01": "image_01.jpg", + "image_02":"image_02.jpg" + } + }, + "thumb_image": { + "type": "load", + "file_dir": "./gocaptcha/thumb_images/", + "file_maps": { + + } + }, + "tile_image": { + "type": "load", + "file_dir": "./gocaptcha/tile_images/", + "file_maps": { + "tile_01": "tile_01.png", + "tile_02": "tile_02.png" + }, + "file_maps_02": { + "tile_mask_01": "tile_mask_01.png", + "tile_mask_02": "tile_mask_02.png" + }, + "file_maps_03": { + "tile_shadow_01": "tile_shadow_01.png", + "tile_shadow_02": "tile_shadow_02.png" + } + } + }, + "builder": { + "click_config_maps": { + "click-default-ch": { + "version": "0.0.1", + "language": "chinese", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click-dark-ch": { + "version": "0.0.1", + "language": "chinese", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#4a85fb", + "#d93ffb", + "#56be01", + "#ee2b2b", + "#cd6904", + "#b49b03", + "#01ad90" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click-default-en": { + "version": "0.0.1", + "language": "english", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 34, + "max": 48 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 34, + "max": 48 + }, + "range_text_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + }, + "click-dark-en": { + "version": "0.0.1", + "language": "english", + "master": { + "image_size": { + "width": 300, + "height": 200 + }, + "range_length": { + "min": 6, + "max": 7 + }, + "range_angles": [ + { + "min": 20, + "max": 35 + }, + { + "min": 35, + "max": 45 + }, + { + "min": 290, + "max": 305 + }, + { + "min": 305, + "max": 325 + }, + { + "min": 325, + "max": 330 + } + ], + "range_size": { + "min": 26, + "max": 32 + }, + "range_colors": [ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8" + ], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { + "x": -1, + "y": -1 + }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { + "width": 150, + "height": 40 + }, + "range_verify_length": { + "min": 2, + "max": 4 + }, + "disabled_range_verify_length": false, + "range_text_size": { + "min": 22, + "max": 28 + }, + "range_text_colors": [ + "#4a85fb", + "#d93ffb", + "#56be01", + "#ee2b2b", + "#cd6904", + "#b49b03", + "#01ad90" + ], + "range_background_colors": [ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c" + ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + } + }, + "click_shape_config_maps": { + "click-shape-default": { + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "range_length": { "min": 6, "max": 7 }, + "range_angles": [ + { "min": 20, "max": 35 }, + { "min": 35, "max": 45 }, + { "min": 290, "max": 305 }, + { "min": 305, "max": 325 }, + { "min": 325, "max": 330 } + ], + "range_size": { "min": 26, "max": 32 }, + "range_colors": [ "#fde98e", "#60c1ff", "#fcb08e", "#fb88ff", "#b4fed4", "#cbfaa9", "#78d6f8"], + "display_shadow": true, + "shadow_color": "#101010", + "shadow_point": { "x": -1, "y": -1 }, + "image_alpha": 1, + "use_shape_original_color": true + }, + "thumb": { + "image_size": { "width": 150, "height": 40}, + "range_verify_length": { "min": 2, "max": 4 }, + "disabled_range_verify_length": false, + "range_text_size": { "min": 22, "max": 28}, + "range_text_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c"], + "range_background_colors": [ "#1f55c4", "#780592", "#2f6b00", "#910000", "#864401", "#675901", "#016e5c" ], + "is_non_deform_ability": false, + "background_distort": 4, + "background_distort_alpha": 1, + "background_circles_num": 24, + "background_slim_line_num": 2 + } + } + }, + "slide_config_maps": { + "slide-default": { + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { "min": 60, "max": 70 }, + "range_graph_angles": [ + { "min": 20, "max": 35 } + ], + "generate_graph_number": 1, + "enable_graph_vertical_random": false, + "range_dead_zone_directions": ["left", "right"] + } + } + }, + "drag_config_maps": { + "drag-default": { + "version": "0.0.1", + "master": { + "image_size": { "width": 300, "height": 200 }, + "image_alpha": 1 + }, + "thumb": { + "range_graph_size": { "min": 60, "max": 70 }, + "range_graph_angles": [ + { "min": 0, "max": 0 } + ], + "generate_graph_number": 2, + "enable_graph_vertical_random": true, + "range_dead_zone_directions": ["left", "right", "top", "bottom"] + } + } + }, + "rotate_config_maps": { + "rotate-default": { + "version": "0.0.1", + "master": { + "image_square_size": 220 + }, + "thumb": { + "range_angles": [{ "min": 30, "max": 330 }], + "range_image_square_sizes": [140, 150, 160, 170], + "image_alpha": 1 + } + } + } + } +} \ No newline at end of file