Skip to content

Commit c628d06

Browse files
committed
Initial commit
0 parents  commit c628d06

File tree

13 files changed

+1131
-0
lines changed

13 files changed

+1131
-0
lines changed

.env.example

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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=

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.env
2+
.env.production
3+
gin-bin
4+
.vercel
5+
wcaptcha

api/api.go

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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+
// var S3 *s3kv.Storage
29+
var Store store.Storer
30+
31+
type Site struct {
32+
SecretKey string
33+
APIKey string
34+
35+
RSAKey *rsa.PrivateKey
36+
OldRSAKey *rsa.PrivateKey
37+
RSAKeyCreateTime int64
38+
OldRSAKeyCreateTime int64
39+
40+
// RSAKey 的总计轮换次数(总共重新生成了多少次 RSAKey)
41+
RSAKeyRegenerateCount int
42+
43+
// 难度,客户端需要计算多少次平方取模,在 2020 年的消费级 CPU 上,Hardness = 2**20 时大约需要 100ms 的时间可计算出结果
44+
Hardness int
45+
46+
CreateTime int64
47+
CreatorUserAgent string
48+
HMACKey []byte
49+
}
50+
51+
func NewSite() *Site {
52+
// 1. 生成站点 KEY 和 SECRET
53+
rand.Seed(time.Now().Unix())
54+
api_secret_buf := make([]byte, 32)
55+
56+
_, err := rand.Read(api_secret_buf)
57+
if err != nil {
58+
log.Printf("无法创建随机数: %v", err)
59+
return nil
60+
}
61+
62+
api_key_buf := sha256.Sum256(api_secret_buf)
63+
64+
api_key_b64 := base64.RawURLEncoding.EncodeToString(api_key_buf[:])
65+
api_secret_b64 := base64.RawURLEncoding.EncodeToString(api_secret_buf)
66+
67+
rsa_key, err := rsa.GenerateKey(crand.Reader, RSA_KEY_SIZE)
68+
if err != nil {
69+
log.Printf("无法生成 RSA 密钥对: %v", err)
70+
return nil
71+
}
72+
73+
s := Site{
74+
APIKey: api_key_b64,
75+
SecretKey: api_secret_b64,
76+
RSAKey: rsa_key,
77+
CreateTime: time.Now().Unix(),
78+
Hardness: 1<<22 - 1,
79+
}
80+
s.HMACKey = make([]byte, 16)
81+
rand.Read(s.HMACKey)
82+
83+
return &s
84+
}
85+
86+
// 视情况更新一个站点的密钥
87+
func (s *Site) UpdateKeyIfNeeded() bool {
88+
isUpdated := false
89+
var err error
90+
91+
ts := time.Now().Unix()
92+
93+
if ts-s.RSAKeyCreateTime < RSA_KEY_TTL {
94+
return false
95+
} else {
96+
isUpdated = true
97+
98+
s.OldRSAKey = s.RSAKey
99+
s.OldRSAKeyCreateTime = s.RSAKeyCreateTime
100+
101+
s.RSAKey, err = rsa.GenerateKey(crand.Reader, RSA_KEY_SIZE)
102+
s.RSAKeyCreateTime = ts
103+
104+
s.RSAKeyRegenerateCount++
105+
106+
if err != nil {
107+
log.Printf("严重错误:更新密钥失败,GenerateKey 返回错误: %v", err)
108+
}
109+
}
110+
111+
return isUpdated
112+
}
113+
114+
// 根据 APIKey 获取一个 site 的数据
115+
func siteGet(apiKey string) (*Site, error) {
116+
var site Site
117+
err := Store.Get(fmt.Sprintf("site/%s", apiKey), &site)
118+
return &site, err
119+
}
120+
121+
func InitGin() *gin.Engine {
122+
var err error
123+
124+
rand.Seed(time.Now().UnixNano())
125+
126+
switch os.Getenv("STORAGE") {
127+
case "s3":
128+
Store = new(store.S3)
129+
case "file":
130+
Store = new(store.File)
131+
default:
132+
fmt.Printf("环境变量 `STORAGE' 配置错误或不存在,请确认环境变量已正确配置")
133+
os.Exit(0)
134+
}
135+
136+
err = Store.Init()
137+
if err != nil {
138+
log.Printf("无法创建存储连接: %v", err)
139+
os.Exit(0)
140+
}
141+
142+
route := gin.Default()
143+
144+
route.Use(cors.Default())
145+
146+
route.GET("/captcha/problem/get", webCaptchaProblem)
147+
route.POST("/captcha/verify", webCaptchaVerify)
148+
route.POST("/site/create", webSiteCreate)
149+
route.POST("/site/read", webSiteRead)
150+
route.POST("/site/update", webSiteUpdate)
151+
152+
route.GET("/ping", func(c *gin.Context) {
153+
// c.String(200, fmt.Sprintf("pong. %v.\nS3_BUCKET=%s\nS3_ENDPOINT=%s\n", time.Now(), os.Getenv("S3_BUCKET"), os.Getenv("S3_ENDPOINT")))
154+
c.String(200, fmt.Sprintf("pong. %v.\nSTORAGE=%s", time.Now(), os.Getenv("STORAGE")))
155+
})
156+
157+
return route
158+
}
159+
160+
func StartWeb() {
161+
portStr := os.Getenv("PORT")
162+
if portStr == "" {
163+
portStr = "8090"
164+
}
165+
port, err := strconv.Atoi(portStr)
166+
167+
if err != nil {
168+
fmt.Fprintf(os.Stderr, "Invalid PORT `%s'", portStr)
169+
os.Exit(0)
170+
}
171+
172+
route := InitGin()
173+
route.Run(fmt.Sprintf(":%d", port))
174+
}
175+
176+
func Handler(w http.ResponseWriter, r *http.Request) {
177+
InitGin().ServeHTTP(w, r)
178+
}
179+
180+
func saveSite(s *Site) error {
181+
return Store.Put(fmt.Sprintf("site/%s", s.APIKey), s)
182+
}
183+
184+
func nonceIsExists(nonce string) bool {
185+
t := time.Now()
186+
p := fmt.Sprintf("nonce/%s-%s", t.Format("2006010215"), nonce)
187+
p2 := fmt.Sprintf("nonce/%s-%s", t.Add(-1*86400*time.Second).Format("2006010215"), nonce)
188+
189+
exists, err := Store.KeyExists(p)
190+
if err != nil {
191+
log.Printf("无法获知 nonce 是否已经存在,认为其不存在: %v", err)
192+
return false
193+
}
194+
195+
exists2, err := Store.KeyExists(p2)
196+
if err != nil {
197+
log.Printf("无法获知 nonce 是否已经存在,认为其不存在: %v", err)
198+
return false
199+
}
200+
201+
return exists || exists2
202+
}
203+
204+
func nonceSet(nonce string) {
205+
p := fmt.Sprintf("nonce/%s-%s", time.Now().Format("2006010215"), nonce)
206+
207+
err := Store.Put(p, []byte(fmt.Sprintf("%d", time.Now().Unix())))
208+
if err != nil {
209+
log.Printf("Unable to set nonce `%v'", nonce)
210+
} else {
211+
log.Printf("设置了一个 nonce `%s'", p)
212+
}
213+
}
214+
215+
// 是否正在执行 nonce 清理的操作。该变量用于避免多个 nonce 清理程序同时运行
216+
var isNonceCleaning bool = false
217+
218+
// 以 prob 的概率,触发清理过期的 nonce 操作
219+
func nonceClean(prob float32) {
220+
if isNonceCleaning == true {
221+
log.Printf("当前有另一个 Nonce 清理程序正在进行中,不会重复运行 Nonce 清理程序")
222+
return
223+
}
224+
isNonceCleaning = true
225+
defer func() {
226+
isNonceCleaning = false
227+
}()
228+
229+
r := rand.Float32()
230+
if r >= prob {
231+
return
232+
}
233+
234+
log.Printf("执行一次清理 nonce 的操作")
235+
236+
keys, err := Store.List("nonce/")
237+
if err != nil {
238+
log.Printf("清理 nonce 操作失败,无法获取 nonce 列表: %v", err)
239+
return
240+
}
241+
242+
t := time.Now()
243+
nowPrefix := fmt.Sprintf("nonce/%s", t.Format("2006010215"))
244+
prevPrefix := fmt.Sprintf("nonce/%s", t.Add(86400*time.Second).Format("2006010215"))
245+
for _, v := range keys {
246+
if strings.HasPrefix(v, nowPrefix) || strings.HasPrefix(v, prevPrefix) {
247+
continue
248+
}
249+
log.Printf("删除 nonce `%s'", v)
250+
Store.Delete(v)
251+
}
252+
253+
log.Printf("nonce 清理操作完成")
254+
}
255+
256+
func init() {
257+
godotenv.Load()
258+
}

0 commit comments

Comments
 (0)