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

133
README.md
View File

@ -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 为空
如果配置了 sessionProvidersession 将会存储在 redis 中,否则存储在内存中
sessionID和deviceID 同时支持HTTP头和Cookie两种传输方式HTTP头优先如果客户端没有传递则服务器会自动分配
服务默认启用 session 和 device 功能,如果不希望使用可以在配置中设置 sessionKey 或 deviceKey 为空
如果配置了 sessionProvidersession 将会存储在 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)

View File

@ -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, "/"
}

View File

@ -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] = &regexProxiesInfo{
Value: v,
_regexProxies[k] = &regexRedirectInfo{
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] = &regexRewriteInfo{
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] = &regexRedirectInfo{
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
}

4
go.mod
View File

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

View File

@ -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),

View File

@ -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 {

View File

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

View File

@ -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<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 {
// 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

114
task.go
View File

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

View File

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

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"
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!')
})
}

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/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

View File

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

View File

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

View File

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

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"
)
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 {