Commit 2e4496e2 authored by Eric's avatar Eric

fix conflicts

parents 74c7c809 25eeae3f
......@@ -25,6 +25,7 @@ RUN chmod 755 ./entrypoint.sh \
&& apk update \
&& apk add -U tzdata \
&& apk add curl \
&& apk add mysql-client \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& apk del tzdata \
......
This diff is collapsed.
......@@ -19,37 +19,37 @@ msgstr ""
#. i18n.T
#: pkg/handler/banner.go:48
msgid "directly login"
msgid "part IP, Hostname, Comment"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:49
msgid "part IP, Hostname, Comment"
#: pkg/handler/banner.go:48
msgid "to search login if unique"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:49
msgid "to search login if unique"
msgid "/ + IP, Hostname, Comment"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:50
msgid "/ + IP, Hostname, Comment"
#: pkg/handler/banner.go:49
msgid "to search, such as: /192.168"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:50
msgid "to search, such as: /192.168"
msgid "display the host you have permission"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:51
msgid "display the host you have permission"
msgid "display the node that you have permission"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:52
msgid "display the node that you have permission"
msgid "display the databases that you have permission"
msgstr ""
#. i18n.T
......@@ -100,15 +100,13 @@ msgstr ""
#. i18n.T
#: pkg/handler/banner.go:96
msgid ""
"\n"
"Tips: Enter the asset ID and directly login the asset.\n"
"Enter ID number directly login the asset, multiple search use // + field, "
"such as: //16"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:97
msgid ""
"\n"
"Page up: P/p\tPage down: Enter|N/n\tBACK: b.\n"
msgid "Page up: b\tPage down: n"
msgstr ""
#. i18n.T
......@@ -147,42 +145,104 @@ msgid "Username"
msgstr ""
#. i18n.T
#: pkg/proxy/parser.go:131
#: pkg/handler/banner.go:105
msgid "all"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:106
msgid "Search: %s"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:107
msgid "DBType"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:108
msgid "DB Name"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:109
msgid "No Databases"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:110
msgid ""
"Enter ID number directly login the database, multiple search use // + field, "
"such as: //16"
msgstr ""
#. i18n.T
#: pkg/proxy/dbproxy.go:112
msgid "Database connecting to %s %.1f"
msgstr ""
#. i18n.T
#: pkg/proxy/dbproxy.go:131
msgid "System user <%s> and database <%s> protocol are inconsistent."
msgstr ""
#. i18n.T
#: pkg/proxy/dbproxy.go:137
msgid "Database %s protocol client not installed."
msgstr ""
#. i18n.T
#: pkg/proxy/dbswitch.go:148
msgid "Database connect idle more than %d minutes, disconnect"
msgstr ""
#. i18n.T
#: pkg/proxy/dbswitch.go:155
msgid "Database connection terminated by administrator"
msgstr ""
#. i18n.T
#: pkg/proxy/parser.go:140
msgid "Command `%s` is forbidden"
msgstr ""
#. i18n.T
#: pkg/proxy/proxy.go:143
#: pkg/proxy/proxy.go:146
msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]"
msgstr ""
#. i18n.T
#: pkg/proxy/proxy.go:161
#: pkg/proxy/proxy.go:164
msgid "Connecting to %s@%s %.1f"
msgstr ""
#. i18n.T
#: pkg/proxy/proxy.go:180
#: pkg/proxy/proxy.go:183
msgid "System user <%s> and asset <%s> protocol are inconsistent."
msgstr ""
#. i18n.T
#: pkg/proxy/proxy.go:186
#: pkg/proxy/proxy.go:189
msgid ""
"Terminal only support protocol ssh/telnet, please use web terminal to access"
msgstr ""
#. i18n.T
#: pkg/proxy/sessmanager.go:67
#: pkg/proxy/sessmanager.go:75
msgid "Connect with api server failed"
msgstr ""
#. i18n.T
#: pkg/proxy/switch.go:159
#: pkg/proxy/sessmanager.go:117
msgid "Create database session failed"
msgstr ""
#. i18n.T
#: pkg/proxy/switch.go:173
msgid "Connect idle more than %d minutes, disconnect"
msgstr ""
#. i18n.T
#: pkg/proxy/switch.go:166
#: pkg/proxy/switch.go:180
msgid "Terminated by administrator"
msgstr ""
......@@ -19,39 +19,42 @@ msgstr "欢迎使用Jumpserver开源堡垒机系统"
#. i18n.T
#: pkg/handler/banner.go:48
msgid "directly login"
msgstr "直接登录"
#. i18n.T
#: pkg/handler/banner.go:49
msgid "part IP, Hostname, Comment"
msgstr "部分IP、主机名、备注"
#. i18n.T
#: pkg/handler/banner.go:49
#: pkg/handler/banner.go:48
#, fuzzy
msgid "to search login if unique"
msgstr "搜索登录(如果唯一)"
#. i18n.T
#: pkg/handler/banner.go:50
#: pkg/handler/banner.go:49
msgid "/ + IP, Hostname, Comment"
msgstr "/ + IP,主机名 or 备注"
#. i18n.T
#: pkg/handler/banner.go:50
#: pkg/handler/banner.go:49
#, fuzzy
msgid "to search, such as: /192.168"
msgstr "搜索,如:/192.168"
#. i18n.T
#: pkg/handler/banner.go:51
#: pkg/handler/banner.go:50
msgid "display the host you have permission"
msgstr "显示您有权限的主机"
#. i18n.T
#: pkg/handler/banner.go:52
#: pkg/handler/banner.go:51
msgid "display the node that you have permission"
msgstr "显示您有权限的节点"
#. i18n.T
#: pkg/handler/banner.go:52
#, fuzzy
msgid "display the databases that you have permission"
msgstr "显示您有权限的数据库"
#. i18n.T
#: pkg/handler/banner.go:53
msgid "refresh your assets and nodes"
......@@ -101,20 +104,15 @@ msgstr "没有资产"
#: pkg/handler/banner.go:96
#, fuzzy
msgid ""
"\n"
"Tips: Enter the asset ID and directly login the asset.\n"
msgstr ""
"\n"
"提示:输入资产ID,登录资产\n"
"Enter ID number directly login the asset, multiple search use // + field, "
"such as: //16"
msgstr "提示:输入资产ID直接登录,二级搜索使用 // + 字段,如://192"
#. i18n.T
#: pkg/handler/banner.go:97
msgid ""
"\n"
"Page up: P/p\tPage down: Enter|N/n\tBACK: b.\n"
msgstr ""
"\n"
"上一页:P/p 下一页:Enter|N/n 返回:B/b\n"
#, fuzzy
msgid "Page up: b\tPage down: n"
msgstr "上一页:b 下一页:n"
#. i18n.T
#: pkg/handler/banner.go:98
......@@ -156,42 +154,111 @@ msgid "Username"
msgstr "用户名"
#. i18n.T
#: pkg/proxy/parser.go:131
#: pkg/handler/banner.go:105
msgid "all"
msgstr "所有"
#. i18n.T
#: pkg/handler/banner.go:106
#, fuzzy
msgid "Search: %s"
msgstr "搜索: %s"
#. i18n.T
#: pkg/handler/banner.go:107
msgid "DBType"
msgstr "数据库类型"
#. i18n.T
#: pkg/handler/banner.go:108
#, fuzzy
msgid "DB Name"
msgstr "数据库名称"
#. i18n.T
#: pkg/handler/banner.go:109
msgid "No Databases"
msgstr "无数据库"
#. i18n.T
#: pkg/handler/banner.go:110
#, fuzzy
msgid ""
"Enter ID number directly login the database, multiple search use // + field, "
"such as: //16"
msgstr "提示:输入数据库ID直接登录,二级搜索使用 // + 字段,如://192"
#. i18n.T
#: pkg/proxy/dbproxy.go:112
#, fuzzy
msgid "Database connecting to %s %.1f"
msgstr "连接数据库 %s %.1f"
#. i18n.T
#: pkg/proxy/dbproxy.go:131
#, fuzzy
msgid "System user <%s> and database <%s> protocol are inconsistent."
msgstr "系统用户<%s>和资产<%s>协议不一致"
#. i18n.T
#: pkg/proxy/dbproxy.go:137
msgid "Database %s protocol client not installed."
msgstr "%s 协议的数据库客户端未安装"
#. i18n.T
#: pkg/proxy/dbswitch.go:148
#, fuzzy
msgid "Database connect idle more than %d minutes, disconnect"
msgstr "数据库连接空闲时间超过 %d 分钟,断开连接"
#. i18n.T
#: pkg/proxy/dbswitch.go:155
#, fuzzy
msgid "Database connection terminated by administrator"
msgstr "管理员中断数据库连接"
#. i18n.T
#: pkg/proxy/parser.go:140
msgid "Command `%s` is forbidden"
msgstr "命令 `%s` 是被禁止的 ..."
#. i18n.T
#: pkg/proxy/proxy.go:143
#: pkg/proxy/proxy.go:146
msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]"
msgstr "复用SSH连接(%s@%s)[连接数量: %d]"
#. i18n.T
#: pkg/proxy/proxy.go:161
#: pkg/proxy/proxy.go:164
msgid "Connecting to %s@%s %.1f"
msgstr "开始连接到 %s@%s %.1f"
#. i18n.T
#: pkg/proxy/proxy.go:180
#: pkg/proxy/proxy.go:183
msgid "System user <%s> and asset <%s> protocol are inconsistent."
msgstr "系统用户<%s>和资产<%s>协议不一致"
#. i18n.T
#: pkg/proxy/proxy.go:186
#: pkg/proxy/proxy.go:189
msgid ""
"Terminal only support protocol ssh/telnet, please use web terminal to access"
msgstr "终端仅支持ssh/telnet协议,请使用web终端登录"
#. i18n.T
#: pkg/proxy/sessmanager.go:67
#: pkg/proxy/sessmanager.go:75
msgid "Connect with api server failed"
msgstr "连接API服务失败"
#. i18n.T
#: pkg/proxy/switch.go:159
#: pkg/proxy/sessmanager.go:117
msgid "Create database session failed"
msgstr "创建数据库会话失败"
#. i18n.T
#: pkg/proxy/switch.go:173
msgid "Connect idle more than %d minutes, disconnect"
msgstr "空闲时间超过%d分钟,断开连接"
#. i18n.T
#: pkg/proxy/switch.go:166
#: pkg/proxy/switch.go:180
msgid "Terminated by administrator"
msgstr "管理员中断连接"
......@@ -19695,49 +19695,49 @@ $.fn.elfindersearchbutton = function(cmd) {
});
})
.one('open', function() {
opts = (fm.api < 2.1)? null : $('<div class="ui-front ui-widget ui-widget-content elfinder-button-menu elfinder-button-search-menu ui-corner-all"/>')
.append(
$('<div class="buttonset"/>')
.append(
$('<input id="'+id('SearchFromCwd')+'" name="serchfrom" type="radio" checked="checked"/><label for="'+id('SearchFromCwd')+'">'+fm.i18n('btnCwd')+'</label>'),
$('<input id="'+id('SearchFromVol')+'" name="serchfrom" type="radio"/><label for="'+id('SearchFromVol')+'">'+fm.i18n('btnVolume')+'</label>'),
$('<input id="'+id('SearchFromAll')+'" name="serchfrom" type="radio"/><label for="'+id('SearchFromAll')+'">'+fm.i18n('btnAll')+'</label>')
),
$('<div class="buttonset elfinder-search-type"/>')
.append(
$('<input id="'+id('SearchName')+'" name="serchcol" type="radio" checked="checked" value="SearchName"/><label for="'+id('SearchName')+'">'+fm.i18n('btnFileName')+'</label>')
)
)
.hide()
.appendTo(fm.getUI());
if (opts) {
if (sTypes) {
typeSet = opts.find('.elfinder-search-type');
$.each(cmd.options.searchTypes, function(i, v) {
typeSet.append($('<input id="'+id(i)+'" name="serchcol" type="radio" value="'+fm.escape(i)+'"/><label for="'+id(i)+'">'+fm.i18n(v.name)+'</label>'));
});
}
opts.find('div.buttonset').buttonset();
$('#'+id('SearchFromAll')).next('label').attr('title', fm.i18n('searchTarget', fm.i18n('btnAll')));
if (sTypes) {
$.each(sTypes, function(i, v) {
if (v.title) {
$('#'+id(i)).next('label').attr('title', fm.i18n(v.title));
}
});
}
opts.on('mousedown', 'div.buttonset', function(e){
e.stopPropagation();
opts.data('infocus', true);
})
.on('click', 'input', function(e) {
e.stopPropagation();
$.trim(input.val())? search() : input.trigger('focus');
})
.on('close', function() {
input.trigger('blur');
});
}
// opts = (fm.api < 2.1)? null : $('<div class="ui-front ui-widget ui-widget-content elfinder-button-menu elfinder-button-search-menu ui-corner-all"/>')
// .append(
// $('<div class="buttonset"/>')
// .append(
// $('<input id="'+id('SearchFromCwd')+'" name="serchfrom" type="radio" checked="checked"/><label for="'+id('SearchFromCwd')+'">'+fm.i18n('btnCwd')+'</label>'),
// $('<input id="'+id('SearchFromVol')+'" name="serchfrom" type="radio"/><label for="'+id('SearchFromVol')+'">'+fm.i18n('btnVolume')+'</label>'),
// $('<input id="'+id('SearchFromAll')+'" name="serchfrom" type="radio"/><label for="'+id('SearchFromAll')+'">'+fm.i18n('btnAll')+'</label>')
// ),
// $('<div class="buttonset elfinder-search-type"/>')
// .append(
// $('<input id="'+id('SearchName')+'" name="serchcol" type="radio" checked="checked" value="SearchName"/><label for="'+id('SearchName')+'">'+fm.i18n('btnFileName')+'</label>')
// )
// )
// .hide()
// .appendTo(fm.getUI());
// if (opts) {
// if (sTypes) {
// typeSet = opts.find('.elfinder-search-type');
// $.each(cmd.options.searchTypes, function(i, v) {
// typeSet.append($('<input id="'+id(i)+'" name="serchcol" type="radio" value="'+fm.escape(i)+'"/><label for="'+id(i)+'">'+fm.i18n(v.name)+'</label>'));
// });
// }
// opts.find('div.buttonset').buttonset();
// $('#'+id('SearchFromAll')).next('label').attr('title', fm.i18n('searchTarget', fm.i18n('btnAll')));
// if (sTypes) {
// $.each(sTypes, function(i, v) {
// if (v.title) {
// $('#'+id(i)).next('label').attr('title', fm.i18n(v.title));
// }
// });
// }
// opts.on('mousedown', 'div.buttonset', function(e){
// e.stopPropagation();
// opts.data('infocus', true);
// })
// .on('click', 'input', function(e) {
// e.stopPropagation();
// $.trim(input.val())? search() : input.trigger('focus');
// })
// .on('close', function() {
// input.trigger('blur');
// });
// }
})
.bind('searchend', function() {
input.val('');
......
......@@ -51,7 +51,10 @@
['copy', 'cut', 'paste'],
['rm'],
['rename'],
['view']
['view'],
{{if eq . "_"}}
['search']
{{end}}
],
cwd : {oldSchool: true}
},
......
......@@ -6,12 +6,13 @@ require (
github.com/Azure/azure-pipeline-go v0.1.9 // indirect
github.com/Azure/azure-storage-blob-go v0.6.0
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/LeeEirc/elfinder v0.0.8
github.com/LeeEirc/elfinder v0.0.11
github.com/aliyun/aliyun-oss-go-sdk v1.9.8
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/aws/aws-sdk-go v1.19.46
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/elastic/go-elasticsearch v0.0.0
github.com/creack/pty v1.1.9
github.com/elastic/go-elasticsearch/v6 v6.8.5
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gliderlabs/ssh v0.2.3-0.20190711180243-866d0ddf7991
github.com/gorilla/mux v1.7.2
......
......@@ -5,8 +5,8 @@ github.com/Azure/azure-storage-blob-go v0.6.0 h1:SEATKb3LIHcaSIX+E6/K4kJpwfuozFE
github.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/LeeEirc/elfinder v0.0.8 h1:y1BSXvtRve6WwMiSYyynY+oEbH+rWMuScoZ1YU67H4E=
github.com/LeeEirc/elfinder v0.0.8/go.mod h1:d1bMAAydkZSBxSN/EuQjBg6B0xcPP3boHuYEpzEHYTs=
github.com/LeeEirc/elfinder v0.0.11 h1:LP+53Q0V2WhxTqR720X7B8rkkX2YDq47dSIGLR1xA9s=
github.com/LeeEirc/elfinder v0.0.11/go.mod h1:d1bMAAydkZSBxSN/EuQjBg6B0xcPP3boHuYEpzEHYTs=
github.com/LeeEirc/sftp v1.10.2 h1:SGpj84RbStlwH+ThXYUsxtxtbzAzpUY8z5gQN4p12OI=
github.com/LeeEirc/sftp v1.10.2/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/aliyun/aliyun-oss-go-sdk v1.9.8 h1:BOflvK0Zs/zGmoabyFIzTg5c3kguktWTXEwewwbuba0=
......@@ -17,11 +17,13 @@ github.com/aws/aws-sdk-go v1.19.46 h1:lRqljzjkGmEeiawkw4z1QgtCnU/S5Jw8lNeUuvmydU
github.com/aws/aws-sdk-go v1.19.46/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elastic/go-elasticsearch v0.0.0 h1:Pd5fqOuBxKxv83b0+xOAJDAkziWYwFinWnBO0y+TZaA=
github.com/elastic/go-elasticsearch v0.0.0/go.mod h1:TkBSJBuTyFdBnrNqoPc54FN0vKf5c04IdM4zuStJ7xg=
github.com/elastic/go-elasticsearch/v6 v6.8.5 h1:U2HtkBseC1FNBmDr0TR2tKltL6FxoY+niDAlj5M8TK8=
github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/go-playground/form v3.1.4+incompatible h1:lvKiHVxE2WvzDIoyMnWcjyiBxKt2+uFJyZcPYWsLnjI=
......
......@@ -2,20 +2,24 @@ package auth
import (
"net"
"strings"
"github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh"
"github.com/jumpserver/koko/pkg/cctx"
"github.com/jumpserver/koko/pkg/common"
"github.com/jumpserver/koko/pkg/config"
"github.com/jumpserver/koko/pkg/logger"
"github.com/jumpserver/koko/pkg/model"
"github.com/jumpserver/koko/pkg/service"
)
var mfaInstruction = "Please enter 6 digits."
var mfaQuestion = "[MFA auth]: "
var confirmInstruction = "Please wait for your admin to confirm."
var confirmQuestion = "Do you want to continue [Y/n]? : "
const (
actionAccepted = "Accepted"
actionFailed = "Failed"
......@@ -31,28 +35,33 @@ func checkAuth(ctx ssh.Context, password, publicKey string) (res ssh.AuthResult)
authMethod = "password"
}
remoteAddr, _, _ := net.SplitHostPort(ctx.RemoteAddr().String())
resp, err := service.Authenticate(username, password, publicKey, remoteAddr, "T")
if err != nil {
action = actionFailed
logger.Infof("%s %s for %s from %s", action, authMethod, username, remoteAddr)
return
userClient, ok := ctx.Value(model.ContextKeyClient).(*service.SessionClient)
if !ok {
sessionClient := service.NewSessionClient(service.Username(username),
service.RemoteAddr(remoteAddr), service.LoginType("T"))
userClient = &sessionClient
ctx.SetValue(model.ContextKeyClient, userClient)
}
if resp != nil && resp.User != nil {
switch resp.User.OTPLevel {
case 0:
userClient.SetOption(service.Password(password), service.PublicKey(publicKey))
user, authStatus := userClient.Authenticate(ctx)
switch authStatus {
case service.AuthMFARequired:
action = actionPartialAccepted
res = ssh.AuthPartiallySuccessful
case service.AuthSuccess:
res = ssh.AuthSuccessful
case 1, 2:
ctx.SetValue(model.ContextKeyUser, &user)
case service.AuthConfirmRequired:
required := true
ctx.SetValue(model.ContextKeyConfirmRequired, &required)
action = actionPartialAccepted
res = ssh.AuthPartiallySuccessful
default:
}
ctx.SetValue(cctx.ContextKeyUser, resp.User)
ctx.SetValue(cctx.ContextKeySeed, resp.Seed)
ctx.SetValue(cctx.ContextKeyToken, resp.Token)
action = actionFailed
}
logger.Infof("%s %s for %s from %s", action, authMethod, username, remoteAddr)
return res
return
}
func CheckUserPassword(ctx ssh.Context, password string) ssh.AuthResult {
......@@ -72,38 +81,71 @@ func CheckUserPublicKey(ctx ssh.Context, key ssh.PublicKey) ssh.AuthResult {
}
func CheckMFA(ctx ssh.Context, challenger gossh.KeyboardInteractiveChallenge) (res ssh.AuthResult) {
if value, ok := ctx.Value(model.ContextKeyConfirmFailed).(*bool); ok && *value {
return ssh.AuthFailed
}
username := ctx.User()
remoteAddr, _, _ := net.SplitHostPort(ctx.RemoteAddr().String())
res = ssh.AuthFailed
defer func() {
authMethod := "MFA"
if res == ssh.AuthSuccessful {
action := actionAccepted
logger.Infof("%s %s for %s from %s", action, authMethod, username, remoteAddr)
} else {
action := actionFailed
logger.Errorf("%s %s for %s from %s", action, authMethod, username, remoteAddr)
var confirmAction bool
instruction := mfaInstruction
question := mfaQuestion
client, ok := ctx.Value(model.ContextKeyClient).(*service.SessionClient)
if !ok {
logger.Errorf("User %s Mfa Auth failed: not found session client.", username, )
return
}
}()
answers, err := challenger(username, mfaInstruction, []string{mfaQuestion}, []bool{true})
value, ok := ctx.Value(model.ContextKeyConfirmRequired).(*bool)
if ok && *value {
confirmAction = true
instruction = confirmInstruction
question = confirmQuestion
}
answers, err := challenger(username, instruction, []string{question}, []bool{true})
if err != nil || len(answers) != 1 {
if confirmAction {
client.CancelConfirm()
}
logger.Errorf("User %s happened err: %s", username, err)
return
}
mfaCode := answers[0]
seed, ok := ctx.Value(cctx.ContextKeySeed).(string)
if !ok {
logger.Error("Mfa Auth failed, may be user password or publickey auth failed")
if confirmAction {
switch strings.TrimSpace(strings.ToLower(answers[0])) {
case "yes", "y", "":
user, authStatus := client.CheckConfirm(ctx)
switch authStatus {
case service.AuthSuccess:
res = ssh.AuthSuccessful
ctx.SetValue(model.ContextKeyUser, &user)
return
}
resp, err := service.CheckUserOTP(seed, mfaCode, remoteAddr, "T")
if err != nil {
logger.Error("Mfa Auth failed: ", err)
case "no", "n":
client.CancelConfirm()
default:
return
}
if resp.Token != "" {
res = ssh.AuthSuccessful
failed := true
ctx.SetValue(model.ContextKeyConfirmFailed, &failed)
return
}
mfaCode := answers[0]
user, authStatus := client.CheckUserOTP(ctx, mfaCode)
switch authStatus {
case service.AuthSuccess:
res = ssh.AuthSuccessful
ctx.SetValue(model.ContextKeyUser, &user)
logger.Infof("%s MFA for %s from %s", actionAccepted, username, remoteAddr)
case service.AuthConfirmRequired:
res = ssh.AuthPartiallySuccessful
required := true
ctx.SetValue(model.ContextKeyConfirmRequired, &required)
logger.Infof("%s MFA for %s from %s", actionPartialAccepted, username, remoteAddr)
default:
logger.Errorf("%s MFA for %s from %s", actionFailed, username, remoteAddr)
}
return
}
......
package cctx
import (
"context"
"github.com/gliderlabs/ssh"
"github.com/jumpserver/koko/pkg/model"
)
type contextKey struct {
name string
}
var (
ContextKeyUser = &contextKey{"user"}
ContextKeyAsset = &contextKey{"asset"}
ContextKeySystemUser = &contextKey{"systemUser"}
ContextKeySSHSession = &contextKey{"sshSession"}
ContextKeyLocalAddr = &contextKey{"localAddr"}
ContextKeyRemoteAddr = &contextKey{"RemoteAddr"}
ContextKeySSHCtx = &contextKey{"sshCtx"}
ContextKeySeed = &contextKey{"seed"}
ContextKeyToken = &contextKey{"token"}
)
type Context interface {
context.Context
User() *model.User
Asset() *model.Asset
SystemUser() *model.SystemUser
SSHSession() *ssh.Session
SSHCtx() *ssh.Context
SetValue(key, value interface{})
}
// Context coco内部使用的Context
type CocoContext struct {
context.Context
}
// user 返回当前连接的用户model
func (ctx *CocoContext) User() *model.User {
return ctx.Value(ContextKeyUser).(*model.User)
}
func (ctx *CocoContext) Asset() *model.Asset {
return ctx.Value(ContextKeyAsset).(*model.Asset)
}
func (ctx *CocoContext) SystemUser() *model.SystemUser {
return ctx.Value(ContextKeySystemUser).(*model.SystemUser)
}
func (ctx *CocoContext) SSHSession() *ssh.Session {
return ctx.Value(ContextKeySSHSession).(*ssh.Session)
}
func (ctx *CocoContext) SSHCtx() *ssh.Context {
return ctx.Value(ContextKeySSHCtx).(*ssh.Context)
}
func (ctx *CocoContext) SetValue(key, value interface{}) {
ctx.Context = context.WithValue(ctx.Context, key, value)
}
func applySessionMetadata(ctx *CocoContext, sess ssh.Session) {
ctx.SetValue(ContextKeySSHSession, &sess)
ctx.SetValue(ContextKeySSHCtx, sess.Context())
ctx.SetValue(ContextKeyLocalAddr, sess.LocalAddr())
}
func NewContext(sess ssh.Session) (*CocoContext, context.CancelFunc) {
sshCtx, cancel := context.WithCancel(sess.Context())
ctx := &CocoContext{sshCtx}
applySessionMetadata(ctx, sess)
return ctx, cancel
}
......@@ -9,6 +9,7 @@ import (
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/cookiejar"
neturl "net/url"
"os"
"path/filepath"
......@@ -38,8 +39,10 @@ type UrlParser interface {
func NewClient(timeout time.Duration, baseHost string) Client {
headers := make(map[string]string)
jar, _ := cookiejar.New(nil)
client := http.Client{
Timeout: timeout * time.Second,
Jar: jar,
}
return Client{
BaseHost: baseHost,
......@@ -80,15 +83,16 @@ func (c *Client) parseUrlQuery(url string, params []map[string]string) string {
if len(params) < 1 {
return url
}
var query []string
for k, v := range params[0] {
query = append(query, fmt.Sprintf("%s=%s", k, v))
query := neturl.Values{}
for _, item := range params {
for k, v := range item {
query.Add(k, v)
}
}
param := strings.Join(query, "&")
if strings.Contains(url, "?") {
url += "&" + param
url += "&" + query.Encode()
} else {
url += "?" + param
url += "?" + query.Encode()
}
return url
}
......@@ -103,11 +107,10 @@ func (c *Client) parseUrl(url string, params []map[string]string) string {
func (c *Client) setAuthHeader(r *http.Request) {
if len(c.cookie) != 0 {
cookie := make([]string, 0)
for k, v := range c.cookie {
cookie = append(cookie, fmt.Sprintf("%s=%s", k, v))
c := http.Cookie{Name: k, Value: v,}
r.AddCookie(&c)
}
r.Header.Add("Cookie", strings.Join(cookie, ";"))
}
if len(c.basicAuth) == 2 {
r.SetBasicAuth(c.basicAuth[0], c.basicAuth[1])
......@@ -157,15 +160,17 @@ func (c *Client) NewRequest(method, url string, body interface{}, params []map[s
// 1. query string if set {"name": "ibuler"}
func (c *Client) Do(method, url string, data, res interface{}, params ...map[string]string) (resp *http.Response, err error) {
req, err := c.NewRequest(method, url, data, params)
if err != nil {
return
}
resp, err = c.http.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
msg := fmt.Sprintf("%s %s failed, get code: %d, %s", req.Method, req.URL, resp.StatusCode, string(body))
msg := fmt.Sprintf("%s %s failed, get code: %d, %s", req.Method, req.URL, resp.StatusCode, body)
err = errors.New(msg)
return
}
......@@ -176,7 +181,9 @@ func (c *Client) Do(method, url string, data, res interface{}, params ...map[str
return
}
// Unmarshal response body to result struct
if res != nil && resp.StatusCode >= 200 && resp.StatusCode <= 300 {
if res != nil {
switch {
case strings.Contains(resp.Header.Get("Content-Type"), "application/json"):
err = json.Unmarshal(body, res)
if err != nil {
msg := fmt.Sprintf("%s %s failed, unmarshal '%s' response failed: %s", req.Method, req.URL, body[:12], err)
......@@ -184,6 +191,7 @@ func (c *Client) Do(method, url string, data, res interface{}, params ...map[str
return
}
}
}
return
}
......
This diff is collapsed.
......@@ -45,11 +45,11 @@ type Menu []MenuItem
func Initial() {
defaultTitle = utils.WrapperTitle(i18n.T("Welcome to use Jumpserver open source fortress system"))
menu = Menu{
{id: 1, instruct: "ID", helpText: i18n.T("directly login")},
{id: 2, instruct: i18n.T("part IP, Hostname, Comment"), helpText: i18n.T("to search login if unique")},
{id: 3, instruct: i18n.T("/ + IP, Hostname, Comment"), helpText: i18n.T("to search, such as: /192.168")},
{id: 4, instruct: "p", helpText: i18n.T("display the host you have permission")},
{id: 5, instruct: "g", helpText: i18n.T("display the node that you have permission")},
{id: 1, instruct: i18n.T("part IP, Hostname, Comment"), helpText: i18n.T("to search login if unique")},
{id: 2, instruct: i18n.T("/ + IP, Hostname, Comment"), helpText: i18n.T("to search, such as: /192.168")},
{id: 3, instruct: "p", helpText: i18n.T("display the host you have permission")},
{id: 4, instruct: "g", helpText: i18n.T("display the node that you have permission")},
{id: 5, instruct: "d", helpText: i18n.T("display the databases that you have permission")},
{id: 6, instruct: "r", helpText: i18n.T("refresh your assets and nodes")},
{id: 7, instruct: "h", helpText: i18n.T("print help")},
{id: 8, instruct: "q", helpText: i18n.T("exit")},
......@@ -93,8 +93,8 @@ func getI18nFromMap(name string) string {
"Comment": i18n.T("comment"),
"AssetTableCaption": i18n.T("Page: %d, Count: %d, Total Page: %d, Total Count: %d"),
"NoAssets": i18n.T("No Assets"),
"LoginTip": i18n.T("\nTips: Enter the asset ID and directly login the asset.\n"),
"PageActionTip": i18n.T("\nPage up: P/p Page down: Enter|N/n BACK: b.\n"),
"LoginTip": i18n.T("Enter ID number directly login the asset, multiple search use // + field, such as: //16"),
"PageActionTip": i18n.T("Page up: b Page down: n"),
"NodeHeaderTip": i18n.T("Node: [ ID.Name(Asset amount) ]"),
"NodeEndTip": i18n.T("Tips: Enter g+NodeID to display the host under the node, such as g1"),
"RefreshDone": i18n.T("Refresh done"),
......@@ -102,6 +102,12 @@ func getI18nFromMap(name string) string {
"BackTip": i18n.T("Back: B/b"),
"Name": i18n.T("Name"),
"Username": i18n.T("Username"),
"All": i18n.T("all"),
"SearchTip": i18n.T("Search: %s"),
"DBType": i18n.T("DBType"),
"DBName": i18n.T("DB Name"),
"NoDatabases": i18n.T("No Databases"),
"DBLoginTip": i18n.T("Enter ID number directly login the database, multiple search use // + field, such as: //16"),
}
})
return i18nMap[name]
......
package handler
import (
"strings"
"sync"
"github.com/jumpserver/koko/pkg/model"
)
type DatabasePaginator interface {
Paginator
RetrievePageData(pageIndex int) []model.Database
SearchAsset(key string) []model.Database
SearchAgain(key string) []model.Database
Name() string
SearchKeys() []string
}
func NewLocalDatabasePaginator(data [] model.Database, pageSize int) DatabasePaginator {
p := localDatabasePaginator{
allData: data,
currentData: data,
pageSize: pageSize,
currentOffset: 0,
currentPage: 1,
search: make([]string, 0, 4),
lock: new(sync.RWMutex),
}
return &p
}
type localDatabasePaginator struct {
allData []model.Database
currentData []model.Database
currentPage int
pageSize int
totalPage int
currentOffset int
search []string
lock *sync.RWMutex
currentResult []model.Database
}
func (l *localDatabasePaginator) Name() string {
return "local"
}
func (l *localDatabasePaginator) SearchKeys() []string {
return l.search
}
func (l *localDatabasePaginator) HasPrev() bool {
l.lock.RLock()
defer l.lock.RUnlock()
return l.currentPage > 1
}
func (l *localDatabasePaginator) HasNext() bool {
l.lock.RLock()
defer l.lock.RUnlock()
return l.currentPage < l.totalPage
}
func (l *localDatabasePaginator) CurrentPage() int {
l.lock.RLock()
defer l.lock.RUnlock()
return l.currentPage
}
func (l *localDatabasePaginator) TotalCount() int {
l.lock.RLock()
defer l.lock.RUnlock()
return len(l.currentData)
}
func (l *localDatabasePaginator) TotalPage() int {
l.lock.RLock()
defer l.lock.RUnlock()
return l.totalPage
}
func (l *localDatabasePaginator) PageSize() int {
l.lock.RLock()
defer l.lock.RUnlock()
return l.pageSize
}
func (l *localDatabasePaginator) SetPageSize(size int) {
if size <= 0 {
size = len(l.currentData)
}
l.lock.Lock()
defer l.lock.Unlock()
if l.pageSize == size {
return
}
l.pageSize = size
}
func (l *localDatabasePaginator) RetrievePageData(pageIndex int) []model.Database {
l.lock.Lock()
defer l.lock.Unlock()
return l.retrievePageData(pageIndex)
}
func (l *localDatabasePaginator) SearchAsset(key string) []model.Database {
l.lock.Lock()
defer l.lock.Unlock()
l.search = l.search[:0]
l.search = append(l.search, key)
l.currentData = searchFromLocalDBs(l.allData, key)
l.currentPage = 1
l.currentOffset = 0
return l.retrievePageData(1)
}
func (l *localDatabasePaginator) SearchAgain(key string) []model.Database {
l.lock.Lock()
defer l.lock.Unlock()
l.currentData = searchFromLocalDBs(l.currentData, key)
l.search = append(l.search, key)
l.currentPage = 1
l.currentOffset = 0
return l.retrievePageData(1)
}
func (l *localDatabasePaginator) retrievePageData(pageIndex int) []model.Database {
offsetPage := pageIndex - l.currentPage
totalOffset := offsetPage * l.pageSize
l.currentOffset += totalOffset
switch {
case l.currentOffset <= 0:
l.currentOffset = 0
case l.currentOffset >= len(l.currentData):
l.currentOffset = len(l.currentData)
case l.pageSize >= len(l.currentData):
l.currentOffset = 0
}
end := l.currentOffset + l.pageSize
if end >= len(l.currentData) {
end = len(l.currentData)
}
l.currentResult = l.currentData[l.currentOffset:end]
l.updatePageInfo()
return l.currentResult
}
func (l *localDatabasePaginator) updatePageInfo() {
pageSize := l.pageSize
totalCount := len(l.currentData)
switch totalCount % pageSize {
case 0:
l.totalPage = totalCount / pageSize
default:
l.totalPage = (totalCount / pageSize) + 1
}
offset := l.currentOffset + len(l.currentResult)
switch offset % pageSize {
case 0:
l.currentPage = offset / pageSize
default:
l.currentPage = (offset / pageSize) + 1
}
}
func searchFromLocalDBs(dbs []model.Database, key string) []model.Database {
displayDBs := make([]model.Database, 0, len(dbs))
key = strings.ToLower(key)
for _, db := range dbs {
contents := []string{strings.ToLower(db.Name),strings.ToLower(db.DBName),
strings.ToLower(db.Host), strings.ToLower(db.Comment)}
if isSubstring(contents, key) {
displayDBs = append(displayDBs, db)
}
}
return displayDBs
}
This diff is collapsed.
package handler
import (
"fmt"
"io"
"strconv"
"strings"
"github.com/jumpserver/koko/pkg/common"
"github.com/jumpserver/koko/pkg/config"
"github.com/jumpserver/koko/pkg/model"
"github.com/jumpserver/koko/pkg/service"
"github.com/jumpserver/koko/pkg/utils"
)
func NewAssetPagination(term *utils.Terminal, assets []model.Asset) AssetPagination {
assetPage := AssetPagination{term: term, assets: assets}
assetPage.Initial()
return assetPage
}
type AssetPagination struct {
term *utils.Terminal
assets []model.Asset
page *common.Pagination
currentData []model.Asset
}
func (p *AssetPagination) Initial() {
pageData := make([]interface{}, len(p.assets))
for i, v := range p.assets {
pageData[i] = v
}
pageSize := p.getPageSize()
p.page = common.NewPagination(pageData, pageSize)
firstPageData := p.page.GetPageData(1)
p.currentData = make([]model.Asset, len(firstPageData))
for i, item := range firstPageData {
p.currentData[i] = item.(model.Asset)
}
}
func (p *AssetPagination) getPageSize() int {
var (
pageSize int
minHeight = 8 // 分页显示的最小高度
)
_, height := p.term.GetSize()
switch config.GetConf().AssetListPageSize {
case "auto":
pageSize = height - minHeight
case "all":
pageSize = len(p.assets)
default:
if value, err := strconv.Atoi(config.GetConf().AssetListPageSize); err == nil {
pageSize = value
} else {
pageSize = height - minHeight
}
}
if pageSize <= 0 {
pageSize = 1
}
return pageSize
}
func (p *AssetPagination) Start() []model.Asset {
p.term.SetPrompt(": ")
defer p.term.SetPrompt("Opt> ")
for {
// 总数据小于page size,则显示所有资产且退出
if p.page.PageSize() >= p.page.TotalCount() {
p.currentData = p.assets
p.displayPageAssets()
return []model.Asset{}
}
p.displayPageAssets()
p.displayTipsInfo()
line, err := p.term.ReadLine()
if err != nil {
return []model.Asset{}
}
pageSize := p.getPageSize()
p.page.SetPageSize(pageSize)
line = strings.TrimSpace(line)
switch len(line) {
case 0, 1:
switch strings.ToLower(line) {
case "p":
if !p.page.HasPrev() {
continue
}
prePageData := p.page.GetPrevPageData()
if len(p.currentData) != len(prePageData) {
p.currentData = make([]model.Asset, len(prePageData))
}
for i, item := range prePageData {
p.currentData[i] = item.(model.Asset)
}
case "", "n":
if !p.page.HasNext() {
continue
}
nextPageData := p.page.GetNextPageData()
if len(p.currentData) != len(nextPageData) {
p.currentData = make([]model.Asset, len(nextPageData))
}
for i, item := range nextPageData {
p.currentData[i] = item.(model.Asset)
}
case "b", "q":
return []model.Asset{}
default:
if indexID, err := strconv.Atoi(line); err == nil {
if indexID > 0 && indexID <= len(p.currentData) {
return []model.Asset{p.currentData[indexID-1]}
}
}
}
default:
if indexID, err := strconv.Atoi(line); err == nil {
if indexID > 0 && indexID <= len(p.currentData) {
return []model.Asset{p.currentData[indexID-1]}
}
}
}
}
}
func (p *AssetPagination) displayPageAssets() {
Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Hostname"),
getI18nFromMap("IP"), getI18nFromMap("Comment")}
fields := []string{"ID", "hostname", "IP", "comment"}
data := make([]map[string]string, len(p.currentData))
for i, j := range p.currentData {
row := make(map[string]string)
row["ID"] = strconv.Itoa(i + 1)
row["hostname"] = j.Hostname
row["IP"] = j.IP
comments := make([]string, 0)
for _, item := range strings.Split(strings.TrimSpace(j.Comment), "\r\n") {
if strings.TrimSpace(item) == "" {
continue
}
comments = append(comments, strings.ReplaceAll(strings.TrimSpace(item), " ", ","))
}
row["comment"] = strings.Join(comments, "|")
data[i] = row
}
w, _ := p.term.GetSize()
caption := fmt.Sprintf(getI18nFromMap("AssetTableCaption"),
p.page.CurrentPage(), p.page.PageSize(), p.page.TotalPage(), p.page.TotalCount(),
)
caption = utils.WrapperString(caption, utils.Green)
table := common.WrapperTable{
Fields: fields,
Labels: Labels,
FieldsSize: map[string][3]int{
"ID": {0, 0, 5},
"hostname": {0, 8, 0},
"IP": {0, 15, 40},
"comment": {0, 0, 0},
},
Data: data,
TotalSize: w,
Caption: caption,
TruncPolicy: common.TruncMiddle,
}
table.Initial()
_, _ = p.term.Write([]byte(utils.CharClear))
_, _ = p.term.Write([]byte(table.Display()))
}
func (p *AssetPagination) displayTipsInfo() {
displayAssetPaginationTipsInfo(p.term)
}
func NewUserPagination(term *utils.Terminal, uid, search string, policy bool) UserAssetPagination {
return UserAssetPagination{
UserID: uid,
offset: 0,
limit: 0,
search: search,
term: term,
displayPolicy: policy,
Data: model.AssetsPaginationResponse{},
}
}
type UserAssetPagination struct {
UserID string
offset int
limit int
search string
term *utils.Terminal
displayPolicy bool
Data model.AssetsPaginationResponse
IsNeedProxy bool
currentData []model.Asset
}
func (p *UserAssetPagination) Start() []model.Asset {
p.term.SetPrompt(": ")
defer p.term.SetPrompt("Opt> ")
for {
p.retrieveData()
if p.displayPolicy && p.Data.Total == 1 {
p.IsNeedProxy = true
return p.Data.Data
}
// 无上下页,则退出循环
if p.Data.NextURL == "" && p.Data.PreviousURL == "" {
p.displayPageAssets()
return p.currentData
}
inLoop:
p.displayPageAssets()
p.displayTipsInfo()
line, err := p.term.ReadLine()
if err != nil {
return p.currentData
}
line = strings.TrimSpace(line)
switch len(line) {
case 0, 1:
switch strings.ToLower(line) {
case "p":
if p.Data.PreviousURL == "" {
continue
}
p.offset -= p.limit
case "", "n":
if p.Data.NextURL == "" {
continue
}
p.offset += p.limit
case "b", "q":
return []model.Asset{}
default:
if indexID, err := strconv.Atoi(line); err == nil {
if indexID > 0 && indexID <= len(p.currentData) {
p.IsNeedProxy = true
return []model.Asset{p.currentData[indexID-1]}
}
}
goto inLoop
}
default:
if indexID, err := strconv.Atoi(line); err == nil {
if indexID > 0 && indexID <= len(p.currentData) {
p.IsNeedProxy = true
return []model.Asset{p.currentData[indexID-1]}
}
}
goto inLoop
}
}
}
func (p *UserAssetPagination) displayPageAssets() {
if len(p.Data.Data) == 0 {
_, _ = p.term.Write([]byte(getI18nFromMap("NoAssets") + "\n\r"))
return
}
Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Hostname"),
getI18nFromMap("IP"), getI18nFromMap("Comment")}
fields := []string{"ID", "hostname", "IP", "comment"}
p.currentData = model.AssetList(p.Data.Data).SortBy(config.GetConf().AssetListSortBy)
data := make([]map[string]string, len(p.currentData))
for i, j := range p.currentData {
row := make(map[string]string)
row["ID"] = strconv.Itoa(i + 1)
row["hostname"] = j.Hostname
row["IP"] = j.IP
comments := make([]string, 0)
for _, item := range strings.Split(strings.TrimSpace(j.Comment), "\r\n") {
if strings.TrimSpace(item) == "" {
continue
}
comments = append(comments, strings.ReplaceAll(strings.TrimSpace(item), " ", ","))
}
row["comment"] = strings.Join(comments, "|")
data[i] = row
}
w, _ := p.term.GetSize()
var pageSize int
var totalPage int
var currentPage int
var totalCount int
currentOffset := p.offset + len(p.currentData)
switch p.limit {
case 0:
pageSize = len(p.currentData)
totalCount = pageSize
totalPage = 1
currentPage = 1
default:
pageSize = p.limit
totalCount = p.Data.Total
switch totalCount % pageSize {
case 0:
totalPage = totalCount / pageSize
default:
totalPage = (totalCount / pageSize) + 1
}
switch currentOffset % pageSize {
case 0:
currentPage = currentOffset / pageSize
default:
currentPage = (currentOffset / pageSize) + 1
}
}
caption := fmt.Sprintf(getI18nFromMap("AssetTableCaption"),
currentPage, pageSize, totalPage, totalCount)
caption = utils.WrapperString(caption, utils.Green)
table := common.WrapperTable{
Fields: fields,
Labels: Labels,
FieldsSize: map[string][3]int{
"ID": {0, 0, 5},
"hostname": {0, 8, 0},
"IP": {0, 15, 40},
"comment": {0, 0, 0},
},
Data: data,
TotalSize: w,
Caption: caption,
TruncPolicy: common.TruncMiddle,
}
table.Initial()
_, _ = p.term.Write([]byte(utils.CharClear))
_, _ = p.term.Write([]byte(table.Display()))
}
func (p *UserAssetPagination) displayTipsInfo() {
displayAssetPaginationTipsInfo(p.term)
}
func (p *UserAssetPagination) retrieveData() {
p.limit = getPageSize(p.term)
if p.limit == 0 || p.offset < 0 || p.limit >= p.Data.Total {
p.offset = 0
}
p.Data = service.GetUserAssets(p.UserID, p.search, p.limit, p.offset)
}
func getPageSize(term *utils.Terminal) int {
var (
pageSize int
minHeight = 8 // 分页显示的最小高度
)
_, height := term.GetSize()
conf := config.GetConf()
switch conf.AssetListPageSize {
case "auto":
pageSize = height - minHeight
case "all":
return 0
default:
if value, err := strconv.Atoi(conf.AssetListPageSize); err == nil {
pageSize = value
} else {
pageSize = height - minHeight
}
}
if pageSize <= 0 {
pageSize = 1
}
return pageSize
}
func displayAssetPaginationTipsInfo(w io.Writer) {
utils.IgnoreErrWriteString(w, getI18nFromMap("LoginTip"))
utils.IgnoreErrWriteString(w, getI18nFromMap("PageActionTip"))
}
......@@ -10,7 +10,6 @@ import (
"github.com/gliderlabs/ssh"
"github.com/xlab/treeprint"
"github.com/jumpserver/koko/pkg/cctx"
"github.com/jumpserver/koko/pkg/common"
"github.com/jumpserver/koko/pkg/config"
"github.com/jumpserver/koko/pkg/logger"
......@@ -21,13 +20,17 @@ import (
)
func SessionHandler(sess ssh.Session) {
user, ok := sess.Context().Value(model.ContextKeyUser).(*model.User)
if !ok || user.ID == "" {
logger.Errorf("SSH User %s not found, exit.", sess.User())
return
}
pty, _, ok := sess.Pty()
if ok {
ctx, cancel := cctx.NewContext(sess)
defer cancel()
handler := newInteractiveHandler(sess, ctx.User())
handler := newInteractiveHandler(sess, user)
logger.Infof("Request %s: User %s request pty %s", handler.sess.ID(), sess.User(), pty.Term)
handler.Dispatch(ctx)
go handler.watchWinSizeChange()
handler.Dispatch()
} else {
utils.IgnoreErrWriteString(sess, "No PTY requested.\n")
return
......@@ -55,19 +58,26 @@ type interactiveHandler struct {
assetSelect *model.Asset
systemUserSelect *model.SystemUser
nodes model.NodeList
searchResult []model.Asset
allAssets []model.Asset
loadDataDone chan struct{}
firstLoadDone chan struct{}
assetLoadPolicy string
currentSortedData []model.Asset
currentData []model.Asset
assetPaginator AssetPaginator
dbPaginator DatabasePaginator
currentDBData []model.Database
}
func (h *interactiveHandler) Initial() {
h.assetLoadPolicy = strings.ToLower(config.GetConf().AssetLoadPolicy)
h.displayBanner()
h.winWatchChan = make(chan bool)
h.loadDataDone = make(chan struct{})
h.winWatchChan = make(chan bool, 5)
h.firstLoadDone = make(chan struct{})
go h.firstLoadData()
}
......@@ -77,7 +87,7 @@ func (h *interactiveHandler) firstLoadData() {
case "all":
h.loadAllAssets()
}
close(h.loadDataDone)
close(h.firstLoadDone)
}
func (h *interactiveHandler) displayBanner() {
......@@ -113,95 +123,13 @@ func (h *interactiveHandler) watchWinSizeChange() {
}
func (h *interactiveHandler) pauseWatchWinSize() {
select {
case <-h.sess.Sess.Context().Done():
return
default:
}
h.winWatchChan <- false
}
func (h *interactiveHandler) resumeWatchWinSize() {
select {
case <-h.sess.Sess.Context().Done():
return
default:
}
h.winWatchChan <- true
}
func (h *interactiveHandler) Dispatch(ctx cctx.Context) {
go h.watchWinSizeChange()
defer logger.Infof("Request %s: User %s stop interactive", h.sess.ID(), h.user.Name)
for {
line, err := h.term.ReadLine()
if err != nil {
logger.Debugf("User %s close connect", h.user.Name)
break
}
line = strings.TrimSpace(line)
switch len(line) {
case 0, 1:
switch strings.ToLower(line) {
case "", "p":
// 展示所有的资产
h.displayAllAssets()
case "g":
<-h.loadDataDone
h.displayNodes(h.nodes)
case "h":
h.displayBanner()
case "r":
h.refreshAssetsAndNodesData()
case "q":
logger.Debugf("user %s enter to exit", h.user.Name)
return
default:
h.searchAssetOrProxy(line)
}
default:
switch {
case line == "exit", line == "quit":
logger.Debugf("user %s enter to exit", h.user.Name)
return
case strings.Index(line, "/") == 0:
searchWord := strings.TrimSpace(line[1:])
h.searchAsset(searchWord)
case strings.Index(line, "g") == 0:
searchWord := strings.TrimSpace(strings.TrimPrefix(line, "g"))
if num, err := strconv.Atoi(searchWord); err == nil {
if num >= 0 {
assets := h.searchNodeAssets(num)
h.displayAssets(assets)
continue
}
}
h.searchAssetOrProxy(line)
default:
h.searchAssetOrProxy(line)
}
}
}
}
func (h *interactiveHandler) displayAllAssets() {
switch h.assetLoadPolicy {
case "all":
<-h.loadDataDone
h.displayAssets(h.allAssets)
default:
pag := NewUserPagination(h.term, h.user.ID, "", false)
result := pag.Start()
if pag.IsNeedProxy && len(result) == 1 {
h.searchResult = h.searchResult[:0]
h.ProxyAsset(result[0])
} else {
h.searchResult = result
}
}
}
func (h *interactiveHandler) chooseSystemUser(asset model.Asset,
systemUsers []model.SystemUser) (systemUser model.SystemUser, ok bool) {
......@@ -269,50 +197,20 @@ func (h *interactiveHandler) chooseSystemUser(asset model.Asset,
}
}
func (h *interactiveHandler) displayAssets(assets model.AssetList) {
if len(assets) == 0 {
_, _ = io.WriteString(h.term, getI18nFromMap("NoAssets")+"\n\r")
} else {
sortedAssets := assets.SortBy(config.GetConf().AssetListSortBy)
pag := NewAssetPagination(h.term, sortedAssets)
selectOneAssets := pag.Start()
if len(selectOneAssets) == 1 {
systemUsers := service.GetUserAssetSystemUsers(h.user.ID, selectOneAssets[0].ID)
systemUser, ok := h.chooseSystemUser(selectOneAssets[0], systemUsers)
if !ok {
return
}
h.assetSelect = &selectOneAssets[0]
h.systemUserSelect = &systemUser
h.Proxy(context.TODO())
}
if pag.page.PageSize() >= pag.page.TotalCount() {
h.searchResult = sortedAssets
}
}
}
func (h *interactiveHandler) displayNodes(nodes []model.Node) {
tree := ConstructAssetNodeTree(nodes)
_, _ = io.WriteString(h.term, "\n\r"+getI18nFromMap("NodeHeaderTip"))
_, _ = io.WriteString(h.term, tree.String())
_, err := io.WriteString(h.term, getI18nFromMap("NodeEndTip")+"\n\r")
if err != nil {
logger.Info("displayAssetNodes err:", err)
}
}
func (h *interactiveHandler) refreshAssetsAndNodesData() {
switch h.assetLoadPolicy {
case "all":
h.loadAllAssets()
default:
_ = service.ForceRefreshUserPemAssets(h.user.ID)
}
h.loadUserNodes("2")
_, err := io.WriteString(h.term, getI18nFromMap("RefreshDone")+"\n\r")
if err != nil {
logger.Error("refresh Assets Nodes err:", err)
}
h.assetPaginator = nil
h.dbPaginator = nil
}
func (h *interactiveHandler) loadUserNodes(cachePolicy string) {
......@@ -323,76 +221,6 @@ func (h *interactiveHandler) loadAllAssets() {
h.allAssets = service.GetUserAllAssets(h.user.ID)
}
func (h *interactiveHandler) searchAsset(key string) {
switch h.assetLoadPolicy {
case "all":
<-h.loadDataDone
var searchData []model.Asset
switch len(h.searchResult) {
case 0:
searchData = h.allAssets
default:
searchData = h.searchResult
}
assets := searchFromLocalAssets(searchData, key)
h.displayAssets(assets)
default:
pag := NewUserPagination(h.term, h.user.ID, key, false)
result := pag.Start()
if pag.IsNeedProxy && len(result) == 1 {
h.searchResult = h.searchResult[:0]
h.ProxyAsset(result[0])
} else {
h.searchResult = result
}
}
}
func (h *interactiveHandler) searchAssetOrProxy(key string) {
if indexNum, err := strconv.Atoi(key); err == nil && len(h.searchResult) > 0 {
if indexNum > 0 && indexNum <= len(h.searchResult) {
assetSelect := h.searchResult[indexNum-1]
h.ProxyAsset(assetSelect)
return
}
}
var assets []model.Asset
switch h.assetLoadPolicy {
case "all":
<-h.loadDataDone
var searchData []model.Asset
switch len(h.searchResult) {
case 0:
searchData = h.allAssets
default:
searchData = h.searchResult
}
assets = searchFromLocalAssets(searchData, key)
if len(assets) != 1 {
h.displayAssets(assets)
return
}
default:
pag := NewUserPagination(h.term, h.user.ID, key, true)
assets = pag.Start()
}
if len(assets) == 1 {
h.ProxyAsset(assets[0])
} else {
h.searchResult = assets
}
}
func (h *interactiveHandler) searchNodeAssets(num int) (assets model.AssetList) {
if num > len(h.nodes) || num == 0 {
return assets
}
node := h.nodes[num-1]
assets = service.GetUserNodeAssets(h.user.ID, node.ID, "1")
return
}
func (h *interactiveHandler) ProxyAsset(assetSelect model.Asset) {
systemUsers := service.GetUserAssetSystemUsers(h.user.ID, assetSelect.ID)
systemUserSelect, ok := h.chooseSystemUser(assetSelect, systemUsers)
......@@ -414,6 +242,7 @@ func (h *interactiveHandler) Proxy(ctx context.Context) {
h.pauseWatchWinSize()
p.Proxy()
h.resumeWatchWinSize()
logger.Infof("Request %s: asset %s proxy end", h.sess.Uuid, h.assetSelect.Hostname)
}
func ConstructAssetNodeTree(assetNodes []model.Node) treeprint.Tree {
......@@ -486,3 +315,29 @@ func searchFromLocalAssets(assets model.AssetList, key string) []model.Asset {
}
return displayAssets
}
func getPageSize(term *utils.Terminal) int {
var (
pageSize int
minHeight = 8 // 分页显示的最小高度
)
_, height := term.GetSize()
conf := config.GetConf()
switch conf.AssetListPageSize {
case "auto":
pageSize = height - minHeight
case "all":
return 0
default:
if value, err := strconv.Atoi(conf.AssetListPageSize); err == nil {
pageSize = value
} else {
pageSize = height - minHeight
}
}
if pageSize <= 0 {
pageSize = 1
}
return pageSize
}
......@@ -12,18 +12,19 @@ import (
"github.com/pkg/sftp"
uuid "github.com/satori/go.uuid"
"github.com/jumpserver/koko/pkg/cctx"
"github.com/jumpserver/koko/pkg/logger"
"github.com/jumpserver/koko/pkg/model"
"github.com/jumpserver/koko/pkg/service"
"github.com/jumpserver/koko/pkg/srvconn"
)
func SftpHandler(sess ssh.Session) {
ctx, cancel := cctx.NewContext(sess)
defer cancel()
currentUser, ok := sess.Context().Value(model.ContextKeyUser).(*model.User)
if !ok || currentUser.ID == "" {
logger.Errorf("SFTP User not found, exit.")
return
}
host, _, _ := net.SplitHostPort(sess.RemoteAddr().String())
userSftp := NewSFTPHandler(ctx.User(), host)
userSftp := NewSFTPHandler(currentUser, host)
handlers := sftp.Handlers{
FileGet: userSftp,
FilePut: userSftp,
......@@ -44,12 +45,11 @@ func SftpHandler(sess ssh.Session) {
}
func NewSFTPHandler(user *model.User, addr string) *sftpHandler {
assets := service.GetUserAllAssets(user.ID)
return &sftpHandler{srvconn.NewUserSFTP(user, addr, assets...)}
return &sftpHandler{srvconn.NewUserSftpConn(user, addr)}
}
type sftpHandler struct {
*srvconn.UserSftp
*srvconn.UserSftpConn
}
func (fs *sftpHandler) Filelist(r *sftp.Request) (sftp.ListerAt, error) {
......@@ -120,7 +120,7 @@ func (fs *sftpHandler) Fileread(r *sftp.Request) (io.ReaderAt, error) {
}
func (fs *sftpHandler) Close() {
fs.UserSftp.Close()
fs.UserSftpConn.Close()
}
type listerat []os.FileInfo
......
......@@ -66,7 +66,8 @@ func (w *WrapperSession) Close() error {
return nil
default:
}
err := w.inWriter.Close()
_ = w.inWriter.Close()
err := w.outReader.Close()
w.initReadPip()
return err
}
......
......@@ -5,6 +5,7 @@ type HostMsg struct {
UserID string `json:"userid"`
Secret string `json:"secret"`
Size []int `json:"size"`
HostType string `json:"type"`
}
type ResizeMsg struct {
......
......@@ -11,7 +11,6 @@ import (
"github.com/LeeEirc/elfinder"
"github.com/gorilla/mux"
"github.com/jumpserver/koko/pkg/cctx"
"github.com/jumpserver/koko/pkg/common"
"github.com/jumpserver/koko/pkg/config"
"github.com/jumpserver/koko/pkg/logger"
......@@ -45,8 +44,8 @@ func AuthDecorator(handler http.HandlerFunc) http.HandlerFunc {
} else {
remoteIP = strings.Split(request.RemoteAddr, ":")[0]
}
ctx := context.WithValue(request.Context(), cctx.ContextKeyUser, user)
ctx = context.WithValue(ctx, cctx.ContextKeyRemoteAddr, remoteIP)
ctx := context.WithValue(request.Context(), model.ContextKeyUser, user)
ctx = context.WithValue(ctx, model.ContextKeyRemoteAddr, remoteIP)
handler(responseWriter, request.WithContext(ctx))
}
}
......@@ -66,8 +65,8 @@ func sftpFinder(wr http.ResponseWriter, req *http.Request) {
func sftpHostConnectorView(wr http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
hostID := vars["host"]
user := req.Context().Value(cctx.ContextKeyUser).(*model.User)
remoteIP := req.Context().Value(cctx.ContextKeyRemoteAddr).(string)
user := req.Context().Value(model.ContextKeyUser).(*model.User)
remoteIP := req.Context().Value(model.ContextKeyRemoteAddr).(string)
switch req.Method {
case "GET":
if err := req.ParseForm(); err != nil {
......
......@@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
"sync"
"time"
"github.com/LeeEirc/elfinder"
"github.com/pkg/sftp"
......@@ -18,26 +19,28 @@ import (
)
func NewUserVolume(user *model.User, addr, hostId string) *UserVolume {
var assets []model.Asset
var userSftp *srvconn.UserSftpConn
homename := "Home"
basePath := "/"
switch hostId {
case "":
assets = service.GetUserAllAssets(user.ID)
userSftp = srvconn.NewUserSftpConn(user, addr)
default:
assets = service.GetUserAssetByID(user.ID, hostId)
assets := service.GetUserAssetByID(user.ID, hostId)
if len(assets) == 1 {
homename = assets[0].Hostname
if assets[0].OrgID != "" {
homename = fmt.Sprintf("%s.%s", assets[0].Hostname, assets[0].OrgName)
folderName := assets[0].Hostname
if strings.Contains(folderName, "/") {
folderName = strings.ReplaceAll(folderName, "/", "_")
}
homename = folderName
basePath = filepath.Join("/", homename)
}
userSftp = srvconn.NewUserSftpConnWithAssets(user, addr, assets...)
}
rawID := fmt.Sprintf("%s@%s", user.Username, addr)
uVolume := &UserVolume{
Uuid: elfinder.GenerateID(rawID),
UserSftp: srvconn.NewUserSFTP(user, addr, assets...),
UserSftp: userSftp,
Homename: homename,
basePath: basePath,
chunkFilesMap: make(map[int]*sftp.File),
......@@ -48,7 +51,7 @@ func NewUserVolume(user *model.User, addr, hostId string) *UserVolume {
type UserVolume struct {
Uuid string
*srvconn.UserSftp
UserSftp *srvconn.UserSftpConn
Homename string
basePath string
......@@ -66,7 +69,7 @@ func (u *UserVolume) Info(path string) (elfinder.FileDir, error) {
if path == "/" {
return u.RootFileDir(), nil
}
originFileInfo, err := u.Stat(filepath.Join(u.basePath, path))
originFileInfo, err := u.UserSftp.Stat(filepath.Join(u.basePath, path))
if err != nil {
return rest, err
}
......@@ -300,22 +303,44 @@ func (u *UserVolume) Paste(dir, filename, suffix string, reader io.ReadCloser) (
func (u *UserVolume) RootFileDir() elfinder.FileDir {
logger.Debug("Root File Dir")
fInfo, _ := u.UserSftp.Stat(u.basePath)
var (
size int64
)
tz := time.Now().UnixNano()
if fInfo, err := u.UserSftp.Stat(u.basePath); err == nil {
size = fInfo.Size()
tz = fInfo.ModTime().Unix()
}
var rest elfinder.FileDir
rest.Name = u.Homename
rest.Hash = hashPath(u.Uuid, "/")
rest.Size = fInfo.Size()
rest.Size = size
rest.Volumeid = u.Uuid
rest.Mime = "directory"
rest.Dirs = 1
rest.Read, rest.Write = 1, 1
rest.Locked = 1
rest.Ts = fInfo.ModTime().Unix()
rest.Ts = tz
return rest
}
func (u *UserVolume) Close() {
u.UserSftp.Close()
logger.Infof("User %s's volume close", u.UserSftp.User.Name)
}
func (u *UserVolume) Search(path, key string, mimes ...string) (res []elfinder.FileDir, err error) {
originFileInfolist, err := u.UserSftp.Search(key)
if err != nil {
return nil, err
}
res = make([]elfinder.FileDir, 0, len(originFileInfolist))
searchPath := fmt.Sprintf("/%s", srvconn.SearchFolderName)
for i := 0; i < len(originFileInfolist); i++ {
res = append(res, NewElfinderFileInfo(u.Uuid, searchPath, originFileInfolist[i]))
}
return
}
func NewElfinderFileInfo(id, dirPath string, originFileInfo os.FileInfo) elfinder.FileDir {
......
......@@ -20,6 +20,10 @@ import (
"github.com/jumpserver/koko/pkg/service"
)
type proxyServer interface {
Proxy()
}
func OnPingHandler(c *neffos.NSConn, msg neffos.Message) error {
c.Emit("pong", []byte(""))
return nil
......@@ -106,10 +110,23 @@ func OnHostHandler(c *neffos.NSConn, msg neffos.Message) (err error) {
emitMsg := RoomMsg{roomID, secret}
roomMsg, _ := json.Marshal(emitMsg)
c.Emit("room", roomMsg)
var databaseAsset model.Database
var asset model.Asset
asset := service.GetAsset(assetID)
systemUser := service.GetSystemUser(systemUserID)
if message.HostType == "database" {
databaseAsset = service.GetDatabase(assetID)
if databaseAsset.ID == "" || systemUser.ID == "" {
msg := "No database id or system user id found, exit"
logger.Info(msg)
dataMsg := DataMsg{Room: roomID, Data: msg}
c.Emit("data", neffos.Marshal(dataMsg))
return
}
logger.Infof("Web terminal want to connect database: %s", databaseAsset.Name)
} else {
asset = service.GetAsset(assetID)
if asset.ID == "" || systemUser.ID == "" {
msg := "No asset id or system user id found, exit"
logger.Debug(msg)
......@@ -117,7 +134,8 @@ func OnHostHandler(c *neffos.NSConn, msg neffos.Message) (err error) {
c.Emit("data", neffos.Marshal(dataMsg))
return
}
logger.Debug("Web terminal want to connect host: ", asset.Hostname)
logger.Infof("Web terminal want to connect host: %s", asset.Hostname)
}
currentUser, ok := cc.Get("currentUser").(*model.User)
if !ok {
err = errors.New("not found current user")
......@@ -149,10 +167,21 @@ func OnHostHandler(c *neffos.NSConn, msg neffos.Message) (err error) {
client.WinChan <- win
clients.AddClient(roomID, client)
conns.AddClient(cc.ID(), roomID)
proxySrv := proxy.ProxyServer{
var proxySrv proxyServer
if message.HostType == "database" {
proxySrv = &proxy.DBProxyServer{
UserConn: client,
User: currentUser,
Database: &databaseAsset,
SystemUser: &systemUser,
}
} else {
proxySrv = &proxy.ProxyServer{
UserConn: client, User: currentUser,
Asset: &asset, SystemUser: &systemUser,
}
}
go func() {
defer logger.Infof("Request %s: Web ssh end proxy process", client.Uuid)
logger.Infof("Request %s: Web ssh start proxy to host", client.Uuid)
......
......@@ -16,7 +16,7 @@ import (
"github.com/jumpserver/koko/pkg/sshd"
)
const Version = "1.5.4"
const Version = "1.5.6"
type Coco struct {
}
......
......@@ -90,7 +90,6 @@ type Asset struct {
IP string `json:"ip"`
Os string `json:"os"`
Domain string `json:"domain"`
Platform string `json:"platform"`
Comment string `json:"comment"`
Protocols []string `json:"protocols,omitempty"`
OrgID string `json:"org_id"`
......
package model
type contextKey int64
const (
ContextKeyUser contextKey = iota + 1
ContextKeyRemoteAddr
ContextKeyClient
ContextKeyConfirmRequired
ContextKeyConfirmFailed
)
package model
import "fmt"
type Database struct {
ID string `json:"id"`
Name string `json:"name"`
DBType string `json:"type"`
Host string `json:"host"`
Port int `json:"port"`
DBName string `json:"database"`
OrgID string `json:"org_id"`
Comment string `json:"comment"`
}
func (db Database) String() string {
return fmt.Sprintf("%s://%s:%d/%s", db.DBType, db.Host, db.Port, db.DBName)
}
package model
import "encoding/json"
type NodeTreeList []NodeTreeAsset
type NodeTreeAsset struct {
ID string `json:"id"`
Name string `json:"name"`
Title string `json:"title"`
Pid string `json:"pId"`
IsParent bool `json:"isParent"`
Meta map[string]interface{} `json:"meta"`
}
func ConvertMetaToNode(body []byte) (node Node, err error) {
err = json.Unmarshal(body, &node)
return
}
func ConvertMetaToAsset(body []byte) (asset Asset, err error) {
err = json.Unmarshal(body, &asset)
return
}
......@@ -18,12 +18,6 @@ package model
'date_expired': '2089-03-21 18:18:24 +0800'}
*/
type AuthResponse struct {
Token string `json:"token"`
Seed string `json:"seed"`
User *User `json:"user"`
}
type User struct {
ID string `json:"id"`
Name string `json:"name"`
......
package proxy
import (
"bytes"
"sync"
"github.com/jumpserver/koko/pkg/logger"
)
const (
DBInputParserName = "DB Input parser"
DBOutputParserName = "DB Output parser"
)
func newDBParser(id string) DBParser {
dbParser := DBParser{
id: id,
}
dbParser.initial()
return dbParser
}
type DBParser struct {
id string
userOutputChan chan []byte
srvOutputChan chan []byte
cmdRecordChan chan [2]string
inputInitial bool
inputPreState bool
inputState bool
once *sync.Once
lock *sync.RWMutex
command string
output string
cmdInputParser *CmdParser
cmdOutputParser *CmdParser
closed chan struct{}
}
func (p *DBParser) initial() {
p.once = new(sync.Once)
p.lock = new(sync.RWMutex)
p.cmdInputParser = NewCmdParser(p.id, DBInputParserName)
p.cmdOutputParser = NewCmdParser(p.id, DBOutputParserName)
p.closed = make(chan struct{})
p.cmdRecordChan = make(chan [2]string, 1024)
}
// ParseStream 解析数据流
func (p *DBParser) ParseStream(userInChan, srvInChan <-chan []byte) (userOut, srvOut <-chan []byte) {
p.userOutputChan = make(chan []byte, 1)
p.srvOutputChan = make(chan []byte, 1)
logger.Infof("DB Session %s: Parser start", p.id)
go func() {
defer func() {
// 会话结束,结算命令结果
p.sendCommandRecord()
close(p.cmdRecordChan)
close(p.userOutputChan)
close(p.srvOutputChan)
logger.Infof("DB Session %s: Parser routine done", p.id)
}()
for {
select {
case <-p.closed:
return
case b, ok := <-userInChan:
if !ok {
return
}
b = p.ParseUserInput(b)
select {
case <-p.closed:
return
case p.userOutputChan <- b:
}
case b, ok := <-srvInChan:
if !ok {
return
}
b = p.ParseServerOutput(b)
select {
case <-p.closed:
return
case p.srvOutputChan <- b:
}
}
}
}()
return p.userOutputChan, p.srvOutputChan
}
// parseInputState 切换用户输入状态, 并结算命令和结果
func (p *DBParser) parseInputState(b []byte) []byte {
p.inputPreState = p.inputState
if bytes.Contains(b, charEnter) {
// 连续输入enter key, 结算上一条可能存在的命令结果
p.sendCommandRecord()
p.inputState = false
// 用户输入了Enter,开始结算命令
p.parseCmdInput()
} else {
p.inputState = true
// 用户又开始输入,并上次不处于输入状态,开始结算上次命令的结果
if !p.inputPreState {
p.sendCommandRecord()
}
}
return b
}
// parseCmdInput 解析命令的输入
func (p *DBParser) parseCmdInput() {
p.command = p.cmdInputParser.Parse()
}
// parseCmdOutput 解析命令输出
func (p *DBParser) parseCmdOutput() {
p.output = p.cmdOutputParser.Parse()
}
// ParseUserInput 解析用户的输入
func (p *DBParser) ParseUserInput(b []byte) []byte {
p.lock.Lock()
defer p.lock.Unlock()
p.once.Do(func() {
p.inputInitial = true
})
nb := p.parseInputState(b)
return nb
}
// splitCmdStream 将服务器输出流分离到命令buffer和命令输出buffer
func (p *DBParser) splitCmdStream(b []byte) {
if !p.inputInitial {
return
}
if p.inputState {
_, _ = p.cmdInputParser.WriteData(b)
return
}
_, _ = p.cmdOutputParser.WriteData(b)
}
// ParseServerOutput 解析服务器输出
func (p *DBParser) ParseServerOutput(b []byte) []byte {
p.lock.Lock()
defer p.lock.Unlock()
p.splitCmdStream(b)
return b
}
// Close 关闭parser
func (p *DBParser) Close() {
select {
case <-p.closed:
return
default:
close(p.closed)
}
_ = p.cmdOutputParser.Close()
_ = p.cmdInputParser.Close()
logger.Infof("DB Session %s: Parser close", p.id)
}
func (p *DBParser) sendCommandRecord() {
if p.command != "" {
p.parseCmdOutput()
p.cmdRecordChan <- [2]string{p.command, p.output}
p.command = ""
p.output = ""
}
}
package proxy
import (
"fmt"
"strings"
"time"
"github.com/jumpserver/koko/pkg/config"
"github.com/jumpserver/koko/pkg/i18n"
"github.com/jumpserver/koko/pkg/logger"
"github.com/jumpserver/koko/pkg/model"
"github.com/jumpserver/koko/pkg/service"
"github.com/jumpserver/koko/pkg/srvconn"
"github.com/jumpserver/koko/pkg/utils"
)
type DBProxyServer struct {
UserConn UserConnection
User *model.User
Database *model.Database
SystemUser *model.SystemUser
}
func (p *DBProxyServer) getAuthOrManualSet() error {
needManualSet := false
if p.SystemUser.LoginMode == model.LoginModeManual {
needManualSet = true
logger.Debugf("Database %s login mode is: %s", p.Database.Name, model.LoginModeManual)
}
if p.SystemUser.Password == "" {
needManualSet = true
logger.Debugf("Database %s neither has password", p.Database.Name)
}
if needManualSet {
term := utils.NewTerminal(p.UserConn, "password: ")
line, err := term.ReadPassword(fmt.Sprintf("%s's password: ", p.SystemUser.Username))
if err != nil {
logger.Errorf("Get password from user err %s", err.Error())
return err
}
p.SystemUser.Password = line
logger.Debug("Get password from user input: ", line)
}
return nil
}
func (p *DBProxyServer) getUsernameIfNeed() (err error) {
if p.SystemUser.Username == "" {
var username string
term := utils.NewTerminal(p.UserConn, "username: ")
for {
username, err = term.ReadLine()
if err != nil {
return err
}
username = strings.TrimSpace(username)
if username != "" {
break
}
}
p.SystemUser.Username = username
logger.Debug("Get username from user input: ", username)
}
return
}
func (p *DBProxyServer) checkProtocolMatch() bool {
return strings.ToLower(p.Database.DBType) == strings.ToLower(p.SystemUser.Protocol)
}
func (p *DBProxyServer) checkProtocolClientInstalled() bool {
switch strings.ToLower(p.Database.DBType) {
case "mysql":
return utils.IsInstalledMysqlClient()
}
return false
}
// validatePermission 检查是否有权限连接
func (p *DBProxyServer) validatePermission() bool {
return true
}
// getSSHConn 获取ssh连接
func (p *DBProxyServer) getMysqlConn() (srvConn *srvconn.ServerMysqlConnection, err error) {
srvConn = srvconn.NewMysqlServer(
srvconn.SqlHost(p.Database.Host),
srvconn.SqlPort(p.Database.Port),
srvconn.SqlUsername(p.SystemUser.Username),
srvconn.SqlPassword(p.SystemUser.Password),
srvconn.SqlDBName(p.Database.DBName),
)
err = srvConn.Connect()
return
}
// getServerConn 获取获取server连接
func (p *DBProxyServer) getServerConn() (srvConn srvconn.ServerConnection, err error) {
done := make(chan struct{})
defer func() {
utils.IgnoreErrWriteString(p.UserConn, "\r\n")
close(done)
}()
go p.sendConnectingMsg(done, config.GetConf().SSHTimeout*time.Second)
return p.getMysqlConn()
}
// sendConnectingMsg 发送连接信息
func (p *DBProxyServer) sendConnectingMsg(done chan struct{}, delayDuration time.Duration) {
delay := 0.0
msg := fmt.Sprintf(i18n.T("Database connecting to %s %.1f"), p.Database, delay)
utils.IgnoreErrWriteString(p.UserConn, msg)
for int(delay) < int(delayDuration/time.Second) {
select {
case <-done:
return
default:
delayS := fmt.Sprintf("%.1f", delay)
data := strings.Repeat("\x08", len(delayS)) + delayS
utils.IgnoreErrWriteString(p.UserConn, data)
time.Sleep(100 * time.Millisecond)
delay += 0.1
}
}
}
// preCheckRequisite 检查是否满足条件
func (p *DBProxyServer) preCheckRequisite() (ok bool) {
if !p.checkProtocolMatch() {
msg := utils.WrapperWarn(i18n.T("System user <%s> and database <%s> protocol are inconsistent."))
msg = fmt.Sprintf(msg, p.SystemUser.Username, p.Database.DBType)
utils.IgnoreErrWriteString(p.UserConn, msg)
return
}
if !p.checkProtocolClientInstalled() {
msg := utils.WrapperWarn(i18n.T("Database %s protocol client not installed."))
msg = fmt.Sprintf(msg, p.Database.DBType)
utils.IgnoreErrWriteString(p.UserConn, msg)
return
}
if !p.validatePermission() {
msg := fmt.Sprintf("You don't have permission login %s", p.Database.Name)
utils.IgnoreErrWriteString(p.UserConn, msg)
return
}
if err := p.checkRequiredAuth(); err != nil {
msg := fmt.Sprintf("You get database %s auth info err: %s", p.Database.Name, err)
utils.IgnoreErrWriteString(p.UserConn, msg)
return
}
return true
}
func (p *DBProxyServer) checkRequiredAuth() error {
info := service.GetSystemUserDatabaseAuthInfo(p.SystemUser.ID)
p.SystemUser.Password = info.Password
if err := p.getUsernameIfNeed(); err != nil {
logger.Errorf("Get database %s auth username err: %s", p.Database.Name, err)
return err
}
if err := p.getAuthOrManualSet(); err != nil {
logger.Errorf("Get database %s auth password err: %s", p.Database.Name, err)
return err
}
return nil
}
// sendConnectErrorMsg 发送连接错误消息
func (p *DBProxyServer) sendConnectErrorMsg(err error) {
msg := fmt.Sprintf("Connect database %s error: %s\r\n", p.Database.Host, err)
utils.IgnoreErrWriteString(p.UserConn, msg)
logger.Error(msg)
}
// Proxy 代理
func (p *DBProxyServer) Proxy() {
if !p.preCheckRequisite() {
logger.Error("Check requisite failed")
return
}
// 创建Session
sw, err := CreateDBSession(p)
if err != nil {
logger.Error("Create database Session failed")
return
}
defer RemoveDBSession(sw)
srvConn, err := p.getServerConn()
// 连接后端服务器失败
if err != nil {
logger.Errorf("Create database server conn failed: %s", err)
p.sendConnectErrorMsg(err)
return
}
if err = sw.Bridge(p.UserConn, srvConn); err != nil {
logger.Errorf("DB Session %s bridge end: %s", sw.ID, err)
}
}
package proxy
import (
"context"
"fmt"
"io"
"strings"
"time"
uuid "github.com/satori/go.uuid"
"github.com/jumpserver/koko/pkg/common"
"github.com/jumpserver/koko/pkg/config"
"github.com/jumpserver/koko/pkg/i18n"
"github.com/jumpserver/koko/pkg/logger"
"github.com/jumpserver/koko/pkg/model"
"github.com/jumpserver/koko/pkg/srvconn"
"github.com/jumpserver/koko/pkg/utils"
)
type DBSwitchSession struct {
ID string
p *DBProxyServer
DateStart string
DateEnd string
finished bool
MaxIdleTime time.Duration
ctx context.Context
cancel context.CancelFunc
}
func (s *DBSwitchSession) Initial() {
s.ID = uuid.NewV4().String()
s.DateStart = common.CurrentUTCTime()
s.MaxIdleTime = config.GetConf().MaxIdleTime
s.ctx, s.cancel = context.WithCancel(context.Background())
}
func (s *DBSwitchSession) Terminate() {
select {
case <-s.ctx.Done():
return
default:
}
s.cancel()
logger.Infof("DBSession %s: receive terminate from admin", s.ID)
}
func (s *DBSwitchSession) SessionID() string {
return s.ID
}
func (s *DBSwitchSession) recordCommand(cmdRecordChan chan [2]string) {
// 命令记录
cmdRecorder := NewCommandRecorder(s.ID)
for command := range cmdRecordChan {
if command[0] == "" {
continue
}
cmd := s.generateCommandResult(command)
cmdRecorder.Record(cmd)
}
// 关闭命令记录
cmdRecorder.End()
}
func (s *DBSwitchSession) generateCommandResult(command [2]string) *model.Command {
var input string
var output string
if len(command[0]) > 128 {
input = command[0][:128]
} else {
input = command[0]
}
i := strings.LastIndexByte(command[1], '\r')
if i <= 0 {
output = command[1]
} else if i > 0 && i < 1024 {
output = command[1][:i]
} else {
output = command[1][:1024]
}
return &model.Command{
SessionID: s.ID,
OrgID: s.p.Database.OrgID,
Input: input,
Output: output,
User: fmt.Sprintf("%s (%s)", s.p.User.Name, s.p.User.Username),
Server: s.p.Database.Name,
SystemUser: s.p.SystemUser.Username,
Timestamp: time.Now().Unix(),
}
}
// postBridge 桥接结束以后执行操作
func (s *DBSwitchSession) postBridge() {
s.DateEnd = common.CurrentUTCTime()
s.finished = true
}
// Bridge 桥接两个链接
func (s *DBSwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerConnection) (err error) {
var (
parser DBParser
replayRecorder ReplyRecorder
userInChan chan []byte
srvInChan chan []byte
done chan struct{}
)
parser = newDBParser(s.ID)
replayRecorder = NewReplyRecord(s.ID)
userInChan = make(chan []byte, 1)
srvInChan = make(chan []byte, 1)
done = make(chan struct{})
userOutChan, srvOutChan := parser.ParseStream(userInChan, srvInChan)
defer func() {
close(done)
_ = userConn.Close()
_ = srvConn.Close()
// 关闭parser
parser.Close()
// 关闭录像
replayRecorder.End()
s.postBridge()
}()
go s.recordCommand(parser.cmdRecordChan)
go s.LoopReadFromSrv(done, srvConn, srvInChan)
go s.LoopReadFromUser(done, userConn, userInChan)
winCh := userConn.WinCh()
maxIdleTime := s.MaxIdleTime * time.Minute
lastActiveTime := time.Now()
tick := time.NewTicker(30 * time.Second)
defer tick.Stop()
for {
select {
// 检测是否超过最大空闲时间
case <-tick.C:
now := time.Now()
outTime := lastActiveTime.Add(maxIdleTime)
if !now.After(outTime) {
continue
}
msg := fmt.Sprintf(i18n.T("Database connect idle more than %d minutes, disconnect"), s.MaxIdleTime)
logger.Infof("DB Session idle more than %d minutes, disconnect: %s", s.MaxIdleTime, s.ID)
msg = utils.WrapperWarn(msg)
utils.IgnoreErrWriteString(userConn, "\n\r"+msg)
return
// 手动结束
case <-s.ctx.Done():
msg := i18n.T("Database connection terminated by administrator")
msg = utils.WrapperWarn(msg)
logger.Infof("DBSession %s: %s", s.ID, msg)
utils.IgnoreErrWriteString(userConn, "\n\r"+msg)
return
// 监控窗口大小变化
case win, ok := <-winCh:
if !ok {
return
}
_ = srvConn.SetWinSize(win.Height, win.Width)
logger.Debugf("Window server change: %d*%d", win.Height, win.Width)
// 经过parse处理的server数据,发给user
case p, ok := <-srvOutChan:
if !ok {
return
}
nw, _ := userConn.Write(p)
replayRecorder.Record(p[:nw])
// 经过parse处理的user数据,发给server
case p, ok := <-userOutChan:
if !ok {
return
}
_, err = srvConn.Write(p)
}
lastActiveTime = time.Now()
}
}
func (s *DBSwitchSession) MapData() map[string]interface{} {
var dataEnd interface{}
if s.DateEnd != "" {
dataEnd = s.DateEnd
}
return map[string]interface{}{
"id": s.ID,
"user": fmt.Sprintf("%s (%s)", s.p.User.Name, s.p.User.Username),
"asset": s.p.Database.Name,
"org_id": s.p.Database.OrgID,
"login_from": s.p.UserConn.LoginFrom(),
"system_user": s.p.SystemUser.Username,
"protocol": s.p.SystemUser.Protocol,
"remote_addr": s.p.UserConn.RemoteAddr(),
"is_finished": s.finished,
"date_start": s.DateStart,
"date_end": dataEnd,
"user_id": s.p.User.ID,
"asset_id": s.p.Database.ID,
"system_user_id": s.p.SystemUser.ID,
}
}
func (s *DBSwitchSession) LoopReadFromUser(done chan struct{}, userConn UserConnection, inChan chan<- []byte) {
defer logger.Infof("DB Session %s: read from user done", s.ID)
s.LoopRead(done, userConn, inChan)
}
func (s *DBSwitchSession) LoopReadFromSrv(done chan struct{}, srvConn srvconn.ServerConnection, inChan chan<- []byte) {
defer logger.Infof("DB Session %s: read from srv done", s.ID)
s.LoopRead(done, srvConn, inChan)
}
func (s *DBSwitchSession) LoopRead(done chan struct{}, read io.Reader, inChan chan<- []byte) {
loop:
for {
buf := make([]byte, 1024)
nr, err := read.Read(buf)
if nr > 0 {
select {
case <-done:
logger.Debug("DB session reader loop break done.")
break loop
case inChan <- buf[:nr]:
}
}
if err != nil {
break
}
}
close(inChan)
}
......@@ -96,8 +96,6 @@ func (p *Parser) ParseStream(userInChan, srvInChan <-chan []byte) (userOut, srvO
close(p.cmdRecordChan)
close(p.userOutputChan)
close(p.srvOutputChan)
//_ = p.cmdOutputParser.Close()
//_ = p.cmdInputParser.Close()
logger.Infof("Session %s: Parser routine done", p.id)
}()
for {
......@@ -109,13 +107,23 @@ func (p *Parser) ParseStream(userInChan, srvInChan <-chan []byte) (userOut, srvO
return
}
b = p.ParseUserInput(b)
p.userOutputChan <- b
select {
case <-p.closed:
return
case p.userOutputChan <- b:
}
case b, ok := <-srvInChan:
if !ok {
return
}
b = p.ParseServerOutput(b)
p.srvOutputChan <- b
select {
case <-p.closed:
return
case p.srvOutputChan <- b:
}
}
}
}()
......@@ -277,6 +285,9 @@ func (p *Parser) Close() {
close(p.closed)
}
_ = p.cmdOutputParser.Close()
_ = p.cmdInputParser.Close()
logger.Infof("Session %s: Parser close", p.id)
}
func (p *Parser) sendCommandRecord() {
......
......@@ -82,17 +82,21 @@ func (cp *CmdParser) initial() {
cp.term.SetEcho(false)
go func() {
logger.Infof("Session %s: %s start", cp.id, cp.name)
defer logger.Infof("Session %s: %s parser close", cp.id, cp.name)
defer logger.Infof("Session %s: %s close", cp.id, cp.name)
loop:
for {
line, err := cp.term.ReadLine()
if err != nil {
select {
case <-cp.closed:
logger.Debugf("Session %s %s term err: %s break loop", cp.id, cp.name, err)
break loop
default:
}
logger.Debugf("Session %s %s term err: %s,loop continue", cp.id, cp.name, err)
goto loop
}
cp.lock.Lock()
cp.currentLength += len(line)
......
......@@ -116,7 +116,10 @@ func (p *ProxyServer) getSSHConn() (srvConn *srvconn.ServerSSHConnection, err er
func (p *ProxyServer) getTelnetConn() (srvConn *srvconn.ServerTelnetConnection, err error) {
conf := config.GetConf()
cusString := conf.TelnetRegex
pattern, _ := regexp.Compile(cusString)
pattern, err := regexp.Compile(cusString)
if err != nil {
logger.Errorf("telnet custom regex %s compile err: %s", cusString, err)
}
srvConn = &srvconn.ServerTelnetConnection{
User: p.User,
Asset: p.Asset,
......
......@@ -5,7 +5,7 @@ import (
"encoding/json"
"fmt"
"github.com/elastic/go-elasticsearch"
"github.com/elastic/go-elasticsearch/v6"
"github.com/jumpserver/koko/pkg/logger"
"github.com/jumpserver/koko/pkg/model"
......
......@@ -31,6 +31,7 @@ func (s S3ReplayStorage) Upload(gZipFilePath, target string) (err error) {
Credentials: credentials.NewStaticCredentials(s.AccessKey, s.SecretKey, ""),
Endpoint: aws.String(s.Endpoint),
Region: aws.String(s.Region),
S3ForcePathStyle: aws.Bool(true),
}
sess := session.Must(session.NewSession(s3Config))
......
......@@ -12,9 +12,14 @@ import (
"github.com/jumpserver/koko/pkg/utils"
)
var sessionMap = make(map[string]*SwitchSession)
var sessionMap = make(map[string]Session)
var lock = new(sync.RWMutex)
type Session interface {
SessionID() string
Terminate()
}
func HandleSessionTask(task model.TerminalTask) {
switch task.Name {
case "kill_session":
......@@ -50,20 +55,23 @@ func RemoveSession(sw *SwitchSession) {
lock.Lock()
defer lock.Unlock()
delete(sessionMap, sw.ID)
finishSession(sw)
data := sw.MapData()
finishSession(data)
logger.Infof("Session %s has finished", sw.ID)
}
func AddSession(sw *SwitchSession) {
func AddSession(sw Session) {
lock.Lock()
defer lock.Unlock()
sessionMap[sw.ID] = sw
sessionMap[sw.SessionID()] = sw
}
func CreateSession(p *ProxyServer) (sw *SwitchSession, err error) {
// 创建Session
sw = NewSwitchSession(p)
// Post到Api端
ok := postSession(sw)
data := sw.MapData()
ok := postSession(data)
msg := i18n.T("Connect with api server failed")
if !ok {
msg = utils.WrapperWarn(msg)
......@@ -84,8 +92,7 @@ func CreateSession(p *ProxyServer) (sw *SwitchSession, err error) {
return
}
func postSession(s *SwitchSession) bool {
data := s.MapData()
func postSession(data map[string]interface{}) bool {
for i := 0; i < 5; i++ {
if service.CreateSession(data) {
return true
......@@ -95,8 +102,33 @@ func postSession(s *SwitchSession) bool {
return false
}
func finishSession(s *SwitchSession) {
data := s.MapData()
func finishSession(data map[string]interface{}) {
service.FinishSession(data)
logger.Debugf("Session %s has finished", s.ID)
}
func CreateDBSession(p *DBProxyServer) (sw *DBSwitchSession, err error) {
// 创建Session
sw = &DBSwitchSession{
p: p,
}
sw.Initial()
data := sw.MapData()
ok := postSession(data)
msg := i18n.T("Create database session failed")
if !ok {
msg = utils.WrapperWarn(msg)
utils.IgnoreErrWriteString(p.UserConn, msg)
logger.Error(msg)
return sw, errors.New("create database session failed")
}
AddSession(sw)
return
}
func RemoveDBSession(sw *DBSwitchSession) {
lock.Lock()
defer lock.Unlock()
delete(sessionMap, sw.ID)
finishSession(sw.MapData())
logger.Infof("DB Session %s has finished", sw.ID)
}
......@@ -55,6 +55,11 @@ func (s *SwitchSession) Terminate() {
default:
}
s.cancel()
logger.Infof("Session %s: receive terminate from admin", s.ID)
}
func (s *SwitchSession) SessionID() string {
return s.ID
}
func (s *SwitchSession) recordCommand(cmdRecordChan chan [2]string) {
......@@ -122,14 +127,15 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo
userInChan chan []byte
srvInChan chan []byte
done chan struct{}
)
parser = newParser(s.ID)
replayRecorder = NewReplyRecord(s.ID)
userInChan = make(chan []byte, 10)
srvInChan = make(chan []byte, 10)
userInChan = make(chan []byte, 1)
srvInChan = make(chan []byte, 1)
done = make(chan struct{})
// 设置parser的命令过滤规则
parser.SetCMDFilterRules(s.cmdRules)
......@@ -137,6 +143,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo
userOutChan, srvOutChan := parser.ParseStream(userInChan, srvInChan)
defer func() {
close(done)
_ = userConn.Close()
_ = srvConn.Close()
// 关闭parser
......@@ -148,9 +155,8 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo
// 记录命令
go s.recordCommand(parser.cmdRecordChan)
go LoopRead(userConn, userInChan)
go LoopRead(srvConn, srvInChan)
go s.LoopReadFromSrv(done, srvConn, srvInChan)
go s.LoopReadFromUser(done, userConn, userInChan)
winCh := userConn.WinCh()
maxIdleTime := s.MaxIdleTime * time.Minute
lastActiveTime := time.Now()
......@@ -174,6 +180,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo
case <-s.ctx.Done():
msg := i18n.T("Terminated by administrator")
msg = utils.WrapperWarn(msg)
logger.Infof("Session %s: %s", s.ID, msg)
utils.IgnoreErrWriteString(userConn, "\n\r"+msg)
return
// 监控窗口大小变化
......@@ -220,16 +227,33 @@ func (s *SwitchSession) MapData() map[string]interface{} {
"is_finished": s.finished,
"date_start": s.DateStart,
"date_end": dataEnd,
"user_id": s.p.User.ID,
"asset_id": s.p.Asset.ID,
"system_user_id": s.p.SystemUser.ID,
}
}
func LoopRead(read io.Reader, inChan chan<- []byte) {
defer logger.Debug("loop read end")
func (s *SwitchSession) LoopReadFromUser(done chan struct{}, userConn UserConnection, inChan chan<- []byte) {
defer logger.Infof("Session %s: read from user done", s.ID)
s.LoopRead(done, userConn, inChan)
}
func (s *SwitchSession) LoopReadFromSrv(done chan struct{}, srvConn srvconn.ServerConnection, inChan chan<- []byte) {
defer logger.Infof("Session %s: read from srv done", s.ID)
s.LoopRead(done, srvConn, inChan)
}
func (s *SwitchSession) LoopRead(done chan struct{}, read io.Reader, inChan chan<- []byte) {
loop:
for {
buf := make([]byte, 1024)
nr, err := read.Read(buf)
if nr > 0 {
inChan <- buf[:nr]
select {
case <-done:
break loop
case inChan <- buf[:nr]:
}
}
if err != nil {
break
......
......@@ -27,42 +27,90 @@ func NewReplayStorage() ReplayStorage {
}
switch tp {
case "azure":
endpointSuffix := cf["ENDPOINT_SUFFIX"].(string)
var accountName string
var accountKey string
var containerName string
var endpointSuffix string
if value, ok := cf["ENDPOINT_SUFFIX"].(string); ok {
endpointSuffix = value
}
if value, ok := cf["ACCOUNT_NAME"].(string); ok {
accountName = value
}
if value, ok := cf["ACCOUNT_KEY"].(string); ok {
accountKey = value
}
if value, ok := cf["CONTAINER_NAME"].(string); ok {
containerName = value
}
if endpointSuffix == "" {
endpointSuffix = "core.chinacloudapi.cn"
}
return storage.AzureReplayStorage{
AccountName: cf["ACCOUNT_NAME"].(string),
AccountKey: cf["ACCOUNT_KEY"].(string),
ContainerName: cf["CONTAINER_NAME"].(string),
AccountName: accountName,
AccountKey: accountKey,
ContainerName: containerName,
EndpointSuffix: endpointSuffix,
}
case "oss":
var endpoint string
var bucket string
var accessKey string
var secretKey string
if value, ok := cf["ENDPOINT"].(string); ok {
endpoint = value
}
if value, ok := cf["BUCKET"].(string); ok {
bucket = value
}
if value, ok := cf["ACCESS_KEY"].(string); ok {
accessKey = value
}
if value, ok := cf["SECRET_KEY"].(string); ok {
secretKey = value
}
return storage.OSSReplayStorage{
Endpoint: cf["ENDPOINT"].(string),
Bucket: cf["BUCKET"].(string),
AccessKey: cf["ACCESS_KEY"].(string),
SecretKey: cf["SECRET_KEY"].(string),
Endpoint: endpoint,
Bucket: bucket,
AccessKey: accessKey,
SecretKey: secretKey,
}
case "s3":
var region string
var endpoint string
bucket := cf["BUCKET"].(string)
endpoint = cf["ENDPOINT"].(string)
var bucket string
var accessKey string
var secretKey string
if value, ok := cf["BUCKET"].(string); ok {
bucket = value
}
if value, ok := cf["ENDPOINT"].(string); ok {
endpoint = value
}
if value, ok := cf["REGION"].(string); ok {
region = value
}
if value, ok := cf["ACCESS_KEY"].(string); ok {
accessKey = value
}
if value, ok := cf["SECRET_KEY"].(string); ok {
secretKey = value
}
if region == "" && endpoint != "" {
endpointArray := strings.Split(endpoint, ".")
if len(endpointArray) >= 2 {
region = endpointArray[1]
}
}
if bucket == "" {
bucket = "jumpserver"
}
if cf["REGION"] != nil {
region = cf["REGION"].(string)
} else {
region = strings.Split(endpoint, ".")[1]
}
return storage.S3ReplayStorage{
Bucket: bucket,
Region: region,
AccessKey: cf["ACCESS_KEY"].(string),
SecretKey: cf["SECRET_KEY"].(string),
AccessKey: accessKey,
SecretKey: secretKey,
Endpoint: endpoint,
}
default:
......
......@@ -70,10 +70,10 @@ func (ak *AccessKey) SaveToFile() error {
}
}
f, err := os.Create(ak.Path)
defer f.Close()
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(fmt.Sprintf("%s:%s", ak.ID, ak.Secret))
if err != nil {
logger.Error(err)
......
package service
import (
"sync"
"github.com/jumpserver/koko/pkg/model"
)
type assetsCacheContainer struct {
mapData map[string]model.AssetList
mapETag map[string]string
mu *sync.RWMutex
}
func (c *assetsCacheContainer) Get(key string) (model.AssetList, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, ok := c.mapData[key]
return value, ok
}
func (c *assetsCacheContainer) GetETag(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, ok := c.mapETag[key]
return value, ok
}
func (c *assetsCacheContainer) SetValue(key string, value model.AssetList) {
c.mu.Lock()
defer c.mu.Unlock()
c.mapData[key] = value
}
func (c *assetsCacheContainer) SetETag(key string, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.mapETag[key] = value
}
type nodesCacheContainer struct {
mapData map[string]model.NodeList
mapETag map[string]string
mu *sync.RWMutex
}
func (c *nodesCacheContainer) Get(key string) (model.NodeList, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, ok := c.mapData[key]
return value, ok
}
func (c *nodesCacheContainer) GetETag(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, ok := c.mapETag[key]
return value, ok
}
func (c *nodesCacheContainer) SetValue(key string, value model.NodeList) {
c.mu.Lock()
defer c.mu.Unlock()
c.mapData[key] = value
}
func (c *nodesCacheContainer) SetETag(key string, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.mapETag[key] = value
}
package service
import (
"fmt"
"github.com/jumpserver/koko/pkg/logger"
"github.com/jumpserver/koko/pkg/model"
)
func GetUserDatabases(uid string) (res []model.Database) {
Url := fmt.Sprintf(DatabaseAPPURL, uid)
_, err := authClient.Get(Url, &res)
if err != nil {
logger.Errorf("Get User databases err: %s", err)
}
return
}
func GetUserDatabaseSystemUsers(userID, assetID string) (sysUsers []model.SystemUser) {
Url := fmt.Sprintf(UserDatabaseSystemUsersURL, userID, assetID)
_, err := authClient.Get(Url, &sysUsers)
if err != nil {
logger.Error("Get user asset system users error: ", err)
}
return
}
func GetSystemUserDatabaseAuthInfo(systemUserID string) (info model.SystemUserAuthInfo) {
Url := fmt.Sprintf(SystemUserAuthURL, systemUserID)
_, err := authClient.Get(Url, &info)
if err != nil {
logger.Errorf("Get system user %s auth info failed", systemUserID)
}
return
}
func GetDatabase(dbID string) (res model.Database) {
Url := fmt.Sprintf(DatabaseDetailURL, dbID)
_, err := authClient.Get(Url, &res)
if err != nil {
logger.Errorf("Get User databases err: %s", err)
}
return
}
\ No newline at end of file
package service
const (
ErrLoginConfirmWait = "login_confirm_wait"
ErrLoginConfirmRejected = "login_confirm_rejected"
ErrLoginConfirmRequired = "login_confirm_required"
ErrMFARequired = "mfa_required"
ErrPasswordFailed = "password_failed"
)
......@@ -13,15 +13,12 @@ import (
"github.com/jumpserver/koko/pkg/logger"
)
var client = common.NewClient(30, "")
var authClient = common.NewClient(30, "")
func Initial(ctx context.Context) {
cf := config.GetConf()
keyPath := cf.AccessKeyFile
client.BaseHost = cf.CoreHost
authClient.BaseHost = cf.CoreHost
client.SetHeader("X-JMS-ORG", "ROOT")
authClient.SetHeader("X-JMS-ORG", "ROOT")
if !path.IsAbs(cf.AccessKeyFile) {
......
This diff is collapsed.
This diff is collapsed.
......@@ -8,9 +8,7 @@ import (
)
func RegisterTerminal(name, token, comment string) (res model.Terminal) {
if client.Headers == nil {
client.Headers = make(map[string]string)
}
client := newClient()
client.Headers["Authorization"] = fmt.Sprintf("BootstrapToken %s", token)
data := map[string]string{"name": name, "comment": comment}
_, err := client.Post(TerminalRegisterURL, data, &res)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -132,7 +132,7 @@ func (tc *ServerTelnetConnection) login(data []byte) AuthStatus {
return AuthSuccess
}
if tc.CustomString != "" {
if tc.CustomSuccessPattern.Match(data) {
if tc.CustomSuccessPattern != nil && tc.CustomSuccessPattern.Match(data) {
return AuthSuccess
}
}
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment