This commit is contained in:
Star 2024-10-18 17:54:37 +08:00
parent d08ccdff7e
commit 9546a0b4f3
18 changed files with 1250 additions and 312 deletions

103
README.md
View File

@ -49,9 +49,7 @@ function main() {
``` ```
这种模式下服务代码会脱离主线程,使用对象池实现高并发 这种模式下服务代码会脱离主线程,使用对象池实现高并发
如果不配置对象池参数则不约束虚拟机数量上限可以达到最佳性能但是对CPU和内存有一定挑战 如果不配置对象池参数则不约束虚拟机数量上限可以达到最佳性能但是对CPU和内存有一定挑战
设置较小的max可以有效保护CPU和内存资源但是设置过小将无法发挥服务器的性能 设置较小的max可以有效保护CPU和内存资源但是设置过小将无法发挥服务器的性能
## 注册到服务发现 ## 注册到服务发现
@ -86,19 +84,14 @@ s.register({ path: '/getUserInfo' }, ({ caller }) => {
``` ```
authLevel 不设置则不进行校验,如果获得 authLevel 2 则允许访问所有 2及以下级别的接口 authLevel 不设置则不进行校验,如果获得 authLevel 2 则允许访问所有 2及以下级别的接口
如果 user 服务不配置 listen 默认使用 h2c 协议随机端口 如果 user 服务不配置 listen 默认使用 h2c 协议随机端口
如果不使用 h2c 协议,调用方配置 calls 时需指定 'http:aaaaaaaa' 如果不使用 h2c 协议,调用方配置 calls 时需指定 'http:aaaaaaaa'
## Session ## Session
服务默认启用 session 和 device 功能,如果不希望使用可以在配置中设置 sessionKey 或 deviceKey 为空 服务默认启用 session 和 device 功能,如果不希望使用可以在配置中设置 sessionKey 或 deviceKey 为空
如果配置了 sessionProvidersession 将会存储在 redis 中,否则存储在内存中 如果配置了 sessionProvidersession 将会存储在 redis 中,否则存储在内存中
sessionID和deviceID 同时支持HTTP头和Cookie两种传输方式HTTP头优先如果客户端没有传递则服务器会自动分配 sessionID和deviceID 同时支持HTTP头和Cookie两种传输方式HTTP头优先如果客户端没有传递则服务器会自动分配
如需使用 session 只需要在接口中直接获取即可 如需使用 session 只需要在接口中直接获取即可
#### 下面是一个使用 session 并且使用参数有效性验证和限流器的例子 #### 下面是一个使用 session 并且使用参数有效性验证和限流器的例子
@ -139,15 +132,10 @@ function main() {
``` ```
session对象自动注入无需任何其他操作。修改session后需要使用 session.save 来保存 session对象自动注入无需任何其他操作。修改session后需要使用 session.save 来保存
调用 session.setAuthLevel 可以设置用户权限,当接口注册的 authLevel 大于0时可以基于 session 中的设置进行访问控制 调用 session.setAuthLevel 可以设置用户权限,当接口注册的 authLevel 大于0时可以基于 session 中的设置进行访问控制
配置了 userIdKey 后,会自动将 session 的用户ID记录在访问日志中方便对用户的访问进行分析和统计 配置了 userIdKey 后,会自动将 session 的用户ID记录在访问日志中方便对用户的访问进行分析和统计
示例中创建了一个每个IP每秒允许10次请求的限流器并且在接口中使用了这个限流器 示例中创建了一个每个IP每秒允许10次请求的限流器并且在接口中使用了这个限流器
login接口配置了 id 和 name 两个参数的有效性验证规则 login接口配置了 id 和 name 两个参数的有效性验证规则
参数有效性验证配置可以支持以下类型: 参数有效性验证配置可以支持以下类型:
- value为string或RegExp对象时进行正则表达式校验 - value为string或RegExp对象时进行正则表达式校验
@ -169,8 +157,6 @@ ssl:
yourdomain.com: yourdomain.com:
certfile: /path/yourdomain.pem certfile: /path/yourdomain.pem
keyfile: /path/yourdomain.pem keyfile: /path/yourdomain.pem
static:
yourdomain.com: /path/www
``` ```
2、在环境配置文件 env.yml 或 env.json 中配置 2、在环境配置文件 env.yml 或 env.json 中配置
@ -188,6 +174,40 @@ docker run -e SERVICE_LISTEN=8080:8443
#### 所有配置方式的优先级为 s.config > 环境变量 > env.yml > service.yml #### 所有配置方式的优先级为 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 ## websocket
```javascript ```javascript
@ -212,5 +232,60 @@ function main() {
注册接口时将 method 指定为 WS 即可创建 websocket 服务,配置 onMessage 来异步处理消息 注册接口时将 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) ## 完整的API参考 [service.ts](https://apigo.cc/gojs/service/src/branch/main/service.ts)

View File

@ -80,7 +80,7 @@ func (cl *Caller) makeHeaderArray(in map[string]any) []string {
func parseAppPath(appURL string) (string, string) { func parseAppPath(appURL string) (string, string) {
arr := strings.SplitN(appURL, "/", 2) arr := strings.SplitN(appURL, "/", 2)
if len(arr) == 2 { if len(arr) == 2 {
return arr[0], arr[1] return arr[0], "/" + arr[1]
} }
return appURL, "/" return appURL, "/"
} }

View File

@ -2,6 +2,7 @@ package service
import ( import (
"fmt" "fmt"
"path"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
@ -13,25 +14,21 @@ import (
"github.com/ssgo/u" "github.com/ssgo/u"
) )
type regexProxiesInfo struct { type regexRedirectInfo struct {
Value string
Regex regexp.Regexp
}
type regexRewriteInfo struct {
To string To string
Regex regexp.Regexp Regex regexp.Regexp
} }
var _proxies = map[string]string{} var _proxies = map[string]string{}
var _proxiesLock = sync.RWMutex{} var _proxiesLock = sync.RWMutex{}
var _regexProxies = map[string]*regexProxiesInfo{} var _regexProxies = map[string]*regexRedirectInfo{}
var _regexRewrites = map[string]*regexRewriteInfo{} var _rewrites = map[string]string{}
var _regexRewrites = map[string]*regexRedirectInfo{}
var _rewritesLock = sync.RWMutex{} var _rewritesLock = sync.RWMutex{}
var _statics = map[string]string{} var _statics = map[string]string{}
var _staticsLock = sync.RWMutex{} var _staticsLock = sync.RWMutex{}
func updateStatic(in map[string]string) bool { func UpdateStatic(in map[string]string) bool {
updated := false updated := false
for k, v := range in { for k, v := range in {
_staticsLock.RLock() _staticsLock.RLock()
@ -52,13 +49,16 @@ func updateStatic(in map[string]string) bool {
if a[0] == "*" { if a[0] == "*" {
a[0] = "" a[0] = ""
} }
if a[1] == "" {
a[1] = "/"
}
s.StaticByHost(a[1], v, a[0]) s.StaticByHost(a[1], v, a[0])
updated = true updated = true
} }
return updated return updated
} }
func updateProxy(in map[string]string) bool { func UpdateProxy(in map[string]string) bool {
updated := false updated := false
//fmt.Println("####000") //fmt.Println("####000")
@ -74,23 +74,22 @@ func updateProxy(in map[string]string) bool {
continue continue
} }
////fmt.Println("####333", k, v) ////fmt.Println("####333", k, v)
if v2 != nil && v == v2.Value { if v2 != nil && v == v2.To {
continue continue
} }
////fmt.Println("####444", k, v) ////fmt.Println("####444", k, v)
if strings.Contains(k, "(") {
if strings.Contains(v, "(") {
// for regexp // for regexp
////fmt.Println("####555", k, v) ////fmt.Println("####555", k, v)
matcher, err := regexp.Compile("^" + v + "$") matcher, err := regexp.Compile("^" + k + "$")
if err != nil { if err != nil {
s.ServerLogger.Error("proxy regexp compile failed", "key", k, "value", v) s.ServerLogger.Error("proxy regexp compile failed", "key", k, "value", v)
//log.Print("Proxy Error Compile ", err) //log.Print("Proxy Error Compile ", err)
} else { } else {
s.ServerLogger.Info(u.StringIf(v2 != nil, "update regexp proxy set", "new regexp proxy set"), "key", k, "value", v) s.ServerLogger.Info(u.StringIf(v2 != nil, "update regexp proxy set", "new regexp proxy set"), "key", k, "value", v)
_proxiesLock.Lock() _proxiesLock.Lock()
_regexProxies[k] = &regexProxiesInfo{ _regexProxies[k] = &regexRedirectInfo{
Value: v, To: v,
Regex: *matcher, Regex: *matcher,
} }
_proxiesLock.Unlock() _proxiesLock.Unlock()
@ -117,11 +116,11 @@ func updateProxy(in map[string]string) bool {
} else { } else {
callConfig = (time.Duration(s.Config.ReadHeaderTimeout) * time.Millisecond).String() callConfig = (time.Duration(s.Config.ReadHeaderTimeout) * time.Millisecond).String()
} }
// if redisPool != nil { if discover.Config.Registry != "" {
if discover.AddExternalApp(v, callConfig) { if discover.AddExternalApp(v, callConfig) {
updated = true updated = true
} }
// } }
} else { } else {
updated = true updated = true
} }
@ -134,79 +133,106 @@ func updateProxy(in map[string]string) bool {
return updated return updated
} }
func updateRewrite(in map[string]string) bool { func UpdateRewrite(in map[string]string) bool {
updated := false updated := false
for k, v := range in { for k, v := range in {
_rewritesLock.RLock() _rewritesLock.RLock()
v1 := _rewrites[k]
v2 := _regexRewrites[k] v2 := _regexRewrites[k]
_rewritesLock.RUnlock() _rewritesLock.RUnlock()
// skip same // skip same
if v == v1 {
continue
}
if v2 != nil && v == v2.To { if v2 != nil && v == v2.To {
continue continue
} }
if strings.Contains(k, "(") {
matcher, err := regexp.Compile("^" + k + "$") matcher, err := regexp.Compile("^" + k + "$")
if err != nil { if err != nil {
s.ServerLogger.Error("rewrite regexp compile failed", "key", k, "value", v) s.ServerLogger.Error("rewrite regexp compile failed", "key", k, "value", v)
} else { } else {
s.ServerLogger.Info(u.StringIf(v2 != nil, "update regexp rewrite set", "new regexp rewrite set"), "key", k, "value", v) s.ServerLogger.Info(u.StringIf(v2 != nil, "update regexp rewrite set", "new regexp rewrite set"), "key", k, "value", v)
_rewritesLock.Lock() _rewritesLock.Lock()
_regexRewrites[k] = &regexRewriteInfo{ _regexRewrites[k] = &regexRedirectInfo{
To: v, To: v,
Regex: *matcher, Regex: *matcher,
} }
_rewritesLock.Unlock() _rewritesLock.Unlock()
updated = true updated = true
} }
} else {
_rewritesLock.Lock()
_rewrites[k] = v
_rewritesLock.Unlock()
updated = true
}
} }
return updated return updated
} }
// TODO 测试各种情况下的 matchRedirect
func rewrite(request *s.Request) (toPath string, rewrite bool) { func rewrite(request *s.Request) (toPath string, rewrite bool) {
list2 := map[string]*regexRewriteInfo{} if toApp, toPath, ok := matchRedirect(request, &_rewrites, &_regexRewrites, &_rewritesLock); ok {
_rewritesLock.RLock() return toApp + toPath, true
for k, v := range _regexRewrites {
list2[k] = v
} }
_rewritesLock.RUnlock() return
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
} }
func proxy(request *s.Request) (authLevel int, toApp, toPath *string, headers map[string]string) { func proxy(request *s.Request) (authLevel int, toApp, toPath *string, headers map[string]string) {
//fmt.Println("proxy", len(_proxies)) if toApp1, toPath1, ok := matchRedirect(request, &_proxies, &_regexProxies, &_proxiesLock); ok {
outHeaders := map[string]string{ outHeaders := map[string]string{
standard.DiscoverHeaderFromApp: "gateway", standard.DiscoverHeaderFromApp: "gateway",
standard.DiscoverHeaderFromNode: s.GetServerAddr(), 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 := "" host1 := ""
host2 := "" host2 := ""
if strings.ContainsRune(request.Host, ':') { if strings.ContainsRune(request.Host, ':') {
@ -215,98 +241,132 @@ func proxy(request *s.Request) (authLevel int, toApp, toPath *string, headers ma
host2 = request.Host host2 = request.Host
} else { } else {
host1 = request.Host 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 := make([]string, 0)
pathMatchers = append(pathMatchers, fmt.Sprint(scheme, "://", host1, request.RequestURI)) pathMatchers = append(pathMatchers, fmt.Sprint(request.URL.Scheme, "://", host1, requestPath))
pathMatchers = append(pathMatchers, fmt.Sprint(scheme, "://", host2, request.RequestURI)) pathMatchers = append(pathMatchers, fmt.Sprint(request.URL.Scheme, "://", host2, requestPath))
pathMatchers = append(pathMatchers, fmt.Sprint(host1, request.RequestURI)) pathMatchers = append(pathMatchers, fmt.Sprint(host1, requestPath))
pathMatchers = append(pathMatchers, fmt.Sprint(host2, request.RequestURI)) pathMatchers = append(pathMatchers, fmt.Sprint(host2, requestPath))
pathMatchers = append(pathMatchers, request.RequestURI) pathMatchers = append(pathMatchers, requestPath)
hostMatchers := make([]string, 0) hostMatchers := make([]string, 0)
hostMatchers = append(hostMatchers, fmt.Sprint(scheme, "://", host1)) hostMatchers = append(hostMatchers, fmt.Sprint(request.URL.Scheme, "://", host1))
hostMatchers = append(hostMatchers, fmt.Sprint(scheme, "://", host2)) hostMatchers = append(hostMatchers, fmt.Sprint(request.URL.Scheme, "://", host2))
hostMatchers = append(hostMatchers, host1) hostMatchers = append(hostMatchers, host1)
hostMatchers = append(hostMatchers, host2) hostMatchers = append(hostMatchers, host2)
list := map[string]string{} if n1 > 0 {
_proxiesLock.RLock() list1 := map[string]string{}
for k, v := range _proxies { (*lock).RLock()
list[k] = v for k, v := range *normalList {
list1[k] = v
} }
_proxiesLock.RUnlock() (*lock).RUnlock()
for p, a := range list { for setKey, setValue := range list1 {
//fmt.Println("check proxy ", p, a)
matchPath := "" matchPath := ""
matchPathArr := strings.SplitN(strings.ReplaceAll(p, "://", ""), "/", 2) matchPathArr := strings.SplitN(strings.ReplaceAll(setKey, "://", ""), "/", 2)
if len(matchPathArr) == 2 { if len(matchPathArr) == 2 {
matchPath = "/" + matchPathArr[1] matchPath = "/" + matchPathArr[1]
} }
toApp, toPath = splitAppAndPath(setValue)
if matchPath == "" { if matchPath == "" {
for _, m := range hostMatchers { for _, matchStr := range hostMatchers {
if m == p { if matchStr == setKey {
//fmt.Println(" >>>>>>>>1", p, m, request.RequestURI) // fmt.Println(" >>>>>>>>1", setKey, matchStr, requestPath)
return 0, fixAppName(a), &request.RequestURI, outHeaders return toApp, path.Join(toPath, requestPath), true
} }
} }
} else { } else {
for _, m := range pathMatchers { for _, matchStr := range pathMatchers {
if strings.HasPrefix(m, p) { // fmt.Println(" >>>>>>>> test", u.BCyan(matchStr), u.BMagenta(setKey), "|", strings.HasPrefix(matchStr, setKey))
if strings.HasPrefix(request.RequestURI, matchPath) { if strings.HasPrefix(matchStr, setKey) {
p2 := request.RequestURI[len(matchPath):] if strings.HasPrefix(requestPath, matchPath) {
p2 := requestPath[len(matchPath):]
if len(p2) == 0 || p2[0] != '/' { if len(p2) == 0 || p2[0] != '/' {
p2 = "/" + p2 p2 = "/" + p2
} }
//fmt.Println(" >>>>>>>>2", p, m, p2) // fmt.Println(" >>>>>>>>2", setKey, matchStr, p2)
return 0, fixAppName(a), &p2, outHeaders return toApp, path.Join(toPath, p2), true
} else { } else {
//fmt.Println(" >>>>>>>>3", p, m, request.RequestURI) // fmt.Println(" >>>>>>>>3", setKey, matchStr, requestPath)
return 0, fixAppName(a), &request.RequestURI, outHeaders return toApp, path.Join(toPath, requestPath), true
}
} }
} }
} }
} }
} }
if n2 > 0 {
// 模糊匹配 // 模糊匹配
list2 := map[string]*regexProxiesInfo{} list2 := map[string]*regexRedirectInfo{}
_proxiesLock.RLock() (*lock).RLock()
for k, v := range _regexProxies { for k, v := range *regexpList {
list2[k] = v list2[k] = v
} }
_proxiesLock.RUnlock() (*lock).RUnlock()
if len(list2) > 0 { // requestUrl := request.Host + requestPath
requestUrl := request.Host + request.RequestURI for setKey, setInfo := range list2 {
for _, rp := range list2 { matchPath := ""
//fmt.Println("check regexp proxy ", rp.Regex, rp.Value) matchPathArr := strings.SplitN(strings.ReplaceAll(setKey, "://", ""), "/", 2)
finds := rp.Regex.FindAllStringSubmatch(requestUrl, 20) if len(matchPathArr) == 2 {
if len(finds) > 0 && len(finds[0]) > 2 { matchPath = "/" + matchPathArr[1]
//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], ':') { // fmt.Println(" >>>>>>>> matchPath", setKey, matchPath, u.JsonP(matchPathArr), 111)
callConfig := "" var matchList []string
if strings.ContainsRune(finds[0][1], ':') { if matchPath == "" {
// support call config in proxy value matchList = hostMatchers
a := strings.SplitN(finds[0][1], ":", 2)
finds[0][1] = a[0]
callConfig = a[1]
} else { } else {
callConfig = (time.Duration(s.Config.ReadHeaderTimeout) * time.Millisecond).String() matchList = pathMatchers
} }
// if redisPool != nil { for _, matchStr := range matchList {
discover.AddExternalApp(finds[0][1], callConfig) 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
// } // }
}
return 0, &finds[0][1], &finds[0][2], outHeaders
}
} }
} }
@ -314,11 +374,52 @@ func proxy(request *s.Request) (authLevel int, toApp, toPath *string, headers ma
return return
} }
func fixAppName(appName string) *string { // func fixAppName(appName string) string {
if !strings.Contains(appName, "://") && strings.ContainsRune(appName, ':') { // if !strings.Contains(appName, "://") && strings.ContainsRune(appName, ':') {
a := strings.SplitN(appName, ":", 2) // a := strings.SplitN(appName, "/", 2)
return &a[0] // 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 { } 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
}

4
go.mod
View File

@ -9,11 +9,11 @@ require (
apigo.cc/gojs/util v0.0.2 apigo.cc/gojs/util v0.0.2
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/ssgo/config v1.7.7 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/httpclient v1.7.8
github.com/ssgo/log v1.7.7 github.com/ssgo/log v1.7.7
github.com/ssgo/redis 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/standard v1.7.7
github.com/ssgo/u v1.7.9 github.com/ssgo/u v1.7.9
) )

View File

@ -11,6 +11,7 @@ import (
type Request struct { type Request struct {
req *s.Request req *s.Request
Id string
Proto string Proto string
Scheme string Scheme string
Host string Host string
@ -62,6 +63,7 @@ func MakeRequest(req *s.Request, args map[string]any, headers map[string]string)
} }
return gojs.MakeMap(&Request{ return gojs.MakeMap(&Request{
req: req, req: req,
Id: req.Id,
Proto: req.Proto, Proto: req.Proto,
Scheme: req.Header.Get(standard.DiscoverHeaderScheme), Scheme: req.Header.Get(standard.DiscoverHeaderScheme),
Host: req.Header.Get(standard.DiscoverHeaderHost), Host: req.Header.Get(standard.DiscoverHeaderHost),

View File

@ -14,6 +14,7 @@ type Response struct {
resp *s.Response resp *s.Response
endCh chan bool endCh chan bool
result any result any
Id string
} }
func (r *Response) End(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { func (r *Response) End(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {

View File

@ -70,8 +70,6 @@ var limiters = map[string]*s.Limiter{}
func init() { func init() {
obj := map[string]any{ obj := map[string]any{
"config": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { "config": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
s.Init()
// 处理配置 // 处理配置
args := gojs.MakeArgs(&argsIn, vm) 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{}} 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 // 身份验证和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) s.SetClientKeys(serviceConfig.DeviceKey, serviceConfig.ClientKey, serviceConfig.SessionKey)
if 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) { 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 setAuthLevel := 0
if serviceConfig.SessionKey != "" { if serviceConfig.SessionKey != "" {
sessionID := request.GetSessionId() sessionID := request.GetSessionId()
if sessionID != "" { if sessionID != "" {
session = NewSession(sessionID, logger) session = NewSession(sessionID, logger)
} }
@ -117,30 +116,9 @@ func init() {
setAuthLevel = u.Int(authLevelBySession) 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中的authLevel验证失败则使用Access-Token中的authLevel服务间调用
// 如果没有session或session中的authLevel为0则使用Access-Token中的authLevel服务间调用 if authAccessToken && setAuthLevel < authLevel {
if authAccessToken && setAuthLevel == 0 && authLevel > 0 {
setAuthLevel = s.GetAuthTokenLevel(request.Header.Get("Access-Token")) setAuthLevel = s.GetAuthTokenLevel(request.Header.Get("Access-Token"))
} }
if setAuthLevel >= authLevel { if setAuthLevel >= authLevel {
@ -159,6 +137,7 @@ func init() {
} }
} }
}) })
s.Init()
} }
// 限流器 // 限流器
@ -189,17 +168,16 @@ func init() {
if server != nil { if server != nil {
panic(vm.NewGoError(errors.New("server already started"))) panic(vm.NewGoError(errors.New("server already started")))
} }
// 处理静态文件 // 处理静态文件
if len(serviceConfig.Static) > 0 { if len(serviceConfig.Static) > 0 {
updateStatic(serviceConfig.Static) UpdateStatic(serviceConfig.Static)
} }
if len(serviceConfig.Rewrite) > 0 { if len(serviceConfig.Rewrite) > 0 {
updateRewrite(serviceConfig.Rewrite) UpdateRewrite(serviceConfig.Rewrite)
s.SetRewriteBy(rewrite) s.SetRewriteBy(rewrite)
} }
if len(serviceConfig.Proxy) > 0 { if len(serviceConfig.Proxy) > 0 {
updateProxy(serviceConfig.Proxy) UpdateProxy(serviceConfig.Proxy)
s.SetProxyBy(proxy) s.SetProxyBy(proxy)
} }
@ -223,6 +201,7 @@ func init() {
panic(vm.NewGoError(errors.New("server not started"))) panic(vm.NewGoError(errors.New("server not started")))
} }
server.Stop() server.Stop()
ClearRewritesAndProxies()
pools = map[string]*gojs.Pool{} pools = map[string]*gojs.Pool{}
server = nil server = nil
return nil return nil
@ -366,6 +345,44 @@ func init() {
poolsLock.Unlock() poolsLock.Unlock()
return nil return nil
}, },
"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, "newCaller": NewCaller,
} }
@ -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) { 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{ var resp *Response
resp: response,
endCh: make(chan bool, 1),
}
params := gojs.Map{ params := gojs.Map{
"args": args, "args": args,
"logger": gojs.MakeLogger(logger), "logger": gojs.MakeLogger(logger),
"request": MakeRequest(request, args, headers), "request": MakeRequest(request, args, headers),
"response": gojs.MakeMap(resp), "client": MakeWSClient(client, request.Id),
"client": MakeWSClient(client), }
if response != nil {
resp = &Response{
resp: response,
endCh: make(chan bool, 1),
Id: response.Id,
}
params["response"] = gojs.MakeMap(resp)
} }
if headers != nil { if headers != nil {
params["headers"] = headers params["headers"] = headers
@ -454,7 +475,7 @@ func makeRequestParams(args map[string]any, headers map[string]string, request *
params["session"] = gojs.MakeMap(session) params["session"] = gojs.MakeMap(session)
} }
if caller != nil { if caller != nil {
params["caller"] = gojs.MakeMap(Caller{client: caller}) params["caller"] = gojs.MakeMap(&Caller{client: caller})
} }
return params, resp return params, resp
} }

View File

@ -5,25 +5,43 @@ export default {
start, start,
stop, stop,
register, 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 { function task(taskFile: string, interval: number = 1000): void { }
return ''
}
function stop(): void { function newCaller(): Caller { return null as any }
}
function register(option: RegisterOption, callback: (params: RequestParams) => void): any { function dataSet(scope: string, key: string, value: any): void { }
return null 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<string, any> { 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 { interface Config {
// github.com/ssgo/s 的配置参数 // github.com/ssgo/s 的配置参数
@ -151,6 +169,7 @@ interface OnMessageParams {
} }
interface WSClient { interface WSClient {
id: string
read: () => WSMessage read: () => WSMessage
write: (data: any) => void write: (data: any) => void
writeMessage: (type: string, data: any) => void writeMessage: (type: string, data: any) => void
@ -199,6 +218,7 @@ interface CookieOption {
} }
interface Request { interface Request {
id: string
proto: string proto: string
scheme: string scheme: string
host: string host: string
@ -227,6 +247,7 @@ interface Request {
} }
interface Response { interface Response {
id: string
setStatus: (code: number) => void setStatus: (code: number) => void
setCookie: (name: string, value: string, option?: CookieOption) => void setCookie: (name: string, value: string, option?: CookieOption) => void
setHeader: (name: string, value: string) => void setHeader: (name: string, value: string) => void

114
task.go
View File

@ -1,6 +1,7 @@
package service package service
import ( import (
"container/list"
"sync" "sync"
"apigo.cc/gojs" "apigo.cc/gojs"
@ -10,7 +11,10 @@ import (
var taskData = map[string]map[string]any{} var taskData = map[string]map[string]any{}
var taskDataLock = sync.RWMutex{} 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) args := gojs.MakeArgs(&argsIn, vm).Check(3)
scope := args.Str(0) scope := args.Str(0)
key := args.Str(1) key := args.Str(1)
@ -24,7 +28,7 @@ func SetTaskData(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
return nil 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) args := gojs.MakeArgs(&argsIn, vm).Check(2)
scope := args.Str(0) scope := args.Str(0)
key := args.Str(1) key := args.Str(1)
@ -36,7 +40,7 @@ func GetTaskData(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
return nil 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) args := gojs.MakeArgs(&argsIn, vm).Check(1)
scope := args.Str(0) scope := args.Str(0)
taskDataLock.RLock() taskDataLock.RLock()
@ -52,3 +56,107 @@ func GetTaskDataKeys(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
} }
return nil 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
}

View File

@ -14,7 +14,10 @@ function main() {
session.save() session.save()
return { code: 1 } 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') } return { code: 1, data: session.get('id', 'name') }
}) })
} }

View File

@ -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" import co from "apigo.cc/gojs/console"
function main() { function main() {
service.register({ s.register({
method: 'WS', path: '/ws', method: 'WS', path: '/ws',
onMessage: ({ client, type, data }) => { onMessage: ({ client, type, data }) => {
client.writeMessage(type, data) client.writeMessage(type, data)
}, },
onClose: () => { onClose: ({ client }) => {
co.info('ws closed') co.info('ws closed', client.id)
s.dataRemove('wsTest', client.id)
} }
}, ({ client }) => { }, ({ client }) => {
co.info('ws connected', client.id)
s.dataSet('wsTest', client.id, client)
client.write('Hello, World!') client.write('Hello, World!')
}) })
} }

534
tests/gateway_test.go Normal file
View File

@ -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()
}

View File

@ -11,6 +11,7 @@ import (
"apigo.cc/gojs/service" "apigo.cc/gojs/service"
_ "apigo.cc/gojs/service" _ "apigo.cc/gojs/service"
_ "apigo.cc/gojs/util" _ "apigo.cc/gojs/util"
"github.com/ssgo/discover"
"github.com/ssgo/httpclient" "github.com/ssgo/httpclient"
"github.com/ssgo/u" "github.com/ssgo/u"
@ -46,6 +47,7 @@ func TestStartByPool(t *testing.T) {
t.Fatal("start failed", err) t.Fatal("start failed", err)
} }
addrByPool = u.String(r) addrByPool = u.String(r)
discover.SetNode("user", addrByPool, 100)
} }
// TODO Caller // TODO Caller

View File

@ -1,7 +1,6 @@
package service_test package service_test
import ( import (
"fmt"
"testing" "testing"
"time" "time"
@ -10,7 +9,6 @@ import (
_ "apigo.cc/gojs/http" _ "apigo.cc/gojs/http"
_ "apigo.cc/gojs/service" _ "apigo.cc/gojs/service"
_ "apigo.cc/gojs/util" _ "apigo.cc/gojs/util"
"github.com/ssgo/httpclient"
"github.com/ssgo/u" "github.com/ssgo/u"
) )
@ -33,80 +31,90 @@ func TestStart(t *testing.T) {
addr = u.String(r) addr = u.String(r)
} }
func TestJsEcho(t *testing.T) { func TestStatic(t *testing.T) {
for i := 0; i < runTimes; i++ { r, err := rt.RunCode("testStatic()")
name := u.UniqueId()
r, err := rt.RunCode("test('" + name + "')")
if err != nil { if err != nil {
t.Fatal("test js get failed, got error", err) t.Fatal("test static failed, got error", err)
} else if r != name {
t.Fatal("test js get failed, name not match", r, name)
} }
if r != true {
t.Fatal("test static failed, name not match", r)
} }
} }
func TestGoEcho(t *testing.T) { // func TestJsEcho(t *testing.T) {
hc := httpclient.GetClientH2C(0) // for i := 0; i < runTimes; i++ {
for i := 0; i < runTimes; i++ { // name := u.UniqueId()
name := u.UniqueId() // r, err := rt.RunCode("test('" + name + "')")
r := hc.Get("http://" + addr + "/echo?name=" + name) // if err != nil {
if r.Error != nil { // t.Fatal("test js get failed, got error", err)
t.Fatal("test go get failed, got error", r.Error) // } else if r != name {
} else if r.String() != name { // t.Fatal("test js get failed, name not match", r, name)
t.Fatal("test go get failed, name not match", r, name) // }
} // }
} // }
}
func TestJsAsyncEcho(t *testing.T) { // func TestGoEcho(t *testing.T) {
ch := make(chan bool, runTimes) // hc := httpclient.GetClientH2C(0)
t1 := time.Now().UnixMilli() // for i := 0; i < runTimes; i++ {
for i := 0; i < runTimes; i++ { // name := u.UniqueId()
go func() { // r := hc.Get("http://" + addr + "/echo?name=" + name)
name := u.UniqueId() // if r.Error != nil {
r, err := rt.RunCode("test('" + name + "')") // t.Fatal("test go get failed, got error", r.Error)
ch <- true // } else if r.String() != name {
if err != nil { // t.Fatal("test go get failed, name not match", r, name)
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) { // func TestJsAsyncEcho(t *testing.T) {
hc := httpclient.GetClientH2C(0) // ch := make(chan bool, runTimes)
ch := make(chan bool, runTimes*10) // t1 := time.Now().UnixMilli()
t1 := time.Now().UnixMilli() // for i := 0; i < runTimes; i++ {
lastName := "" // go func() {
lastResult := "" // name := u.UniqueId()
for i := 0; i < runTimes*10; i++ { // r, err := rt.RunCode("test('" + name + "')")
name := fmt.Sprint("N", i) // ch <- true
lastName = name // if err != nil {
go func() { // t.Fatal("test js async get failed, got error", err)
r := hc.Get("http://" + addr + "/echo?name=" + name) // } else if r != name {
lastResult = r.String() // t.Fatal("test js async get failed, name not match", r, name)
ch <- true // }
if r.Error != nil { // }()
t.Fatal("test go async get failed, got error", r.Error) // }
} else if r.String() != name { // for i := 0; i < runTimes; i++ {
t.Fatal("test go async get failed, name not match", r, name) // <-ch
} // }
}() // t2 := time.Now().UnixMilli() - t1
} // fmt.Println(u.BGreen("js async test time:"), t2, "ms")
for i := 0; i < runTimes*10; i++ { // }
<-ch
} // func TestGoAsyncEcho(t *testing.T) {
t2 := time.Now().UnixMilli() - t1 // hc := httpclient.GetClientH2C(0)
fmt.Println(u.BGreen("go async test time:"), t2, "ms") // ch := make(chan bool, runTimes*10)
fmt.Println(u.BGreen("last name:"), lastName, lastResult) // 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) { func TestStop(t *testing.T) {
go func() { go func() {

View File

@ -7,6 +7,13 @@ let h2c = http
let urlPrefix let urlPrefix
function main() { function main() {
s.config({ s.config({
app: 'user',
accessTokens: {
'testToken1122': 2
},
calls: {
'user': 'testToken1122'
},
cpuMonitor: true, cpuMonitor: true,
memoryMonitor: true, memoryMonitor: true,
sessionKey: 'SessionID', sessionKey: 'SessionID',
@ -21,6 +28,12 @@ function main() {
times: 10 times: 10
} }
}, },
static: {
'/': 'api/',
},
rewrite: {
'/echo2.js': '/echo.js'
}
}) })
s.register({ path: '/echo', noLog200: true }, ({ args, response }) => { s.register({ path: '/echo', noLog200: true }, ({ args, response }) => {
// setTimeout(() => { // setTimeout(() => {
@ -43,6 +56,12 @@ function test(name) {
return r.string() 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) { function test2(name) {
let r = h2c.get('/echo2?name=' + name) let r = h2c.get('/echo2?name=' + name)
return r.string() return r.string()

View File

@ -7,6 +7,7 @@ let hc = http
let urlPrefix let urlPrefix
function main() { function main() {
s.load('api/ws.js') s.load('api/ws.js')
s.task('task.js', 100)
let host = s.start() let host = s.start()
hc = http.new({ baseURL: 'http://' + host }) hc = http.new({ baseURL: 'http://' + host })
return host return host
@ -30,7 +31,7 @@ function testWS() {
co.info('test ws abc ok') co.info('test ws abc ok')
// ws.ping() // ws.ping()
u.sleep(100) u.sleep(10)
let pc = ws.pingCount() let pc = ws.pingCount()
co.info('test ws ping ok', pc.pingTimes, pc.pongTimes) co.info('test ws ping ok', pc.pingTimes, pc.pongTimes)
@ -51,6 +52,14 @@ function testWS() {
} }
co.info('test ws json ok') 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() ws.close()
return true return true
} }

30
tests/task.js Normal file
View File

@ -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'))
}

3
ws.go
View File

@ -12,8 +12,9 @@ import (
"github.com/ssgo/u" "github.com/ssgo/u"
) )
func MakeWSClient(client *websocket.Conn) gojs.Map { func MakeWSClient(client *websocket.Conn, id string) gojs.Map {
return gojs.Map{ return gojs.Map{
"id": id,
"read": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { "read": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
typ, data, err := readWSMessage(client) typ, data, err := readWSMessage(client)
if err != nil { if err != nil {