diff --git a/config/config.go b/config/config.go index 99281cf6..04381c39 100644 --- a/config/config.go +++ b/config/config.go @@ -4,12 +4,15 @@ import ( "errors" "os" "path/filepath" + "strings" "github.com/caarlos0/env/v6" "github.com/gin-gonic/gin" + "github.com/google/uuid" "github.com/joho/godotenv" "github.com/yaoapp/kun/exception" "github.com/yaoapp/kun/log" + "github.com/yaoapp/yao/crypto" ) // Conf 配置参数 @@ -54,6 +57,17 @@ func Load() Config { exception.New("Can't read config %s", 500, err.Error()).Throw() } cfg.Root, _ = filepath.Abs(cfg.Root) + + // Studio Secret + if cfg.Studio.Secret == nil { + v, err := crypto.Hash(crypto.HashTypes["SHA256"], uuid.New().String()) + if err != nil { + exception.New("Can't gengrate studio secret %s", 500, err.Error()).Throw() + } + cfg.Studio.Secret = []byte(strings.ToUpper(v)) + cfg.Studio.Auto = true + } + return cfg } diff --git a/config/types.go b/config/types.go index e7fd6f48..d3ed6c12 100644 --- a/config/types.go +++ b/config/types.go @@ -20,8 +20,9 @@ type Config struct { // StudioConfig the studio config type StudioConfig struct { - Port int `json:"studio_port,omitempty" env:"YAO_STUDIO_PORT" envDefault:"5077"` // Studio port - Secret int `json:"studio_secret,omitempty" env:"YAO_STUDIO_SECRET"` // Studio Secret, if does not set, auto-generate a secret + Port int `json:"studio_port,omitempty" env:"YAO_STUDIO_PORT" envDefault:"5077"` // Studio port + Secret []byte `json:"studio_secret,omitempty" env:"YAO_STUDIO_SECRET"` // Studio Secret, if does not set, auto-generate a secret + Auto bool `json:"-"` } // DBConfig 数据库配置 diff --git a/crypto/crypto.go b/crypto/crypto.go index 96f99954..eb5aaf0b 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -13,7 +13,8 @@ func init() { crypto.RegisterHash(crypto.MD4, md4.New) } -var hashTypes = map[string]crypto.Hash{ +// HashTypes string +var HashTypes = map[string]crypto.Hash{ "MD4": crypto.MD4, "MD5": crypto.MD5, "SHA1": crypto.SHA1, diff --git a/crypto/process.go b/crypto/process.go index 235950cb..be828002 100644 --- a/crypto/process.go +++ b/crypto/process.go @@ -18,7 +18,7 @@ func ProcessHash(process *gou.Process) interface{} { typ := process.ArgsString(0) value := process.ArgsString(1) - h, has := hashTypes[typ] + h, has := HashTypes[typ] if !has { exception.New("%s does not support", 400, typ).Throw() } @@ -41,7 +41,7 @@ func ProcessHmac(process *gou.Process) interface{} { value := process.ArgsString(1) key := process.ArgsString(2) - h, has := hashTypes[typ] + h, has := HashTypes[typ] if !has { exception.New("%s does not support", 400, typ).Throw() } diff --git a/helper/jwt.go b/helper/jwt.go index 8a978e26..fb04926f 100644 --- a/helper/jwt.go +++ b/helper/jwt.go @@ -28,9 +28,15 @@ type JwtToken struct { } // JwtValidate JWT 校验 -func JwtValidate(tokenString string) *JwtClaims { +func JwtValidate(tokenString string, secret ...[]byte) *JwtClaims { + + jwtSecret := []byte(config.Conf.JWTSecret) + if len(secret) > 0 { + jwtSecret = secret[0] + } + token, err := jwt.ParseWithClaims(tokenString, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(config.Conf.JWTSecret), nil + return jwtSecret, nil }) if err != nil { @@ -49,7 +55,13 @@ func JwtValidate(tokenString string) *JwtClaims { // JwtMake 生成 JWT // option: {"subject":"<主题>", "audience": "<接收人>", "issuer":"<签发人>", "timeout": "<有效期,单位秒>", "sid":"<会话ID>"} -func JwtMake(id int, data map[string]interface{}, option map[string]interface{}) JwtToken { +func JwtMake(id int, data map[string]interface{}, option map[string]interface{}, secret ...[]byte) JwtToken { + + jwtSecret := []byte(config.Conf.JWTSecret) + if len(secret) > 0 { + jwtSecret = secret[0] + } + now := time.Now().Unix() sid := "" timeout := int64(3600) @@ -96,7 +108,7 @@ func JwtMake(id int, data map[string]interface{}, option map[string]interface{}) }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(config.Conf.JWTSecret)) + tokenString, err := token.SignedString([]byte(jwtSecret)) if err != nil { exception.New("生成令牌失败", 500).Ctx(err).Throw() } diff --git a/importer/importer.go b/importer/importer.go index 5dca3bf0..6f09f3c5 100644 --- a/importer/importer.go +++ b/importer/importer.go @@ -69,6 +69,12 @@ func Open(name string) from.Source { return nil } +// WithSid attch sid +func (imp *Importer) WithSid(sid string) *Importer { + imp.Sid = sid + return imp +} + // AutoMapping 根据文件信息获取字段映射表 func (imp *Importer) AutoMapping(src from.Source) *Mapping { sourceColumns := getSourceColumns(src) @@ -405,7 +411,7 @@ func (imp *Importer) Run(src from.Source, mapping *Mapping) interface{} { return } - response, err := process.Exec() + response, err := process.WithSID(imp.Sid).Exec() if err != nil { failed = failed + length log.With(log.F{"line": line}).Error("导入失败: %s", err.Error()) @@ -437,7 +443,7 @@ func (imp *Importer) Run(src from.Source, mapping *Mapping) interface{} { } if imp.Output != "" { - res, err := gou.NewProcess(imp.Output, output).Exec() + res, err := gou.NewProcess(imp.Output, output).WithSID(imp.Sid).Exec() if err != nil { log.With(log.F{"output": imp.Output}).Error(err.Error()) return output diff --git a/importer/process.go b/importer/process.go index a5a91641..1c4e3f05 100644 --- a/importer/process.go +++ b/importer/process.go @@ -21,7 +21,7 @@ func init() { func ProcessRun(process *gou.Process) interface{} { process.ValidateArgNums(3) name := process.ArgsString(0) - imp := Select(name) + imp := Select(name).WithSid(process.Sid) filename := process.ArgsString(1) src := Open(filename) defer src.Close() @@ -34,7 +34,7 @@ func ProcessRun(process *gou.Process) interface{} { func ProcessSetting(process *gou.Process) interface{} { process.ValidateArgNums(1) name := process.ArgsString(0) - imp := Select(name) + imp := Select(name).WithSid(process.Sid) return map[string]interface{}{ "mappingPreview": imp.Option.MappingPreview, "dataPreview": imp.Option.DataPreview, @@ -48,7 +48,7 @@ func ProcessSetting(process *gou.Process) interface{} { func ProcessData(process *gou.Process) interface{} { process.ValidateArgNums(5) name := process.ArgsString(0) - imp := Select(name) + imp := Select(name).WithSid(process.Sid) filename := process.ArgsString(1) src := Open(filename) @@ -66,7 +66,7 @@ func ProcessData(process *gou.Process) interface{} { func ProcessDataSetting(process *gou.Process) interface{} { process.ValidateArgNums(1) name := process.ArgsString(0) - imp := Select(name) + imp := Select(name).WithSid(process.Sid) return imp.DataSetting() } @@ -75,7 +75,7 @@ func ProcessDataSetting(process *gou.Process) interface{} { func ProcessMapping(process *gou.Process) interface{} { process.ValidateArgNums(2) name := process.ArgsString(0) - imp := Select(name) + imp := Select(name).WithSid(process.Sid) filename := process.ArgsString(1) src := Open(filename) @@ -88,7 +88,7 @@ func ProcessMapping(process *gou.Process) interface{} { func ProcessMappingSetting(process *gou.Process) interface{} { process.ValidateArgNums(2) name := process.ArgsString(0) - imp := Select(name) + imp := Select(name).WithSid(process.Sid) filename := process.ArgsString(1) src := Open(filename) diff --git a/importer/types.go b/importer/types.go index db0ca322..ebf8114c 100644 --- a/importer/types.go +++ b/importer/types.go @@ -17,6 +17,7 @@ type Importer struct { Columns []Column `json:"columns"` // 字段列表 Option Option `json:"option,omitempty"` // 导入配置项 Rules map[string]string `json:"rules,omitempty"` // 许可导入规则 + Sid string `json:"-"` // sid } // Column 导入字段定义 diff --git a/studio/middleware.go b/studio/middleware.go index 411855f7..437d7a31 100644 --- a/studio/middleware.go +++ b/studio/middleware.go @@ -3,12 +3,17 @@ package studio import ( "fmt" "net/http" + "strings" "github.com/gin-gonic/gin" "github.com/yaoapp/kun/exception" + "github.com/yaoapp/kun/log" "github.com/yaoapp/xun" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/helper" ) +// hdRecovered custom recovered func hdRecovered(c *gin.Context, recovered interface{}) { var code = http.StatusInternalServerError @@ -39,3 +44,37 @@ func hdRecovered(c *gin.Context, recovered interface{}) { c.AbortWithStatus(code) } + +// cross domian +func hdCrossDomain(c *gin.Context) { +} + +// studio API Auth +func hdAuth(c *gin.Context) { + + tokenString := c.Request.Header.Get("Authorization") + if strings.HasPrefix(tokenString, "Bearer") { + tokenString = strings.TrimSpace(strings.TrimPrefix(tokenString, "Bearer ")) + if tokenString == "" { + c.JSON(403, gin.H{"code": 403, "message": "No permission"}) + c.Abort() + return + } + + claims := helper.JwtValidate(tokenString, config.Conf.Studio.Secret) + c.Set("__sid", claims.SID) + return + + } else if strings.HasPrefix(tokenString, "Signature ") { // For Yao Studio + signature := strings.TrimSpace(strings.TrimPrefix(tokenString, "Signature ")) + nonce := c.Request.Header.Get("Studio-Nonce") + ts := c.Request.Header.Get("Studio-Timestamp") + query := c.Request.URL.Query() + log.Trace("[Studio] %s, %s %s %v", signature, nonce, ts, query) + return + } + + c.JSON(403, gin.H{"code": 403, "message": "No permission"}) + c.Abort() + return +} diff --git a/studio/router.go b/studio/router.go index 6bb90e54..8623f684 100644 --- a/studio/router.go +++ b/studio/router.go @@ -18,7 +18,7 @@ var regExcp = regexp.MustCompile("^Exception\\|([0-9]+):(.+)$") // Serve start the api server func setRouter(router *gin.Engine) { - router.Use(gin.CustomRecovery(hdRecovered)) + router.Use(gin.CustomRecovery(hdRecovered), hdAuth) // DSL ReadDir, ReadFile router.GET("/dsl/:method", func(c *gin.Context) { @@ -202,7 +202,12 @@ func setRouter(router *gin.Engine) { return } - res, err := gou.Yao.Engine.Call(map[string]interface{}{}, service, fun.Method, fun.Args...) + req := gou.Yao.New(service, fun.Method) + if sid, has := c.Get("__sid"); has { + req.WithSid(fmt.Sprintf("%s", sid)) + } + + res, err := req.Call(fun.Args...) if err != nil { // parse Exception code := 500 diff --git a/studio/studio_test.go b/studio/studio_test.go index 97548c6d..2bb773c2 100644 --- a/studio/studio_test.go +++ b/studio/studio_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/yaoapp/gou" "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/helper" ) type kv map[string]interface{} @@ -57,7 +58,7 @@ func TestStartStopError(t *testing.T) { time.Sleep(100 * time.Millisecond) } -func TestGetAPI(t *testing.T) { +func TestAPI(t *testing.T) { Load(config.Conf) @@ -132,7 +133,16 @@ func httpGet[T kv | arr | interface{} | map[string]interface{} | int | []interfa var data T url = fmt.Sprintf("http://127.0.0.1:%d%s", config.Conf.Studio.Port, url) - res, err := http.Get(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + t.Fatal(err) + } + + token := getToken(t) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + client := http.Client{} + res, err := client.Do(req) if err != nil { t.Fatal(err) } @@ -143,10 +153,10 @@ func httpGet[T kv | arr | interface{} | map[string]interface{} | int | []interfa t.Fatal(err) } - if body != nil { + if body != nil && len(body) > 0 { err = jsoniter.Unmarshal(body, &data) if err != nil { - t.Fatal(err) + t.Fatal(fmt.Sprintf("%s\n%s\n", err.Error(), string(body))) } } } @@ -164,11 +174,16 @@ func httpPost[T kv | arr | interface{} | map[string]interface{} | int | []interf } url = fmt.Sprintf("http://127.0.0.1:%d%s", config.Conf.Studio.Port, url) - res, err := http.Post(url, "application/json", buff) + req, err := http.NewRequest("POST", url, buff) if err != nil { t.Fatal(err) } + token := getToken(t) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + client := http.Client{} + res, err := client.Do(req) if res.Body != nil { body, err := io.ReadAll(res.Body) if err != nil { @@ -197,3 +212,12 @@ func httpPostJSON[T kv | arr | interface{} | map[string]interface{} | int | []in } return httpPost[T](url, data, t) } + +func getToken(t *testing.T) string { + return helper.JwtMake( + 1, + map[string]interface{}{"id": 1, "user_id": 1, "user": kv{"id": 1, "name": "test"}}, + map[string]interface{}{"issuer": "unit-test", "timeout": 3600}, + config.Conf.Studio.Secret, + ).Token +}