diff --git a/README.md b/README.md index 13e6c2f..0254e5b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # 低代码的服务器端应用框架 基于 [ssgo/s](https://github.com/ssgo/s) -快速创建一个web服务,提供 http、https、http2、h2c、websocket 服务 -支持作为静态文件服务器 -支持 rewrite 和 proxy反向代理(可以代理到discover应用或其他http服务器) +快速创建一个web服务,提供 http、https、http2、h2c、websocket 服务 +支持作为静态文件服务器 +支持 rewrite 和 proxy反向代理(可以代理到discover应用或其他http服务器) 支持服务发现 [ssgo/discover](https://github.com/ssgo/discover) 快速构建一个服务网络 ## 快速开始 @@ -48,10 +48,8 @@ function main() { } ``` -这种模式下服务代码会脱离主线程,使用对象池实现高并发 - -如果不配置对象池参数则不约束虚拟机数量上限,可以达到最佳性能但是对CPU和内存有一定挑战 - +这种模式下服务代码会脱离主线程,使用对象池实现高并发 +如果不配置对象池参数则不约束虚拟机数量上限,可以达到最佳性能但是对CPU和内存有一定挑战 设置较小的max可以有效保护CPU和内存资源但是设置过小将无法发挥服务器的性能 ## 注册到服务发现 @@ -85,20 +83,15 @@ s.register({ path: '/getUserInfo' }, ({ caller }) => { }) ``` -authLevel 不设置则不进行校验,如果获得 authLevel 2 则允许访问所有 2及以下级别的接口 - -如果 user 服务不配置 listen 默认使用 h2c 协议随机端口 - +authLevel 不设置则不进行校验,如果获得 authLevel 2 则允许访问所有 2及以下级别的接口 +如果 user 服务不配置 listen 默认使用 h2c 协议随机端口 如果不使用 h2c 协议,调用方配置 calls 时需指定 'http:aaaaaaaa' ## Session -服务默认启用 session 和 device 功能,如果不希望使用可以在配置中设置 sessionKey 或 deviceKey 为空 - -如果配置了 sessionProvider,session 将会存储在 redis 中,否则存储在内存中 - -sessionID和deviceID 同时支持HTTP头和Cookie两种传输方式,HTTP头优先,如果客户端没有传递则服务器会自动分配 - +服务默认启用 session 和 device 功能,如果不希望使用可以在配置中设置 sessionKey 或 deviceKey 为空 +如果配置了 sessionProvider,session 将会存储在 redis 中,否则存储在内存中 +sessionID和deviceID 同时支持HTTP头和Cookie两种传输方式,HTTP头优先,如果客户端没有传递则服务器会自动分配 如需使用 session 只需要在接口中直接获取即可 #### 下面是一个使用 session 并且使用参数有效性验证和限流器的例子 @@ -138,16 +131,11 @@ function main() { } ``` -session对象自动注入,无需任何其他操作。修改session后需要使用 session.save 来保存 - -调用 session.setAuthLevel 可以设置用户权限,当接口注册的 authLevel 大于0时可以基于 session 中的设置进行访问控制 - -配置了 userIdKey 后,会自动将 session 的用户ID记录在访问日志中,方便对用户的访问进行分析和统计 - -示例中创建了一个每个IP每秒允许10次请求的限流器并且在接口中使用了这个限流器 - -login接口配置了 id 和 name 两个参数的有效性验证规则 - +session对象自动注入,无需任何其他操作。修改session后需要使用 session.save 来保存 +调用 session.setAuthLevel 可以设置用户权限,当接口注册的 authLevel 大于0时可以基于 session 中的设置进行访问控制 +配置了 userIdKey 后,会自动将 session 的用户ID记录在访问日志中,方便对用户的访问进行分析和统计 +示例中创建了一个每个IP每秒允许10次请求的限流器并且在接口中使用了这个限流器 +login接口配置了 id 和 name 两个参数的有效性验证规则 参数有效性验证配置可以支持以下类型: - value为string或RegExp对象时进行正则表达式校验 @@ -169,8 +157,6 @@ ssl: yourdomain.com: certfile: /path/yourdomain.pem keyfile: /path/yourdomain.pem -static: - yourdomain.com: /path/www ``` 2、在环境配置文件 env.yml 或 env.json 中配置 @@ -188,6 +174,40 @@ docker run -e SERVICE_LISTEN=8080:8443 #### 所有配置方式的优先级为 s.config > 环境变量 > env.yml > service.yml +## 静态文件 + +```yaml +service: + static: + yourdomain.com: /path/www + yourdomain.com:8080: /path/www8080 + yourdomain.com:80/abc: /path/abc + /def: /path/def +``` + +可以根据域名和路径配置静态文件 +可以使用 file 模块将文件加载到内存中加速访问 + +```javascript +import file from "apigo.cc/gojs/file" + +file.cache('/path/www', true) +``` + +## 反向代理和Rewrite + +```yaml +service: + proxy: + yourdomain.com: serverA + /abc: http://HOST:PORT/PATH + yourdomain.com/def/(.*): http://127.0.0.1:8080/$1 + rewrite: + yourdomain.com/001/(.*): /path/001/$1 + yourdomain.com/002/(.*): http://127.0.0.1:8080/$1 + http://yourdomain.com(.*): https://yourdomain.com$1 +``` + ## websocket ```javascript @@ -212,5 +232,60 @@ function main() { 注册接口时将 method 指定为 WS 即可创建 websocket 服务,配置 onMessage 来异步处理消息 +## 后台任务 + +#### taskA.js + +```javascript +import s from "apigo.cc/gojs/service" + +// function onStart() { +// // TODO 任务启动时执行 +// } + +function onRun() { + // TODO 在指定间隔时间到达时被调用,根据需要对资源进行处理 + if ( s.dataCount('websocketClients') > 0 ) { + let conns = s.dataFetch('websocketClients') + for ( let conn of conns ) { + conn.write('Hello, World!') + } + } +} + +function onStop() { + // TODO 服务结束时被调用,用来收尾或释放资源 + s.dataRemove('websocketClients') +} +``` + +#### main.js + +```javascript +import s from "apigo.cc/gojs/service" + +function main() { + s.task('task.js', 1000) // 每秒执行一次 + s.register({method: 'WS', path: '/ws'}, ({ client }) => { + // 将连接放到资源池中供后台任务使用 + s.dataSet('websocketClients', client.id, client) + }) + s.start() +} +``` + +task 必须在单独的js文件中定义 +每个 task 都会运行在单独的vm中 +定义 task 必须在服务启动(s.start)之前 +服务停止(s.stop)所有任务会被停止 + +#### 任务队列 + +```javascript +s.listPush('taskA', {}) +s.listPop('taskA', {}) +``` + +可以使用 list 相关操作实现基于队列的后台任务处理 ## 完整的API参考 [service.ts](https://apigo.cc/gojs/service/src/branch/main/service.ts) diff --git a/caller.go b/caller.go index cfeaa89..1c39c7f 100644 --- a/caller.go +++ b/caller.go @@ -80,7 +80,7 @@ func (cl *Caller) makeHeaderArray(in map[string]any) []string { func parseAppPath(appURL string) (string, string) { arr := strings.SplitN(appURL, "/", 2) if len(arr) == 2 { - return arr[0], arr[1] + return arr[0], "/" + arr[1] } return appURL, "/" } diff --git a/gateway.go b/gateway.go index b797626..69d8f90 100644 --- a/gateway.go +++ b/gateway.go @@ -2,6 +2,7 @@ package service import ( "fmt" + "path" "regexp" "strings" "sync" @@ -13,25 +14,21 @@ import ( "github.com/ssgo/u" ) -type regexProxiesInfo struct { - Value string - Regex regexp.Regexp -} - -type regexRewriteInfo struct { +type regexRedirectInfo struct { To string Regex regexp.Regexp } var _proxies = map[string]string{} var _proxiesLock = sync.RWMutex{} -var _regexProxies = map[string]*regexProxiesInfo{} -var _regexRewrites = map[string]*regexRewriteInfo{} +var _regexProxies = map[string]*regexRedirectInfo{} +var _rewrites = map[string]string{} +var _regexRewrites = map[string]*regexRedirectInfo{} var _rewritesLock = sync.RWMutex{} var _statics = map[string]string{} var _staticsLock = sync.RWMutex{} -func updateStatic(in map[string]string) bool { +func UpdateStatic(in map[string]string) bool { updated := false for k, v := range in { _staticsLock.RLock() @@ -52,13 +49,16 @@ func updateStatic(in map[string]string) bool { if a[0] == "*" { a[0] = "" } + if a[1] == "" { + a[1] = "/" + } s.StaticByHost(a[1], v, a[0]) updated = true } return updated } -func updateProxy(in map[string]string) bool { +func UpdateProxy(in map[string]string) bool { updated := false //fmt.Println("####000") @@ -74,23 +74,22 @@ func updateProxy(in map[string]string) bool { continue } ////fmt.Println("####333", k, v) - if v2 != nil && v == v2.Value { + if v2 != nil && v == v2.To { continue } ////fmt.Println("####444", k, v) - - if strings.Contains(v, "(") { + if strings.Contains(k, "(") { // for regexp ////fmt.Println("####555", k, v) - matcher, err := regexp.Compile("^" + v + "$") + matcher, err := regexp.Compile("^" + k + "$") if err != nil { s.ServerLogger.Error("proxy regexp compile failed", "key", k, "value", v) //log.Print("Proxy Error Compile ", err) } else { s.ServerLogger.Info(u.StringIf(v2 != nil, "update regexp proxy set", "new regexp proxy set"), "key", k, "value", v) _proxiesLock.Lock() - _regexProxies[k] = ®exProxiesInfo{ - Value: v, + _regexProxies[k] = ®exRedirectInfo{ + To: v, Regex: *matcher, } _proxiesLock.Unlock() @@ -117,11 +116,11 @@ func updateProxy(in map[string]string) bool { } else { callConfig = (time.Duration(s.Config.ReadHeaderTimeout) * time.Millisecond).String() } - // if redisPool != nil { - if discover.AddExternalApp(v, callConfig) { - updated = true + if discover.Config.Registry != "" { + if discover.AddExternalApp(v, callConfig) { + updated = true + } } - // } } else { updated = true } @@ -134,28 +133,39 @@ func updateProxy(in map[string]string) bool { return updated } -func updateRewrite(in map[string]string) bool { +func UpdateRewrite(in map[string]string) bool { updated := false for k, v := range in { _rewritesLock.RLock() + v1 := _rewrites[k] v2 := _regexRewrites[k] _rewritesLock.RUnlock() // skip same + if v == v1 { + continue + } if v2 != nil && v == v2.To { continue } - matcher, err := regexp.Compile("^" + k + "$") - if err != nil { - s.ServerLogger.Error("rewrite regexp compile failed", "key", k, "value", v) - } else { - s.ServerLogger.Info(u.StringIf(v2 != nil, "update regexp rewrite set", "new regexp rewrite set"), "key", k, "value", v) - _rewritesLock.Lock() - _regexRewrites[k] = ®exRewriteInfo{ - To: v, - Regex: *matcher, + if strings.Contains(k, "(") { + matcher, err := regexp.Compile("^" + k + "$") + if err != nil { + s.ServerLogger.Error("rewrite regexp compile failed", "key", k, "value", v) + } else { + s.ServerLogger.Info(u.StringIf(v2 != nil, "update regexp rewrite set", "new regexp rewrite set"), "key", k, "value", v) + _rewritesLock.Lock() + _regexRewrites[k] = ®exRedirectInfo{ + To: v, + Regex: *matcher, + } + _rewritesLock.Unlock() + updated = true } + } else { + _rewritesLock.Lock() + _rewrites[k] = v _rewritesLock.Unlock() updated = true } @@ -163,50 +173,66 @@ func updateRewrite(in map[string]string) bool { return updated } +// TODO 测试各种情况下的 matchRedirect func rewrite(request *s.Request) (toPath string, rewrite bool) { - list2 := map[string]*regexRewriteInfo{} - _rewritesLock.RLock() - for k, v := range _regexRewrites { - list2[k] = v + if toApp, toPath, ok := matchRedirect(request, &_rewrites, &_regexRewrites, &_rewritesLock); ok { + return toApp + toPath, true } - _rewritesLock.RUnlock() - - if len(list2) > 0 { - requestUrl := fmt.Sprint(request.Header.Get("X-Scheme"), "://", request.Host, request.RequestURI) - requestUrlWithoutScheme := fmt.Sprint(request.Host, request.RequestURI) - - for _, rr := range list2 { - finds := rr.Regex.FindAllStringSubmatch(requestUrl, 20) - if len(finds) == 0 { - finds = rr.Regex.FindAllStringSubmatch(requestUrlWithoutScheme, 20) - } - if len(finds) == 0 { - continue - } - - to := rr.To - if len(finds[0]) > 1 { - for i := 1; i < len(finds[0]); i++ { - varName := fmt.Sprintf("$%d", i) - to = strings.ReplaceAll(to, varName, finds[0][i]) - } - return to, true - } - } - } - - // 不进行代理 - return "", false + return } func proxy(request *s.Request) (authLevel int, toApp, toPath *string, headers map[string]string) { - //fmt.Println("proxy", len(_proxies)) - outHeaders := map[string]string{ - standard.DiscoverHeaderFromApp: "gateway", - standard.DiscoverHeaderFromNode: s.GetServerAddr(), + if toApp1, toPath1, ok := matchRedirect(request, &_proxies, &_regexProxies, &_proxiesLock); ok { + outHeaders := map[string]string{ + standard.DiscoverHeaderFromApp: "gateway", + standard.DiscoverHeaderFromNode: s.GetServerAddr(), + } + requestPath := request.RequestURI + if requestPath == "" { + requestPath = request.URL.Path + } + + pos := strings.Index(requestPath, toPath1) + if pos > 0 { + outHeaders["Proxy-Path"] = requestPath[0:pos] + } + return 0, &toApp1, &toPath1, outHeaders + } + return +} + +func ClearRewritesAndProxies() { + _staticsLock.Lock() + _statics = map[string]string{} + _staticsLock.Unlock() + _rewritesLock.Lock() + _rewrites = map[string]string{} + _regexRewrites = map[string]*regexRedirectInfo{} + _rewritesLock.Unlock() + _proxiesLock.Lock() + _proxies = map[string]string{} + _regexProxies = map[string]*regexRedirectInfo{} + _proxiesLock.Unlock() +} + +func MatchRewrite(request *s.Request) (toApp, toPath string, ok bool) { + return matchRedirect(request, &_rewrites, &_regexRewrites, &_rewritesLock) +} + +func MatchProxy(request *s.Request) (toApp, toPath string, ok bool) { + return matchRedirect(request, &_proxies, &_regexProxies, &_proxiesLock) +} + +func matchRedirect(request *s.Request, normalList *map[string]string, regexpList *map[string]*regexRedirectInfo, lock *sync.RWMutex) (toApp, toPath string, ok bool) { + (*lock).RLock() + n1 := len(*normalList) + n2 := len(*regexpList) + (*lock).RUnlock() + + if n1 == 0 && n2 == 0 { + return } - scheme := u.StringIf(request.TLS == nil, "http", "https") host1 := "" host2 := "" if strings.ContainsRune(request.Host, ':') { @@ -215,98 +241,132 @@ func proxy(request *s.Request) (authLevel int, toApp, toPath *string, headers ma host2 = request.Host } else { host1 = request.Host - host2 = request.Host + ":" + u.StringIf(request.TLS == nil, "80", "443") + host2 = request.Host + ":" + u.StringIf(request.URL.Scheme == "https", "443", "80") } + requestPath := request.RequestURI + if requestPath == "" { + requestPath = request.URL.Path + } pathMatchers := make([]string, 0) - pathMatchers = append(pathMatchers, fmt.Sprint(scheme, "://", host1, request.RequestURI)) - pathMatchers = append(pathMatchers, fmt.Sprint(scheme, "://", host2, request.RequestURI)) - pathMatchers = append(pathMatchers, fmt.Sprint(host1, request.RequestURI)) - pathMatchers = append(pathMatchers, fmt.Sprint(host2, request.RequestURI)) - pathMatchers = append(pathMatchers, request.RequestURI) + pathMatchers = append(pathMatchers, fmt.Sprint(request.URL.Scheme, "://", host1, requestPath)) + pathMatchers = append(pathMatchers, fmt.Sprint(request.URL.Scheme, "://", host2, requestPath)) + pathMatchers = append(pathMatchers, fmt.Sprint(host1, requestPath)) + pathMatchers = append(pathMatchers, fmt.Sprint(host2, requestPath)) + pathMatchers = append(pathMatchers, requestPath) hostMatchers := make([]string, 0) - hostMatchers = append(hostMatchers, fmt.Sprint(scheme, "://", host1)) - hostMatchers = append(hostMatchers, fmt.Sprint(scheme, "://", host2)) + hostMatchers = append(hostMatchers, fmt.Sprint(request.URL.Scheme, "://", host1)) + hostMatchers = append(hostMatchers, fmt.Sprint(request.URL.Scheme, "://", host2)) hostMatchers = append(hostMatchers, host1) hostMatchers = append(hostMatchers, host2) - list := map[string]string{} - _proxiesLock.RLock() - for k, v := range _proxies { - list[k] = v - } - _proxiesLock.RUnlock() - for p, a := range list { - //fmt.Println("check proxy ", p, a) - matchPath := "" - matchPathArr := strings.SplitN(strings.ReplaceAll(p, "://", ""), "/", 2) - if len(matchPathArr) == 2 { - matchPath = "/" + matchPathArr[1] + if n1 > 0 { + list1 := map[string]string{} + (*lock).RLock() + for k, v := range *normalList { + list1[k] = v } - - if matchPath == "" { - for _, m := range hostMatchers { - if m == p { - //fmt.Println(" >>>>>>>>1", p, m, request.RequestURI) - return 0, fixAppName(a), &request.RequestURI, outHeaders - } + (*lock).RUnlock() + for setKey, setValue := range list1 { + matchPath := "" + matchPathArr := strings.SplitN(strings.ReplaceAll(setKey, "://", ""), "/", 2) + if len(matchPathArr) == 2 { + matchPath = "/" + matchPathArr[1] } - } else { - for _, m := range pathMatchers { - if strings.HasPrefix(m, p) { - if strings.HasPrefix(request.RequestURI, matchPath) { - p2 := request.RequestURI[len(matchPath):] - if len(p2) == 0 || p2[0] != '/' { - p2 = "/" + p2 + + toApp, toPath = splitAppAndPath(setValue) + if matchPath == "" { + for _, matchStr := range hostMatchers { + if matchStr == setKey { + // fmt.Println(" >>>>>>>>1", setKey, matchStr, requestPath) + return toApp, path.Join(toPath, requestPath), true + } + } + } else { + for _, matchStr := range pathMatchers { + // fmt.Println(" >>>>>>>> test", u.BCyan(matchStr), u.BMagenta(setKey), "|", strings.HasPrefix(matchStr, setKey)) + if strings.HasPrefix(matchStr, setKey) { + if strings.HasPrefix(requestPath, matchPath) { + p2 := requestPath[len(matchPath):] + if len(p2) == 0 || p2[0] != '/' { + p2 = "/" + p2 + } + // fmt.Println(" >>>>>>>>2", setKey, matchStr, p2) + return toApp, path.Join(toPath, p2), true + } else { + // fmt.Println(" >>>>>>>>3", setKey, matchStr, requestPath) + return toApp, path.Join(toPath, requestPath), true } - //fmt.Println(" >>>>>>>>2", p, m, p2) - return 0, fixAppName(a), &p2, outHeaders - } else { - //fmt.Println(" >>>>>>>>3", p, m, request.RequestURI) - return 0, fixAppName(a), &request.RequestURI, outHeaders } } } } } - // 模糊匹配 - list2 := map[string]*regexProxiesInfo{} - _proxiesLock.RLock() - for k, v := range _regexProxies { - list2[k] = v - } - _proxiesLock.RUnlock() + if n2 > 0 { + // 模糊匹配 + list2 := map[string]*regexRedirectInfo{} + (*lock).RLock() + for k, v := range *regexpList { + list2[k] = v + } + (*lock).RUnlock() - if len(list2) > 0 { - requestUrl := request.Host + request.RequestURI - for _, rp := range list2 { - //fmt.Println("check regexp proxy ", rp.Regex, rp.Value) - finds := rp.Regex.FindAllStringSubmatch(requestUrl, 20) - if len(finds) > 0 && len(finds[0]) > 2 { - //fmt.Println(" >>>>>>>>2", request.RequestURI, finds[0][2]) - pos := strings.Index(request.RequestURI, finds[0][2]) - if pos > 0 { - outHeaders["Proxy-Path"] = request.RequestURI[0:pos] - } - - if !strings.Contains(finds[0][1], "://") && strings.ContainsRune(finds[0][1], ':') { - callConfig := "" - if strings.ContainsRune(finds[0][1], ':') { - // support call config in proxy value - a := strings.SplitN(finds[0][1], ":", 2) - finds[0][1] = a[0] - callConfig = a[1] - } else { - callConfig = (time.Duration(s.Config.ReadHeaderTimeout) * time.Millisecond).String() - } - // if redisPool != nil { - discover.AddExternalApp(finds[0][1], callConfig) - // } - } - return 0, &finds[0][1], &finds[0][2], outHeaders + // requestUrl := request.Host + requestPath + for setKey, setInfo := range list2 { + matchPath := "" + matchPathArr := strings.SplitN(strings.ReplaceAll(setKey, "://", ""), "/", 2) + if len(matchPathArr) == 2 { + matchPath = "/" + matchPathArr[1] } + + // fmt.Println(" >>>>>>>> matchPath", setKey, matchPath, u.JsonP(matchPathArr), 111) + var matchList []string + if matchPath == "" { + matchList = hostMatchers + } else { + matchList = pathMatchers + } + for _, matchStr := range matchList { + finds := setInfo.Regex.FindStringSubmatch(matchStr) + if len(finds) > 0 { + matchResult := setInfo.To + for i := 1; i < len(finds); i++ { + matchResult = strings.ReplaceAll(matchResult, fmt.Sprintf("$%d", i), finds[i]) + } + // fmt.Println(" >>>>>>>> test", u.BCyan(matchStr), u.Cyan(matchPath), u.BMagenta(setKey), "|", matchResult, fixAppName(matchResult), "...") + toApp, toPath := splitAppAndPath(matchResult) + return toApp, toPath, true + } + } + + // //fmt.Println("check regexp proxy ", rp.Regex, rp.Value) + // // finds := setInfo.Regex.FindAllStringSubmatch(requestUrl, 20) + // fmt.Println(" >>>>>>>> test", u.BCyan(matchStr), u.BMagenta(setKey), "|", strings.HasPrefix(matchStr, setKey)) + // if len(finds) > 0 && len(finds[0]) > 2 { + // //fmt.Println(" >>>>>>>>2", requestPath, finds[0][2]) + // // pos := strings.Index(requestPath, finds[0][2]) + // // if pos > 0 { + // // outHeaders["Proxy-Path"] = requestPath[0:pos] + // // } + + // if !strings.Contains(finds[0][1], "://") && strings.ContainsRune(finds[0][1], ':') { + // callConfig := "" + // if strings.ContainsRune(finds[0][1], ':') { + // // support call config in proxy value + // a := strings.SplitN(finds[0][1], ":", 2) + // finds[0][1] = a[0] + // callConfig = a[1] + // } else { + // callConfig = (time.Duration(s.Config.ReadHeaderTimeout) * time.Millisecond).String() + // } + // if discover.Config.Registry != "" { + // discover.AddExternalApp(finds[0][1], callConfig) + // } + // } + // return finds[0][1], finds[0][2], true + // } } } @@ -314,11 +374,52 @@ func proxy(request *s.Request) (authLevel int, toApp, toPath *string, headers ma return } -func fixAppName(appName string) *string { - if !strings.Contains(appName, "://") && strings.ContainsRune(appName, ':') { - a := strings.SplitN(appName, ":", 2) - return &a[0] +// func fixAppName(appName string) string { +// if !strings.Contains(appName, "://") && strings.ContainsRune(appName, ':') { +// a := strings.SplitN(appName, "/", 2) +// return a[0] +// } else { +// return appName +// } +// } + +func splitAppAndPath(to string) (toApp, toPath string) { + if strings.Contains(to, "://") { + to = strings.Replace(to, "://", ":--", 1) + a := strings.SplitN(to, "/", 2) + if len(a) == 1 { + a = append(a, "") + } + a[0] = strings.Replace(a[0], ":--", "://", 1) + return a[0], "/" + a[1] } else { - return &appName + if strings.HasPrefix(to, "/") { + toApp = "" + toPath = makeAppConfig(to) + } else { + a := strings.SplitN(to, "/", 2) + if len(a) == 1 { + a = append(a, "") + } + toApp = makeAppConfig(a[0]) + toPath = "/" + a[1] + } } + return +} + +func makeAppConfig(toStr string) (toApp string) { + toAppConfig := "" + if strings.ContainsRune(toApp, ':') { + a := strings.SplitN(toApp, ":", 2) + toApp = a[0] + toAppConfig = a[1] + } else { + toApp = toStr + // toAppConfig = (time.Duration(s.Config.RedirectTimeout) * time.Millisecond).String() + } + if discover.Config.Registry != "" { + discover.AddExternalApp(toApp, toAppConfig) + } + return toApp } diff --git a/go.mod b/go.mod index 8edd816..e76d583 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,11 @@ require ( apigo.cc/gojs/util v0.0.2 github.com/gorilla/websocket v1.5.3 github.com/ssgo/config v1.7.7 - github.com/ssgo/discover v1.7.8 + github.com/ssgo/discover v1.7.9 github.com/ssgo/httpclient v1.7.8 github.com/ssgo/log v1.7.7 github.com/ssgo/redis v1.7.7 - github.com/ssgo/s v1.7.14 + github.com/ssgo/s v1.7.16 github.com/ssgo/standard v1.7.7 github.com/ssgo/u v1.7.9 ) diff --git a/request.go b/request.go index 3ea2710..92c5634 100644 --- a/request.go +++ b/request.go @@ -11,6 +11,7 @@ import ( type Request struct { req *s.Request + Id string Proto string Scheme string Host string @@ -62,6 +63,7 @@ func MakeRequest(req *s.Request, args map[string]any, headers map[string]string) } return gojs.MakeMap(&Request{ req: req, + Id: req.Id, Proto: req.Proto, Scheme: req.Header.Get(standard.DiscoverHeaderScheme), Host: req.Header.Get(standard.DiscoverHeaderHost), diff --git a/response.go b/response.go index 1b6293f..b8b51b6 100644 --- a/response.go +++ b/response.go @@ -14,6 +14,7 @@ type Response struct { resp *s.Response endCh chan bool result any + Id string } func (r *Response) End(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { diff --git a/service.go b/service.go index 496cc53..b953faf 100644 --- a/service.go +++ b/service.go @@ -70,8 +70,6 @@ var limiters = map[string]*s.Limiter{} func init() { obj := map[string]any{ "config": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - s.Init() - // 处理配置 args := gojs.MakeArgs(&argsIn, vm) serviceConfig = Config{"Session", "Device", "Client", "userId", "", 3600, "auth failed", "verify failed", "too many requests", nil, "", map[string]string{}, map[string]string{}, map[string]string{}} @@ -97,7 +95,7 @@ func init() { } // 身份验证和Session - authAccessToken := s.Config.AccessTokens != nil && len(s.Config.AccessTokens) > 0 + authAccessToken := len(s.Config.AccessTokens) > 0 s.SetClientKeys(serviceConfig.DeviceKey, serviceConfig.ClientKey, serviceConfig.SessionKey) if serviceConfig.SessionKey != "" { 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) { @@ -105,6 +103,7 @@ func init() { setAuthLevel := 0 if serviceConfig.SessionKey != "" { sessionID := request.GetSessionId() + if sessionID != "" { session = NewSession(sessionID, logger) } @@ -117,30 +116,9 @@ func init() { setAuthLevel = u.Int(authLevelBySession) } } - // if auth != nil { - // requestParams, _ := makeRequestParams(args, nil, request, response, nil, session, logger) - // requestParams["authLevel"] = authLevel - // if r, err := auth(nil, vm.ToValue(requestParams)); err == nil { - // if r.ExportType().Kind() == reflect.Bool { - // if r.ToBoolean() { - // return true, session - // } else { - // return false, nil - // } - // } else { - // return false, r.Export() - // } - // } else { - // logger.Error(err.Error()) - // return false, nil - // } - // } else { - // return true, session - // } } - - // 如果没有session或session中的authLevel为0,则使用Access-Token中的authLevel(服务间调用) - if authAccessToken && setAuthLevel == 0 && authLevel > 0 { + // 如果没有session中的authLevel验证失败,则使用Access-Token中的authLevel(服务间调用) + if authAccessToken && setAuthLevel < authLevel { setAuthLevel = s.GetAuthTokenLevel(request.Header.Get("Access-Token")) } if setAuthLevel >= authLevel { @@ -159,6 +137,7 @@ func init() { } } }) + s.Init() } // 限流器 @@ -189,17 +168,16 @@ func init() { if server != nil { panic(vm.NewGoError(errors.New("server already started"))) } - // 处理静态文件 if len(serviceConfig.Static) > 0 { - updateStatic(serviceConfig.Static) + UpdateStatic(serviceConfig.Static) } if len(serviceConfig.Rewrite) > 0 { - updateRewrite(serviceConfig.Rewrite) + UpdateRewrite(serviceConfig.Rewrite) s.SetRewriteBy(rewrite) } if len(serviceConfig.Proxy) > 0 { - updateProxy(serviceConfig.Proxy) + UpdateProxy(serviceConfig.Proxy) s.SetProxyBy(proxy) } @@ -223,6 +201,7 @@ func init() { panic(vm.NewGoError(errors.New("server not started"))) } server.Stop() + ClearRewritesAndProxies() pools = map[string]*gojs.Pool{} server = nil return nil @@ -366,7 +345,45 @@ func init() { poolsLock.Unlock() return nil }, - "newCaller": NewCaller, + "task": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := gojs.MakeArgs(&argsIn, vm).Check(1) + taskFile := args.Path(0) + interval := args.Int(1) + if interval == 0 { + interval = 1000 + } + if interval < 100 { + interval = 100 + } + if !u.FileExists(taskFile) { + panic(vm.NewGoError(errors.New("taskFile must be a js file path"))) + } + rt := gojs.New() + _, err := rt.RunFile(taskFile) + if err != nil { + panic(vm.NewGoError(err)) + } + println(u.BMagenta("taskFile: "), taskFile, interval) + s.NewTimerServer(taskFile, time.Duration(interval)*time.Millisecond, func(isRunning *bool) { + rt.RunCode("if(onRun)onRun()") + }, func() { + rt.RunCode("if(onStart)onStart()") + }, func() { + rt.RunCode("if(onStop)onStop()") + }) + return nil + }, + "dataSet": DataSet, + "dataGet": DataGet, + "dataKeys": DataKeys, + "dataCount": DataCount, + "dataFetch": DataFetch, + "dataRemove": DataRemove, + "listPop": ListPop, + "listPush": ListPush, + "listCount": ListCount, + "listRemove": ListRemove, + "newCaller": NewCaller, } gojs.Register("apigo.cc/gojs/service", gojs.Module{ @@ -436,16 +453,20 @@ func verifyFunc(callback goja.Callable, thisObj goja.Value) func(any, *goja.Runt } func makeRequestParams(args map[string]any, headers map[string]string, request *s.Request, response *s.Response, client *websocket.Conn, caller *discover.Caller, session *Session, logger *log.Logger) (gojs.Map, *Response) { - resp := &Response{ - resp: response, - endCh: make(chan bool, 1), - } + var resp *Response params := gojs.Map{ - "args": args, - "logger": gojs.MakeLogger(logger), - "request": MakeRequest(request, args, headers), - "response": gojs.MakeMap(resp), - "client": MakeWSClient(client), + "args": args, + "logger": gojs.MakeLogger(logger), + "request": MakeRequest(request, args, headers), + "client": MakeWSClient(client, request.Id), + } + if response != nil { + resp = &Response{ + resp: response, + endCh: make(chan bool, 1), + Id: response.Id, + } + params["response"] = gojs.MakeMap(resp) } if headers != nil { params["headers"] = headers @@ -454,7 +475,7 @@ func makeRequestParams(args map[string]any, headers map[string]string, request * params["session"] = gojs.MakeMap(session) } if caller != nil { - params["caller"] = gojs.MakeMap(Caller{client: caller}) + params["caller"] = gojs.MakeMap(&Caller{client: caller}) } return params, resp } diff --git a/service.ts b/service.ts index 8b5f3ca..e311e49 100644 --- a/service.ts +++ b/service.ts @@ -5,25 +5,43 @@ export default { start, stop, register, - load + load, + task, + newCaller, + dataSet, + dataGet, + dataKeys, + dataCount, + dataFetch, + dataRemove, + listPush, + listPop, + listCount, + listRemove, } -function config(config?: Config): void { -} +function config(config?: Config): void { } +function start(): string { return '' } +function stop(): void { } +function register(option: RegisterOption, callback: (params: RequestParams) => void): any { return null } +function load(serviceFile: string, poolConfig?: PoolConfig): void { } -function start(): string { - return '' -} +function task(taskFile: string, interval: number = 1000): void { } -function stop(): void { -} +function newCaller(): Caller { return null as any } -function register(option: RegisterOption, callback: (params: RequestParams) => void): any { - return null -} +function dataSet(scope: string, key: string, value: any): void { } +function dataGet(scope: string, key: string): any { return null } +function dataKeys(scope: string): string[] { return [] } +function dataCount(scope: string): number { return 0 } +function dataFetch(scope: string): Map { return null as any } +function dataRemove(scope: string, key?: string): void { } + +function listPush(scope: string, key: string, value: any): void { } +function listPop(scope: string, key: string): any { return null } +function listCount(scope: string): number { return 0 } +function listRemove(scope: string): void { } -function load(serviceFile: string, poolConfig?: PoolConfig): void { -} interface Config { // github.com/ssgo/s 的配置参数 @@ -151,6 +169,7 @@ interface OnMessageParams { } interface WSClient { + id: string read: () => WSMessage write: (data: any) => void writeMessage: (type: string, data: any) => void @@ -199,6 +218,7 @@ interface CookieOption { } interface Request { + id: string proto: string scheme: string host: string @@ -227,6 +247,7 @@ interface Request { } interface Response { + id: string setStatus: (code: number) => void setCookie: (name: string, value: string, option?: CookieOption) => void setHeader: (name: string, value: string) => void diff --git a/task.go b/task.go index 75f6128..4a6c960 100644 --- a/task.go +++ b/task.go @@ -1,6 +1,7 @@ package service import ( + "container/list" "sync" "apigo.cc/gojs" @@ -10,7 +11,10 @@ import ( var taskData = map[string]map[string]any{} var taskDataLock = sync.RWMutex{} -func SetTaskData(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +var taskList = map[string]*list.List{} +var taskListLock = sync.RWMutex{} + +func DataSet(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { args := gojs.MakeArgs(&argsIn, vm).Check(3) scope := args.Str(0) key := args.Str(1) @@ -24,7 +28,7 @@ func SetTaskData(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { return nil } -func GetTaskData(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +func DataGet(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { args := gojs.MakeArgs(&argsIn, vm).Check(2) scope := args.Str(0) key := args.Str(1) @@ -36,7 +40,7 @@ func GetTaskData(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { return nil } -func GetTaskDataKeys(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +func DataKeys(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { args := gojs.MakeArgs(&argsIn, vm).Check(1) scope := args.Str(0) taskDataLock.RLock() @@ -52,3 +56,107 @@ func GetTaskDataKeys(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { } return nil } + +func DataCount(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := gojs.MakeArgs(&argsIn, vm).Check(1) + scope := args.Str(0) + taskDataLock.RLock() + defer taskDataLock.RUnlock() + if taskData[scope] != nil { + return vm.ToValue(len(taskData[scope])) + } + return vm.ToValue(0) +} + +func DataFetch(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := gojs.MakeArgs(&argsIn, vm).Check(1) + scope := args.Str(0) + taskDataLock.RLock() + defer taskDataLock.RUnlock() + if taskData[scope] != nil { + all := make(map[string]any) + for k, v := range taskData[scope] { + all[k] = v + } + return vm.ToValue(all) + } + return nil +} + +func DataRemove(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := gojs.MakeArgs(&argsIn, vm).Check(1) + scope := args.Str(0) + key := args.Str(1) + taskDataLock.Lock() + defer taskDataLock.Unlock() + if taskData[scope] != nil { + if key != "" { + delete(taskData[scope], key) + } else { + delete(taskData, scope) + } + } + return nil +} + +func ListPush(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := gojs.MakeArgs(&argsIn, vm).Check(2) + scope := args.Str(0) + value := args.Any(1) + fromHead := args.Bool(2) + taskListLock.Lock() + defer taskListLock.Unlock() + list1 := taskList[scope] + if list1 == nil { + list1 = list.New() + taskList[scope] = list1 + } + if fromHead { + list1.PushFront(value) + } else { + list1.PushBack(value) + } + return nil +} + +func ListPop(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := gojs.MakeArgs(&argsIn, vm).Check(1) + scope := args.Str(0) + fromEnd := args.Bool(1) + taskListLock.Lock() + var item *list.Element + defer taskListLock.Unlock() + list1 := taskList[scope] + if list1 != nil { + if fromEnd { + item = list1.Front() + } else { + item = list1.Back() + } + if item != nil { + list1.Remove(item) + return vm.ToValue(item.Value) + } + } + return nil +} + +func ListCount(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := gojs.MakeArgs(&argsIn, vm).Check(1) + scope := args.Str(0) + taskListLock.RLock() + defer taskListLock.RUnlock() + if taskList[scope] != nil { + return vm.ToValue(taskList[scope].Len()) + } + return vm.ToValue(0) +} + +func ListRemove(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := gojs.MakeArgs(&argsIn, vm).Check(1) + scope := args.Str(0) + taskListLock.Lock() + defer taskListLock.Unlock() + delete(taskList, scope) + return nil +} diff --git a/tests/api/user.js b/tests/api/user.js index 882b73f..d60d9d8 100644 --- a/tests/api/user.js +++ b/tests/api/user.js @@ -14,7 +14,10 @@ function main() { session.save() return { code: 1 } }) - service.register({ method: 'GET', path: '/userInfo', authLevel: 1, limiters: ['ip1s'] }, ({ session }) => { + service.register({ method: 'GET', path: '/userInfo', authLevel: 1, limiters: ['ip1s'] }, ({ caller }) => { + return caller.get('user/userInfoX').object() + }) + service.register({ method: 'GET', path: '/userInfoX', authLevel: 2 }, ({ session }) => { return { code: 1, data: session.get('id', 'name') } }) } diff --git a/tests/api/ws.js b/tests/api/ws.js index 22c182b..272a4af 100644 --- a/tests/api/ws.js +++ b/tests/api/ws.js @@ -1,16 +1,19 @@ -import service from "apigo.cc/gojs/service" +import s from "apigo.cc/gojs/service" import co from "apigo.cc/gojs/console" function main() { - service.register({ + s.register({ method: 'WS', path: '/ws', onMessage: ({ client, type, data }) => { client.writeMessage(type, data) }, - onClose: () => { - co.info('ws closed') + onClose: ({ client }) => { + co.info('ws closed', client.id) + s.dataRemove('wsTest', client.id) } }, ({ client }) => { + co.info('ws connected', client.id) + s.dataSet('wsTest', client.id, client) client.write('Hello, World!') }) } diff --git a/tests/gateway_test.go b/tests/gateway_test.go new file mode 100644 index 0000000..7b93661 --- /dev/null +++ b/tests/gateway_test.go @@ -0,0 +1,534 @@ +package service_test + +import ( + "fmt" + "net/http" + "testing" + + "apigo.cc/gojs/service" + "github.com/ssgo/s" + "github.com/ssgo/u" +) + +func TestRedirect(t *testing.T) { + type TestCase struct { + Url string + CheckToApp string + CheckToPath string + CheckOK bool + } + + type TestSet struct { + Key string + Value string + Cases []TestCase + } + + testSets := []TestSet{ + { + Key: "https://abc.com:443/user/", + Value: "user", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "https://abc.com/user/", + Value: "user", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "abc.com/user/", + Value: "user", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "/user/", + Value: "user", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + + { + Key: "https://abc.com:443/user/(.*)", + Value: "user/$1", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "https://abc.com/user/(.*)", + Value: "user/$1", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "abc.com/user/(.*)", + Value: "user/$1", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "/user/(.*)", + Value: "user/$1", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "user", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + + { + Key: "https://abc.com:443/user/", + Value: "https://def.com", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "https://abc.com/user/", + Value: "https://def.com", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "abc.com/user/", + Value: "https://def.com", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "/user/", + Value: "https://def.com", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + + { + Key: "https://abc.com:443/user/(.*)", + Value: "https://def.com/$1", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "https://abc.com/user/(.*)", + Value: "https://def.com/$1", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "abc.com/user/(.*)", + Value: "https://def.com/$1", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + { + Key: "/user/(.*)", + Value: "https://def.com/$1", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/login", + CheckOK: true, + }, + }, + }, + + { + Key: "abc.com:443", + Value: "https://def.com", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/user/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/user/login", + CheckOK: true, + }, + }, + }, + { + Key: "https://abc.com", + Value: "https://def.com", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/user/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/user/login", + CheckOK: true, + }, + }, + }, + { + Key: "abc.com", + Value: "https://def.com", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/user/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/user/login", + CheckOK: true, + }, + }, + }, + { + Key: "/", + Value: "https://def.com", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/user/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "https://def.com", + CheckToPath: "/user/login", + CheckOK: true, + }, + }, + }, + + { + Key: "https://abc.com:443", + Value: "/def/", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "", + CheckToPath: "/def/user/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "", + CheckToPath: "/def/user/login", + CheckOK: true, + }, + }, + }, + { + Key: "abc.com:443", + Value: "/def/", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "", + CheckToPath: "/def/user/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "", + CheckToPath: "/def/user/login", + CheckOK: true, + }, + }, + }, + { + Key: "https://abc.com", + Value: "/def/", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "", + CheckToPath: "/def/user/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "", + CheckToPath: "/def/user/login", + CheckOK: true, + }, + }, + }, + { + Key: "abc.com", + Value: "/def/", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "", + CheckToPath: "/def/user/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "", + CheckToPath: "/def/user/login", + CheckOK: true, + }, + }, + }, + { + Key: "/", + Value: "/def/", + Cases: []TestCase{ + { + Url: "https://abc.com:443/user/login", + CheckToApp: "", + CheckToPath: "/def/user/login", + CheckOK: true, + }, + { + Url: "https://abc.com/user/login", + CheckToApp: "", + CheckToPath: "/def/user/login", + CheckOK: true, + }, + }, + }, + { + Key: "/aaa/111.txt", + Value: "/bbb/222/.txt", + Cases: []TestCase{ + { + Url: "https://abc.com:443/aaa/111.txt", + CheckToApp: "", + CheckToPath: "/bbb/222/.txt", + CheckOK: true, + }, + { + Url: "https://abc.com/aaa/111.txt", + CheckToApp: "", + CheckToPath: "/bbb/222/.txt", + CheckOK: true, + }, + }, + }, + } + + makeRequest := func(url string) *s.Request { + req, _ := http.NewRequest("GET", url, nil) + return &s.Request{Request: req} + } + + // s.ServerLogger = log.NewLogger(log.Config{File: "/dev/null"}) + for _, testSet := range testSets { + service.ClearRewritesAndProxies() + service.UpdateProxy(map[string]string{testSet.Key: testSet.Value}) + for _, testCase := range testSet.Cases { + toApp, toPath, ok := service.MatchProxy(makeRequest(testCase.Url)) + if toApp != testCase.CheckToApp || toPath != testCase.CheckToPath || ok != testCase.CheckOK { + c1 := u.TextGreen + c2 := u.TextGreen + c3 := u.TextGreen + if toApp != testCase.CheckToApp { + c1 = u.TextRed + } + if toPath != testCase.CheckToPath { + c2 = u.TextRed + } + if ok != testCase.CheckOK { + c3 = u.TextRed + } + fmt.Println(u.BRed("test failed"), testCase.Url, u.Green(testCase.CheckToApp), u.Color(toApp, c1, u.BgNone), u.Green(testCase.CheckToPath), u.Color(toPath, c2, u.BgNone), u.Green(testCase.CheckOK), u.Color(ok, c3, u.BgNone)) + } else { + fmt.Println(u.BGreen("test success"), testCase.Url, u.Green(testCase.CheckToApp), u.BGreen(toApp), u.Green(testCase.CheckToPath), u.BGreen(toPath), u.Green(testCase.CheckOK), u.BGreen(ok)) + } + } + } + service.ClearRewritesAndProxies() +} diff --git a/tests/service_pool_test.go b/tests/service_pool_test.go index 312916d..fa70c65 100644 --- a/tests/service_pool_test.go +++ b/tests/service_pool_test.go @@ -11,6 +11,7 @@ import ( "apigo.cc/gojs/service" _ "apigo.cc/gojs/service" _ "apigo.cc/gojs/util" + "github.com/ssgo/discover" "github.com/ssgo/httpclient" "github.com/ssgo/u" @@ -46,6 +47,7 @@ func TestStartByPool(t *testing.T) { t.Fatal("start failed", err) } addrByPool = u.String(r) + discover.SetNode("user", addrByPool, 100) } // TODO Caller diff --git a/tests/service_test.go b/tests/service_test.go index 5a39e60..65a879d 100644 --- a/tests/service_test.go +++ b/tests/service_test.go @@ -1,7 +1,6 @@ package service_test import ( - "fmt" "testing" "time" @@ -10,7 +9,6 @@ import ( _ "apigo.cc/gojs/http" _ "apigo.cc/gojs/service" _ "apigo.cc/gojs/util" - "github.com/ssgo/httpclient" "github.com/ssgo/u" ) @@ -33,80 +31,90 @@ func TestStart(t *testing.T) { addr = u.String(r) } -func TestJsEcho(t *testing.T) { - for i := 0; i < runTimes; i++ { - name := u.UniqueId() - r, err := rt.RunCode("test('" + name + "')") - if err != nil { - t.Fatal("test js get failed, got error", err) - } else if r != name { - t.Fatal("test js get failed, name not match", r, name) - } +func TestStatic(t *testing.T) { + r, err := rt.RunCode("testStatic()") + if err != nil { + t.Fatal("test static failed, got error", err) + } + if r != true { + t.Fatal("test static failed, name not match", r) } } -func TestGoEcho(t *testing.T) { - hc := httpclient.GetClientH2C(0) - for i := 0; i < runTimes; i++ { - name := u.UniqueId() - r := hc.Get("http://" + addr + "/echo?name=" + name) - if r.Error != nil { - t.Fatal("test go get failed, got error", r.Error) - } else if r.String() != name { - t.Fatal("test go get failed, name not match", r, name) - } - } -} +// func TestJsEcho(t *testing.T) { +// for i := 0; i < runTimes; i++ { +// name := u.UniqueId() +// r, err := rt.RunCode("test('" + name + "')") +// if err != nil { +// t.Fatal("test js get failed, got error", err) +// } else if r != name { +// t.Fatal("test js get failed, name not match", r, name) +// } +// } +// } -func TestJsAsyncEcho(t *testing.T) { - ch := make(chan bool, runTimes) - t1 := time.Now().UnixMilli() - for i := 0; i < runTimes; i++ { - go func() { - name := u.UniqueId() - r, err := rt.RunCode("test('" + name + "')") - ch <- true - if err != nil { - t.Fatal("test js async get failed, got error", err) - } else if r != name { - t.Fatal("test js async get failed, name not match", r, name) - } - }() - } - for i := 0; i < runTimes; i++ { - <-ch - } - t2 := time.Now().UnixMilli() - t1 - fmt.Println(u.BGreen("js async test time:"), t2, "ms") -} +// func TestGoEcho(t *testing.T) { +// hc := httpclient.GetClientH2C(0) +// for i := 0; i < runTimes; i++ { +// name := u.UniqueId() +// r := hc.Get("http://" + addr + "/echo?name=" + name) +// if r.Error != nil { +// t.Fatal("test go get failed, got error", r.Error) +// } else if r.String() != name { +// t.Fatal("test go get failed, name not match", r, name) +// } +// } +// } -func TestGoAsyncEcho(t *testing.T) { - hc := httpclient.GetClientH2C(0) - ch := make(chan bool, runTimes*10) - t1 := time.Now().UnixMilli() - lastName := "" - lastResult := "" - for i := 0; i < runTimes*10; i++ { - name := fmt.Sprint("N", i) - lastName = name - go func() { - r := hc.Get("http://" + addr + "/echo?name=" + name) - lastResult = r.String() - ch <- true - if r.Error != nil { - t.Fatal("test go async get failed, got error", r.Error) - } else if r.String() != name { - t.Fatal("test go async get failed, name not match", r, name) - } - }() - } - for i := 0; i < runTimes*10; i++ { - <-ch - } - t2 := time.Now().UnixMilli() - t1 - fmt.Println(u.BGreen("go async test time:"), t2, "ms") - fmt.Println(u.BGreen("last name:"), lastName, lastResult) -} +// func TestJsAsyncEcho(t *testing.T) { +// ch := make(chan bool, runTimes) +// t1 := time.Now().UnixMilli() +// for i := 0; i < runTimes; i++ { +// go func() { +// name := u.UniqueId() +// r, err := rt.RunCode("test('" + name + "')") +// ch <- true +// if err != nil { +// t.Fatal("test js async get failed, got error", err) +// } else if r != name { +// t.Fatal("test js async get failed, name not match", r, name) +// } +// }() +// } +// for i := 0; i < runTimes; i++ { +// <-ch +// } +// t2 := time.Now().UnixMilli() - t1 +// fmt.Println(u.BGreen("js async test time:"), t2, "ms") +// } + +// func TestGoAsyncEcho(t *testing.T) { +// hc := httpclient.GetClientH2C(0) +// ch := make(chan bool, runTimes*10) +// t1 := time.Now().UnixMilli() +// lastName := "" +// lastResult := "" +// for i := 0; i < runTimes*10; i++ { +// name := fmt.Sprint("N", i) +// lastName = name +// go func() { +// r := hc.Get("http://" + addr + "/echo?name=" + name) +// lastResult = r.String() +// ch <- true +// if r.Error != nil { +// t.Fatal("test go async get failed, got error", r.Error) +// } else if r.String() != name { +// t.Fatal("test go async get failed, name not match", r, name) +// } +// }() +// } +// for i := 0; i < runTimes*10; i++ { +// <-ch +// } +// t2 := time.Now().UnixMilli() - t1 +// fmt.Println(u.BGreen("go async test time:"), t2, "ms") +// fmt.Println(u.BGreen("last name:"), lastName, lastResult) +// } func TestStop(t *testing.T) { go func() { diff --git a/tests/start.js b/tests/start.js index 7235dab..bbceffc 100644 --- a/tests/start.js +++ b/tests/start.js @@ -7,6 +7,13 @@ let h2c = http let urlPrefix function main() { s.config({ + app: 'user', + accessTokens: { + 'testToken1122': 2 + }, + calls: { + 'user': 'testToken1122' + }, cpuMonitor: true, memoryMonitor: true, sessionKey: 'SessionID', @@ -21,6 +28,12 @@ function main() { times: 10 } }, + static: { + '/': 'api/', + }, + rewrite: { + '/echo2.js': '/echo.js' + } }) s.register({ path: '/echo', noLog200: true }, ({ args, response }) => { // setTimeout(() => { @@ -43,6 +56,12 @@ function test(name) { return r.string() } +function testStatic() { + let r = h2c.get('/echo2.js') + if (r.string().indexOf('/echo2') === -1) return r.string() + return true +} + function test2(name) { let r = h2c.get('/echo2?name=' + name) return r.string() diff --git a/tests/start_ws.js b/tests/start_ws.js index 6fee651..031e3be 100644 --- a/tests/start_ws.js +++ b/tests/start_ws.js @@ -7,6 +7,7 @@ let hc = http let urlPrefix function main() { s.load('api/ws.js') + s.task('task.js', 100) let host = s.start() hc = http.new({ baseURL: 'http://' + host }) return host @@ -30,7 +31,7 @@ function testWS() { co.info('test ws abc ok') // ws.ping() - u.sleep(100) + u.sleep(10) let pc = ws.pingCount() co.info('test ws ping ok', pc.pingTimes, pc.pongTimes) @@ -51,6 +52,14 @@ function testWS() { } co.info('test ws json ok') + u.sleep(1000) + for (let i = 0; i < 5; i++) { + let j = ws.read() + if (i !== j.data) { + return j + } + } + ws.close() return true } diff --git a/tests/task.js b/tests/task.js new file mode 100644 index 0000000..fb1e2eb --- /dev/null +++ b/tests/task.js @@ -0,0 +1,30 @@ +import s from 'apigo.cc/gojs/service' +import co from 'apigo.cc/gojs/console' + +function onStart() { + co.info('task start') +} + +let i = 0 +function onRun() { + let connCount = s.dataCount('wsTest') + if (connCount > 0) { + let conns = s.dataFetch('wsTest') + for (let id in conns) { + let conn = conns[id] + try { + conn.write(i++) + } catch (e) { + co.error(e) + s.dataRemove('wsTest', id) + } + } + } + co.info('task run', connCount) +} + + +function onStop() { + s.dataRemove('wsTest') + co.info('task stop', s.dataCount('wsTest')) +} diff --git a/ws.go b/ws.go index 5c4e044..181283b 100644 --- a/ws.go +++ b/ws.go @@ -12,8 +12,9 @@ import ( "github.com/ssgo/u" ) -func MakeWSClient(client *websocket.Conn) gojs.Map { +func MakeWSClient(client *websocket.Conn, id string) gojs.Map { return gojs.Map{ + "id": id, "read": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { typ, data, err := readWSMessage(client) if err != nil {