Commit e142c28d authored by 贾浩@五瓣科技's avatar 贾浩@五瓣科技

init

parents
Pipeline #768 canceled with stages
.idea
*.iml
out
gen
*.sol
*.txt
.DS_Store
*.exe
demo.png
*.html
build
nohup.out
\ No newline at end of file
FROM golang:1.21-alpine AS base
# Set up dependencies
ENV PACKAGES git openssh-client build-base
# Install dependencies
RUN apk add --update $PACKAGES
# Add source files
RUN mkdir -p ./sdk-api
COPY ./ ./sdk-api/
FROM base AS build
RUN cd sdk-api && go mod tidy && go build -v -o /tmp/api ./cmd
FROM alpine
WORKDIR /app
COPY ./config.toml /config.toml
COPY --from=build /tmp/api /usr/bin/sdk_api
EXPOSE 8080
\ No newline at end of file
.PHONY: default all clean dev
GOBIN = $(shell pwd)/build/bin
default: all
all: sdk-api
sdk-api:
go build $(BUILD_FLAGS) -v -o=${GOBIN}/$@ ./cmd
dev:
go build $(BUILD_FLAGS) -v -o=${GOBIN}/$@ -gcflags "all=-N -l" ./cmd
docker:
docker build -t sdk-api:latest -f Dockerfile .
package main
import (
"flag"
"sdk_api/config"
"sdk_api/dao"
"sdk_api/server"
"sdk_api/service"
log "github.com/sirupsen/logrus"
)
func init() {
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
}
func main() {
flag.Parse()
conf, err := config.New()
if err != nil {
panic(err)
}
da, err := dao.New(conf)
if err != nil {
panic(err)
}
if conf.Debug {
log.SetLevel(log.DebugLevel)
}
svs := service.New(conf, da)
server.StartServer(svs, conf)
}
debug = true
[mysql]
host = "127.0.0.1"
port = 3306
user = "root"
password = "XN2UARuys3zy4Oux"
database = "sdk"
max_conn = 10
max_idle_conn = 2
[server]
listen = "0.0.0.0:8080"
package config
import (
"flag"
"github.com/BurntSushi/toml"
)
type Config struct {
Debug bool `toml:"debug"`
Sender SenderConfig `toml:"sender"`
MySQL MysqlConfig `toml:"mysql"`
Server ServerConfig `toml:"server"`
}
type SenderConfig struct {
EmailUsername string `json:"email_username"`
EmailPassword string `json:"email_password"`
}
type MysqlConfig struct {
Host string `toml:"host"`
Port int `toml:"port"`
User string `toml:"user"`
Password string `toml:"password"`
Database string `toml:"database"`
MaxConn int `toml:"max_conn"`
MaxIdleConn int `toml:"max_idle_conn"`
}
type ServerConfig struct {
Listen string `toml:"listen"`
}
var confPath = flag.String("c", "config.toml", "config file path")
func New() (config *Config, err error) {
config = new(Config)
_, err = toml.DecodeFile(*confPath, config)
return
}
package constant
const JwtSecret = "cxcZa005Y5zWH1wFgXvPGDL02Ey4ZCLA"
const (
InvalidParam = "invalid param"
UnsupportedPlatform = "unsupported platform"
InternalError = "internal error"
)
const (
PlatformTelegram = "telegram"
PlatformFingerprint = "fingerprint"
)
package dao
import (
"fmt"
"sdk_api/config"
dbModel "sdk_api/model/db"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
type Dao struct {
c *config.Config
db *gorm.DB
}
func New(_c *config.Config) (dao *Dao, err error) {
dao = &Dao{
c: _c,
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
_c.MySQL.User, _c.MySQL.Password, _c.MySQL.Host, _c.MySQL.Port, _c.MySQL.Database)
dao.db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
return
}
sqlDB, err := dao.db.DB()
if err != nil {
return
}
sqlDB.SetMaxOpenConns(_c.MySQL.MaxConn)
sqlDB.SetMaxIdleConns(_c.MySQL.MaxIdleConn)
sqlDB.SetConnMaxIdleTime(time.Hour)
err = dao.db.AutoMigrate(&dbModel.User{})
if err != nil {
return
}
return dao, nil
}
package dao
import (
dbModel "sdk_api/model/db"
"gorm.io/gorm"
)
func (d *Dao) CreateUser(user *dbModel.User) (err error) {
return d.db.Create(user).Error
}
func (d *Dao) CheckUser(platform string, platformId string) (user *dbModel.User, err error) {
user = &dbModel.User{}
err = d.db.Model(user).
Where("platform = ? AND platform_id = ?", platform, platformId).
First(user).
Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return
}
func (d *Dao) GetUserByUid(uid string) (user *dbModel.User, err error) {
user = &dbModel.User{}
err = d.db.Model(user).
Where("uid = ?", uid).
First(user).
Error
if err == gorm.ErrRecordNotFound {
return nil, err
}
return
}
func (d *Dao) CreateEmptyUser(user *dbModel.User) (err error) {
return d.db.Create(user).Error
}
func (d *Dao) SetKeystore(uid, address, keystore string) (err error) {
// set keystore only not exist
return d.db.Model(&dbModel.User{}).
Where("uid = ? AND keystore = ?", uid, "").
// Update("keystore", keystore).
Updates(map[string]interface{}{"keystore": keystore, "address": address}).
Error
}
version: "3.5"
networks:
default:
name: aon-sdk
services:
sdk-db:
image: mysql:8
ports:
- "13307:3306"
volumes:
- ./data/sdk-api/db:/var/lib/mysql
- ./conf/sdk-api/db/conf.d:/etc/mysql/conf.d
environment:
MYSQL_ROOT_PASSWORD: "XN2UARuys3zy4Oux"
MYSQL_DATABASE: "sdk"
sdk-api:
image: caduceus/sdk-api:v0.0.1
container_name: sdk-api
ports:
- "16666:8080"
depends_on:
- sdk-db
volumes:
- ./conf/sdk-api/config.toml:/config.toml
command:
- "/bin/sh"
- "-c"
- "/usr/bin/sdk_api -c /config.toml"
restart:
unless-stopped
sdk-kv-api:
image: caduceus/sdk-kv-api:v0.0.1
ports:
- "16667:8080"
depends_on:
- sdk-db
volumes:
- ./conf/sdk-kv-api/config.toml:/config.toml
command:
- "/bin/sh"
- "-c"
- "/usr/bin/sdk_kv_api -c /config.toml"
restart:
unless-stopped
\ No newline at end of file
module sdk_api
go 1.21.4
require (
github.com/BurntSushi/toml v0.3.1
github.com/gin-contrib/cors v1.7.2
github.com/gin-gonic/gin v1.10.0
github.com/sirupsen/logrus v1.6.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/telebot.v3 v3.2.1
gorm.io/driver/mysql v1.5.6
gorm.io/gorm v1.25.10
)
require (
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.12.1 // indirect
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/ethereum/c-kzg-4844/bindings/go v0.0.0-20230126171313-363c7d7593b4 // indirect
github.com/ethereum/go-ethereum v1.14.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mmcloughlin/addchain v0.4.0 // 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.2.2 // indirect
github.com/supranational/blst v0.3.12 // indirect
github.com/tidwall/gjson v1.17.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)
This diff is collapsed.
package middleware
import (
"bytes"
"io"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func PrintRequestResponseBodyMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 读取请求 body
var requestBody []byte
if c.Request.Body != nil {
requestBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
}
log.WithFields(log.Fields{"method": c.Request.Method, "uri": c.Request.RequestURI, "body": string(requestBody)}).Debug("request body")
bodyWriter := &responseBodyWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = bodyWriter
c.Next()
responseBody := bodyWriter.body.String()
log.WithFields(log.Fields{"status": c.Writer.Status(), "body": responseBody}).Debug("response body")
}
}
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (r *responseBodyWriter) Write(b []byte) (int, error) {
r.body.Write(b)
return r.ResponseWriter.Write(b)
}
package middleware
import (
"sdk_api/util"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func JWTMiddleware(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
log.Debugln(tokenString)
if tokenString == "" || len(tokenString) < 7 {
c.JSON(200, gin.H{
"code": 1,
"msg": "invalid token",
"data": "",
})
c.Abort()
return
}
ok, expired, uid, _, _ := util.ParseJWT(tokenString[7:])
if !ok {
c.JSON(200, gin.H{
"code": 1,
"msg": "invalid token",
"data": "",
})
c.Abort()
return
}
if expired {
c.JSON(200, gin.H{
"code": 1,
"msg": "token expired",
"data": "",
})
c.Abort()
return
}
log.WithField("uid", uid).Debug("jwt uid")
c.Set("jwt-uid", uid)
c.Next()
}
package api_model
type CheckUserRequest struct {
InitData string `json:"initData"`
Platform string `json:"platform"`
VisitorID string `json:"visitorId"`
}
type CheckUserResponse struct {
IsNewUser bool `json:"isNewUser"`
Keystore string `json:"keystore"`
Token string `json:"token"`
}
type CreateUserRequest struct {
Keystore string `json:"keystore"`
Signature string `json:"signature"`
}
type CreateUserResponse struct {
}
package db_model
import (
"gorm.io/gorm"
)
type User struct {
Id int `gorm:"primaryKey"`
Uid string `gorm:"type:varchar(255);uniqueIndex;not null;column:uid;comment:用户id"`
Platform string `gorm:"type:varchar(255);uniqueIndex:platform_id;not null;column:platform;comment:所在平台,telegram,fingerprint"`
PlatformId string `gorm:"type:varchar(255);uniqueIndex:platform_id;not null;column:platform_id;comment:所在平台id,telegram时id,指纹时hash"`
Address string `json:"type:varchar(255);index;column:address;comment:地址"`
Keystore string `gorm:"type:text;column:keystore;comment:keystore"`
gorm.Model
}
## telegram
## email
## sms
## fingerprint
\ No newline at end of file
package sender
import (
"gopkg.in/gomail.v2"
)
var (
smtpServer = "smtp.gmail.com"
smtpPort = 587
username string
password string
)
type EmailSender struct {
Username string
Password string
}
func NewEmailSender() *EmailSender {
return &EmailSender{
Username: username,
Password: password,
}
}
func (s *EmailSender) Send(dest string, code string) (err error) {
m := gomail.NewMessage()
m.SetHeader("From", s.Username)
m.SetHeader("To", dest)
m.SetHeader("Subject", "Test Email")
m.SetBody("text/html", "<h1>Hello, this is a test email!</h1>")
d := gomail.NewDialer(smtpServer, smtpPort, s.Username, s.Password)
return d.DialAndSend(m)
}
package sender
import (
"sdk_api/config"
)
type Sender struct {
sms *SMSSender
email *EmailSender
}
func NewSender(cfg *config.SenderConfig) (*Sender, error) {
sms := NewSMSSender()
email := NewEmailSender()
return &Sender{
sms: sms,
email: email,
}, nil
}
package sender
type SMSSender struct {
}
func NewSMSSender() *SMSSender {
return &SMSSender{}
}
package server
import (
"sdk_api/middleware"
"github.com/gin-gonic/gin"
)
func initRouter(e *gin.Engine) {
e.Use(middleware.PrintRequestResponseBodyMiddleware())
v1 := e.Group("/api/v1")
{
user := v1.Group("/user")
user.POST("/check", checkUser)
user.POST("/create", middleware.JWTMiddleware, createUser)
}
}
package server
import (
"sdk_api/config"
"sdk_api/service"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
var srv *service.Service
var conf *config.Config
func StartServer(_srv *service.Service, _conf *config.Config) {
srv = _srv
conf = _conf
if !conf.Debug {
gin.SetMode(gin.ReleaseMode)
}
engine := gin.Default()
_cors := cors.DefaultConfig()
_cors.AllowAllOrigins = true
_cors.AllowHeaders = []string{"*"}
engine.Use(cors.New(_cors))
initRouter(engine)
log.Infof("start http server listening %s", conf.Server.Listen)
if err := engine.Run(conf.Server.Listen); err != nil {
log.Error("http server run error: ", err)
}
}
func withSuccess(obj interface{}) interface{} {
return gin.H{
"code": 0,
"msg": "ok",
"data": obj,
}
}
func withError(msg string) interface{} {
return gin.H{
"code": 1,
"error": msg,
"data": "",
}
}
package server
import (
"sdk_api/constant"
apiModel "sdk_api/model/api"
"sdk_api/util"
"strings"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)
func checkUser(c *gin.Context) {
req := &apiModel.CheckUserRequest{}
if err := c.ShouldBindJSON(req); err != nil {
withError(constant.InvalidParam)
return
}
switch req.Platform {
case constant.PlatformTelegram:
ok, userId := util.VerifyInitData(req.InitData, "5000822884:AAHtZnIGjV1yYMfN6s_pAzoiZWDf5JWWTko")
if !ok {
c.JSON(200, withError("invalid initData"))
return
}
ok, uid, keystore, err := srv.CheckUser(constant.PlatformTelegram, userId)
if err != nil {
c.JSON(200, withError(constant.InternalError))
return
}
token := util.GenerateJWT(uid, constant.PlatformTelegram, userId)
resp := &apiModel.CheckUserResponse{
IsNewUser: !ok,
Keystore: keystore,
Token: token,
}
c.JSON(200, withSuccess(resp))
return
case constant.PlatformFingerprint:
userId := req.VisitorID
ok, uid, keystore, err := srv.CheckUser(constant.PlatformFingerprint, userId)
if err != nil {
c.JSON(200, withError(constant.InternalError))
return
}
token := util.GenerateJWT(uid, constant.PlatformFingerprint, userId)
resp := &apiModel.CheckUserResponse{
IsNewUser: !ok,
Keystore: keystore,
Token: token,
}
c.JSON(200, withSuccess(resp))
default:
c.JSON(200, withError(constant.UnsupportedPlatform))
return
}
}
func createUser(c *gin.Context) {
req := &apiModel.CreateUserRequest{}
if err := c.ShouldBindJSON(req); err != nil {
c.JSON(200, withError(constant.InvalidParam))
return
}
uid := c.GetString("jwt-uid")
address := gjson.Get(req.Keystore, "address").String()
binSignature, err := hexutil.Decode(req.Signature)
if err != nil || len(binSignature) < 65 {
c.JSON(200, withError("invalid signature"))
return
}
binSignature[64] -= 27
ecdsaPub, err := crypto.SigToPub(accounts.TextHash([]byte(req.Keystore)), binSignature)
if err != nil {
c.JSON(200, withError("invalid signature"))
return
}
addr := crypto.PubkeyToAddress(*ecdsaPub)
log.Debugln(addr.Hex())
if strings.ToLower(addr.Hex()[2:]) != address {
c.JSON(200, withError("invalid signature"))
return
}
ok, err := srv.SetKeystore(uid, strings.ToLower(addr.Hex()), req.Keystore)
if err != nil {
c.JSON(200, withError(constant.InternalError))
return
}
if !ok {
c.JSON(200, withError("keystore already exist"))
return
}
c.JSON(200, withSuccess(""))
}
package service
import (
"sdk_api/config"
"sdk_api/dao"
)
type Service struct {
d *dao.Dao
cfg *config.Config
}
func New(conf *config.Config, da *dao.Dao) *Service {
return &Service{
d: da,
cfg: conf,
}
}
package service
import (
dbModel "sdk_api/model/db"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
func (s *Service) CheckUser(platform string, platformId string) (exist bool, uid, keystore string, err error) {
user, err := s.d.CheckUser(platform, platformId)
if err != nil {
log.WithError(err).Error("get user failed")
return
}
if user == nil {
uid := uuid.New().String()
err = s.d.CreateEmptyUser(&dbModel.User{
Uid: uid,
Platform: platform,
PlatformId: platformId,
})
if err != nil {
log.WithError(err).Error("create user failed")
return false, "", "", err
}
return false, uid, "", nil
}
return user.Keystore != "", user.Uid, user.Keystore, nil
}
func (s *Service) SetKeystore(uid, address, keystore string) (ok bool, err error) {
user, err := s.d.GetUserByUid(uid)
if err != nil {
log.WithError(err).Error("get user failed")
return
}
if user.Keystore != "" {
log.WithField("uid", uid).Warn("keystore already exist")
return false, nil
}
err = s.d.SetKeystore(uid, address, keystore)
if err != nil {
log.WithError(err).Error("set keystore failed")
return
}
return true, nil
}
package util
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
const secret = "cxcZa005Y5zWH1wFgXvPGDL02Ey4ZCLAh2XFcfp7HhG3wTg5TbcnhuYhNvN3YLgt"
func GenerateJWT(uid, platform, platformId string) string {
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"uid": uid,
"platform": platform,
"platformId": platformId,
"iat": time.Now().Unix(),
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
})
j, _ := tk.SignedString([]byte(secret))
return j
}
func ParseJWT(token string) (ok, expired bool, uid, platform, platformId string) {
claims := jwt.MapClaims{}
tk, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return
}
if !tk.Valid {
return
}
uid = claims["uid"].(string)
platform = claims["platform"].(string)
platformId = claims["platformId"].(string)
exp := claims["exp"].(float64)
if time.Now().Unix() > int64(exp) {
expired = true
return
}
ok = true
return
}
package util
import (
"testing"
)
func TestA(t *testing.T) {
token := GenerateJWT("12345", "telegram", "0x12345")
t.Logf("token: %s", token)
ParseJWT(token)
}
func TestB(t *testing.T) {
t.Log(ParseJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTgwMTU2MTQsImlhdCI6MTcxNzQxMDgxNCwicGxhdGZvcm0iOiJ0ZWxlZ3JhbSIsInBsYXRmb3JtSWQiOiIweDEyMzQ1IiwidWlkIjoiMTIzNDUifQ.yZ1V_cGozBrwK55Y9iZsG4C-B5T96V2E3-AqP6CqkR8"))
}
package util
import (
"crypto/hmac"
"crypto/sha256"
"fmt"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/tidwall/gjson"
)
func VerifyInitData(initData, botToken string) (ok bool, userId string) {
h := hmac.New(sha256.New, []byte("WebAppData"))
h.Write([]byte(botToken))
secret := h.Sum(nil)
h2 := hmac.New(sha256.New, secret)
params, err := url.ParseQuery(initData)
if err != nil {
return
}
var hashval string
var keys []string
for key := range params {
if key == "hash" {
hashval = params.Get(key)
continue
}
if key == "auth_date" {
authDate, _ := strconv.Atoi(params.Get(key))
if int64(authDate) < time.Now().Unix()-1800 {
// todo 可以限制超时时间
// return false
}
}
if key == "user" {
userId = gjson.Get(params.Get(key), "id").String()
}
keys = append(keys, key)
}
sort.Strings(keys)
var payloads []string
for _, key := range keys {
payloads = append(payloads, fmt.Sprintf("%s=%s", key, params.Get(key)))
}
payload := strings.Join(payloads, "\n")
h2.Write([]byte(payload))
h2sum := h2.Sum(nil)
ok = fmt.Sprintf("%x", h2sum) == hashval
return
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment