diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6fc6043 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# 监听端口,默认值为 8090 +# Listen port, defaut to 8090 +# PORT=8090 + +# 存储类型,可以是以下类型: +# file: 使用本地文件存储 +# s3: 使用 AWS S3 兼容的 KV 存储 +# Storage type, available values are: +# file: Store data in native filesystem +# s3: Store data in AWS S3 compatiable KV storage +STORAGE=file +#STORAGE=s3 + +# 如果使用 file 存储,在此指定存储目录 +# When using file storage, set dir path here +STORAGE_PATH=/data + +# 如果使用 S3 兼容的 KV 存储,在此指定连接参数 +# When using s3 storage, set connection parameters here +S3_ENDPOINT=https://xxx.r2.cloudflarestorage.com +S3_BUCKET=wcaptcha +S3_ACCESS_KEY= +S3_SECRET_KEY= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cafa219 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +.env.production +gin-bin +.vercel +wcaptcha diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..68dea2c --- /dev/null +++ b/api/api.go @@ -0,0 +1,258 @@ +package api + +import ( + crand "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "fmt" + "log" + "math/rand" + "net/http" + "os" + "strconv" + "strings" + "time" + "wcaptcha/store" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" +) + +const ( + RSA_KEY_SIZE = 512 + RSA_KEY_TTL = 600 +) + +// var S3 *s3kv.Storage +var Store store.Storer + +type Site struct { + SecretKey string + APIKey string + + RSAKey *rsa.PrivateKey + OldRSAKey *rsa.PrivateKey + RSAKeyCreateTime int64 + OldRSAKeyCreateTime int64 + + // RSAKey 的总计轮换次数(总共重新生成了多少次 RSAKey) + RSAKeyRegenerateCount int + + // 难度,客户端需要计算多少次平方取模,在 2020 年的消费级 CPU 上,Hardness = 2**20 时大约需要 100ms 的时间可计算出结果 + Hardness int + + CreateTime int64 + CreatorUserAgent string + HMACKey []byte +} + +func NewSite() *Site { + // 1. 生成站点 KEY 和 SECRET + rand.Seed(time.Now().Unix()) + api_secret_buf := make([]byte, 32) + + _, err := rand.Read(api_secret_buf) + if err != nil { + log.Printf("无法创建随机数: %v", err) + return nil + } + + api_key_buf := sha256.Sum256(api_secret_buf) + + api_key_b64 := base64.RawURLEncoding.EncodeToString(api_key_buf[:]) + api_secret_b64 := base64.RawURLEncoding.EncodeToString(api_secret_buf) + + rsa_key, err := rsa.GenerateKey(crand.Reader, RSA_KEY_SIZE) + if err != nil { + log.Printf("无法生成 RSA 密钥对: %v", err) + return nil + } + + s := Site{ + APIKey: api_key_b64, + SecretKey: api_secret_b64, + RSAKey: rsa_key, + CreateTime: time.Now().Unix(), + Hardness: 1<<22 - 1, + } + s.HMACKey = make([]byte, 16) + rand.Read(s.HMACKey) + + return &s +} + +// 视情况更新一个站点的密钥 +func (s *Site) UpdateKeyIfNeeded() bool { + isUpdated := false + var err error + + ts := time.Now().Unix() + + if ts-s.RSAKeyCreateTime < RSA_KEY_TTL { + return false + } else { + isUpdated = true + + s.OldRSAKey = s.RSAKey + s.OldRSAKeyCreateTime = s.RSAKeyCreateTime + + s.RSAKey, err = rsa.GenerateKey(crand.Reader, RSA_KEY_SIZE) + s.RSAKeyCreateTime = ts + + s.RSAKeyRegenerateCount++ + + if err != nil { + log.Printf("严重错误:更新密钥失败,GenerateKey 返回错误: %v", err) + } + } + + return isUpdated +} + +// 根据 APIKey 获取一个 site 的数据 +func siteGet(apiKey string) (*Site, error) { + var site Site + err := Store.Get(fmt.Sprintf("site/%s", apiKey), &site) + return &site, err +} + +func InitGin() *gin.Engine { + var err error + + rand.Seed(time.Now().UnixNano()) + + switch os.Getenv("STORAGE") { + case "s3": + Store = new(store.S3) + case "file": + Store = new(store.File) + default: + fmt.Printf("环境变量 `STORAGE' 配置错误或不存在,请确认环境变量已正确配置") + os.Exit(0) + } + + err = Store.Init() + if err != nil { + log.Printf("无法创建存储连接: %v", err) + os.Exit(0) + } + + route := gin.Default() + + route.Use(cors.Default()) + + route.GET("/captcha/problem/get", webCaptchaProblem) + route.POST("/captcha/verify", webCaptchaVerify) + route.POST("/site/create", webSiteCreate) + route.POST("/site/read", webSiteRead) + route.POST("/site/update", webSiteUpdate) + + route.GET("/ping", func(c *gin.Context) { + // c.String(200, fmt.Sprintf("pong. %v.\nS3_BUCKET=%s\nS3_ENDPOINT=%s\n", time.Now(), os.Getenv("S3_BUCKET"), os.Getenv("S3_ENDPOINT"))) + c.String(200, fmt.Sprintf("pong. %v.\nSTORAGE=%s", time.Now(), os.Getenv("STORAGE"))) + }) + + return route +} + +func StartWeb() { + portStr := os.Getenv("PORT") + if portStr == "" { + portStr = "8090" + } + port, err := strconv.Atoi(portStr) + + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid PORT `%s'", portStr) + os.Exit(0) + } + + route := InitGin() + route.Run(fmt.Sprintf(":%d", port)) +} + +func Handler(w http.ResponseWriter, r *http.Request) { + InitGin().ServeHTTP(w, r) +} + +func saveSite(s *Site) error { + return Store.Put(fmt.Sprintf("site/%s", s.APIKey), s) +} + +func nonceIsExists(nonce string) bool { + t := time.Now() + p := fmt.Sprintf("nonce/%s-%s", t.Format("2006010215"), nonce) + p2 := fmt.Sprintf("nonce/%s-%s", t.Add(-1*86400*time.Second).Format("2006010215"), nonce) + + exists, err := Store.KeyExists(p) + if err != nil { + log.Printf("无法获知 nonce 是否已经存在,认为其不存在: %v", err) + return false + } + + exists2, err := Store.KeyExists(p2) + if err != nil { + log.Printf("无法获知 nonce 是否已经存在,认为其不存在: %v", err) + return false + } + + return exists || exists2 +} + +func nonceSet(nonce string) { + p := fmt.Sprintf("nonce/%s-%s", time.Now().Format("2006010215"), nonce) + + err := Store.Put(p, []byte(fmt.Sprintf("%d", time.Now().Unix()))) + if err != nil { + log.Printf("Unable to set nonce `%v'", nonce) + } else { + log.Printf("设置了一个 nonce `%s'", p) + } +} + +// 是否正在执行 nonce 清理的操作。该变量用于避免多个 nonce 清理程序同时运行 +var isNonceCleaning bool = false + +// 以 prob 的概率,触发清理过期的 nonce 操作 +func nonceClean(prob float32) { + if isNonceCleaning == true { + log.Printf("当前有另一个 Nonce 清理程序正在进行中,不会重复运行 Nonce 清理程序") + return + } + isNonceCleaning = true + defer func() { + isNonceCleaning = false + }() + + r := rand.Float32() + if r >= prob { + return + } + + log.Printf("执行一次清理 nonce 的操作") + + keys, err := Store.List("nonce/") + if err != nil { + log.Printf("清理 nonce 操作失败,无法获取 nonce 列表: %v", err) + return + } + + t := time.Now() + nowPrefix := fmt.Sprintf("nonce/%s", t.Format("2006010215")) + prevPrefix := fmt.Sprintf("nonce/%s", t.Add(86400*time.Second).Format("2006010215")) + for _, v := range keys { + if strings.HasPrefix(v, nowPrefix) || strings.HasPrefix(v, prevPrefix) { + continue + } + log.Printf("删除 nonce `%s'", v) + Store.Delete(v) + } + + log.Printf("nonce 清理操作完成") +} + +func init() { + godotenv.Load() +} diff --git a/api/web.go b/api/web.go new file mode 100644 index 0000000..8f1fc19 --- /dev/null +++ b/api/web.go @@ -0,0 +1,321 @@ +package api + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "log" + "math/big" + "math/rand" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/greensea/vdf" +) + +// 创建一个网站,为其生成 API_KEY 和 SECRET_KEY +func webSiteCreate(c *gin.Context) { + // 创建网站数据 + s := NewSite() + if s == nil { + webError(c, -1, "Unable to create site") + return + } + + // 保存到数据库 + s.CreatorUserAgent = c.GetHeader("User-Agent") + err := saveSite(s) + if err != nil { + log.Printf("无法将 Site 保存到数据库: %v", err) + webError(c, -2, "Unable to save site data") + return + } + + // 返回结果 + c.JSON(200, gin.H{ + "code": 0, + "site": gin.H{ + "api_key": s.APIKey, + "api_secret": s.SecretKey, + }, + }) +} + +// 读取一个网站的数据,需要提供 API Secret +// 注意!该接口会返回 APISecret,如果不再使用 APISecret 作为查询参数,则接口应该做相应的修改 +func webSiteRead(c *gin.Context) { + var req struct { + APISecret string `form:"api_secret" binding:"required"` + } + err := c.ShouldBind(&req) + if err != nil { + webError(c, -1, err.Error()) + return + } + + // 1. 根据 API Secret 计算 API Key + apiSecretBuf, err := base64.RawURLEncoding.DecodeString(req.APISecret) + if err != nil { + webError(c, -2, "Invalid API Secret: "+err.Error()) + return + } + apiKeyBuf := sha256.Sum256(apiSecretBuf) + apiKey := base64.RawURLEncoding.EncodeToString(apiKeyBuf[:]) + + site, err := siteGet(apiKey) + if err != nil { + webError(c, -10, "Can't find site: "+err.Error()) + return + } + + c.JSON(0, gin.H{ + "code": 0, + "data": gin.H{ + "site": gin.H{ + "api_key": site.APIKey, + "api_secret": site.SecretKey, + "hardness": site.Hardness, + "create_time": site.CreateTime, + "rsa_key_regenerate_count": site.RSAKeyRegenerateCount, + "rsa_key_create_time": site.RSAKeyCreateTime, + }, + }, + }) +} + +// 修改一个网站的数据,需要提供 API Secret +func webSiteUpdate(c *gin.Context) { + var req struct { + APISecret string `form:"api_secret" binding:"required"` + Hardness int `form:"hardness" binding:"required"` + } + err := c.ShouldBind(&req) + if err != nil { + webError(c, -1, err.Error()) + return + } + + // 1. 根据 API Secret 计算 API Key + apiSecretBuf, err := base64.RawURLEncoding.DecodeString(req.APISecret) + if err != nil { + webError(c, -2, "Invalid API Secret: "+err.Error()) + return + } + apiKeyBuf := sha256.Sum256(apiSecretBuf) + apiKey := base64.RawURLEncoding.EncodeToString(apiKeyBuf[:]) + + site, err := siteGet(apiKey) + if err != nil { + webError(c, -10, "Can't find site: "+err.Error()) + return + } + + site.Hardness = req.Hardness + err = saveSite(site) + if err != nil { + webError(c, -20, "Can't save site info"+err.Error()) + return + } + + c.JSON(0, gin.H{ + "code": 0, + "data": gin.H{ + "site": gin.H{ + "api_key": site.APIKey, + "hardness": site.Hardness, + "create_time": site.CreateTime, + "rsa_key_regenerate_count": site.RSAKeyRegenerateCount, + "rsa_key_create_time": site.RSAKeyCreateTime, + }, + }, + }) +} + +// 生成一个问题 +// 我们在服务端生成以下数据: +// x: 随机数 +// h: 对 x 的签名,使用 HMAC_SHA256 以及服务器自定义的密钥进行签名 +// n: 模数,这个数据不需要生成,直接使用 site 的 RSA 素数生成 +// 给客户端返回: +// question = {X: X, H: H, N: N} +func webCaptchaProblem(c *gin.Context) { + var req struct { + APIKey string `form:"api_key" binding:"required"` + } + err := c.ShouldBind(&req) + if err != nil { + webError(c, -1, err.Error()) + return + } + + // 1. 查询网站是否存在 + var site Site + err = Store.Get(fmt.Sprintf("site/%s", req.APIKey), &site) + if err != nil { + webError(c, -2, fmt.Sprintf("Site not exists. (Internal error: %s)", err.Error())) + return + } + + // 2. 检查 RSA 钥匙对是否已经过期,若已经过期,则更新之 + is_updated := site.UpdateKeyIfNeeded() + if is_updated { + log.Printf("站点 %s 的 RSA 密钥对已更新,更新数据库数据", site.APIKey) + err := saveSite(&site) + if err != nil { + log.Printf("保存站点数据失败: %v", err) + webError(c, -10, "Unable to update key") + return + } + } + + // 3. 生成数据并返回 + n := new(big.Int).Set(site.RSAKey.Primes[0]) + n = n.Mul(n, site.RSAKey.Primes[1]) + x := big.NewInt(rand.Int63()) + + h := hmac.New(sha256.New, site.HMACKey).Sum([]byte(x.Text(16))) + + // nB64 := base64.RawStdEncoding.EncodeToString(n.Bytes()) + // xB64 := base64.RawStdEncoding.EncodeToString([]byte(x)) + hB64 := base64.RawURLEncoding.EncodeToString(h) + + //question := fmt.Sprintf("%s.%s.%s", nB64, xB64, hB64) + + // 以 1% 的概率去触发 Nonce 清除操作 + go nonceClean(001) + + c.JSON(200, gin.H{ + "code": 0, + "data": gin.H{ + "question": gin.H{ + "x": x.Text(16), + "h": hB64, + "n": n.Text(16), + "t": site.Hardness, + }, + }, + }) +} + +// 检查客户端计算出的结果是否正确 +// 客户端应提交 prove 和 api_key 参数。其中 prove 数据格式如下: +// X.Y.H +// 其中 X 是此前服务器返回的 x 的原始内容;Y 是计算结果,为小写的十六进制表达式;H 为此前服务器返回的签名原始内容; +// N 为模数,为小写的十六进制格式 +func webCaptchaVerify(c *gin.Context) { + // 1. 解析客户端数据 + var req struct { + Prove string `form:"prove" binding:"required"` + APIKey string `form:"api_key" binding:"required"` + } + err := c.Bind(&req) + if err != nil { + webError(c, -10, err.Error()) + return + } + + tokens := strings.Split(req.Prove, ".") + if len(tokens) != 3 { + webError(c, -20, "Invalid parameter prove") + return + } + + xRaw := tokens[0] + yRaw := tokens[1] + hRaw := tokens[2] + //nRaw := tokens[3] + + x, xSuccess := new(big.Int).SetString(xRaw, 16) + if xSuccess != true { + webError(c, -24, "Invalid parameter x") + return + } + + site, err := siteGet(req.APIKey) + if err != nil { + webError(c, -25, "No such site. Invalid api_key? "+err.Error()) + return + } + + // 2. 验证签名是否正确,同时检查 nonce 是否已使用 + ourH := hmac.New(sha256.New, site.HMACKey).Sum([]byte(x.Text(16))) + ourHB64 := base64.RawURLEncoding.EncodeToString(ourH) + if ourHB64 != hRaw { + webError(c, -30, "Invalid signature for x") + return + } + + // 2.2 检查 nonce 是否已经使用 + if nonceIsExists(hRaw) { + webError(c, -40, "This proof is already used") + return + } + + // 3. 检查计算结果是否正确 + isCorrect := false + var v *vdf.VDF + ts := time.Now().Unix() + x, successX := new(big.Int).SetString(xRaw, 16) + y, successY := new(big.Int).SetString(yRaw, 16) + if successX != true || successY != true { + webError(c, -30, "Invalid x or y") + return + } + + /// 3.1 使用最新的 RSAKey 检查结果是否正确 + if ts-site.RSAKeyCreateTime < RSA_KEY_TTL { + v = vdf.New(site.RSAKey.Primes[0], site.RSAKey.Primes[1]) + stime := time.Now() + if v.Verify(x, site.Hardness, y) == true { + // 验证成功,什么都不用做 + isCorrect = true + } else { + isCorrect = false + } + log.Printf("校验证明耗时 %v,模数长度为 %d", time.Now().Sub(stime), site.RSAKey.Primes[0].BitLen()*2) + } else { + log.Printf("网站 %s 的 RSAKey 已经超时了,不会使用 RSAKey 进行检查", site.APIKey) + } + + /// 3.2 使用次新的 RSAKey 检查结果是否正确 + if isCorrect == false { + if ts-site.OldRSAKeyCreateTime > RSA_KEY_TTL*2 { + v = vdf.New(site.OldRSAKey.Primes[0], site.OldRSAKey.Primes[1]) + if v.Verify(x, site.Hardness, y) != true { + isCorrect = false + } else { + isCorrect = true + } + } else { + log.Printf("网站 %s 的 OldRSAKey 已经超时了,不会使用 OldRSAKey 进行检查", site.APIKey) + } + } + + // 5. 若验证成功则记录一次 nonce + var msg string + if isCorrect { + nonceSet(hRaw) + msg = "Prove is correct" + } else { + msg = "Prove is INVALID" + } + + c.JSON(200, gin.H{ + "code": 0, + "message": msg, + "data": gin.H{ + "prove": req.Prove, + "is_correct": isCorrect, + }, + }) +} + +// 返回一个错误 JSON +func webError(c *gin.Context, code int, msg string) { + c.JSON(200, gin.H{ + "code": code, + "message": msg, + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2ed9faa --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module wcaptcha + +go 1.18 + +require ( + github.com/gin-contrib/cors v1.4.0 + github.com/gin-gonic/gin v1.8.1 + github.com/greensea/s3kv v0.0.1 + github.com/greensea/vdf v0.0.0-20221208093952-88e134082256 + github.com/joho/godotenv v1.4.0 +) + +require ( + github.com/aws/aws-sdk-go v1.44.159 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.1 // indirect + github.com/goccy/go-json v0.10.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/ugorji/go/codec v1.2.7 // indirect + golang.org/x/crypto v0.4.0 // indirect + golang.org/x/net v0.4.0 // indirect + golang.org/x/sys v0.3.0 // indirect + golang.org/x/text v0.5.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7aabeb3 --- /dev/null +++ b/go.sum @@ -0,0 +1,151 @@ +github.com/aws/aws-sdk-go v1.44.155 h1:PMHMuUS0atPD4LhiXuYrLasrlIm4u3lpNQBl9h+Lr2s= +github.com/aws/aws-sdk-go v1.44.155/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.159 h1:9odtuHAYQE9tQKyuX6ny1U1MHeH5/yzeCJi96g9H4DU= +github.com/aws/aws-sdk-go v1.44.159/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= +github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= +github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/greensea/s3kv v0.0.0-20221208082517-3e8afac9daa7 h1:HP6LCGNQVZfwNSOnpjiucm6zETI7gfAELpCEPP8guNQ= +github.com/greensea/s3kv v0.0.0-20221208082517-3e8afac9daa7/go.mod h1:TKaFnpedkg09KXidBwNrAAjJfHKkM+v3iEE//dM6iTI= +github.com/greensea/s3kv v0.0.0-20221208085308-9d8372a60c13 h1:Di1wEMpqCsK/HvugRRgEuDSpNdXZNA1i7pHbzGpeT48= +github.com/greensea/s3kv v0.0.0-20221208085308-9d8372a60c13/go.mod h1:TKaFnpedkg09KXidBwNrAAjJfHKkM+v3iEE//dM6iTI= +github.com/greensea/s3kv v0.0.1 h1:ECSxRb86l6cVx2XroHwegcCnt0D6NePAJgI7DdF1E8k= +github.com/greensea/s3kv v0.0.1/go.mod h1:TKaFnpedkg09KXidBwNrAAjJfHKkM+v3iEE//dM6iTI= +github.com/greensea/vdf v0.0.0-20221208075714-d95aa40a815f h1:skFfz53zhgwukKRecMfSrVox3CkFJIe/Lu+EHV9Mfbs= +github.com/greensea/vdf v0.0.0-20221208075714-d95aa40a815f/go.mod h1:pPCE0Sa7iOw28y3FJYGW8Y0psyrRLxxUvJc7JB0aYhg= +github.com/greensea/vdf v0.0.0-20221208093952-88e134082256 h1:2nsgrRVN1rJCdhc6o4ItaYaZqYPuH5WbPbWRtEBy1Ng= +github.com/greensea/vdf v0.0.0-20221208093952-88e134082256/go.mod h1:pPCE0Sa7iOw28y3FJYGW8Y0psyrRLxxUvJc7JB0aYhg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20210806184541-e5e7981a1069/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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-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/yaml.v2 v2.2.8/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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..65b13e8 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "wcaptcha/api" + +func main() { + api.StartWeb() +} diff --git a/store/file.go b/store/file.go new file mode 100644 index 0000000..5f65e2c --- /dev/null +++ b/store/file.go @@ -0,0 +1,138 @@ +package store + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strings" +) + +type File struct { + dir string +} + +// Init Storage +func (f *File) Init() error { + var err error + + f.dir = os.Getenv("STORAGE_PATH") + if len(f.dir) == 0 { + return errors.New("必须指定 STORAGE_PATH 环境变量") + } + + if f.dir[len(f.dir)-1:] != "/" { + f.dir += "/" + } + + err = os.MkdirAll(f.dir, os.ModePerm) + if err != nil { + return err + } + + return nil +} + +// List objects by prefix +func (f *File) List(prefix string) ([]string, error) { + p := prefix + if len(p) > 0 && p[0:1] == "/" { + p = p[1:] + } + if len(p) > 0 && p[len(p)-1:] == "/" { + p = p[0 : len(p)-1] + } + + entries, err := os.ReadDir(f.dir + p) + if err != nil { + if os.IsNotExist(err) { + return []string{}, nil + } else { + return nil, err + } + } + + var ret []string + for _, v := range entries { + if v.IsDir() { + continue + } + + ret = append(ret, p+"/"+v.Name()) + } + + return ret, nil +} + +// Put an object into storage +func (f *File) Put(key string, obj any) error { + err := f.ensureFileDir(key) + if err != nil { + return fmt.Errorf("Put() failed: %v", err) + } + + fi, err := os.OpenFile(f.dir+key, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm) + if err != nil { + return err + } + defer fi.Close() + + e := json.NewEncoder(fi) + e.SetIndent("", " ") + err = e.Encode(obj) + + if err != nil { + return err + } + + return err +} + +// Get an object from storage +func (f *File) Get(key string, obj any) error { + fi, err := os.OpenFile(f.dir+key, os.O_RDONLY, os.ModePerm) + if err != nil { + return err + } + defer fi.Close() + + d := json.NewDecoder(fi) + + err = d.Decode(&obj) + + return err +} + +// Check if an object is in storage +func (f *File) KeyExists(key string) (bool, error) { + _, err := os.Stat(f.dir + key) + if err == nil { + return true, nil + } else { + if os.IsNotExist(err) { + return false, nil + } else { + return false, err + } + } +} + +func (f *File) Delete(key string) error { + return os.Remove(f.dir + key) +} + +func (f *File) ensureFileDir(key string) error { + p := f.dir + key + tokens := strings.Split(p, "/") + if len(tokens) > 0 { + tokens = tokens[0 : len(tokens)-1] + } + p = strings.Join(tokens, "/") + + err := os.MkdirAll(p, os.ModePerm) + if err != nil { + return fmt.Errorf("Unable to ensureFileDir `%s': %v", p, err) + } else { + return nil + } +} diff --git a/store/file_test.go b/store/file_test.go new file mode 100644 index 0000000..db671ca --- /dev/null +++ b/store/file_test.go @@ -0,0 +1,111 @@ +package store + +import ( + "fmt" + "testing" +) + +func TestFile(t *testing.T) { + testItem(t, "abc") + testItem(t, "a/b/c") + testItem(t, "/a/b/c") +} + +func TestList(t *testing.T) { + var err error + Store := new(File) + if err != nil { + t.FailNow() + } + + Store.Put("d1/foo", "bar") + + l, err := Store.List("d1") + if err != nil { + fmt.Println(err) + t.FailNow() + } + if len(l) != 1 || l[0] != "d1/foo" { + fmt.Println(l) + t.FailNow() + } + + l, err = Store.List("d1/") + if err != nil { + fmt.Println(err) + t.FailNow() + } + if len(l) != 1 || l[0] != "d1/foo" { + t.FailNow() + } + + l, err = Store.List("d1_not_exists/") + if err != nil { + fmt.Println(err) + t.FailNow() + } + if len(l) != 0 { + t.FailNow() + } + +} + +func testItem(t *testing.T, key string) { + var err error + + Store := new(File) + err = Store.Init() + if err != nil { + fmt.Println(err) + t.FailNow() + } + + err = Store.Put(key, []string{"foo", "bar"}) + if err != nil { + fmt.Println(err) + t.FailNow() + } + + var obj []string + err = Store.Get(key, &obj) + if err != nil { + fmt.Println(err) + t.FailNow() + } + if len(obj) != 2 { + t.FailNow() + } + if obj[0] != "foo" || obj[1] != "bar" { + t.FailNow() + } + + b, err := Store.KeyExists(key) + if err != nil { + fmt.Println(err) + t.FailNow() + } + if b != true { + t.FailNow() + } + + b, err = Store.KeyExists("not_exists") + if err != nil { + fmt.Println(err) + t.FailNow() + } + if b != false { + t.FailNow() + } + + err = Store.Delete(key) + if err != nil { + fmt.Println(err) + t.FailNow() + } + + b, err = Store.KeyExists(key) + if b == true && err != nil { + fmt.Println(err) + t.FailNow() + } +} diff --git a/store/s3.go b/store/s3.go new file mode 100644 index 0000000..fcf88dd --- /dev/null +++ b/store/s3.go @@ -0,0 +1,48 @@ +package store + +import ( + "os" + + "github.com/greensea/s3kv" +) + +type S3 struct { + s3 *s3kv.Storage +} + +// Init Storage +func (s *S3) Init() error { + var err error + s.s3, err = s3kv.New(&s3kv.Config{ + Endpoint: os.Getenv("S3_ENDPOINT"), + Bucket: os.Getenv("S3_BUCKET"), + AccessKey: os.Getenv("S3_ACCESS_KEY"), + SecretKey: os.Getenv("S3_SECRET_KEY"), + }) + return err +} + +// List objects by prefix +func (s *S3) List(prefix string) ([]string, error) { + ret, err := s.s3.List(prefix) + return ret, err +} + +// Put an object into storage +func (s *S3) Put(key string, obj any) error { + return s.s3.PutObject(key, obj) +} + +// Get an object from storage +func (s *S3) Get(key string, obj any) error { + return s.s3.GetJSON(key, obj) +} + +// Check if an object is in storage +func (s *S3) KeyExists(key string) (bool, error) { + return s.s3.KeyExists(key) +} + +func (s *S3) Delete(key string) error { + return s.s3.Delete(key) +} diff --git a/store/store.go b/store/store.go new file mode 100644 index 0000000..021885b --- /dev/null +++ b/store/store.go @@ -0,0 +1,21 @@ +package store + +type Storer interface { + // Init storage + Init() error + + // Put an object into storage + Put(key string, obj any) error + + // Get an object from storage + Get(key string, obj any) error + + // Check if an object is in storage + KeyExists(key string) (bool, error) + + // List objects by prefix + List(prefix string) ([]string, error) + + // Delete an object + Delete(key string) error +} diff --git a/vercel-deploy.sh b/vercel-deploy.sh new file mode 100755 index 0000000..b390cf4 --- /dev/null +++ b/vercel-deploy.sh @@ -0,0 +1,4 @@ +#!/bin/sh +export $(xargs <.env.production) +vercel -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 + diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..f7f0ca8 --- /dev/null +++ b/vercel.json @@ -0,0 +1,10 @@ +{ + "trailingSlash": false, + "rewrites": + [ + { + "source": "/(.*)", + "destination": "/api/api.go" + } + ] +}