├── .env.example ├── .gitignore ├── README.md ├── README.zh.md ├── api ├── api.go └── web.go ├── go.mod ├── go.sum ├── main.go ├── store ├── file.go ├── file_test.go ├── s3.go └── store.go ├── vercel-deploy.sh └── vercel.json /.env.example: -------------------------------------------------------------------------------- 1 | # 监听端口,默认值为 8090 2 | # Listen port, defaut to 8090 3 | # PORT=8090 4 | 5 | # 存储类型,可以是以下类型: 6 | # file: 使用本地文件存储 7 | # s3: 使用 AWS S3 兼容的 KV 存储 8 | # Storage type, available values are: 9 | # file: Store data in native filesystem 10 | # s3: Store data in AWS S3 compatiable KV storage 11 | STORAGE=file 12 | #STORAGE=s3 13 | 14 | # 如果使用 file 存储,在此指定存储目录 15 | # When using file storage, set dir path here 16 | STORAGE_PATH=/data 17 | 18 | # 如果使用 S3 兼容的 KV 存储,在此指定连接参数 19 | # When using s3 storage, set connection parameters here 20 | S3_ENDPOINT=https://xxx.r2.cloudflarestorage.com 21 | S3_BUCKET=wcaptcha 22 | S3_ACCESS_KEY= 23 | S3_SECRET_KEY= 24 | 25 | # 触发清理过期的 Nonce 操作的概率 26 | # Nonce cleanup probility 27 | NONCE_CLEANUP_PROB=0.01 28 | 29 | # 在调试时可将此项设为空 30 | # When debugging, leave this variable empty 31 | GIN_MODE=release 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.production 3 | gin-bin 4 | .vercel 5 | wcaptcha 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wcaptcha-api 2 | Backend API of wCaptcha 3 | 4 | [中文说明点此](https://github.com/wcaptcha/wcaptcha-api/blob/master/README.zh.md) 5 | 6 | ## Private Deployment 7 | 8 | wCaptcha provides official services, so if you don't want to deploy it yourself, you can go directly to using [official services](https://wcaptcha.pingflash.com/). 9 | 10 | If you want to deploy privately, follow the steps below. 11 | 12 | ### General Deployment 13 | 14 | Before installing, make sure you have go installed 15 | 16 | Build from source 17 | ```shell 18 | git clone https://github.com/wcaptcha/wcaptcha-api 19 | cd wcaptcha-api 20 | go build 21 | ``` 22 | 23 | You can also download the pre-compiled binaries at the [Release page](https://github.com/wcaptcha/wcaptcha-api/releases) 24 | 25 | Next, create a configuration file named `.env`, you can just `cp .env.example .env` then edit it. 26 | 27 | Once the configuration is set, you can start the service. 28 | 29 | ```shell 30 | ./wcaptcha 31 | ``` 32 | 33 | Now let's create a site. 34 | ```shell 35 | curl -X POST localhost:8090/site/create 36 | ``` 37 | The result is returned in JSON format, containing `api_key` and `api_secret`, which is the key of your site. 38 | 39 | To modify the difficulty (aka client proofing time), you can execute. 40 | ```shell 41 | curl -X POST localhost:8090/site/update --data "api_secret=YOUR_API_SECRET&hardness=HARDNESS 42 | ``` 43 | Where HARDNESS is a number, the default HARDNESS for a site is `4194303` (`2^22-1`), you can set any number. 44 | 45 | ### Docker Deployment 46 | 47 | ```shell 48 | docker pull wcaptcha/wcaptcha-api 49 | docker run -p 8090:8090 -v /data:/data wcaptcha/wcaptcha-api 50 | ``` 51 | 52 | When using docker deployment, the default STORAGE is `file`. This is configurable via environment variables. See `.env.example` for available environment variables. 53 | 54 | ### Deploy to Vercel 55 | 56 | You can also deploy the service to vercel. Please be noticed `file` storage is not available while deploying to vercel, because vercel is a serverless and the filesystem is not persist. 57 | 58 | First install the vercel cli tool, 59 | 60 | ```shell 61 | npm i -g vercel 62 | ``` 63 | 64 | Then run: 65 | 66 | ```shell 67 | cp .env .env.production 68 | ./vercel-deploy.sh 69 | ``` 70 | 71 | then follow the instructions to finish deployment. 72 | 73 | 74 | ## Set wcaptcha-js To Use Private Deployed Service 75 | 76 | ```javascript 77 | w = new wcaptcha(API_KEY) 78 | w.setEndpoint("https://your-deployed-service.com/") 79 | 80 | // Then use wcaptcha as usual 81 | // w.bind("any-selector") 82 | ``` 83 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # wcaptcha-api 2 | Backend API of wCaptcha 3 | 4 | ## 私有化部署 5 | 6 | wCaptcha 提供官方的服务,如果不想自己部署,可以直接到使用[官方的服务](https://wcaptcha.pingflash.com/)。 7 | 8 | 如果想要私有化部署,参考下面的操作步骤。目前提供三种部署方式: 9 | * 常规部署 10 | * Docker 部署 11 | * Vercel 部署 12 | 13 | ### 常规部署 14 | 15 | 安装前,请确保你已经安装了 go 16 | 17 | 直接从源代码构建 18 | ```shell 19 | git clone https://github.com/wcaptcha/wcaptcha-api 20 | cd wcaptcha-api 21 | go build 22 | ``` 23 | 24 | 也可在从 [Release 页面](https://github.com/wcaptcha/wcaptcha-api/releases)下载预先编译好的二进制文件 25 | 26 | 接下来,创建名为 `.env` 的配置文件,配置文件的内容可以参考 `.env.example` 27 | 28 | 写好配置后,就可以直接运行了 29 | 30 | ```shell 31 | ./wcaptcha 32 | ``` 33 | 34 | 接下来创建站点: 35 | ```shell 36 | curl -X POST localhost:8090/site/create 37 | ``` 38 | 返回结果为 JSON 格式的数据,其中包含 api_key 和 api_secret,这就是你的站点的密钥。 39 | 40 | 若要修改难度,可执行: 41 | ```shell 42 | curl -X POST localhost:8090/site/update --data "api_secret=YOUR_API_SECRET&hardness=HARDNESS 43 | ``` 44 | 其中的 HARDNESS 是一个数字,站点创建后默认的 HARDNESS 是 `4194303`,即`2^22-1`,你可以填写任意数字。 45 | 46 | ### 使用 Docker 部署 47 | 48 | ```shell 49 | docker pull wcaptcha/wcaptcha-api 50 | docker run -p 8090:8090 -v /data:/data wcaptcha/wcaptcha-api 51 | ``` 52 | 53 | Docker 部署默认使用 `file` 作为存储,可通过环境变量修改设置。具体的配置项请参考 .env.example 文件。 54 | 55 | 56 | ### 部署到 Vercel 57 | 58 | 请注意,如果要部署到 vercel,则配置文件中不能使用 `file` 类型的存储。因为 vercel 是无服务模式,它的文件系统上内容是不会被保存的。 59 | 60 | 首先安装 vercel 的命令行: 61 | ```shell 62 | npm i -g vercel 63 | ``` 64 | 65 | 然后直接在代码目录下运行: 66 | 67 | ```shell 68 | cp .env .env.production 69 | ./vercel-deploy.sh 70 | ``` 71 | 72 | 之后按照提示操作即可。 73 | 74 | 75 | ## 设置 wcaptcha-js 使用私有化部署的服务 76 | 77 | ```javascript 78 | w = new wcaptcha(API_KEY) 79 | w.setEndpoint("https://your-deployed-service.com/") 80 | 81 | // 接下来就可以正常使用 wcaptcha 了 82 | // w.bind("any-selector") 83 | ``` 84 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | crand "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "fmt" 9 | "log" 10 | "math/rand" 11 | "net/http" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "time" 16 | "wcaptcha/store" 17 | 18 | "github.com/gin-contrib/cors" 19 | "github.com/gin-gonic/gin" 20 | "github.com/joho/godotenv" 21 | ) 22 | 23 | const ( 24 | RSA_KEY_SIZE = 512 25 | RSA_KEY_TTL = 600 26 | ) 27 | 28 | // 执行 Nonce 清理程序的概率 29 | var nonceCleanupProb float64 = 0.01 30 | 31 | // var S3 *s3kv.Storage 32 | var Store store.Storer 33 | 34 | type Site struct { 35 | SecretKey string 36 | APIKey string 37 | 38 | RSAKey *rsa.PrivateKey 39 | OldRSAKey *rsa.PrivateKey 40 | RSAKeyCreateTime int64 41 | OldRSAKeyCreateTime int64 42 | 43 | // RSAKey 的总计轮换次数(总共重新生成了多少次 RSAKey) 44 | RSAKeyRegenerateCount int 45 | 46 | // 难度,客户端需要计算多少次平方取模,在 2020 年的消费级 CPU 上,Hardness = 2**20 时大约需要 100ms 的时间可计算出结果 47 | Hardness int 48 | 49 | CreateTime int64 50 | CreatorUserAgent string 51 | HMACKey []byte 52 | } 53 | 54 | func NewSite() *Site { 55 | // 1. 生成站点 KEY 和 SECRET 56 | rand.Seed(time.Now().Unix()) 57 | api_secret_buf := make([]byte, 32) 58 | 59 | _, err := rand.Read(api_secret_buf) 60 | if err != nil { 61 | log.Printf("无法创建随机数: %v", err) 62 | return nil 63 | } 64 | 65 | api_key_buf := sha256.Sum256(api_secret_buf) 66 | 67 | api_key_b64 := base64.RawURLEncoding.EncodeToString(api_key_buf[:]) 68 | api_secret_b64 := base64.RawURLEncoding.EncodeToString(api_secret_buf) 69 | 70 | rsa_key, err := rsa.GenerateKey(crand.Reader, RSA_KEY_SIZE) 71 | if err != nil { 72 | log.Printf("无法生成 RSA 密钥对: %v", err) 73 | return nil 74 | } 75 | 76 | s := Site{ 77 | APIKey: api_key_b64, 78 | SecretKey: api_secret_b64, 79 | RSAKey: rsa_key, 80 | CreateTime: time.Now().Unix(), 81 | Hardness: 1<<22 - 1, 82 | } 83 | s.HMACKey = make([]byte, 16) 84 | rand.Read(s.HMACKey) 85 | 86 | return &s 87 | } 88 | 89 | // 视情况更新一个站点的密钥 90 | func (s *Site) UpdateKeyIfNeeded() bool { 91 | isUpdated := false 92 | var err error 93 | 94 | ts := time.Now().Unix() 95 | 96 | if ts-s.RSAKeyCreateTime < RSA_KEY_TTL { 97 | return false 98 | } else { 99 | isUpdated = true 100 | 101 | s.OldRSAKey = s.RSAKey 102 | s.OldRSAKeyCreateTime = s.RSAKeyCreateTime 103 | 104 | s.RSAKey, err = rsa.GenerateKey(crand.Reader, RSA_KEY_SIZE) 105 | s.RSAKeyCreateTime = ts 106 | 107 | s.RSAKeyRegenerateCount++ 108 | 109 | if err != nil { 110 | log.Printf("严重错误:更新密钥失败,GenerateKey 返回错误: %v", err) 111 | } 112 | } 113 | 114 | return isUpdated 115 | } 116 | 117 | // 根据 APIKey 获取一个 site 的数据 118 | func siteGet(apiKey string) (*Site, error) { 119 | var site Site 120 | err := Store.Get(fmt.Sprintf("site/%s", apiKey), &site) 121 | return &site, err 122 | } 123 | 124 | func InitGin() *gin.Engine { 125 | var err error 126 | 127 | rand.Seed(time.Now().UnixNano()) 128 | 129 | switch os.Getenv("STORAGE") { 130 | case "s3": 131 | Store = new(store.S3) 132 | case "file": 133 | Store = new(store.File) 134 | default: 135 | fmt.Printf("环境变量 `STORAGE' 配置错误或不存在,请确认环境变量已正确配置") 136 | os.Exit(0) 137 | } 138 | 139 | err = Store.Init() 140 | if err != nil { 141 | log.Printf("无法创建存储连接: %v", err) 142 | os.Exit(0) 143 | } 144 | 145 | nonceCleanupProb, err = strconv.ParseFloat(os.Getenv("NONCE_CLEANUP_PROB"), 64) 146 | if err != nil { 147 | log.Printf("未设定 NONCE_CLEANUP_PROB 环境变量或设置不正确,将使用默认值 0.01") 148 | nonceCleanupProb = 0.01 149 | } 150 | log.Printf("Nonce 清理程序触发概率被设定为 %f%%", nonceCleanupProb*100) 151 | 152 | route := gin.Default() 153 | 154 | route.Use(cors.Default()) 155 | 156 | route.GET("/captcha/problem/get", webCaptchaProblem) 157 | route.POST("/captcha/verify", webCaptchaVerify) 158 | route.POST("/site/create", webSiteCreate) 159 | route.POST("/site/read", webSiteRead) 160 | route.POST("/site/update", webSiteUpdate) 161 | 162 | route.GET("/ping", func(c *gin.Context) { 163 | // c.String(200, fmt.Sprintf("pong. %v.\nS3_BUCKET=%s\nS3_ENDPOINT=%s\n", time.Now(), os.Getenv("S3_BUCKET"), os.Getenv("S3_ENDPOINT"))) 164 | c.String(200, fmt.Sprintf("pong. %v.\nSTORAGE=%s", time.Now(), os.Getenv("STORAGE"))) 165 | }) 166 | 167 | return route 168 | } 169 | 170 | func StartWeb() { 171 | portStr := os.Getenv("PORT") 172 | if portStr == "" { 173 | portStr = "8090" 174 | } 175 | port, err := strconv.Atoi(portStr) 176 | 177 | if err != nil { 178 | fmt.Fprintf(os.Stderr, "Invalid PORT `%s'", portStr) 179 | os.Exit(0) 180 | } 181 | 182 | route := InitGin() 183 | route.Run(fmt.Sprintf(":%d", port)) 184 | } 185 | 186 | func Handler(w http.ResponseWriter, r *http.Request) { 187 | InitGin().ServeHTTP(w, r) 188 | } 189 | 190 | func saveSite(s *Site) error { 191 | return Store.Put(fmt.Sprintf("site/%s", s.APIKey), s) 192 | } 193 | 194 | func nonceIsExists(nonce string) bool { 195 | t := time.Now() 196 | p := fmt.Sprintf("nonce/%s-%s", t.Format("2006010215"), nonce) 197 | p2 := fmt.Sprintf("nonce/%s-%s", t.Add(-1*86400*time.Second).Format("2006010215"), nonce) 198 | 199 | exists, err := Store.KeyExists(p) 200 | if err != nil { 201 | log.Printf("无法获知 nonce 是否已经存在,认为其不存在: %v", err) 202 | return false 203 | } 204 | 205 | exists2, err := Store.KeyExists(p2) 206 | if err != nil { 207 | log.Printf("无法获知 nonce 是否已经存在,认为其不存在: %v", err) 208 | return false 209 | } 210 | 211 | return exists || exists2 212 | } 213 | 214 | func nonceSet(nonce string) { 215 | p := fmt.Sprintf("nonce/%s-%s", time.Now().Format("2006010215"), nonce) 216 | 217 | err := Store.Put(p, []byte(fmt.Sprintf("%d", time.Now().Unix()))) 218 | if err != nil { 219 | log.Printf("Unable to set nonce `%v'", nonce) 220 | } else { 221 | log.Printf("设置了一个 nonce `%s'", p) 222 | } 223 | } 224 | 225 | // 是否正在执行 nonce 清理的操作。该变量用于避免多个 nonce 清理程序同时运行 226 | var isNonceCleaning bool = false 227 | 228 | // 以 prob 的概率,触发清理过期的 nonce 操作 229 | func nonceCleanup(prob float64) { 230 | if isNonceCleaning == true { 231 | log.Printf("当前有另一个 Nonce 清理程序正在进行中,不会重复运行 Nonce 清理程序") 232 | return 233 | } 234 | isNonceCleaning = true 235 | defer func() { 236 | isNonceCleaning = false 237 | }() 238 | 239 | r := rand.Float64() 240 | if r >= prob { 241 | return 242 | } 243 | 244 | log.Printf("执行一次清理 nonce 的操作") 245 | 246 | keys, err := Store.List("nonce/") 247 | if err != nil { 248 | log.Printf("清理 nonce 操作失败,无法获取 nonce 列表: %v", err) 249 | return 250 | } 251 | 252 | t := time.Now() 253 | nowPrefix := fmt.Sprintf("nonce/%s", t.Format("2006010215")) 254 | prevPrefix := fmt.Sprintf("nonce/%s", t.Add(86400*time.Second).Format("2006010215")) 255 | for _, v := range keys { 256 | if strings.HasPrefix(v, nowPrefix) || strings.HasPrefix(v, prevPrefix) { 257 | continue 258 | } 259 | log.Printf("删除 nonce `%s'", v) 260 | Store.Delete(v) 261 | } 262 | 263 | log.Printf("nonce 清理操作完成") 264 | } 265 | 266 | func init() { 267 | godotenv.Load() 268 | } 269 | -------------------------------------------------------------------------------- /api/web.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "fmt" 8 | "log" 9 | "math/big" 10 | "math/rand" 11 | "strings" 12 | "time" 13 | "net/http" 14 | "github.com/gin-gonic/gin" 15 | "github.com/greensea/vdf" 16 | ) 17 | 18 | // 创建一个网站,为其生成 API_KEY 和 SECRET_KEY 19 | func webSiteCreate(c *gin.Context) { 20 | // 创建网站数据 21 | s := NewSite() 22 | if s == nil { 23 | webError(c, -1, "Unable to create site") 24 | return 25 | } 26 | 27 | // 保存到数据库 28 | s.CreatorUserAgent = c.GetHeader("User-Agent") 29 | err := saveSite(s) 30 | if err != nil { 31 | log.Printf("无法将 Site 保存到数据库: %v", err) 32 | webError(c, -2, "Unable to save site data") 33 | return 34 | } 35 | 36 | // 返回结果 37 | c.JSON(200, gin.H{ 38 | "code": 0, 39 | "site": gin.H{ 40 | "api_key": s.APIKey, 41 | "api_secret": s.SecretKey, 42 | }, 43 | }) 44 | } 45 | 46 | // 读取一个网站的数据,需要提供 API Secret 47 | // 注意!该接口会返回 APISecret,如果不再使用 APISecret 作为查询参数,则接口应该做相应的修改 48 | func webSiteRead(c *gin.Context) { 49 | var req struct { 50 | APISecret string `form:"api_secret" binding:"required"` 51 | } 52 | err := c.ShouldBind(&req) 53 | if err != nil { 54 | webError(c, -1, err.Error()) 55 | return 56 | } 57 | 58 | // 1. 根据 API Secret 计算 API Key 59 | apiSecretBuf, err := base64.RawURLEncoding.DecodeString(req.APISecret) 60 | if err != nil { 61 | webError(c, -2, "Invalid API Secret: "+err.Error()) 62 | return 63 | } 64 | apiKeyBuf := sha256.Sum256(apiSecretBuf) 65 | apiKey := base64.RawURLEncoding.EncodeToString(apiKeyBuf[:]) 66 | 67 | site, err := siteGet(apiKey) 68 | if err != nil { 69 | webError(c, -10, "Can't find site: "+err.Error()) 70 | return 71 | } 72 | 73 | c.JSON(0, gin.H{ 74 | "code": 0, 75 | "data": gin.H{ 76 | "site": gin.H{ 77 | "api_key": site.APIKey, 78 | "api_secret": site.SecretKey, 79 | "hardness": site.Hardness, 80 | "create_time": site.CreateTime, 81 | "rsa_key_regenerate_count": site.RSAKeyRegenerateCount, 82 | "rsa_key_create_time": site.RSAKeyCreateTime, 83 | }, 84 | }, 85 | }) 86 | } 87 | 88 | // 修改一个网站的数据,需要提供 API Secret 89 | func webSiteUpdate(c *gin.Context) { 90 | var req struct { 91 | APISecret string `form:"api_secret" binding:"required"` 92 | Hardness int `form:"hardness" binding:"required"` 93 | } 94 | err := c.ShouldBind(&req) 95 | if err != nil { 96 | webError(c, -1, err.Error()) 97 | return 98 | } 99 | 100 | // 1. 根据 API Secret 计算 API Key 101 | apiSecretBuf, err := base64.RawURLEncoding.DecodeString(req.APISecret) 102 | if err != nil { 103 | webError(c, -2, "Invalid API Secret: "+err.Error()) 104 | return 105 | } 106 | apiKeyBuf := sha256.Sum256(apiSecretBuf) 107 | apiKey := base64.RawURLEncoding.EncodeToString(apiKeyBuf[:]) 108 | 109 | site, err := siteGet(apiKey) 110 | if err != nil { 111 | webError(c, -10, "Can't find site: "+err.Error()) 112 | return 113 | } 114 | 115 | site.Hardness = req.Hardness 116 | err = saveSite(site) 117 | if err != nil { 118 | webError(c, -20, "Can't save site info"+err.Error()) 119 | return 120 | } 121 | 122 | c.JSON(0, gin.H{ 123 | "code": 0, 124 | "data": gin.H{ 125 | "site": gin.H{ 126 | "api_key": site.APIKey, 127 | "hardness": site.Hardness, 128 | "create_time": site.CreateTime, 129 | "rsa_key_regenerate_count": site.RSAKeyRegenerateCount, 130 | "rsa_key_create_time": site.RSAKeyCreateTime, 131 | }, 132 | }, 133 | }) 134 | } 135 | 136 | // 生成一个问题 137 | // 我们在服务端生成以下数据: 138 | // x: 随机数 139 | // h: 对 x 的签名,使用 HMAC_SHA256 以及服务器自定义的密钥进行签名 140 | // n: 模数,这个数据不需要生成,直接使用 site 的 RSA 素数生成 141 | // 给客户端返回: 142 | // question = {X: X, H: H, N: N} 143 | func webCaptchaProblem(c *gin.Context) { 144 | var req struct { 145 | APIKey string `form:"api_key" binding:"required"` 146 | } 147 | err := c.ShouldBind(&req) 148 | if err != nil { 149 | webError(c, -1, err.Error()) 150 | return 151 | } 152 | 153 | // 1. 查询网站是否存在 154 | var site Site 155 | err = Store.Get(fmt.Sprintf("site/%s", req.APIKey), &site) 156 | if err != nil { 157 | webError(c, -2, fmt.Sprintf("Site not exists. (Internal error: %s)", err.Error())) 158 | return 159 | } 160 | 161 | // 2. 检查 RSA 钥匙对是否已经过期,若已经过期,则更新之 162 | is_updated := site.UpdateKeyIfNeeded() 163 | if is_updated { 164 | log.Printf("站点 %s 的 RSA 密钥对已更新,更新数据库数据", site.APIKey) 165 | err := saveSite(&site) 166 | if err != nil { 167 | log.Printf("保存站点数据失败: %v", err) 168 | webError(c, -10, "Unable to update key") 169 | return 170 | } 171 | } 172 | 173 | // 3. 生成数据并返回 174 | n := new(big.Int).Set(site.RSAKey.Primes[0]) 175 | n = n.Mul(n, site.RSAKey.Primes[1]) 176 | x := big.NewInt(rand.Int63()) 177 | 178 | h := hmac.New(sha256.New, site.HMACKey).Sum([]byte(x.Text(16))) 179 | 180 | // nB64 := base64.RawStdEncoding.EncodeToString(n.Bytes()) 181 | // xB64 := base64.RawStdEncoding.EncodeToString([]byte(x)) 182 | hB64 := base64.RawURLEncoding.EncodeToString(h) 183 | 184 | //question := fmt.Sprintf("%s.%s.%s", nB64, xB64, hB64) 185 | 186 | // 以 1% 的概率去触发 Nonce 清除操作 187 | go nonceCleanup(nonceCleanupProb) 188 | 189 | c.JSON(200, gin.H{ 190 | "code": 0, 191 | "data": gin.H{ 192 | "question": gin.H{ 193 | "x": x.Text(16), 194 | "h": hB64, 195 | "n": n.Text(16), 196 | "t": site.Hardness, 197 | }, 198 | }, 199 | }) 200 | } 201 | 202 | // 检查客户端计算出的结果是否正确 203 | // 客户端应提交 prove 和 api_key 参数。其中 prove 数据格式如下: 204 | // X.Y.H 205 | // 其中 X 是此前服务器返回的 x 的原始内容;Y 是计算结果,为小写的十六进制表达式;H 为此前服务器返回的签名原始内容; 206 | // N 为模数,为小写的十六进制格式 207 | func webCaptchaVerify(c *gin.Context) { 208 | var verifyElapsed time.Duration 209 | 210 | // 1. 解析客户端数据 211 | var req struct { 212 | Prove string `form:"prove" binding:"required"` 213 | APIKey string `form:"api_key" binding:"required"` 214 | } 215 | err := c.Bind(&req) 216 | if err != nil { 217 | webError(c, -10, err.Error()) 218 | return 219 | } 220 | 221 | tokens := strings.Split(req.Prove, ".") 222 | if len(tokens) != 3 { 223 | webError(c, -20, "Invalid parameter prove") 224 | return 225 | } 226 | 227 | xRaw := tokens[0] 228 | yRaw := tokens[1] 229 | hRaw := tokens[2] 230 | //nRaw := tokens[3] 231 | 232 | x, xSuccess := new(big.Int).SetString(xRaw, 16) 233 | if xSuccess != true { 234 | webError(c, -24, "Invalid parameter x") 235 | return 236 | } 237 | 238 | site, err := siteGet(req.APIKey) 239 | if err != nil { 240 | webError(c, -25, "No such site. Invalid api_key? "+err.Error()) 241 | return 242 | } 243 | 244 | // 2. 验证签名是否正确,同时检查 nonce 是否已使用 245 | ourH := hmac.New(sha256.New, site.HMACKey).Sum([]byte(x.Text(16))) 246 | ourHB64 := base64.RawURLEncoding.EncodeToString(ourH) 247 | if ourHB64 != hRaw { 248 | webError(c, -30, "Invalid signature for x") 249 | return 250 | } 251 | 252 | // 2.2 检查 nonce 是否已经使用 253 | if nonceIsExists(hRaw) { 254 | webError(c, -40, "This proof is already used") 255 | return 256 | } 257 | 258 | // 3. 检查计算结果是否正确 259 | isCorrect := false 260 | var v *vdf.VDF 261 | ts := time.Now().Unix() 262 | x, successX := new(big.Int).SetString(xRaw, 16) 263 | y, successY := new(big.Int).SetString(yRaw, 16) 264 | if successX != true || successY != true { 265 | webError(c, -30, "Invalid x or y") 266 | return 267 | } 268 | 269 | /// 3.1 使用最新的 RSAKey 检查结果是否正确 270 | stime := time.Now() 271 | 272 | if ts-site.RSAKeyCreateTime < RSA_KEY_TTL { 273 | v = vdf.New(site.RSAKey.Primes[0], site.RSAKey.Primes[1]) 274 | 275 | if v.Verify(x, site.Hardness, y) == true { 276 | // 验证成功,什么都不用做 277 | isCorrect = true 278 | } else { 279 | isCorrect = false 280 | } 281 | 282 | log.Printf("校验证明耗时 %v,模数长度为 %d", verifyElapsed, site.RSAKey.Primes[0].BitLen()*2) 283 | } else { 284 | log.Printf("网站 %s 的 RSAKey 已经超时了,不会使用 RSAKey 进行检查", site.APIKey) 285 | } 286 | 287 | /// 3.2 使用次新的 RSAKey 检查结果是否正确 288 | if isCorrect == false { 289 | if ts-site.OldRSAKeyCreateTime > RSA_KEY_TTL*2 { 290 | v = vdf.New(site.OldRSAKey.Primes[0], site.OldRSAKey.Primes[1]) 291 | if v.Verify(x, site.Hardness, y) != true { 292 | isCorrect = false 293 | } else { 294 | isCorrect = true 295 | } 296 | } else { 297 | log.Printf("网站 %s 的 OldRSAKey 已经超时了,不会使用 OldRSAKey 进行检查", site.APIKey) 298 | } 299 | } 300 | 301 | verifyElapsed = time.Now().Sub(stime) 302 | 303 | // 5. 若验证成功则记录一次 nonce 304 | var msg string 305 | if isCorrect { 306 | nonceSet(hRaw) 307 | msg = fmt.Sprintf("Prove is correct. Verification takes %v", verifyElapsed) 308 | } else { 309 | msg = "Prove is INVALID" 310 | } 311 | 312 | c.JSON(200, gin.H{ 313 | "code": 0, 314 | "message": msg, 315 | "data": gin.H{ 316 | "prove": req.Prove, 317 | "is_correct": isCorrect, 318 | "verify_time_ms": float64(verifyElapsed.Microseconds()) / 1000, 319 | }, 320 | }) 321 | } 322 | 323 | // 返回一个错误 JSON 324 | func webError(c *gin.Context, code int, msg string) { 325 | c.JSON(200, gin.H{ 326 | "code": code, 327 | "message": msg, 328 | }) 329 | } 330 | 331 | func Hello(w http.ResponseWriter, r *http.Request) { 332 | fmt.Fprintf(w, "Hello World!") 333 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module wcaptcha 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gin-contrib/cors v1.4.0 7 | github.com/gin-gonic/gin v1.8.1 8 | github.com/greensea/s3kv v0.0.1 9 | github.com/greensea/vdf v0.0.0-20221208093952-88e134082256 10 | github.com/joho/godotenv v1.4.0 11 | ) 12 | 13 | require ( 14 | github.com/aws/aws-sdk-go v1.44.159 // indirect 15 | github.com/gin-contrib/sse v0.1.0 // indirect 16 | github.com/go-playground/locales v0.14.0 // indirect 17 | github.com/go-playground/universal-translator v0.18.0 // indirect 18 | github.com/go-playground/validator/v10 v10.11.1 // indirect 19 | github.com/goccy/go-json v0.10.0 // indirect 20 | github.com/jmespath/go-jmespath v0.4.0 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/leodido/go-urn v1.2.1 // indirect 23 | github.com/mattn/go-isatty v0.0.16 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 27 | github.com/ugorji/go/codec v1.2.7 // indirect 28 | golang.org/x/crypto v0.4.0 // indirect 29 | golang.org/x/net v0.4.0 // indirect 30 | golang.org/x/sys v0.3.0 // indirect 31 | golang.org/x/text v0.5.0 // indirect 32 | google.golang.org/protobuf v1.28.1 // indirect 33 | gopkg.in/yaml.v2 v2.4.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.44.155 h1:PMHMuUS0atPD4LhiXuYrLasrlIm4u3lpNQBl9h+Lr2s= 2 | github.com/aws/aws-sdk-go v1.44.155/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= 3 | github.com/aws/aws-sdk-go v1.44.159 h1:9odtuHAYQE9tQKyuX6ny1U1MHeH5/yzeCJi96g9H4DU= 4 | github.com/aws/aws-sdk-go v1.44.159/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= 10 | github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= 11 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 12 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 13 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 14 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 15 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 16 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 17 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 18 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 19 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 20 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 21 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 22 | github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= 23 | github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= 24 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 25 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= 26 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 27 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 28 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 29 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/greensea/s3kv v0.0.0-20221208082517-3e8afac9daa7 h1:HP6LCGNQVZfwNSOnpjiucm6zETI7gfAELpCEPP8guNQ= 32 | github.com/greensea/s3kv v0.0.0-20221208082517-3e8afac9daa7/go.mod h1:TKaFnpedkg09KXidBwNrAAjJfHKkM+v3iEE//dM6iTI= 33 | github.com/greensea/s3kv v0.0.0-20221208085308-9d8372a60c13 h1:Di1wEMpqCsK/HvugRRgEuDSpNdXZNA1i7pHbzGpeT48= 34 | github.com/greensea/s3kv v0.0.0-20221208085308-9d8372a60c13/go.mod h1:TKaFnpedkg09KXidBwNrAAjJfHKkM+v3iEE//dM6iTI= 35 | github.com/greensea/s3kv v0.0.1 h1:ECSxRb86l6cVx2XroHwegcCnt0D6NePAJgI7DdF1E8k= 36 | github.com/greensea/s3kv v0.0.1/go.mod h1:TKaFnpedkg09KXidBwNrAAjJfHKkM+v3iEE//dM6iTI= 37 | github.com/greensea/vdf v0.0.0-20221208075714-d95aa40a815f h1:skFfz53zhgwukKRecMfSrVox3CkFJIe/Lu+EHV9Mfbs= 38 | github.com/greensea/vdf v0.0.0-20221208075714-d95aa40a815f/go.mod h1:pPCE0Sa7iOw28y3FJYGW8Y0psyrRLxxUvJc7JB0aYhg= 39 | github.com/greensea/vdf v0.0.0-20221208093952-88e134082256 h1:2nsgrRVN1rJCdhc6o4ItaYaZqYPuH5WbPbWRtEBy1Ng= 40 | github.com/greensea/vdf v0.0.0-20221208093952-88e134082256/go.mod h1:pPCE0Sa7iOw28y3FJYGW8Y0psyrRLxxUvJc7JB0aYhg= 41 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 42 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 43 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 44 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 45 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 46 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 47 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 48 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 49 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 50 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 51 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 52 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 53 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 54 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 55 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 56 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 57 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 58 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 59 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 60 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 61 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 62 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 64 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 65 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 66 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 67 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 68 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= 69 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 70 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 71 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 72 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 73 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 74 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 75 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 76 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 77 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 79 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 80 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 81 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 84 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 85 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 86 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 87 | github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= 88 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 89 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 90 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 91 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 92 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 93 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 94 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 95 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 96 | golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= 97 | golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= 98 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 99 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 100 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 101 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 102 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 103 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 104 | golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= 105 | golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 106 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 109 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 113 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 116 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 119 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 121 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 122 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 123 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 124 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 125 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 126 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 127 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 128 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 129 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 130 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 131 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 132 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 133 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 135 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 136 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 137 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 138 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 139 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 140 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 141 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 142 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 143 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 144 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 145 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 146 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 147 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 148 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 149 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 150 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 151 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 152 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "wcaptcha/api" 4 | 5 | func main() { 6 | api.StartWeb() 7 | } 8 | -------------------------------------------------------------------------------- /store/file.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | type File struct { 12 | dir string 13 | } 14 | 15 | // Init Storage 16 | func (f *File) Init() error { 17 | var err error 18 | 19 | f.dir = os.Getenv("STORAGE_PATH") 20 | if len(f.dir) == 0 { 21 | return errors.New("必须指定 STORAGE_PATH 环境变量") 22 | } 23 | 24 | if f.dir[len(f.dir)-1:] != "/" { 25 | f.dir += "/" 26 | } 27 | 28 | err = os.MkdirAll(f.dir, os.ModePerm) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // List objects by prefix 37 | func (f *File) List(prefix string) ([]string, error) { 38 | p := prefix 39 | if len(p) > 0 && p[0:1] == "/" { 40 | p = p[1:] 41 | } 42 | if len(p) > 0 && p[len(p)-1:] == "/" { 43 | p = p[0 : len(p)-1] 44 | } 45 | 46 | entries, err := os.ReadDir(f.dir + p) 47 | if err != nil { 48 | if os.IsNotExist(err) { 49 | return []string{}, nil 50 | } else { 51 | return nil, err 52 | } 53 | } 54 | 55 | var ret []string 56 | for _, v := range entries { 57 | if v.IsDir() { 58 | continue 59 | } 60 | 61 | ret = append(ret, p+"/"+v.Name()) 62 | } 63 | 64 | return ret, nil 65 | } 66 | 67 | // Put an object into storage 68 | func (f *File) Put(key string, obj any) error { 69 | err := f.ensureFileDir(key) 70 | if err != nil { 71 | return fmt.Errorf("Put() failed: %v", err) 72 | } 73 | 74 | fi, err := os.OpenFile(f.dir+key, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm) 75 | if err != nil { 76 | return err 77 | } 78 | defer fi.Close() 79 | 80 | e := json.NewEncoder(fi) 81 | e.SetIndent("", " ") 82 | err = e.Encode(obj) 83 | 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return err 89 | } 90 | 91 | // Get an object from storage 92 | func (f *File) Get(key string, obj any) error { 93 | fi, err := os.OpenFile(f.dir+key, os.O_RDONLY, os.ModePerm) 94 | if err != nil { 95 | return err 96 | } 97 | defer fi.Close() 98 | 99 | d := json.NewDecoder(fi) 100 | 101 | err = d.Decode(&obj) 102 | 103 | return err 104 | } 105 | 106 | // Check if an object is in storage 107 | func (f *File) KeyExists(key string) (bool, error) { 108 | _, err := os.Stat(f.dir + key) 109 | if err == nil { 110 | return true, nil 111 | } else { 112 | if os.IsNotExist(err) { 113 | return false, nil 114 | } else { 115 | return false, err 116 | } 117 | } 118 | } 119 | 120 | func (f *File) Delete(key string) error { 121 | return os.Remove(f.dir + key) 122 | } 123 | 124 | func (f *File) ensureFileDir(key string) error { 125 | p := f.dir + key 126 | tokens := strings.Split(p, "/") 127 | if len(tokens) > 0 { 128 | tokens = tokens[0 : len(tokens)-1] 129 | } 130 | p = strings.Join(tokens, "/") 131 | 132 | err := os.MkdirAll(p, os.ModePerm) 133 | if err != nil { 134 | return fmt.Errorf("Unable to ensureFileDir `%s': %v", p, err) 135 | } else { 136 | return nil 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /store/file_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestFile(t *testing.T) { 9 | testItem(t, "abc") 10 | testItem(t, "a/b/c") 11 | testItem(t, "/a/b/c") 12 | } 13 | 14 | func TestList(t *testing.T) { 15 | var err error 16 | Store := new(File) 17 | if err != nil { 18 | t.FailNow() 19 | } 20 | 21 | Store.Put("d1/foo", "bar") 22 | 23 | l, err := Store.List("d1") 24 | if err != nil { 25 | fmt.Println(err) 26 | t.FailNow() 27 | } 28 | if len(l) != 1 || l[0] != "d1/foo" { 29 | fmt.Println(l) 30 | t.FailNow() 31 | } 32 | 33 | l, err = Store.List("d1/") 34 | if err != nil { 35 | fmt.Println(err) 36 | t.FailNow() 37 | } 38 | if len(l) != 1 || l[0] != "d1/foo" { 39 | t.FailNow() 40 | } 41 | 42 | l, err = Store.List("d1_not_exists/") 43 | if err != nil { 44 | fmt.Println(err) 45 | t.FailNow() 46 | } 47 | if len(l) != 0 { 48 | t.FailNow() 49 | } 50 | 51 | } 52 | 53 | func testItem(t *testing.T, key string) { 54 | var err error 55 | 56 | Store := new(File) 57 | err = Store.Init() 58 | if err != nil { 59 | fmt.Println(err) 60 | t.FailNow() 61 | } 62 | 63 | err = Store.Put(key, []string{"foo", "bar"}) 64 | if err != nil { 65 | fmt.Println(err) 66 | t.FailNow() 67 | } 68 | 69 | var obj []string 70 | err = Store.Get(key, &obj) 71 | if err != nil { 72 | fmt.Println(err) 73 | t.FailNow() 74 | } 75 | if len(obj) != 2 { 76 | t.FailNow() 77 | } 78 | if obj[0] != "foo" || obj[1] != "bar" { 79 | t.FailNow() 80 | } 81 | 82 | b, err := Store.KeyExists(key) 83 | if err != nil { 84 | fmt.Println(err) 85 | t.FailNow() 86 | } 87 | if b != true { 88 | t.FailNow() 89 | } 90 | 91 | b, err = Store.KeyExists("not_exists") 92 | if err != nil { 93 | fmt.Println(err) 94 | t.FailNow() 95 | } 96 | if b != false { 97 | t.FailNow() 98 | } 99 | 100 | err = Store.Delete(key) 101 | if err != nil { 102 | fmt.Println(err) 103 | t.FailNow() 104 | } 105 | 106 | b, err = Store.KeyExists(key) 107 | if b == true && err != nil { 108 | fmt.Println(err) 109 | t.FailNow() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /store/s3.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/greensea/s3kv" 7 | ) 8 | 9 | type S3 struct { 10 | s3 *s3kv.Storage 11 | } 12 | 13 | // Init Storage 14 | func (s *S3) Init() error { 15 | var err error 16 | s.s3, err = s3kv.New(&s3kv.Config{ 17 | Endpoint: os.Getenv("S3_ENDPOINT"), 18 | Bucket: os.Getenv("S3_BUCKET"), 19 | AccessKey: os.Getenv("S3_ACCESS_KEY"), 20 | SecretKey: os.Getenv("S3_SECRET_KEY"), 21 | }) 22 | return err 23 | } 24 | 25 | // List objects by prefix 26 | func (s *S3) List(prefix string) ([]string, error) { 27 | ret, err := s.s3.List(prefix) 28 | return ret, err 29 | } 30 | 31 | // Put an object into storage 32 | func (s *S3) Put(key string, obj any) error { 33 | return s.s3.PutObject(key, obj) 34 | } 35 | 36 | // Get an object from storage 37 | func (s *S3) Get(key string, obj any) error { 38 | return s.s3.GetJSON(key, obj) 39 | } 40 | 41 | // Check if an object is in storage 42 | func (s *S3) KeyExists(key string) (bool, error) { 43 | return s.s3.KeyExists(key) 44 | } 45 | 46 | func (s *S3) Delete(key string) error { 47 | return s.s3.Delete(key) 48 | } 49 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | type Storer interface { 4 | // Init storage 5 | Init() error 6 | 7 | // Put an object into storage 8 | Put(key string, obj any) error 9 | 10 | // Get an object from storage 11 | Get(key string, obj any) error 12 | 13 | // Check if an object is in storage 14 | KeyExists(key string) (bool, error) 15 | 16 | // List objects by prefix 17 | List(prefix string) ([]string, error) 18 | 19 | // Delete an object 20 | Delete(key string) error 21 | } 22 | -------------------------------------------------------------------------------- /vercel-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export $(xargs <.env.production) 3 | vercel -e STORAGE=$STORAGE -e STORAGE_PATH=$STORAGE_PATH -e GIN_MODE=$GIN_MODE -e S3_ENDPOINT=$S3_ENDPOINT -e S3_BUCKET=$S3_BUCKET -e S3_ACCESS_KEY=$S3_ACCESS_KEY -e S3_SECRET_KEY=$S3_SECRET_KEY --prod 4 | 5 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingSlash": false, 3 | "rewrites": 4 | [ 5 | { 6 | "source": "/(.*)", 7 | "destination": "/api/api.go" 8 | } 9 | ] 10 | } 11 | --------------------------------------------------------------------------------