From 839f580e4cb1ed4cf299ffc2096f0d9fbf58f4f1 Mon Sep 17 00:00:00 2001 From: Star <> Date: Mon, 27 May 2024 17:03:15 +0800 Subject: [PATCH] first --- .gitignore | 6 + README.md | 5 +- go.mod | 33 ++ plugin.go | 999 ++++++++++++++++++++++++++++++++++++++++ tests/api/hello.js | 6 + tests/api/ws.js | 8 + tests/config.yml | 48 ++ tests/go.mod | 37 ++ tests/jsImports_test.go | 3 + tests/plugin_test.go | 28 ++ tests/plugin_test.js | 56 +++ 11 files changed, 1227 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 plugin.go create mode 100644 tests/api/hello.js create mode 100644 tests/api/ws.js create mode 100644 tests/config.yml create mode 100644 tests/go.mod create mode 100644 tests/jsImports_test.go create mode 100644 tests/plugin_test.go create mode 100644 tests/plugin_test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6aa1496 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.* +!.gitignore +go.sum +/build +/node_modules +/package.json diff --git a/README.md b/README.md index 82ce6b7..ddc13ec 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # service -create a web service over http/https/h2/h2c -service plugin for https://github.com/ssgo/s \ No newline at end of file +create a web service over http/https/h2/h2c + +service plugin for https://github.com/ssgo/s diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..466db1b --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module apigo.cloud/git/apigo/service + +go 1.18 + +require ( + apigo.cloud/git/apigo/gojs v0.0.8 + apigo.cloud/git/apigo/plugin v1.0.1 + github.com/gorilla/websocket v1.5.1 + github.com/ssgo/discover v1.7.5 + github.com/ssgo/httpclient v1.7.5 + github.com/ssgo/log v1.7.5 + github.com/ssgo/redis v1.7.5 + github.com/ssgo/s v1.7.5 + github.com/ssgo/u v1.7.5 +) + +require ( + apigo.cloud/git/apigo/qjs v0.0.1 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gomodule/redigo v1.8.8 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.22.10 // indirect + github.com/ssgo/config v1.7.5 // indirect + github.com/ssgo/standard v1.7.5 // indirect + github.com/tklauser/go-sysconf v0.3.10 // indirect + github.com/tklauser/numcpus v0.4.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..026f962 --- /dev/null +++ b/plugin.go @@ -0,0 +1,999 @@ +package service + +import ( + "apigo.cloud/git/apigo/gojs" + "apigo.cloud/git/apigo/plugin" + "errors" + "fmt" + "github.com/gorilla/websocket" + "github.com/ssgo/discover" + "github.com/ssgo/httpclient" + "github.com/ssgo/log" + "github.com/ssgo/redis" + "github.com/ssgo/s" + "github.com/ssgo/u" + "net/http" + "path/filepath" + "regexp" + "strings" + "sync" + "time" +) + +// 常驻内存的API池 + +type APICache struct { + Filename string + Mtime int64 + Code *gojs.PreCompiledCode +} + +var _apiCache = map[string]*APICache{} +var _apiCacheLock = sync.RWMutex{} + +type APIConfig struct { + AuthLevel int + Method string + Host string + Path string + Memo string + ActionFile string +} + +func (conf *APIConfig) String() string { + return fmt.Sprintf("%s_%s_%s_%d", conf.Host, conf.Method, conf.Path, conf.AuthLevel) +} + +var apiPath = "api" +var apiHotLoadInterval = 5000 + +var _data = map[string]any{} +var _dataLock = sync.RWMutex{} + +type Session struct { + id string + conn *redis.Redis + data map[string]any +} + +var sessionRedis *redis.Redis +var sessionTimeout = 3600 +var memorySessionData = map[string]map[string]any{} +var memorySessionDataLock = sync.RWMutex{} + +func NewSession(id string, logger *log.Logger) *Session { + data := map[string]any{} + conn := sessionRedis + if sessionRedis == nil { + memorySessionDataLock.RLock() + data1 := memorySessionData[id] + memorySessionDataLock.RUnlock() + if data1 != nil { + data = data1 + } + } else { + conn = sessionRedis.CopyByLogger(logger) + _ = conn.GET("SESS_" + id).To(&data) + } + return &Session{ + id: id, + conn: conn, + data: data, + } +} + +func (session *Session) Set(key string, value any) { + session.data[key] = value +} + +func (session *Session) MSet(data map[string]any) { + for key, value := range data { + session.data[key] = value + } +} + +func (session *Session) Get(key string) any { + return session.data[key] +} + +func (session *Session) MGet(keys ...string) map[string]any { + out := make(map[string]any) + for _, key := range keys { + out[key] = session.data[key] + } + return out +} + +func (session *Session) Remove(keys ...string) { + for _, key := range keys { + delete(session.data, key) + } +} + +func (session *Session) Save() { + if session.conn == nil { + session.data["_time"] = time.Now().Unix() + memorySessionDataLock.Lock() + memorySessionData[session.id] = session.data + memorySessionDataLock.Unlock() + } else { + session.conn.SETEX("SESS_"+session.id, sessionTimeout, session.data) + } +} + +type ProxyByResult struct { + AuthLevel int + ToApp *string + ToPath *string + Headers map[string]string +} + +var apiMatcher = regexp.MustCompile(`(?im)^\s*//(.*?)(GET|POST|PUT|DELETE|HEAD|OPTION|WS|\*)\s+(/\S*)(.*?)$`) +var authLevelMatcher = regexp.MustCompile(`(?i)AuthLevel:\s*(\S+)`) +var hostMatcher = regexp.MustCompile(`(?i)Host:\s*(\S+)`) + +func autoLoadAPI(root string) { + for _, f := range u.ReadDirN(root) { + if !strings.HasPrefix(f.Name, ".") && !strings.HasPrefix(f.Name, "_") { + filename := filepath.Join(root, f.Name) + if f.IsDir { + autoLoadAPI(filename) + } else if strings.HasSuffix(f.Name, ".js") { + if m := apiMatcher.FindStringSubmatch(u.ReadFileN(filename)); m != nil { + method := strings.ToUpper(m[2]) + if method == "*" { + method = "" + } + path := m[3] + authLevel := 0 + host := "" + memo := m[1] + " " + m[4] + + if m2 := authLevelMatcher.FindStringSubmatch(memo); m2 != nil { + authLevel = u.Int(m2[1]) + memo = strings.Replace(memo, m2[0], "", 1) + } + if m2 := hostMatcher.FindStringSubmatch(memo); m2 != nil { + host = m2[1] + memo = strings.Replace(memo, m2[0], "", 1) + } + memo = strings.TrimSpace(memo) + + //fmt.Println(filename, u.BMagenta(authLevel), u.BCyan(method), u.BCyan(path), u.BMagenta(host), memo) + conf := APIConfig{ + Path: path, + Method: method, + AuthLevel: authLevel, + Host: host, + Memo: memo, + ActionFile: filename, + } + if method == "WS" { + registerWebsocket(&conf) + } else { + register(&conf) + } + } + } + } + } +} + +func makeAPICache(apiKey, filename string) { + var apiCache *APICache + if code, err := gojs.PreCompileFile(filename, s.ServerLogger); err == nil { + apiCache = &APICache{ + Filename: filename, + Mtime: u.GetFileInfo(filename).ModTime.UnixMilli(), + Code: code, + } + _apiCacheLock.Lock() + _apiCache[apiKey] = apiCache + _apiCacheLock.Unlock() + } +} + +func register(conf *APIConfig) { + apiKey := conf.String() + makeAPICache(apiKey, conf.ActionFile) + s.RestfulWithOptions(conf.AuthLevel, conf.Method, conf.Path, func(args map[string]any, headers map[string]string, request *s.Request, response *s.Response, caller *discover.Caller, session *Session, logger *log.Logger) any { + _apiCacheLock.RLock() + apiCache := _apiCache[apiKey] + _apiCacheLock.RUnlock() + if apiCache != nil { + return runAPI(apiCache.Code, args, headers, request, response, caller, session, logger) + } + return nil + }, conf.Memo, s.WebServiceOptions{Host: conf.Host}) +} + +func registerWebsocket(conf *APIConfig) { + apiKey := conf.String() + makeAPICache(apiKey, conf.ActionFile) + s.RegisterSimpleWebsocketWithOptions(conf.AuthLevel, conf.Path, func(args map[string]any, headers map[string]string, request *s.Request, client *websocket.Conn, caller *discover.Caller, session *Session, logger *log.Logger) any { + _apiCacheLock.RLock() + apiCache := _apiCache[apiKey] + _apiCacheLock.RUnlock() + if apiCache != nil { + return runWS(apiCache.Code, args, headers, request, client, caller, session, logger) + } + return nil + }, conf.Memo, s.WebServiceOptions{Host: conf.Host}) +} + +func preStart() { + gojs.LoadPluginsConfig("config.yml") + autoLoadAPI(apiPath) + if apiHotLoadInterval > 0 { + if apiHotLoadInterval < 100 { + apiHotLoadInterval = 100 + } + s.NewTimerServer("_apiHotLoad", time.Millisecond*time.Duration(apiHotLoadInterval), func(isRunning *bool) { + checkList := map[string]*APICache{} + _apiCacheLock.RLock() + for apiKey, apiCache := range _apiCache { + checkList[apiKey] = apiCache + } + _apiCacheLock.RUnlock() + if *isRunning == false { + return + } + for apiKey, apiCache := range checkList { + if fileInfo := u.GetFileInfo(apiCache.Filename); fileInfo != nil { + if fileInfo.ModTime.UnixMilli() > apiCache.Mtime { + s.ServerLogger.Info("api file changed, reloading...", "filename", apiCache.Filename) + makeAPICache(apiKey, apiCache.Filename) + } + } + if *isRunning == false { + break + } + } + }, nil, nil) + } +} + +func init() { + plugin.Register(plugin.Plugin{ + Id: "apigo.cloud/git/apigo/service", + Name: "web service framework by github.com/ssgo/s", + Objects: map[string]any{ + "start": func() { + preStart() + s.Start() + }, + "asyncStart": func() *AsyncServer { + preStart() + as := s.AsyncStart() + c := &AsyncServer{ + Addr: as.Addr, + Proto: as.Proto, + ProtoName: as.ProtoName, + as: as, + globalHeaders: nil, + } + return c + }, + "addTask": func(name string, intervalMS uint, onRunning func(running *bool), onStart func(), onStop func()) { + s.NewTimerServer(name, time.Duration(intervalMS)*time.Microsecond, onRunning, onStart, onStop) + }, + "onStop": func(cb func()) { + s.AddShutdownHook(cb) + }, + "startSession": func(authenticator func(authLevel int, url *string, args map[string]any, request *s.Request, response *s.Response, session *Session) (pass bool), ctx plugin.Context) { + s.SetAuthChecker(func(authLevel int, logger *log.Logger, url *string, args map[string]any, request *s.Request, response *s.Response, options *s.WebServiceOptions) (pass bool, object any) { + session := NewSession(request.GetSessionId(), logger) + return authenticator(authLevel, url, args, request, response, session), session + }) + }, + "setAuthenticator": func(authenticator func(authLevel int, logger *log.Logger, url *string, args map[string]any, request *s.Request, response *s.Response, options *s.WebServiceOptions) (pass bool, object any), ctx plugin.Context) { + s.SetAuthChecker(authenticator) + }, + "setInFilter": func(cb func(args *map[string]any, request *s.Request, response *s.Response) (out any)) { + s.SetInFilter(func(args *map[string]any, request *s.Request, response *s.Response, logger *log.Logger) (out any) { + return cb(args, request, response) + }) + }, + "setOutFilter": func(cb func(args map[string]any, request *s.Request, response *s.Response, out any) (isOver bool)) { + s.SetOutFilter(func(args map[string]any, request *s.Request, response *s.Response, out any, logger *log.Logger) (newOut any, isOver bool) { + return nil, cb(args, request, response, out) + }) + }, + "proxy": func(authLevel int, path string, toApp, toPath string) { + s.Proxy(authLevel, path, toApp, toPath) + }, + "proxyBy": func(cb func(request *s.Request) ProxyByResult) { + s.SetProxyBy(func(request *s.Request) (authLevel int, toApp, toPath *string, headers map[string]string) { + r := cb(request) + return r.AuthLevel, r.ToApp, r.ToPath, r.Headers + }) + }, + "rewrite": func(path string, toPath string) { + s.Rewrite(path, toPath) + }, + "rewriteBy": func(cb func(request *s.Request) (toPath string)) { + s.SetRewriteBy(func(request *s.Request) (toPath string, rewrite bool) { + toPath = cb(request) + return toPath, toPath != "" + }) + }, + "static": func(requestPath string, filePath string) { + s.Static(requestPath, filePath) + }, + "staticByHost": func(hostname string, requestPath string, filePath string) { + s.StaticByHost(requestPath, filePath, hostname) + }, + "register": func(conf *APIConfig) { + register(conf) + }, + "registerWebsocket": func(conf *APIConfig) { + registerWebsocket(conf) + }, + "getArgs": func(ctx *plugin.Context) map[string]any { + if v, ok := ctx.GetData("args").(map[string]any); ok { + return v + } + return nil + }, + "getHeaders": func(ctx *plugin.Context) map[string]string { + if v, ok := ctx.GetData("headers").(map[string]string); ok { + return v + } + return nil + }, + "getRequest": func(ctx *plugin.Context) *s.Request { + if v, ok := ctx.GetData("request").(*s.Request); ok { + return v + } + return nil + }, + "getResponse": func(ctx *plugin.Context) *s.Response { + if v, ok := ctx.GetData("response").(*s.Response); ok { + return v + } + return nil + }, + "getClient": func(ctx *plugin.Context) *WS { + if v, ok := ctx.GetData("client").(*websocket.Conn); ok { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return &WS{ + conn: v, + closed: false, + logger: logger, + } + } + return nil + }, + "getCaller": func(ctx *plugin.Context) *DiscoverClient { + if v, ok := ctx.GetData("caller").(*DiscoverClient); ok { + return v + } + return &DiscoverClient{ + caller: &discover.Caller{}, + logger: ctx.GetInject("*log.Logger").(*log.Logger), + globalHeaders: make(map[string]string), + } + }, + "getSession": func(ctx *plugin.Context) *Session { + if v, ok := ctx.GetData("session").(*Session); ok { + return v + } + return nil + }, + "setData": func(ctx *plugin.Context, key string, value any) { + _dataLock.Lock() + defer _dataLock.Unlock() + _data[key] = value + }, + "getData": func(ctx *plugin.Context, key string) any { + _dataLock.RLock() + defer _dataLock.RUnlock() + return _data[key] + }, + "addDiscoverApp": func(ctx *plugin.Context, app, callConfig string) bool { + return discover.AddExternalApp(app, callConfig) + }, + "addDiscoverAppForTest": func(ctx *plugin.Context, app, callConfig string) bool { + return discover.AddExternalAppManually(app, callConfig) + }, + "setDiscoverNode": func(ctx *plugin.Context, app, addr string, weight int) { + discover.SetNode(app, addr, weight) + }, + }, + ConfigSample: `listen: 80 # 监听端口(|隔开多个监听)(,隔开多个选项)例如 80,http|443|443:h2|127.0.0.1:8080,h2c +ssl: # SSL证书配置,key为域名,value为cert和key的文件路径 + a.com: # 证书匹配的域名 + certFile: /path/to/ssl.pem # cert文件路径 + keyFile: /path/to/ssl.key # key文件路径 +registry: # 服务发现使用的redis连接,例如:redis://:@127.0.0.1:6379/15 +app: # 注册成为一个服务,指定一个名称即可让其他服务使用这个名称来调用,默认不注册为服务 +weight: 100 # 服务节点的权重,默认值:100 +accessTokens: # 请求接口时使用指定的Access-Token进行验证,值为token对应的auth-level + huDWA-78we2-89uji1da: 1 # 该token将获得level为1的权限 + 89dsj-aADSsn-Uds1dad: 2 # 该token将获得level为2的权限 +calls: # 定义将会调用的服务,可以指定调用时使用的Access-Token、请求使用的协议、超时时间(:间隔) + app1: huDWA-78we2-89uji1da # 携带指定的Access-Token请求服务app1(h2c协议,超时时间为10s) + app2: 1:500ms # 不携带Access-Token请求服务app2(http协议,超时时间为500ms) + app3: s:30s:89dsj-aADSsn-Uds1dad # 携带指定Access-Token请求服务app3(https协议,超时时间为30s) +callRetryTimes: 10 # 节点连续失败超过指定次数将会被注销(只要有其他节点能工作不会影响正常访问),默认值:10 +ipPrefix: # 用discover注册发现服务时指定使用的IP网段,默认排除 172.17.(Docker) +keepaliveTimeout: 15000 # 连接允许空闲的最大时间,单位ms,默认值:15000 +noLogHeaders: Accept,Accept-Encoding,Cache-Control,Pragma,Connection,Upgrade-Insecure-Requests # 不记录请求头中包含的这些字段,多个字段用逗号分隔 +logInputArrayNum: 10 # 请求字段中容器类型(数组、Map)在日志打印个数限制 默认为10个,多余的数据将不再日志中记录 +logInputFieldSize: 500 # 请求字段中单个字段在日志打印长度限制 默认为500个字符,多余的数据将不再日志中记录 +logOutputArrayNum: 3 # 响应字段中容器类型(数组、Map)在日志打印个数限制 默认为3个,多余的数据将不再日志中记录 +logOutputFieldSize: 100 # 响应字段中单个字段在日志打印长度限制 默认为100个字符,多余的数据将不再日志中记录 +compress: false # 是否启用压缩,默认不启用 +compressMinSize: 1024 # 小于设定值的应答内容将不进行压缩,默认值:1024 +compressMaxSize: 4096000 # 大于设定值的应答内容将不进行压缩,默认值:4096000 +redirectTimeout: 10000 # proxy和discover发起请求时的超时时间,单位ms,默认值:10000 +acceptXRealIpWithoutRequestId: false # 是否允许头部没有携带请求ID的X-Real-IP信息,默认不允许(防止伪造客户端IP) +statisticTime: false # 是否开启请求时间统计,默认不开启 +statisticTimeInterval: 10000 # 统计时间间隔,单位ms,默认值:10000 +maxUploadSize: 104857600 # 最大上传文件大小(multipart/form-data请求的总空间),单位字节,默认值:104857600(100M) +cpu: 0 # CPU占用的核数,默认为0,即不做限制 +memory: 0 # 内存(单位M),默认为0,即不做限制 +cpuMonitor: false # 在日志中记录CPU使用情况,默认不开启 +memoryMonitor: false # 在日志中记录内存使用情况,默认不开启 +cpuLimitValue: 100 # CPU超过最高占用值(10-100)超过次数将自动重启(如果CpuMonitor开启的话),默认100 +memoryLimitValue: 95 # 内存超过最高占用值(10-100)超过次数将自动重启(如果MemoryMonitor开启的话),默认95 +cpuLimitTimes: 6 # CPU超过最高占用值超过次数(1-100)将报警(如果CpuMonitor开启的话),默认6(即30秒内连续6次) +memoryLimitTimes: 6 # 内存超过最高占用值超过次数(1-100)将报警(如果MemoryMonitor开启的话),默认6(即30秒内连续6次) +cookieScope: host # 启用Session时Cookie的有效范围,host|domain|topDomain,默认值为host +idServer: redis://:@127.0.0.1:6379/15 # 用uniqueId、id来生成唯一ID(雪花算法)时所需的redis服务器连接,例如:redis://:@127.0.0.1:6379/15,如果不指定将不能实现跨服务的全局唯一 +sessionServer: redis://:@127.0.0.1:6379/14 # 用来存储Session所需的redis服务器连接,例如:redis://:@127.0.0.1:6379/14,如果不指定将使用内存存储来实现Session共享 +sessionTimeout: 3600 # session过期时间,单位秒,默认值:3600 +apiPath: api # API接口文件的目录,默认为当前目录下的 api/ +apiHotLoadInterval: 0 # API文件热加载,当API接口检测到文件变化自动加载,单位毫秒,默认值:0(不开启) +`, + Init: func(conf map[string]any) { + if conf["listen"] != nil { + s.Config.Listen = u.String(conf["listen"]) + } + if conf["ssl"] != nil { + u.Convert(conf["ssl"], &s.Config.SSL) + } + if conf["registry"] != nil { + discover.Config.Registry = u.String(conf["registry"]) + } + if conf["app"] != nil { + discover.Config.App = u.String(conf["app"]) + } + if conf["weight"] != nil { + discover.Config.Weight = u.Int(conf["weight"]) + } + if conf["accessTokens"] != nil { + u.Convert(conf["accessTokens"], &s.Config.AccessTokens) + } + if conf["calls"] != nil { + u.Convert(conf["calls"], &discover.Config.Calls) + } + if conf["callRetryTimes"] != nil { + discover.Config.CallRetryTimes = u.Int(conf["callRetryTimes"]) + } + if conf["ipPrefix"] != nil { + s.Config.IpPrefix = u.String(conf["ipPrefix"]) + discover.Config.IpPrefix = u.String(conf["ipPrefix"]) + } + if conf["keepaliveTimeout"] != nil { + s.Config.KeepaliveTimeout = u.Int(conf["keepaliveTimeout"]) + } + if conf["noLogHeaders"] != nil { + s.Config.NoLogHeaders = u.String(conf["noLogHeaders"]) + } + if conf["logInputArrayNum"] != nil { + s.Config.LogInputArrayNum = u.Int(conf["logInputArrayNum"]) + } + if conf["logInputFieldSize"] != nil { + s.Config.LogInputFieldSize = u.Int(conf["logInputFieldSize"]) + } + if conf["logOutputArrayNum"] != nil { + s.Config.LogOutputArrayNum = u.Int(conf["logOutputArrayNum"]) + } + if conf["logOutputFieldSize"] != nil { + s.Config.LogOutputFieldSize = u.Int(conf["logOutputFieldSize"]) + } + if conf["compress"] != nil { + s.Config.Compress = u.Bool(conf["compress"]) + } + if conf["compressMinSize"] != nil { + s.Config.CompressMinSize = u.Int(conf["compressMinSize"]) + } + if conf["compressMaxSize"] != nil { + s.Config.CompressMaxSize = u.Int(conf["compressMaxSize"]) + } + if conf["redirectTimeout"] != nil { + s.Config.RedirectTimeout = u.Int(conf["redirectTimeout"]) + } + if conf["acceptXRealIpWithoutRequestId"] != nil { + s.Config.AcceptXRealIpWithoutRequestId = u.Bool(conf["acceptXRealIpWithoutRequestId"]) + } + if conf["statisticTime"] != nil { + s.Config.StatisticTime = u.Bool(conf["statisticTime"]) + } + if conf["statisticTimeInterval"] != nil { + s.Config.StatisticTimeInterval = u.Int(conf["statisticTimeInterval"]) + } + if conf["maxUploadSize"] != nil { + s.Config.MaxUploadSize = u.Int64(conf["maxUploadSize"]) + } + if conf["cpu"] != nil { + s.Config.Cpu = u.Int(conf["cpu"]) + } + if conf["memory"] != nil { + s.Config.Memory = u.Int(conf["memory"]) + } + if conf["cpuMonitor"] != nil { + s.Config.CpuMonitor = u.Bool(conf["cpuMonitor"]) + } + if conf["memoryMonitor"] != nil { + s.Config.MemoryMonitor = u.Bool(conf["memoryMonitor"]) + } + if conf["cpuLimitValue"] != nil { + s.Config.CpuLimitValue = u.Uint(conf["cpuLimitValue"]) + } + if conf["memoryLimitValue"] != nil { + s.Config.MemoryLimitValue = u.Uint(conf["memoryLimitValue"]) + } + if conf["cpuLimitTimes"] != nil { + s.Config.CpuLimitTimes = u.Uint(conf["cpuLimitTimes"]) + } + if conf["memoryLimitTimes"] != nil { + s.Config.MemoryLimitTimes = u.Uint(conf["memoryLimitTimes"]) + } + if conf["cookieScope"] != nil { + s.Config.CookieScope = u.String(conf["cookieScope"]) + } + if conf["idServer"] != nil { + s.Config.IdServer = u.String(conf["idServer"]) + } + if conf["sessionServer"] != nil { + sessionRedis = redis.GetRedis(u.String(conf["sessionServer"]), nil) + } + if conf["sessionTimeout"] != nil { + sessionTimeout = u.Int(conf["sessionTimeout"]) + } + if conf["apiPath"] != nil { + apiPath = u.String(conf["apiPath"]) + } + if conf["apiHotLoadInterval"] != nil { + apiHotLoadInterval = u.Int(conf["apiHotLoadInterval"]) + } + }, + }) +} + +func runAPI(code *gojs.PreCompiledCode, args map[string]any, headers map[string]string, request *s.Request, response *s.Response, caller *discover.Caller, session *Session, logger *log.Logger) any { + rt := gojs.New(&gojs.RuntimeOption{Logger: logger}) + rt.GoCtx.SetData("args", args) + rt.GoCtx.SetData("headers", headers) + rt.GoCtx.SetData("request", request) + rt.GoCtx.SetData("response", response) + rt.GoCtx.SetData("caller", &DiscoverClient{ + caller: caller, + logger: logger, + globalHeaders: make(map[string]string), + }) + rt.GoCtx.SetData("session", session) + r, _ := rt.RunPreCompiled(code) + return r +} + +func runWS(code *gojs.PreCompiledCode, args map[string]any, headers map[string]string, request *s.Request, client *websocket.Conn, caller *discover.Caller, session *Session, logger *log.Logger) any { + rt := gojs.New(&gojs.RuntimeOption{Logger: logger}) + rt.GoCtx.SetData("args", args) + rt.GoCtx.SetData("headers", headers) + rt.GoCtx.SetData("request", request) + rt.GoCtx.SetData("client", client) + rt.GoCtx.SetData("caller", &DiscoverClient{ + caller: caller, + logger: logger, + globalHeaders: make(map[string]string), + }) + rt.GoCtx.SetData("session", session) + r, _ := rt.RunPreCompiled(code) + return r +} + +func makeResult(logger *log.Logger, result *httpclient.Result) (*Result, error) { + err, headers, statusCode, output := _makeResult(logger, result) + return &Result{ + result: result, + Error: err, + StatusCode: statusCode, + Headers: headers, + Data: output, + }, result.Error +} + +func _makeResult(logger *log.Logger, result *httpclient.Result) (err string, headers map[string]string, statusCode int, output interface{}) { + if result.Error != nil { + err = result.Error.Error() + logger.Error(result.Error.Error()) + } + + if result.Response != nil { + headers = map[string]string{} + for k, v := range result.Response.Header { + if len(v) == 1 { + headers[k] = v[0] + } else { + headers[k] = strings.Join(v, " ") + } + } + statusCode = result.Response.StatusCode + + if strings.Contains(result.Response.Header.Get("Content-Type"), "application/json") { + output = u.UnJson(result.String(), nil) + } else { + output = result.String() + } + } + return +} + +type AsyncServer struct { + Addr string + Proto string + ProtoName string + as *s.AsyncServer + baseURL string + globalHeaders map[string]string +} + +func (c *AsyncServer) OnStop(f func()) { + c.as.OnStop(f) +} + +func (c *AsyncServer) Wait() { + c.as.Wait() +} + +func (c *AsyncServer) Stop() { + c.as.Stop() +} + +func (c *AsyncServer) makeURL(url string) string { + if !strings.Contains(url, "://") && c.baseURL != "" { + if strings.HasSuffix(c.baseURL, "/") && strings.HasPrefix(url, "/") { + return c.baseURL + url[1:] + } else if !strings.HasSuffix(c.baseURL, "/") && !strings.HasPrefix(url, "/") { + return c.baseURL + "/" + url + } + return c.baseURL + url + } + return url +} + +func (c *AsyncServer) makeHeaderArray(in *map[string]string) []string { + out := make([]string, 0) + if c.globalHeaders != nil { + for k, v := range c.globalHeaders { + out = append(out, k, v) + } + } + if in != nil { + for k, v := range *in { + out = append(out, k, v) + } + } + return out +} + +// SetBaseURL 设置一个URL前缀,后续请求中可以只提供path部分 +// SetBaseURL url 以http://或https://开头的URL地址 +func (c *AsyncServer) SetBaseURL(url string) { + c.baseURL = url +} + +// SetGlobalHeaders 设置固定的HTTP头部信息,在每个请求中都加入这些HTTP头 +// SetGlobalHeaders headers 传入一个Key-Value对象的HTTP头信息 +func (c *AsyncServer) SetGlobalHeaders(headers map[string]string) { + c.globalHeaders = headers +} + +// Get 发送GET请求 +// * url 以http://或https://开头的URL地址,如果设置了baseURL可以只提供path部分 +// * headers 传入一个Key-Value对象的HTTP头信息,如果不指定头信息这个参数可以省略不传 +// * return 返回结果对象,如果返回值是JSON格式,将自动转化为对象否则将字符串放在.result中,如发生错误将抛出异常,返回的对象中还包括:headers、statusCode、statusMessage +func (c *AsyncServer) Get(ctx *plugin.Context, url string, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.as.Get(c.makeURL(url), c.makeHeaderArray(headers)...)) +} + +// Post 发送POST请求 +// * body 可以传入任意类型,如果不是字符串或二进制数组时会自动添加application/json头,数据将以json格式发送 +func (c *AsyncServer) Post(ctx *plugin.Context, url string, body interface{}, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.as.Post(c.makeURL(url), body, c.makeHeaderArray(headers)...)) +} + +// Put 发送PUT请求 +func (c *AsyncServer) Put(ctx *plugin.Context, url string, body interface{}, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.as.Put(c.makeURL(url), body, c.makeHeaderArray(headers)...)) +} + +// Delete 发送DELETE请求 +func (c *AsyncServer) Delete(ctx *plugin.Context, url string, body interface{}, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.as.Delete(c.makeURL(url), body, c.makeHeaderArray(headers)...)) +} + +// Head 发送HEAD请求 +func (c *AsyncServer) Head(ctx *plugin.Context, url string, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.as.Head(c.makeURL(url), c.makeHeaderArray(headers)...)) +} + +// Do 发送请求 +// * method 请求方法,GET、POST等 +func (c *AsyncServer) Do(ctx *plugin.Context, method string, url string, body interface{}, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.as.Do(method, c.makeURL(url), body, c.makeHeaderArray(headers)...)) +} + +// ManualDo 手动处理请求,需要自行从返回结果中读取数据,可实现SSE客户端 +// ManualDo return 应答的对象(需手动读取数据并关闭请求) +func (c *AsyncServer) ManualDo(ctx *plugin.Context, method string, url string, body interface{}, headers *map[string]string) (*ManualResult, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + result := c.as.ManualDo(method, url, body, c.makeHeaderArray(headers)...) + r1, _ := makeResult(logger, result) + return &ManualResult{ + Result: *r1, + }, result.Error +} + +// Open 打开一个Websocket连接 +// Open return Websocket对象(使用完毕请关闭连接) +func (c *AsyncServer) Open(ctx *plugin.Context, url string, headers *map[string]string) (*WS, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + reqHeader := http.Header{} + if headers != nil { + for k, v := range *headers { + reqHeader.Set(k, v) + } + } + urlPrefix := "ws://" + if c.ProtoName == "https" { + urlPrefix = "wss://" + } + if conn, _, err := websocket.DefaultDialer.Dial(urlPrefix+c.Addr+url, reqHeader); err == nil { + return &WS{conn: conn, logger: logger}, err + } else { + logger.Error(err.Error()) + return nil, err + } +} + +type WS struct { + conn *websocket.Conn + closed bool + logger *log.Logger +} + +// Read 读取文本数据 +// Read return 读取到的字符串 +func (ws *WS) Read() (string, error) { + _, buf, err := ws.conn.ReadMessage() + return string(buf), err +} + +// ReadBytes 读取二进制数据 +// ReadBytes return 读取到的二进制数据 +func (ws *WS) ReadBytes() ([]byte, error) { + _, buf, err := ws.conn.ReadMessage() + return buf, err +} + +// ReadJSON 读取JSON对象 +// ReadJSON return 读取到的对象 +func (ws *WS) ReadJSON() (interface{}, error) { + var obj interface{} + err := ws.conn.ReadJSON(&obj) + return obj, err +} + +// Write 写入文本数据 +// Write content 文本数据 +func (ws *WS) Write(content string) error { + return ws.conn.WriteMessage(websocket.TextMessage, []byte(content)) +} + +// WriteBytes 写入二进制数据 +// WriteBytes content 二进制数据 +func (ws *WS) WriteBytes(content []byte) error { + return ws.conn.WriteMessage(websocket.BinaryMessage, content) +} + +// WriteJSON 写入对象 +// WriteJSON content 对象 +func (ws *WS) WriteJSON(content interface{}) error { + return ws.conn.WriteJSON(content) +} + +//// OnClose 关闭事件 +//// OnClose callback 对方关闭时调用 +//func (ws *WS) OnClose(callback func()) { +// ws.conn.SetCloseHandler(func(code int, text string) error { +// callback() +// return nil +// }) +//} + +// Close 关闭连接 +func (ws *WS) Close() error { + if ws.closed { + return nil + } + ws.closed = true + return ws.conn.Close() +} + +// EnableCompression 启用压缩 +func (ws *WS) EnableCompression() { + ws.conn.EnableWriteCompression(true) +} + +type Result struct { + result *httpclient.Result + Error string + StatusCode int + Headers map[string]string + Data interface{} +} + +func (r *Result) String() string { + return r.result.String() +} + +func (r *Result) Bytes() []byte { + return r.result.Bytes() +} + +type ManualResult struct { + Result + closed bool +} + +func (r *ManualResult) Save(filename string) error { + if r.closed { + return errors.New("http client reader closed") + } + r.closed = true + return r.result.Save(filename) +} + +func (r *ManualResult) Read(n int) (string, error) { + if r.closed { + return "", errors.New("http client reader closed") + } + buf := make([]byte, n) + n1, err := r.result.Response.Body.Read(buf) + return string(buf[0:n1]), err +} + +func (r *ManualResult) ReadBytes(n int) ([]byte, error) { + if r.closed { + return nil, errors.New("http client reader closed") + } + buf := make([]byte, n) + n1, err := r.result.Response.Body.Read(buf) + return buf[0:n1], err +} + +func (r *ManualResult) Close() error { + if r.closed { + return nil + } + r.closed = true + return r.result.Response.Body.Close() +} + +type DiscoverClient struct { + caller *discover.Caller + logger *log.Logger + globalHeaders map[string]string +} + +//func (c *DiscoverClient) makeURL(url string) string { +// if !strings.Contains(url, "://") && c.baseURL != "" { +// if strings.HasSuffix(c.baseURL, "/") && strings.HasPrefix(url, "/") { +// return c.baseURL + url[1:] +// } else if !strings.HasSuffix(c.baseURL, "/") && !strings.HasPrefix(url, "/") { +// return c.baseURL + "/" + url +// } +// return c.baseURL + url +// } +// return url +//} + +func (c *DiscoverClient) makeHeaderArray(in *map[string]string) []string { + out := make([]string, 0) + if c.globalHeaders != nil { + for k, v := range c.globalHeaders { + out = append(out, k, v) + } + } + if in != nil { + for k, v := range *in { + out = append(out, k, v) + } + } + return out +} + +// SetGlobalHeaders 设置固定的HTTP头部信息,在每个请求中都加入这些HTTP头 +// SetGlobalHeaders headers 传入一个Key-Value对象的HTTP头信息 +func (c *DiscoverClient) SetGlobalHeaders(headers map[string]string) { + c.globalHeaders = headers +} + +// Get 发送GET请求 +// * url 以http://或https://开头的URL地址,如果设置了baseURL可以只提供path部分 +// * headers 传入一个Key-Value对象的HTTP头信息,如果不指定头信息这个参数可以省略不传 +// * return 返回结果对象,如果返回值是JSON格式,将自动转化为对象否则将字符串放在.result中,如发生错误将抛出异常,返回的对象中还包括:headers、statusCode、statusMessage +func (c *DiscoverClient) Get(ctx *plugin.Context, app, url string, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.caller.Get(app, url, c.makeHeaderArray(headers)...)) +} + +// Post 发送POST请求 +// * body 可以传入任意类型,如果不是字符串或二进制数组时会自动添加application/json头,数据将以json格式发送 +func (c *DiscoverClient) Post(ctx *plugin.Context, app, url string, body interface{}, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.caller.Post(app, url, body, c.makeHeaderArray(headers)...)) +} + +// Put 发送PUT请求 +func (c *DiscoverClient) Put(ctx *plugin.Context, app, url string, body interface{}, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.caller.Put(app, url, body, c.makeHeaderArray(headers)...)) +} + +// Delete 发送DELETE请求 +func (c *DiscoverClient) Delete(ctx *plugin.Context, app, url string, body interface{}, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.caller.Delete(app, url, body, c.makeHeaderArray(headers)...)) +} + +// Head 发送HEAD请求 +func (c *DiscoverClient) Head(ctx *plugin.Context, app, url string, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.caller.Head(app, url, c.makeHeaderArray(headers)...)) +} + +// Do 发送请求 +// * method 请求方法,GET、POST等 +func (c *DiscoverClient) Do(ctx *plugin.Context, method, app, url string, body interface{}, headers *map[string]string) (*Result, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + return makeResult(logger, c.caller.Do(method, app, url, body, c.makeHeaderArray(headers)...)) +} + +// ManualDo 手动处理请求,需要自行从返回结果中读取数据,可实现SSE客户端 +// ManualDo return 应答的对象(需手动读取数据并关闭请求) +func (c *DiscoverClient) ManualDo(ctx *plugin.Context, method, app string, url string, body interface{}, headers *map[string]string) (*ManualResult, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + result := c.caller.ManualDo(method, app, url, body, c.makeHeaderArray(headers)...) + r1, _ := makeResult(logger, result) + return &ManualResult{ + Result: *r1, + }, result.Error +} + +// Open 打开一个Websocket连接 +// Open return Websocket对象(使用完毕请关闭连接) +func (c *DiscoverClient) Open(ctx *plugin.Context, app, url string, headers *map[string]string) (*WS, error) { + logger := ctx.GetInject("*log.Logger").(*log.Logger) + reqHeader := http.Header{} + if headers != nil { + for k, v := range *headers { + reqHeader.Set(k, v) + } + } + if conn := c.caller.Open(app, url, c.makeHeaderArray(headers)...); conn == nil { + return &WS{conn: conn, logger: logger}, nil + } else { + return nil, errors.New("open websocket failed") + } +} diff --git a/tests/api/hello.js b/tests/api/hello.js new file mode 100644 index 0000000..d74c4aa --- /dev/null +++ b/tests/api/hello.js @@ -0,0 +1,6 @@ +// Host:localhost authLevel:1 * /hello Hello World +import service from "apigo.cloud/git/apigo/service" + +let args = service.getArgs() + +return 'Hello '+args.name diff --git a/tests/api/ws.js b/tests/api/ws.js new file mode 100644 index 0000000..694d405 --- /dev/null +++ b/tests/api/ws.js @@ -0,0 +1,8 @@ +// Host:localhost authLevel:1 WS /ws Echo over Websocket +import service from "apigo.cloud/git/apigo/service" + +let c = service.getClient() +let msg = c.readJSON() +if (msg.bbb === 222) msg.aaa = 111 +c.writeJSON(msg) +c.close() diff --git a/tests/config.yml b/tests/config.yml new file mode 100644 index 0000000..857ad3b --- /dev/null +++ b/tests/config.yml @@ -0,0 +1,48 @@ +apigo.cloud/git/apigo/service: + listen: 18087 # 监听端口(|隔开多个监听)(,隔开多个选项)例如 80,http|443|443:h2|127.0.0.1:8080,h2c + ssl: # SSL证书配置,key为域名,value为cert和key的文件路径 + a.com: # 证书匹配的域名 + certFile: /path/to/ssl.pem # cert文件路径 + keyFile: /path/to/ssl.key # key文件路径 + registry: # 服务发现使用的redis连接,例如:redis://:@127.0.0.1:6379/15 + app: # 注册成为一个服务,指定一个名称即可让其他服务使用这个名称来调用,默认不注册为服务 + weight: 100 # 服务节点的权重,默认值:100 + accessTokens: # 请求接口时使用指定的Access-Token进行验证,值为token对应的auth-level + huDWA-78we2-89uji1da: 1 # 该token将获得level为1的权限 + 89dsj-aADSsn-Uds1dad: 2 # 该token将获得level为2的权限 + calls: # 定义将会调用的服务,可以指定调用时使用的Access-Token、请求使用的协议、超时时间(:间隔) +# app1: huDWA-78we2-89uji1da # 携带指定的Access-Token请求服务app1(h2c协议,超时时间为10s) +# app2: http:500ms # 不携带Access-Token请求服务app2(http协议,超时时间为500ms) +# app3: https:30s:89dsj-aADSsn-Uds1dad # 携带指定Access-Token请求服务app3(https协议,超时时间为30s) + callRetryTimes: 10 # 节点连续失败超过指定次数将会被注销(只要有其他节点能工作不会影响正常访问),默认值:10 + ipPrefix: # 用discover注册发现服务时指定使用的IP网段,默认排除 172.17.(Docker) + keepaliveTimeout: 15000 # 连接允许空闲的最大时间,单位ms,默认值:15000 + noLogHeaders: Accept,Accept-Encoding,Cache-Control,Pragma,Connection,Upgrade-Insecure-Requests # 不记录请求头中包含的这些字段,多个字段用逗号分隔 + logInputArrayNum: 10 # 请求字段中容器类型(数组、Map)在日志打印个数限制 默认为10个,多余的数据将不再日志中记录 + logInputFieldSize: 500 # 请求字段中单个字段在日志打印长度限制 默认为500个字符,多余的数据将不再日志中记录 + logOutputArrayNum: 3 # 响应字段中容器类型(数组、Map)在日志打印个数限制 默认为3个,多余的数据将不再日志中记录 + logOutputFieldSize: 100 # 响应字段中单个字段在日志打印长度限制 默认为100个字符,多余的数据将不再日志中记录 + compress: false # 是否启用压缩,默认不启用 + compressMinSize: 1024 # 小于设定值的应答内容将不进行压缩,默认值:1024 + compressMaxSize: 4096000 # 大于设定值的应答内容将不进行压缩,默认值:4096000 + redirectTimeout: 10000 # proxy和discover发起请求时的超时时间,单位ms,默认值:10000 + acceptXRealIpWithoutRequestId: false # 是否允许头部没有携带请求ID的X-Real-IP信息,默认不允许(防止伪造客户端IP) + statisticTime: false # 是否开启请求时间统计,默认不开启 + statisticTimeInterval: 10000 # 统计时间间隔,单位ms,默认值:10000 + maxUploadSize: 104857600 # 最大上传文件大小(multipart/form-data请求的总空间),单位字节,默认值:104857600(100M) + cpu: 0 # CPU占用的核数,默认为0,即不做限制 + memory: 0 # 内存(单位M),默认为0,即不做限制 + cpuMonitor: false # 在日志中记录CPU使用情况,默认不开启 + memoryMonitor: false # 在日志中记录内存使用情况,默认不开启 + cpuLimitValue: 100 # CPU超过最高占用值(10-100)超过次数将自动重启(如果CpuMonitor开启的话),默认100 + memoryLimitValue: 95 # 内存超过最高占用值(10-100)超过次数将自动重启(如果MemoryMonitor开启的话),默认95 + cpuLimitTimes: 6 # CPU超过最高占用值超过次数(1-100)将报警(如果CpuMonitor开启的话),默认6(即30秒内连续6次) + memoryLimitTimes: 6 # 内存超过最高占用值超过次数(1-100)将报警(如果MemoryMonitor开启的话),默认6(即30秒内连续6次) + cookieScope: host # 启用Session时Cookie的有效范围,host|domain|topDomain,默认值为host + idServer: # 用uniqueId、id来生成唯一ID(雪花算法)时所需的redis服务器连接,例如:redis://:@127.0.0.1:6379/15,如果不指定将不能实现跨服务的全局唯一 + logLevel: info # 指定日志等级(error/warning/info/debug),默认值:info + logFile: # 指定日志文件,默认记录到标准输出 + splitTag: 20060102 # 日志文件切分格式,格式语法见golang日期时间格式化,默认不切分 + sensitive: phone,password,secret,token,accessToken # 日志脱敏字段,逗号隔开,请求、应答、HTTP头、请求应答JSON数据的下标等匹配到的值将进行脱敏 + apiPath: api # API接口文件的目录,默认为当前目录下的 api/ + apiHotLoadInterval: 100 # API文件热加载,当API接口检测到文件变化自动加载,单位毫秒,默认值:0(不开启) diff --git a/tests/go.mod b/tests/go.mod new file mode 100644 index 0000000..e9e1e42 --- /dev/null +++ b/tests/go.mod @@ -0,0 +1,37 @@ +module tests + +go 1.18 + +require ( + apigo.cloud/git/apigo/gojs v0.0.8 + apigo.cloud/git/apigo/plugins v1.0.5 + current-plugin v0.0.0 + github.com/ssgo/u v1.7.5 +) + +require ( + apigo.cloud/git/apigo/plugin v1.0.1 // indirect + apigo.cloud/git/apigo/qjs v0.0.1 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gomodule/redigo v1.8.8 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.22.10 // indirect + github.com/ssgo/config v1.7.5 // indirect + github.com/ssgo/discover v1.7.5 // indirect + github.com/ssgo/httpclient v1.7.5 // indirect + github.com/ssgo/log v1.7.5 // indirect + github.com/ssgo/redis v1.7.5 // indirect + github.com/ssgo/s v1.7.5 // indirect + github.com/ssgo/standard v1.7.5 // indirect + github.com/tklauser/go-sysconf v0.3.10 // indirect + github.com/tklauser/numcpus v0.4.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace current-plugin v0.0.0 => ../ diff --git a/tests/jsImports_test.go b/tests/jsImports_test.go new file mode 100644 index 0000000..752609b --- /dev/null +++ b/tests/jsImports_test.go @@ -0,0 +1,3 @@ +package service_test +import _ "apigo.cloud/git/apigo/plugins/file" +import _ "apigo.cloud/git/apigo/plugins/runtime" diff --git a/tests/plugin_test.go b/tests/plugin_test.go new file mode 100644 index 0000000..95550d7 --- /dev/null +++ b/tests/plugin_test.go @@ -0,0 +1,28 @@ +package service_test + +import ( + "apigo.cloud/git/apigo/gojs" + _ "current-plugin" + "fmt" + "github.com/ssgo/u" + "os" + "strings" + "testing" +) + +func TestPlugin(t *testing.T) { + + if files, err := os.ReadDir("."); err == nil { + for _, f := range files { + if !f.IsDir() && strings.HasSuffix(f.Name(), "_test.js") { + testName := f.Name()[0 : len(f.Name())-8] + r, err := gojs.RunFile(f.Name(), nil) + if err != nil || r != true { + t.Fatal("test "+testName+" failed", r, err) + } else { + fmt.Println(u.Green("test "+testName), u.BGreen("OK")) + } + } + } + } +} diff --git a/tests/plugin_test.js b/tests/plugin_test.js new file mode 100644 index 0000000..87adbdc --- /dev/null +++ b/tests/plugin_test.js @@ -0,0 +1,56 @@ +import s from 'apigo.cloud/git/apigo/service' +import file from 'apigo.cloud/git/apigo/plugins/file' +import runtime from 'apigo.cloud/git/apigo/plugins/runtime' +import console from "console"; + +// test register api manually +file.write('api/echo.js', 'import service from "apigo.cloud/git/apigo/service"\nreturn service.getArgs().data') +s.register({ + actionFile: 'api/echo.js', + method: 'POST', + path: '/echo', +}) + +// start server asynchronously for test +let c = s.asyncStart() + +// test /hello without access token +if(c.get('/hello?name=World', {Host:'localhost'}).statusCode !== 403) throw new Error('test hello World 403 failed') + +// test /hello without host (api limited host is localhost) +if(c.get('/hello?name=World').statusCode !== 404) throw new Error('test hello World 404 failed') + +// test /hello with access token (auth level 1) +if(c.get('/hello?name=World', {Host:'localhost', 'Access-Token':'huDWA-78we2-89uji1da'}).string() !== 'Hello World') throw new Error('test hello World failed') + +// test /hello with access token (auth level 2) +if(c.get('/hello?name=ABC', {Host:'localhost', 'Access-Token':'89dsj-aADSsn-Uds1dad'}).string() !== 'Hello ABC') throw new Error('test hello ABC failed') + +// test /echo by get (will response 404) +if(c.get('/echo?data=World').statusCode !== 404) throw new Error('test echo over get failed') + +// test /echo by post +if(c.post('/echo', {data:'World'}).string() !== 'World') throw new Error('test echo over post failed') + +// test api hot load +file.write('api/echo.js', 'import service from "apigo.cloud/git/apigo/service"\nreturn service.getArgs().data+"!"') +runtime.sleep(200) +if(c.post('/echo', {data:'World'}).string() !== 'World!') throw new Error('t est echo over post failed') +file.remove('api/echo.js') + +// test websocket +let wc = c.open('/ws', {Host:'localhost', 'Access-Token':'huDWA-78we2-89uji1da'}) +wc.writeJSON({aaa:'hello', bbb:222}) +let msg = wc.readJSON() +if(msg.aaa !== 111) throw new Error('test echo over websocket failed') + +// test discover +s.addDiscoverAppForTest('test', 'http:huDWA-78we2-89uji1da') +s.setDiscoverNode('test', c.addr, 100) +let caller = s.getCaller() +if(caller.get('test', '/hello?name=World', {Host:'localhost'}).string() !== 'Hello World') throw new Error('test hello World 403 failed') + +// stop server +c.stop() + +return true