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:
res = ssh.AuthSuccessful
case 1, 2:
action = actionPartialAccepted
res = ssh.AuthPartiallySuccessful
default:
}
ctx.SetValue(cctx.ContextKeyUser, resp.User)
ctx.SetValue(cctx.ContextKeySeed, resp.Seed)
ctx.SetValue(cctx.ContextKeyToken, resp.Token)
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
ctx.SetValue(model.ContextKeyUser, &user)
case service.AuthConfirmRequired:
required := true
ctx.SetValue(model.ContextKeyConfirmRequired, &required)
action = actionPartialAccepted
res = ssh.AuthPartiallySuccessful
default:
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,37 +81,70 @@ 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)
}
}()
answers, err := challenger(username, mfaInstruction, []string{mfaQuestion}, []bool{true})
if err != nil || len(answers) != 1 {
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
}
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")
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
}
resp, err := service.CheckUserOTP(seed, mfaCode, remoteAddr, "T")
if err != nil {
logger.Error("Mfa Auth failed: ", err)
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
}
case "no", "n":
client.CancelConfirm()
default:
return
}
failed := true
ctx.SetValue(model.ContextKeyConfirmFailed, &failed)
return
}
if resp.Token != "" {
mfaCode := answers[0]
user, authStatus := client.CheckUserOTP(ctx, mfaCode)
switch authStatus {
case service.AuthSuccess:
res = ssh.AuthSuccessful
return
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,12 +181,15 @@ 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 {
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)
err = errors.New(msg)
return
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)
err = errors.New(msg)
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
}
......
package httpd
type HostMsg struct {
Uuid string `json:"uuid"`
UserID string `json:"userid"`
Secret string `json:"secret"`
Size []int `json:"size"`
Uuid string `json:"uuid"`
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),
......@@ -47,8 +50,8 @@ func NewUserVolume(user *model.User, addr, hostId string) *UserVolume {
}
type UserVolume struct {
Uuid string
*srvconn.UserSftp
Uuid string
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,18 +110,32 @@ 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
}
if asset.ID == "" || systemUser.ID == "" {
msg := "No asset id or system user id found, exit"
logger.Debug(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)
dataMsg := DataMsg{Room: roomID, Data: msg}
c.Emit("data", neffos.Marshal(dataMsg))
return
}
logger.Infof("Web terminal want to connect host: %s", asset.Hostname)
}
logger.Debug("Web terminal want to connect host: ", asset.Hostname)
currentUser, ok := cc.Get("currentUser").(*model.User)
if !ok {
err = errors.New("not found current user")
......@@ -149,9 +167,20 @@ 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{
UserConn: client, User: currentUser,
Asset: &asset, SystemUser: &systemUser,
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)
......
......@@ -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"
......
......@@ -28,9 +28,10 @@ func (s S3ReplayStorage) Upload(gZipFilePath, target string) (err error) {
}
defer file.Close()
s3Config := &aws.Config{
Credentials: credentials.NewStaticCredentials(s.AccessKey, s.SecretKey, ""),
Endpoint: aws.String(s.Endpoint),
Region: aws.String(s.Region),
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
// 监控窗口大小变化
......@@ -209,27 +216,44 @@ func (s *SwitchSession) MapData() map[string]interface{} {
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.Asset.Hostname,
"org_id": s.p.Asset.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,
"id": s.ID,
"user": fmt.Sprintf("%s (%s)", s.p.User.Name, s.p.User.Username),
"asset": s.p.Asset.Hostname,
"org_id": s.p.Asset.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.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
}
This diff is collapsed.
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.
This diff is collapsed.
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