Commit a26bcf75 authored by Eric's avatar Eric

Merge branch '1.5.5' into dev

parents a4cfa7c4 bb90c98e
......@@ -19,135 +19,138 @@ msgstr ""
#. i18n.T
#: pkg/handler/banner.go:48
msgid "directly login"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:49
msgid "part IP, Hostname, Comment"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:49
#: pkg/handler/banner.go:48
msgid "to search login if unique"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:50
#: pkg/handler/banner.go:49
msgid "/ + IP, Hostname, Comment"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:50
#: pkg/handler/banner.go:49
msgid "to search, such as: /192.168"
msgstr ""
#. 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:53
#: pkg/handler/banner.go:52
msgid "refresh your assets and nodes"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:54
#: pkg/handler/banner.go:53
msgid "print help"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:55
#: pkg/handler/banner.go:54
msgid "exit"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:90
#: pkg/handler/banner.go:89
msgid "ID"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:91
#: pkg/handler/banner.go:90
msgid "hostname"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:92
#: pkg/handler/banner.go:91
msgid "IP"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:93
#: pkg/handler/banner.go:92
msgid "comment"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:94
#: pkg/handler/banner.go:93
msgid "Page: %d, Count: %d, Total Page: %d, Total Count: %d"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:95
#: pkg/handler/banner.go:94
msgid "No Assets"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:96
#: pkg/handler/banner.go:95
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:96
msgid "Page up: b\tPage down: n"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:97
msgid ""
"\n"
"Page up: P/p\tPage down: Enter|N/n\tBACK: b.\n"
msgid "Node: [ ID.Name(Asset amount) ]"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:98
msgid "Node: [ ID.Name(Asset amount) ]"
msgid "Tips: Enter g+NodeID to display the host under the node, such as g1"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:99
msgid "Tips: Enter g+NodeID to display the host under the node, such as g1"
msgid "Refresh done"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:100
msgid "Refresh done"
msgid "Tips: Enter system user ID and directly login the asset [ %s(%s) ]"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:101
msgid "Tips: Enter system user ID and directly login the asset [ %s(%s) ]"
msgid "Back: B/b"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:102
msgid "Back: B/b"
msgid "Name"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:103
msgid "Name"
msgid "Username"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:104
msgid "Username"
msgid "all"
msgstr ""
#. i18n.T
#: pkg/handler/banner.go:105
msgid "Search: %s"
msgstr ""
#. i18n.T
#: pkg/proxy/parser.go:131
#: pkg/proxy/parser.go:130
msgid "Command `%s` is forbidden"
msgstr ""
......@@ -178,11 +181,11 @@ msgid "Connect with api server failed"
msgstr ""
#. i18n.T
#: pkg/proxy/switch.go:159
#: pkg/proxy/switch.go:168
msgid "Connect idle more than %d minutes, disconnect"
msgstr ""
#. i18n.T
#: pkg/proxy/switch.go:166
#: pkg/proxy/switch.go:175
msgid "Terminated by administrator"
msgstr ""
......@@ -19,120 +19,112 @@ 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:53
#: pkg/handler/banner.go:52
msgid "refresh your assets and nodes"
msgstr "刷新最新的机器和节点信息"
#. i18n.T
#: pkg/handler/banner.go:54
#: pkg/handler/banner.go:53
msgid "print help"
msgstr "显示帮助"
#. i18n.T
#: pkg/handler/banner.go:55
#: pkg/handler/banner.go:54
msgid "exit"
msgstr "退出"
#. i18n.T
#: pkg/handler/banner.go:90
#: pkg/handler/banner.go:89
msgid "ID"
msgstr "ID"
#. i18n.T
#: pkg/handler/banner.go:91
#: pkg/handler/banner.go:90
msgid "hostname"
msgstr "主机名"
#. i18n.T
#: pkg/handler/banner.go:92
#: pkg/handler/banner.go:91
msgid "IP"
msgstr "IP"
#. i18n.T
#: pkg/handler/banner.go:93
#: pkg/handler/banner.go:92
msgid "comment"
msgstr "备注"
#. i18n.T
#: pkg/handler/banner.go:94
#: pkg/handler/banner.go:93
msgid "Page: %d, Count: %d, Total Page: %d, Total Count: %d"
msgstr "页码:%d,每页行数:%d,总页数:%d,总数量:%d"
#. i18n.T
#: pkg/handler/banner.go:95
#: pkg/handler/banner.go:94
msgid "No Assets"
msgstr "没有资产"
#. i18n.T
#: pkg/handler/banner.go:96
#: pkg/handler/banner.go:95
#, 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"
#: pkg/handler/banner.go:96
#, fuzzy
msgid "Page up: b\tPage down: n"
msgstr "上一页:b 下一页:n"
#. i18n.T
#: pkg/handler/banner.go:98
#: pkg/handler/banner.go:97
msgid "Node: [ ID.Name(Asset amount) ]"
msgstr "节点:[ ID.名称(资产数量) ]"
#. i18n.T
#: pkg/handler/banner.go:99
#: pkg/handler/banner.go:98
msgid "Tips: Enter g+NodeID to display the host under the node, such as g1"
msgstr "提示:输入 g+节点ID 显示节点下主机,如: g1"
#. i18n.T
#: pkg/handler/banner.go:100
#: pkg/handler/banner.go:99
msgid "Refresh done"
msgstr "刷新完成"
#. i18n.T
#: pkg/handler/banner.go:101
#: pkg/handler/banner.go:100
#, fuzzy
msgid "Tips: Enter system user ID and directly login the asset [ %s(%s) ]"
msgstr ""
......@@ -140,23 +132,34 @@ msgstr ""
"提示:输入系统用户ID,登录资产[ %s(%s) ]\n"
#. i18n.T
#: pkg/handler/banner.go:102
#: pkg/handler/banner.go:101
msgid "Back: B/b"
msgstr "返回:B/b"
#. i18n.T
#: pkg/handler/banner.go:103
#: pkg/handler/banner.go:102
msgid "Name"
msgstr "名称"
#. i18n.T
#: pkg/handler/banner.go:104
#: pkg/handler/banner.go:103
#, fuzzy
msgid "Username"
msgstr "用户名"
#. i18n.T
#: pkg/proxy/parser.go:131
#: pkg/handler/banner.go:104
msgid "all"
msgstr "所有"
#. i18n.T
#: pkg/handler/banner.go:105
#, fuzzy
msgid "Search: %s"
msgstr "搜索: %s"
#. i18n.T
#: pkg/proxy/parser.go:130
msgid "Command `%s` is forbidden"
msgstr "命令 `%s` 是被禁止的 ..."
......@@ -187,11 +190,11 @@ msgid "Connect with api server failed"
msgstr "连接API服务失败"
#. i18n.T
#: pkg/proxy/switch.go:159
#: pkg/proxy/switch.go:168
msgid "Connect idle more than %d minutes, disconnect"
msgstr "空闲时间超过%d分钟,断开连接"
#. i18n.T
#: pkg/proxy/switch.go:166
#: pkg/proxy/switch.go:175
msgid "Terminated by administrator"
msgstr "管理员中断连接"
......@@ -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,14 +45,13 @@ 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: 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")},
{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: "r", helpText: i18n.T("refresh your assets and nodes")},
{id: 6, instruct: "h", helpText: i18n.T("print help")},
{id: 7, instruct: "q", helpText: i18n.T("exit")},
}
}
......@@ -93,8 +92,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 +101,8 @@ 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"),
}
})
return i18nMap[name]
......
package handler
import (
"fmt"
"io"
"strconv"
"strings"
"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/utils"
)
func (h *interactiveHandler) Dispatch() {
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.resetPaginator()
case "b":
if h.assetPaginator != nil {
h.movePrePage()
break
}
if ok := h.searchOrProxy(line); ok {
continue
}
case "n":
if h.assetPaginator != nil {
h.moveNextPage()
break
}
if ok := h.searchOrProxy(line); ok {
continue
}
case "":
if h.assetPaginator != nil {
h.moveNextPage()
} else {
h.resetPaginator()
}
case "g":
h.displayNodeTree()
continue
case "h":
h.displayBanner()
continue
case "r":
h.refreshAssetsAndNodesData()
continue
case "q":
logger.Debugf("user %s enter to exit", h.user.Name)
return
default:
if ok := h.searchOrProxy(line); ok {
continue
}
}
default:
switch {
case line == "exit", line == "quit":
logger.Debugf("user %s enter to exit", h.user.Name)
return
case strings.Index(line, "/") == 0:
if strings.Index(line[1:], "/") == 0 {
line = strings.TrimSpace(line[2:])
h.searchAssetsAgain(line)
break
}
line = strings.TrimSpace(line[1:])
h.searchAssetAndDisplay(line)
case strings.Index(line, "g") == 0:
searchWord := strings.TrimSpace(strings.TrimPrefix(line, "g"))
if num, err := strconv.Atoi(searchWord); err == nil {
if num >= 0 {
h.searchNewNodeAssets(num)
break
}
}
if ok := h.searchOrProxy(line); ok {
continue
}
default:
if ok := h.searchOrProxy(line); ok {
continue
}
}
}
h.displayPageAssets()
}
}
func (h *interactiveHandler) resetPaginator() {
h.assetPaginator = h.getAssetPaginator()
h.currentData = h.assetPaginator.RetrievePageData(1)
}
func (h *interactiveHandler) displayPageAssets() {
if len(h.currentData) == 0 {
_, _ = h.term.Write([]byte(getI18nFromMap("NoAssets") + "\n\r"))
h.assetPaginator = nil
h.currentSortedData = nil
return
}
Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Hostname"),
getI18nFromMap("IP"), getI18nFromMap("Comment")}
fields := []string{"ID", "hostname", "IP", "comment"}
h.currentSortedData = model.AssetList(h.currentData).SortBy(config.GetConf().AssetListSortBy)
data := make([]map[string]string, len(h.currentSortedData))
for i, j := range h.currentSortedData {
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, _ := h.term.GetSize()
currentPage := h.assetPaginator.CurrentPage()
pageSize := h.assetPaginator.PageSize()
totalPage := h.assetPaginator.TotalPage()
totalCount := h.assetPaginator.TotalCount()
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()
header := getI18nFromMap("All")
keys := h.assetPaginator.SearchKeys()
switch h.assetPaginator.Name() {
case "local", "remote":
if len(keys) != 0 {
header = strings.Join(keys, " ")
}
default:
header = fmt.Sprintf("%s %s", h.assetPaginator.Name(), strings.Join(keys, " "))
}
searchHeader := fmt.Sprintf(getI18nFromMap("SearchTip"), header)
actionTip := fmt.Sprintf("%s %s", getI18nFromMap("LoginTip"), getI18nFromMap("PageActionTip"))
_, _ = h.term.Write([]byte(utils.CharClear))
_, _ = h.term.Write([]byte(table.Display()))
utils.IgnoreErrWriteString(h.term, utils.WrapperString(actionTip, utils.Green))
utils.IgnoreErrWriteString(h.term, utils.CharNewLine)
utils.IgnoreErrWriteString(h.term, utils.WrapperString(searchHeader, utils.Green))
utils.IgnoreErrWriteString(h.term, utils.CharNewLine)
}
func (h *interactiveHandler) movePrePage() {
if h.assetPaginator == nil || !h.assetPaginator.HasPrev() {
return
}
h.assetPaginator.SetPageSize(getPageSize(h.term))
prePage := h.assetPaginator.CurrentPage() - 1
h.currentData = h.assetPaginator.RetrievePageData(prePage)
}
func (h *interactiveHandler) moveNextPage() {
if h.assetPaginator == nil || !h.assetPaginator.HasNext() {
return
}
h.assetPaginator.SetPageSize(getPageSize(h.term))
nextPage := h.assetPaginator.CurrentPage() + 1
h.currentData = h.assetPaginator.RetrievePageData(nextPage)
}
func (h *interactiveHandler) searchAssets(key string) []model.Asset {
if _, ok := h.assetPaginator.(*nodeAssetsPaginator); ok {
h.assetPaginator = nil
}
if h.assetPaginator == nil {
h.assetPaginator = h.getAssetPaginator()
}
return h.assetPaginator.SearchAsset(key)
}
func (h *interactiveHandler) searchOrProxy(key string) bool {
if indexNum, err := strconv.Atoi(key); err == nil && len(h.currentSortedData) > 0 {
if indexNum > 0 && indexNum <= len(h.currentSortedData) {
assetSelect := h.currentSortedData[indexNum-1]
h.ProxyAsset(assetSelect)
h.assetPaginator = nil
h.currentSortedData = nil
return true
}
}
if data := h.searchAssets(key); len(data) == 1 {
h.ProxyAsset(data[0])
h.assetPaginator = nil
h.currentSortedData = nil
return true
} else {
h.currentData = data
}
return false
}
func (h *interactiveHandler) searchAssetAndDisplay(key string) {
h.currentData = h.searchAssets(key)
}
func (h *interactiveHandler) searchAssetsAgain(key string) {
if h.assetPaginator == nil {
h.assetPaginator = h.getAssetPaginator()
h.currentData = h.assetPaginator.SearchAsset(key)
return
}
h.currentData = h.assetPaginator.SearchAgain(key)
}
func (h *interactiveHandler) displayNodeTree() {
<-h.firstLoadDone
tree := ConstructAssetNodeTree(h.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) searchNewNodeAssets(num int) {
<-h.firstLoadDone
if num > len(h.nodes) || num == 0 {
h.currentData = nil
return
}
node := h.nodes[num-1]
h.assetPaginator = h.getNodeAssetPaginator(node)
h.currentData = h.assetPaginator.RetrievePageData(1)
}
func (h *interactiveHandler) getAssetPaginator() AssetPaginator {
switch h.assetLoadPolicy {
case "all":
<-h.firstLoadDone
return NewLocalAssetPaginator(h.allAssets, getPageSize(h.term))
default:
}
return NewRemoteAssetPaginator(*h.user, getPageSize(h.term))
}
func (h *interactiveHandler) getNodeAssetPaginator(node model.Node) AssetPaginator {
return NewNodeAssetPaginator(*h.user, node, getPageSize(h.term))
}
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,23 @@ 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
}
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 +84,7 @@ func (h *interactiveHandler) firstLoadData() {
case "all":
h.loadAllAssets()
}
close(h.loadDataDone)
close(h.firstLoadDone)
}
func (h *interactiveHandler) displayBanner() {
......@@ -113,95 +120,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 +194,19 @@ 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
}
func (h *interactiveHandler) loadUserNodes(cachePolicy string) {
......@@ -323,76 +217,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)
......@@ -486,3 +310,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
}
\ No newline at end of file
......@@ -12,7 +12,6 @@ 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"
......@@ -20,10 +19,13 @@ import (
)
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,
......
......@@ -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
}
......
......@@ -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 {
......
package model
type contextKey int64
const (
ContextKeyUser contextKey = iota + 1
ContextKeyRemoteAddr
ContextKeyClient
ContextKeyConfirmRequired
ContextKeyConfirmFailed
)
......@@ -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"`
......
......@@ -99,13 +99,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:
}
}
}
}()
......
......@@ -82,7 +82,7 @@ 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()
......
......@@ -122,14 +122,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 +138,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 +150,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()
......@@ -223,13 +224,27 @@ func (s *SwitchSession) MapData() map[string]interface{} {
}
}
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
......
......@@ -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
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) {
......
package service
type AuthStatus int64
const (
AuthSuccess AuthStatus = iota + 1
AuthFailed
AuthMFARequired
AuthConfirmRequired
)
type SessionOption func(*SessionOptions)
func Username(username string) SessionOption {
return func(args *SessionOptions) {
args.Username = username
}
}
func Password(password string) SessionOption {
return func(args *SessionOptions) {
args.Password = password
}
}
func PublicKey(publicKey string) SessionOption {
return func(args *SessionOptions) {
args.PublicKey = publicKey
}
}
func RemoteAddr(remoteAddr string) SessionOption {
return func(args *SessionOptions) {
args.RemoteAddr = remoteAddr
}
}
func LoginType(loginType string) SessionOption {
return func(args *SessionOptions) {
args.LoginType = loginType
}
}
type SessionOptions struct {
Username string
Password string
PublicKey string
RemoteAddr string
LoginType string
}
......@@ -9,24 +9,30 @@ import (
"github.com/jumpserver/koko/pkg/model"
)
func GetUserAssets(userID, search string, pageSize, offset int) (resp model.AssetsPaginationResponse) {
func GetUserAssets(userID string, pageSize, offset int, searches ...string) (resp model.AssetsPaginationResponse) {
if pageSize < 0 {
pageSize = 0
}
paramsArray := make([]map[string]string, 0, len(searches)+2)
for i := 0; i < len(searches); i++ {
paramsArray = append(paramsArray, map[string]string{
"search": url.QueryEscape(searches[i]),
})
}
params := map[string]string{
"search": url.QueryEscape(search),
"limit": strconv.Itoa(pageSize),
"offset": strconv.Itoa(offset),
}
paramsArray = append(paramsArray, params)
Url := fmt.Sprintf(UserAssetsURL, userID)
var err error
if pageSize > 0 {
_, err = authClient.Get(Url, &resp, params)
_, err = authClient.Get(Url, &resp, paramsArray...)
} else {
var data model.AssetList
_, err = authClient.Get(Url, &data, params)
_, err = authClient.Get(Url, &data, paramsArray...)
resp.Data = data
resp.Total = len(data)
}
if err != nil {
logger.Error("Get user assets error: ", err)
......@@ -34,6 +40,21 @@ func GetUserAssets(userID, search string, pageSize, offset int) (resp model.Asse
return
}
func ForceRefreshUserPemAssets(userID string) error {
params := map[string]string{
"limit": "1",
"offset": "0",
"cache": "2",
}
Url := fmt.Sprintf(UserAssetsURL, userID)
var resp model.AssetsPaginationResponse
_, err := authClient.Get(Url, resp, params)
if err != nil {
logger.Errorf("Refresh user assets error: %s", err)
}
return err
}
func GetUserAllAssets(userID string) (assets []model.Asset) {
Url := fmt.Sprintf(UserAssetsURL, userID)
_, err := authClient.Get(Url, &assets)
......@@ -91,6 +112,38 @@ func GetUserNodeAssets(userID, nodeID, cachePolicy string) (assets model.AssetLi
return
}
func GetUserNodePaginationAssets(userID, nodeID string, pageSize, offset int, searches ...string) (resp model.AssetsPaginationResponse) {
if pageSize < 0 {
pageSize = 0
}
paramsArray := make([]map[string]string, 0, len(searches)+2)
for i := 0; i < len(searches); i++ {
paramsArray = append(paramsArray, map[string]string{
"search": url.QueryEscape(searches[i]),
})
}
params := map[string]string{
"limit": strconv.Itoa(pageSize),
"offset": strconv.Itoa(offset),
}
paramsArray = append(paramsArray, params)
Url := fmt.Sprintf(UserNodeAssetsListURL, userID, nodeID)
var err error
if pageSize > 0 {
_, err = authClient.Get(Url, &resp, paramsArray...)
} else {
var data model.AssetList
_, err = authClient.Get(Url, &data, paramsArray...)
resp.Data = data
resp.Total = len(data)
}
if err != nil {
logger.Error("Get user node assets error: ", err)
}
return
}
func ValidateUserAssetPermission(userID, assetID, systemUserID, action string) bool {
payload := map[string]string{
"user_id": userID,
......
......@@ -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)
......
......@@ -38,3 +38,9 @@ const (
const (
UserAssetSystemUsersURL = "/api/v1/perms/users/%s/assets/%s/system-users/" // 获取用户授权资产的系统用户列表
)
// 1.5.5
const (
UserTokenAuthURL = "/api/v1/authentication/tokens/" // 用户登录验证
UserConfirmAuthURL = "/api/v1/authentication/login-confirm-ticket/status/"
)
package service
import (
"context"
"fmt"
"time"
"github.com/pkg/errors"
"github.com/jumpserver/koko/pkg/common"
"github.com/jumpserver/koko/pkg/logger"
"github.com/jumpserver/koko/pkg/model"
)
type AuthResp struct {
Token string `json:"token"`
Seed string `json:"seed"`
User *model.User `json:"user"`
type authResponse struct {
Err string `json:"error,omitempty"`
Msg string `json:"msg,omitempty"`
Data dataResponse `json:"data,omitempty"`
Username string `json:"username,omitempty"`
Token string `json:"token,omitempty"`
Keyword string `json:"keyword,omitempty"`
DateExpired string `json:"date_expired,omitempty"`
User model.User `json:"user,omitempty"`
}
type dataResponse struct {
Choices []string `json:"choices,omitempty"`
Url string `json:"url,omitempty"`
}
type AuthOptions struct {
Name string
Url string
}
func NewSessionClient(setters ...SessionOption) SessionClient {
option := &SessionOptions{}
for _, setter := range setters {
setter(option)
}
conn := newClient()
return SessionClient{
option: option,
client: &conn,
authOptions: make(map[string]AuthOptions),
}
}
type SessionClient struct {
option *SessionOptions
client *common.Client
authOptions map[string]AuthOptions
}
func (u *SessionClient) SetOption(setters ...SessionOption) {
for _, setter := range setters {
setter(u.option)
}
}
func (u *SessionClient) Authenticate(ctx context.Context) (user model.User, authStatus AuthStatus) {
authStatus = AuthFailed
data := map[string]string{
"username": u.option.Username,
"password": u.option.Password,
"public_key": u.option.PublicKey,
"remote_addr": u.option.RemoteAddr,
"login_type": u.option.LoginType,
}
var resp authResponse
_, err := u.client.Post(UserTokenAuthURL, data, &resp)
if err != nil {
logger.Errorf("User %s Authenticate err: %s", u.option.Username, err)
return
}
if resp.Err != "" {
switch resp.Err {
case ErrLoginConfirmWait:
logger.Infof("User %s login need confirmation", u.option.Username)
authStatus = AuthConfirmRequired
case ErrMFARequired:
for _, item := range resp.Data.Choices {
u.authOptions[item] = AuthOptions{
Name: item,
Url: resp.Data.Url,
}
}
logger.Infof("User %s login need MFA", u.option.Username)
authStatus = AuthMFARequired
default:
logger.Errorf("User %s login err: %s", u.option.Username, resp.Err)
}
return
}
if resp.Token != "" {
return resp.User, AuthSuccess
}
return
}
func Authenticate(username, password, publicKey, remoteAddr, loginType string) (resp *AuthResp, err error) {
func (u *SessionClient) CheckUserOTP(ctx context.Context, code string) (user model.User, authStatus AuthStatus) {
var err error
authStatus = AuthFailed
data := map[string]string{
"username": username,
"password": password,
"public_key": publicKey,
"remote_addr": remoteAddr,
"login_type": loginType,
"code": code,
}
for name, authData := range u.authOptions {
var resp authResponse
switch name {
case "opt":
data["type"] = name
}
_, err = u.client.Post(authData.Url, data, &resp)
if err != nil {
logger.Errorf("User %s use %s check MFA err: %s", u.option.Username, name, err)
continue
}
if resp.Err != "" {
logger.Errorf("User %s use %s check MFA err: %s", u.option.Username, name, resp.Err)
continue
}
if resp.Msg == "ok" {
logger.Infof("User %s check MFA success, check if need admin confirm", u.option.Username)
return u.Authenticate(ctx)
}
}
_, err = client.Post(UserAuthURL, data, &resp)
logger.Errorf("User %s failed to check MFA", u.option.Username)
return
}
func (u *SessionClient) CheckConfirm(ctx context.Context) (user model.User, authStatus AuthStatus) {
var err error
for {
select {
case <-ctx.Done():
logger.Errorf("User %s exit and cancel confirmation", u.option.Username)
u.CancelConfirm()
return
case <-time.After(5 * time.Second):
var resp authResponse
_, err = u.client.Get(UserConfirmAuthURL, &resp)
if err != nil {
logger.Errorf("User %s check confirm err: %s", u.option.Username, err)
return
}
if resp.Err != "" {
switch resp.Err {
case ErrLoginConfirmWait:
logger.Infof("User %s still wait confirm", u.option.Username)
continue
case ErrLoginConfirmRejected:
logger.Infof("User %s confirmation was rejected by admin", u.option.Username)
default:
logger.Infof("User %s confirmation was rejected by err: %s", u.option.Username, resp.Err)
}
return
}
if resp.Msg == "ok" {
logger.Infof("User %s confirmation was accepted", u.option.Username)
return u.Authenticate(ctx)
}
}
}
}
func (u *SessionClient) CancelConfirm() {
_, err := u.client.Delete(UserConfirmAuthURL, nil)
if err != nil {
logger.Errorf("Cancel User %s confirmation err: %s", u.option.Username, err)
return
}
logger.Infof("Cancel User %s confirmation success", u.option.Username)
}
func GetUserDetail(userID string) (user *model.User) {
Url := fmt.Sprintf(UserDetailURL, userID)
_, err := authClient.Get(Url, &user)
......@@ -56,20 +204,6 @@ func GetUserByUsername(username string) (user *model.User, err error) {
return
}
func CheckUserOTP(seed, code, remoteAddr, loginType string) (resp *AuthResp, err error) {
data := map[string]string{
"seed": seed,
"otp_code": code,
"remote_addr": remoteAddr,
"login_type": loginType,
}
_, err = client.Post(UserAuthOTPURL, data, &resp)
if err != nil {
return
}
return
}
func CheckUserCookie(sessionID, csrfToken string) (user *model.User, err error) {
cli := newClient()
cli.SetCookie("csrftoken", csrfToken)
......
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