diff --git a/Dockerfile b/Dockerfile index 63887af69..3463e8b68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ FROM golang:1.12-alpine as stage-build LABEL stage=stage-build WORKDIR /opt/koko -ARG GOPROXY +ARG GOPROXY=https://goproxy.io +ARG KUBECTLDOWNLOADURL=https://download.jumpserver.org/public/kubectl.tar.gz +ARG ALIASESURL=http://download.jumpserver.org/public/kubectl_aliases.tar.gz ARG VERSION ENV GOPROXY=$GOPROXY ENV VERSION=$VERSION @@ -11,41 +13,39 @@ ENV GOARCH=amd64 ENV CGO_ENABLED=0 COPY . . +RUN wget "$KUBECTLDOWNLOADURL" -O kubectl.tar.gz && tar -xzf kubectl.tar.gz \ + && chmod +x kubectl && mv kubectl rawkubectl +RUN wget "$ALIASESURL" -O kubectl_aliases.tar.gz && tar -xzvf kubectl_aliases.tar.gz RUN cd utils && sh -ixeu build.sh FROM debian:stretch-slim RUN sed -i 's/deb.debian.org/mirrors.163.com/g' /etc/apt/sources.list \ && sed -i 's/security.debian.org/mirrors.163.com/g' /etc/apt/sources.list RUN apt-get update -y \ + && apt-get install -y locales \ + && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \ && apt-get install -y --no-install-recommends gnupg dirmngr openssh-client procps curl \ && rm -rf /var/lib/apt/lists/* - -RUN set -ex; \ -# gpg: key 5072E1F5: public key "MySQL Release Engineering " imported - key='A4A9406876FCBD3C456770C88C718D3B5072E1F5'; \ - export GNUPGHOME="$(mktemp -d)"; \ - ( gpg --batch --keyserver p80.pool.sks-keyservers.net --recv-keys "$key" \ - || gpg --batch --keyserver hkps.pool.sks-keyservers.net --recv-keys "$key" \ - || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" \ - || gpg --batch --keyserver pgp.mit.edu --recv-keys "$key" \ - || gpg --batch --keyserver keyserver.pgp.com --recv-keys "$key" ); \ - gpg --batch --export "$key" > /etc/apt/trusted.gpg.d/mysql.gpg; \ - gpgconf --kill all; \ - rm -rf "$GNUPGHOME"; \ - apt-key list > /dev/null +ENV LANG en_US.utf8 ENV MYSQL_MAJOR 8.0 RUN echo "deb http://mirrors.tuna.tsinghua.edu.cn/mysql/apt/debian stretch mysql-${MYSQL_MAJOR}" > /etc/apt/sources.list.d/mysql.list -RUN apt-get update && apt-get install -y gdb ca-certificates mysql-community-client && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --allow-unauthenticated --no-install-recommends mysql-community-client \ + && apt-get install -y --no-install-recommends gdb ca-certificates jq iproute2 less bash-completion unzip sysstat acl net-tools iputils-ping telnet dnsutils wget vim git \ + && rm -rf /var/lib/apt/lists/* ENV TZ Asia/Shanghai WORKDIR /opt/koko/ COPY --from=stage-build /opt/koko/release/koko /opt/koko +COPY --from=stage-build /opt/koko/release/koko/kubectl /usr/local/bin/kubectl +COPY --from=stage-build /opt/koko/rawkubectl /usr/local/bin/rawkubectl COPY --from=stage-build /usr/local/go/src/runtime/sys_linux_amd64.s /usr/local/go/src/runtime/sys_linux_amd64.s -COPY --from=stage-build /opt/koko/tools/coredump.sh . +COPY --from=stage-build /opt/koko/utils/coredump.sh . COPY --from=stage-build /opt/koko/entrypoint.sh . +COPY --from=stage-build /opt/koko/utils/init-kubectl.sh . +COPY --from=stage-build /opt/koko/.kubectl_aliases /opt/kubectl-aliases/.kubectl_aliases -RUN chmod 755 entrypoint.sh +RUN chmod 755 entrypoint.sh && chmod 755 init-kubectl.sh EXPOSE 2222 5000 CMD ["./entrypoint.sh"] diff --git a/cmd/kubectl.go b/cmd/kubectl.go new file mode 100644 index 000000000..b43ff14b9 --- /dev/null +++ b/cmd/kubectl.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/jumpserver/koko/pkg/config" + "github.com/jumpserver/koko/pkg/utils" +) + +const ( + commandName = "rawkubectl" + envName = "K8S_ENCRYPTED_TOKEN" +) + +func main() { + encryptToken := os.Getenv(envName) + var token string + if encryptToken != "" { + token, _ = utils.Decrypt(encryptToken, config.CipherKey) + } + + args := os.Args[1:] + var s strings.Builder + for i := range args { + s.WriteString(args[i]) + s.WriteString(" ") + } + commandPrefix := commandName + if token != "" { + commandPrefix = fmt.Sprintf("%s --token=%s", commandName, token) + } + + commandString := fmt.Sprintf("%s %s", commandPrefix, s.String()) + c := exec.Command("bash", "-c", commandString) + c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr + _ = c.Run() +} diff --git a/cmd/locale/en_US/LC_MESSAGES/koko.po b/cmd/locale/en_US/LC_MESSAGES/koko.po index fd88da4bc..5c8d8d9b4 100644 --- a/cmd/locale/en_US/LC_MESSAGES/koko.po +++ b/cmd/locale/en_US/LC_MESSAGES/koko.po @@ -8,241 +8,324 @@ msgstr "" "X-Generator: xgotext\n" #. i18n.T -#: pkg/handler/banner.go:31 -msgid "\t%d) Enter {{.GreenBoldColor}}%s{{.ColorEnd}} to %s.%s" +#: pkg/handler/app_database.go:102 +msgid "Search: %s" msgstr "" #. i18n.T -#: pkg/handler/banner.go:46 -msgid "Welcome to use Jumpserver open source fortress system" +#: pkg/handler/app_database.go:104 +msgid "No Databases" msgstr "" #. i18n.T -#: pkg/handler/banner.go:48 -msgid "part IP, Hostname, Comment" +#: pkg/handler/app_database.go:115 +msgid "ID" msgstr "" #. i18n.T -#: pkg/handler/banner.go:48 -msgid "to search login if unique" +#: pkg/handler/app_database.go:116 +msgid "Name" msgstr "" #. i18n.T -#: pkg/handler/banner.go:49 -msgid "/ + IP, Hostname, Comment" +#: pkg/handler/app_database.go:117 +msgid "IP" msgstr "" #. i18n.T -#: pkg/handler/banner.go:49 -msgid "to search, such as: /192.168" +#: pkg/handler/app_database.go:118 +msgid "DBType" msgstr "" #. i18n.T -#: pkg/handler/banner.go:50 -msgid "display the host you have permission" +#: pkg/handler/app_database.go:119 +msgid "DB Name" msgstr "" #. i18n.T -#: pkg/handler/banner.go:51 -msgid "display the node that you have permission" +#: pkg/handler/app_database.go:120 +msgid "Comment" msgstr "" #. i18n.T -#: pkg/handler/banner.go:52 -msgid "display the databases that you have permission" +#: pkg/handler/app_database.go:146 +msgid "Page: %d, Count: %d, Total Page: %d, Total Count: %d" msgstr "" #. i18n.T -#: pkg/handler/banner.go:53 -msgid "refresh your assets and nodes" +#: pkg/handler/app_database.go:167 +msgid "" +"Enter ID number directly login the database, multiple search use // + field, " +"such as: //16" msgstr "" #. i18n.T -#: pkg/handler/banner.go:54 -msgid "print help" +#: pkg/handler/app_database.go:168 +msgid "Page up: b\tPage down: n" msgstr "" #. i18n.T -#: pkg/handler/banner.go:55 -msgid "exit" -msgstr "" - #. i18n.T -#: pkg/handler/banner.go:90 -msgid "ID" +#: pkg/handler/app_k8s.go:101 pkg/handler/app_k8s.go:103 +msgid "No kubernetes" msgstr "" #. i18n.T -#: pkg/handler/banner.go:91 -msgid "hostname" +#. i18n.T +#. i18n.T +#: pkg/handler/app_k8s.go:114 pkg/handler/app_k8s.go:115 +#: pkg/handler/app_k8s.go:116 +msgid "Cluster" msgstr "" #. i18n.T -#: pkg/handler/banner.go:92 -msgid "IP" +#. i18n.T +#. i18n.T +#: pkg/handler/app_k8s.go:117 pkg/handler/app_k8s.go:140 +#: pkg/handler/app_k8s.go:160 +msgid "" +"Enter ID number directly login the kubernetes, multiple search use // + " +"field, such as: //16" msgstr "" #. i18n.T -#: pkg/handler/banner.go:93 -msgid "comment" +#. i18n.T +#. i18n.T +#: pkg/handler/app_k8s.go:161 pkg/handler/asset.go:110 pkg/handler/asset.go:113 +msgid "No Assets" msgstr "" #. i18n.T -#: pkg/handler/banner.go:94 -msgid "Page: %d, Count: %d, Total Page: %d, Total Count: %d" +#: pkg/handler/asset.go:116 +msgid "%s node has no assets" msgstr "" #. i18n.T -#: pkg/handler/banner.go:95 -msgid "No Assets" +#. i18n.T +#: pkg/handler/asset.go:130 pkg/handler/asset.go:131 +msgid "Hostname" msgstr "" #. i18n.T -#: pkg/handler/banner.go:96 +#. i18n.T +#. i18n.T +#. i18n.T +#: pkg/handler/asset.go:132 pkg/handler/asset.go:133 pkg/handler/asset.go:155 +#: pkg/handler/asset.go:174 msgid "" "Enter ID number directly login the asset, multiple search use // + field, " "such as: //16" msgstr "" #. i18n.T -#: pkg/handler/banner.go:97 -msgid "Page up: b\tPage down: n" +#. i18n.T +#: pkg/handler/asset.go:175 pkg/handler/banner.go:30 +msgid "\t%d) Enter {{.GreenBoldColor}}%s{{.ColorEnd}} to %s.%s" msgstr "" #. i18n.T -#: pkg/handler/banner.go:98 -msgid "Node: [ ID.Name(Asset amount) ]" +#: pkg/handler/banner.go:45 +msgid "Welcome to use JumpServer open source fortress system" msgstr "" #. i18n.T -#: pkg/handler/banner.go:99 -msgid "Tips: Enter g+NodeID to display the host under the node, such as g1" +#: pkg/handler/banner.go:47 +msgid "part IP, Hostname, Comment" msgstr "" #. i18n.T -#: pkg/handler/banner.go:100 -msgid "Refresh done" +#: pkg/handler/banner.go:47 +msgid "to search login if unique" msgstr "" #. i18n.T -#: pkg/handler/banner.go:101 -msgid "Tips: Enter system user ID and directly login the asset [ %s(%s) ]" +#: pkg/handler/banner.go:48 +msgid "/ + IP, Hostname, Comment" msgstr "" #. i18n.T -#: pkg/handler/banner.go:102 -msgid "Back: B/b" +#: pkg/handler/banner.go:48 +msgid "to search, such as: /192.168" msgstr "" #. i18n.T -#: pkg/handler/banner.go:103 -msgid "Name" +#: pkg/handler/banner.go:49 +msgid "display the host you have permission" +msgstr "" + +#. i18n.T +#: pkg/handler/banner.go:50 +msgid "display the node that you have permission" +msgstr "" + +#. i18n.T +#: pkg/handler/banner.go:51 +msgid "display the databases that you have permission" msgstr "" #. i18n.T -#: pkg/handler/banner.go:104 +#: pkg/handler/banner.go:52 +msgid "display the kubernetes that you have permission" +msgstr "" + +#. i18n.T +#: pkg/handler/banner.go:53 +msgid "refresh your assets and nodes" +msgstr "" + +#. i18n.T +#: pkg/handler/banner.go:54 +msgid "print help" +msgstr "" + +#. i18n.T +#: pkg/handler/banner.go:55 +msgid "exit" +msgstr "" + +#. i18n.T +#: pkg/handler/dispatch.go:119 +msgid "Node: [ ID.Name(Asset amount) ]" +msgstr "" + +#. i18n.T +#: pkg/handler/dispatch.go:121 +msgid "Tips: Enter g+NodeID to display the host under the node, such as g1" +msgstr "" + +#. i18n.T +#: pkg/handler/session.go:153 +msgid "No system user found." +msgstr "" + +#. i18n.T +#. i18n.T +#. i18n.T +#: pkg/handler/session.go:165 pkg/handler/session.go:166 +#: pkg/handler/session.go:167 msgid "Username" msgstr "" #. i18n.T -#: pkg/handler/banner.go:105 -msgid "all" +#: pkg/handler/session.go:196 +msgid "Tips: Enter system user ID and directly login" msgstr "" #. i18n.T -#: pkg/handler/banner.go:106 -msgid "Search: %s" +#: pkg/handler/session.go:197 +msgid "Back: B/b" msgstr "" #. i18n.T -#: pkg/handler/banner.go:107 -msgid "DBType" +#: pkg/handler/session.go:229 +msgid "Refresh done" msgstr "" #. i18n.T -#: pkg/handler/banner.go:108 -msgid "DB Name" +#: pkg/proxy/commonswitch.go:179 +msgid "Connect idle more than %d minutes, disconnect" msgstr "" #. i18n.T -#: pkg/handler/banner.go:109 -msgid "No Databases" +#: pkg/proxy/commonswitch.go:188 +msgid "Terminated by administrator" msgstr "" #. i18n.T -#: pkg/handler/banner.go:110 -msgid "" -"Enter ID number directly login the database, multiple search use // + field, " -"such as: //16" +#: pkg/proxy/dbparser.go:119 +msgid "Command `%s` is forbidden" msgstr "" #. i18n.T -#: pkg/proxy/dbproxy.go:112 -msgid "Database connecting to %s %.1f" +#: pkg/proxy/dbproxy.go:117 +msgid "Connecting to Database %s %.1f" msgstr "" #. i18n.T -#: pkg/proxy/dbproxy.go:131 +#: pkg/proxy/dbproxy.go:136 msgid "System user <%s> and database <%s> protocol are inconsistent." msgstr "" #. i18n.T -#: pkg/proxy/dbproxy.go:137 +#: pkg/proxy/dbproxy.go:144 msgid "Database %s protocol client not installed." msgstr "" #. i18n.T -#: pkg/proxy/dbswitch.go:148 +#: pkg/proxy/dbproxy.go:203 +msgid "Create database session failed" +msgstr "" + +#. i18n.T +#. i18n.T +#: pkg/proxy/dbproxy.go:241 pkg/proxy/dbswitch.go:183 msgid "Database connect idle more than %d minutes, disconnect" msgstr "" #. i18n.T -#: pkg/proxy/dbswitch.go:155 +#: pkg/proxy/dbswitch.go:192 msgid "Database connection terminated by administrator" msgstr "" #. i18n.T -#: pkg/proxy/parser.go:140 -msgid "Command `%s` is forbidden" +#: pkg/proxy/k8sproxy.go:69 +msgid "Connecting to Kubernetes %s %.1f" msgstr "" #. i18n.T -#: pkg/proxy/proxy.go:146 -msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]" +#: pkg/proxy/k8sproxy.go:88 +msgid "System user <%s> and kubernetes <%s> protocol are inconsistent." msgstr "" #. i18n.T -#: pkg/proxy/proxy.go:164 -msgid "Connecting to %s@%s %.1f" +#: pkg/proxy/k8sproxy.go:96 +msgid "%s protocol client not installed." msgstr "" #. i18n.T -#: pkg/proxy/proxy.go:183 -msgid "System user <%s> and asset <%s> protocol are inconsistent." +#: pkg/proxy/k8sproxy.go:104 +msgid "You don't have permission login %s" msgstr "" #. i18n.T -#: pkg/proxy/proxy.go:189 -msgid "" -"Terminal only support protocol ssh/telnet, please use web terminal to access" +#: pkg/proxy/k8sproxy.go:112 +msgid "You get auth token failed" msgstr "" #. i18n.T -#: pkg/proxy/sessmanager.go:75 -msgid "Connect with api server failed" +#: pkg/proxy/k8sproxy.go:157 +msgid "Create k8s session failed" msgstr "" #. i18n.T -#: pkg/proxy/sessmanager.go:117 -msgid "Create database session failed" +#. i18n.T +#. i18n.T +#: pkg/proxy/k8sproxy.go:195 pkg/proxy/parser.go:150 pkg/proxy/proxy.go:184 +msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]" msgstr "" #. i18n.T -#: pkg/proxy/switch.go:173 -msgid "Connect idle more than %d minutes, disconnect" +#: pkg/proxy/proxy.go:238 +msgid "Connecting to %s@%s %.1f" msgstr "" #. i18n.T -#: pkg/proxy/switch.go:180 -msgid "Terminated by administrator" +#: pkg/proxy/proxy.go:257 +msgid "System user <%s> and asset <%s> protocol are inconsistent." +msgstr "" + +#. i18n.T +#: pkg/proxy/proxy.go:265 +msgid "" +"Terminal only support protocol ssh/telnet, please use web terminal to access" +msgstr "" + +#. i18n.T +#: pkg/proxy/proxy.go:355 +msgid "Connect with api server failed" +msgstr "" + +#. i18n.T +#: pkg/proxy/proxy.go:404 +msgid "Create session failed" msgstr "" diff --git a/cmd/locale/zh_CN/LC_MESSAGES/koko.mo b/cmd/locale/zh_CN/LC_MESSAGES/koko.mo index a92810312..b008a8fac 100644 Binary files a/cmd/locale/zh_CN/LC_MESSAGES/koko.mo and b/cmd/locale/zh_CN/LC_MESSAGES/koko.mo differ diff --git a/cmd/locale/zh_CN/LC_MESSAGES/koko.po b/cmd/locale/zh_CN/LC_MESSAGES/koko.po index 7666c782e..3e2248143 100644 --- a/cmd/locale/zh_CN/LC_MESSAGES/koko.po +++ b/cmd/locale/zh_CN/LC_MESSAGES/koko.po @@ -8,53 +8,179 @@ msgstr "" "X-Generator: xgotext\n" #. i18n.T -#: pkg/handler/banner.go:31 +#: pkg/handler/app_database.go:102 +#, fuzzy +msgid "Search: %s" +msgstr "搜索: %s" + +#. i18n.T +#: pkg/handler/app_database.go:104 +msgid "No Databases" +msgstr "无数据库" + +#. i18n.T +#: pkg/handler/app_database.go:115 +msgid "ID" +msgstr "ID" + +#. i18n.T +#: pkg/handler/app_database.go:116 +msgid "Name" +msgstr "名称" + +#. i18n.T +#: pkg/handler/app_database.go:117 +msgid "IP" +msgstr "IP" + +#. i18n.T +#: pkg/handler/app_database.go:118 +msgid "DBType" +msgstr "数据库类型" + +#. i18n.T +#: pkg/handler/app_database.go:119 +#, fuzzy +msgid "DB Name" +msgstr "数据库名称" + +#. i18n.T +#: pkg/handler/app_database.go:120 +#, fuzzy +msgid "Comment" +msgstr "备注" + +#. i18n.T +#: pkg/handler/app_database.go:146 +msgid "Page: %d, Count: %d, Total Page: %d, Total Count: %d" +msgstr "页码:%d,每页行数:%d,总页数:%d,总数量:%d" + +#. i18n.T +#: pkg/handler/app_database.go:167 +#, fuzzy +msgid "" +"Enter ID number directly login the database, multiple search use // + field, " +"such as: //16" +msgstr "提示:输入数据库ID直接登录,二级搜索使用 // + 字段,如://192" + +#. i18n.T +#: pkg/handler/app_database.go:168 +#, fuzzy +msgid "Page up: b\tPage down: n" +msgstr "上一页:b 下一页:n" + +#. i18n.T +#. i18n.T +#: pkg/handler/app_k8s.go:101 pkg/handler/app_k8s.go:103 +msgid "No kubernetes" +msgstr "没有kubernetes" + +#. i18n.T +#. i18n.T +#. i18n.T +#: pkg/handler/app_k8s.go:114 pkg/handler/app_k8s.go:115 +#: pkg/handler/app_k8s.go:116 +msgid "Cluster" +msgstr "集群" + +#. i18n.T +#. i18n.T +#. i18n.T +#: pkg/handler/app_k8s.go:117 pkg/handler/app_k8s.go:140 +#: pkg/handler/app_k8s.go:160 +#, fuzzy +msgid "" +"Enter ID number directly login the kubernetes, multiple search use // + " +"field, such as: //16" +msgstr "提示:输入Kubernetes的ID直接登录,二级搜索使用 // + 字段,如://192" + +#. i18n.T +#. i18n.T +#. i18n.T +#: pkg/handler/app_k8s.go:161 pkg/handler/asset.go:110 pkg/handler/asset.go:113 +msgid "No Assets" +msgstr "没有资产" + +#. i18n.T +#: pkg/handler/asset.go:116 +msgid "%s node has no assets" +msgstr "%s节点没有资产" + +#. i18n.T +#. i18n.T +#: pkg/handler/asset.go:130 pkg/handler/asset.go:131 +#, fuzzy +msgid "Hostname" +msgstr "主机名" + +#. i18n.T +#. i18n.T +#. i18n.T +#. i18n.T +#: pkg/handler/asset.go:132 pkg/handler/asset.go:133 pkg/handler/asset.go:155 +#: pkg/handler/asset.go:174 +#, fuzzy +msgid "" +"Enter ID number directly login the asset, multiple search use // + field, " +"such as: //16" +msgstr "提示:输入资产ID直接登录,二级搜索使用 // + 字段,如://192" + +#. i18n.T +#. i18n.T +#: pkg/handler/asset.go:175 pkg/handler/banner.go:30 msgid "\t%d) Enter {{.GreenBoldColor}}%s{{.ColorEnd}} to %s.%s" msgstr "\t%d) 输入 {{.GreenBoldColor}}%s{{.ColorEnd}} 进行%s.%s" #. i18n.T -#: pkg/handler/banner.go:46 -msgid "Welcome to use Jumpserver open source fortress system" -msgstr "欢迎使用Jumpserver开源堡垒机系统" +#: pkg/handler/banner.go:45 +#, fuzzy +msgid "Welcome to use JumpServer open source fortress system" +msgstr "欢迎使用JumpServer开源堡垒机系统" #. i18n.T -#: pkg/handler/banner.go:48 +#: pkg/handler/banner.go:47 msgid "part IP, Hostname, Comment" msgstr "部分IP、主机名、备注" #. i18n.T -#: pkg/handler/banner.go:48 +#: pkg/handler/banner.go:47 #, fuzzy msgid "to search login if unique" msgstr "搜索登录(如果唯一)" #. i18n.T -#: pkg/handler/banner.go:49 +#: pkg/handler/banner.go:48 msgid "/ + IP, Hostname, Comment" msgstr "/ + IP,主机名 or 备注" #. i18n.T -#: pkg/handler/banner.go:49 +#: pkg/handler/banner.go:48 #, fuzzy msgid "to search, such as: /192.168" msgstr "搜索,如:/192.168" #. i18n.T -#: pkg/handler/banner.go:50 +#: pkg/handler/banner.go:49 msgid "display the host you have permission" msgstr "显示您有权限的主机" #. i18n.T -#: pkg/handler/banner.go:51 +#: pkg/handler/banner.go:50 msgid "display the node that you have permission" msgstr "显示您有权限的节点" #. i18n.T -#: pkg/handler/banner.go:52 +#: pkg/handler/banner.go:51 #, fuzzy msgid "display the databases that you have permission" msgstr "显示您有权限的数据库" +#. i18n.T +#: pkg/handler/banner.go:52 +#, fuzzy +msgid "display the kubernetes that you have permission" +msgstr "显示您有权限的Kubernetes" + #. i18n.T #: pkg/handler/banner.go:53 msgid "refresh your assets and nodes" @@ -71,194 +197,161 @@ msgid "exit" msgstr "退出" #. i18n.T -#: pkg/handler/banner.go:90 -msgid "ID" -msgstr "ID" - -#. i18n.T -#: pkg/handler/banner.go:91 -msgid "hostname" -msgstr "主机名" +#: pkg/handler/dispatch.go:119 +msgid "Node: [ ID.Name(Asset amount) ]" +msgstr "节点:[ ID.名称(资产数量) ]" #. i18n.T -#: pkg/handler/banner.go:92 -msgid "IP" -msgstr "IP" +#: pkg/handler/dispatch.go:121 +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:93 -msgid "comment" -msgstr "备注" +#: pkg/handler/session.go:153 +msgid "No system user found." +msgstr "没有系统用户" #. i18n.T -#: pkg/handler/banner.go:94 -msgid "Page: %d, Count: %d, Total Page: %d, Total Count: %d" -msgstr "页码:%d,每页行数:%d,总页数:%d,总数量:%d" - #. i18n.T -#: pkg/handler/banner.go:95 -msgid "No Assets" -msgstr "没有资产" - #. i18n.T -#: pkg/handler/banner.go:96 +#: pkg/handler/session.go:165 pkg/handler/session.go:166 +#: pkg/handler/session.go:167 #, fuzzy -msgid "" -"Enter ID number directly login the asset, multiple search use // + field, " -"such as: //16" -msgstr "提示:输入资产ID直接登录,二级搜索使用 // + 字段,如://192" +msgid "Username" +msgstr "用户名" #. i18n.T -#: pkg/handler/banner.go:97 +#: pkg/handler/session.go:196 #, fuzzy -msgid "Page up: b\tPage down: n" -msgstr "上一页:b 下一页:n" - -#. i18n.T -#: pkg/handler/banner.go:98 -msgid "Node: [ ID.Name(Asset amount) ]" -msgstr "节点:[ ID.名称(资产数量) ]" +msgid "Tips: Enter system user ID and directly login" +msgstr "" +"\n" +"提示:输入系统用户ID,登录资产[ %s(%s) ]\n" #. i18n.T -#: pkg/handler/banner.go:99 -msgid "Tips: Enter g+NodeID to display the host under the node, such as g1" -msgstr "提示:输入 g+节点ID 显示节点下主机,如: g1" +#: pkg/handler/session.go:197 +msgid "Back: B/b" +msgstr "返回:B/b" #. i18n.T -#: pkg/handler/banner.go:100 +#: pkg/handler/session.go:229 msgid "Refresh done" msgstr "刷新完成" #. i18n.T -#: pkg/handler/banner.go:101 -#, fuzzy -msgid "Tips: Enter system user ID and directly login the asset [ %s(%s) ]" -msgstr "" -"\n" -"提示:输入系统用户ID,登录资产[ %s(%s) ]\n" +#: pkg/proxy/commonswitch.go:179 +msgid "Connect idle more than %d minutes, disconnect" +msgstr "空闲时间超过%d分钟,断开连接" #. i18n.T -#: pkg/handler/banner.go:102 -msgid "Back: B/b" -msgstr "返回:B/b" +#: pkg/proxy/commonswitch.go:188 +msgid "Terminated by administrator" +msgstr "管理员中断连接" #. i18n.T -#: pkg/handler/banner.go:103 -msgid "Name" -msgstr "名称" +#: pkg/proxy/dbparser.go:119 +msgid "Command `%s` is forbidden" +msgstr "命令 `%s` 是被禁止的 ..." #. i18n.T -#: pkg/handler/banner.go:104 +#: pkg/proxy/dbproxy.go:117 #, fuzzy -msgid "Username" -msgstr "用户名" +msgid "Connecting to Database %s %.1f" +msgstr "开始连接数据库%s %.1f" #. i18n.T -#: pkg/handler/banner.go:105 -msgid "all" -msgstr "所有" +#: pkg/proxy/dbproxy.go:136 +#, fuzzy +msgid "System user <%s> and database <%s> protocol are inconsistent." +msgstr "系统用户<%s>和资产<%s>协议不一致" #. i18n.T -#: pkg/handler/banner.go:106 -#, fuzzy -msgid "Search: %s" -msgstr "搜索: %s" +#: pkg/proxy/dbproxy.go:144 +msgid "Database %s protocol client not installed." +msgstr "%s 协议的数据库客户端未安装" #. i18n.T -#: pkg/handler/banner.go:107 -msgid "DBType" -msgstr "数据库类型" +#: pkg/proxy/dbproxy.go:203 +msgid "Create database session failed" +msgstr "创建数据库会话失败" #. i18n.T -#: pkg/handler/banner.go:108 +#. i18n.T +#: pkg/proxy/dbproxy.go:241 pkg/proxy/dbswitch.go:183 #, fuzzy -msgid "DB Name" -msgstr "数据库名称" +msgid "Database connect idle more than %d minutes, disconnect" +msgstr "数据库连接空闲时间超过 %d 分钟,断开连接" #. i18n.T -#: pkg/handler/banner.go:109 -msgid "No Databases" -msgstr "无数据库" +#: pkg/proxy/dbswitch.go:192 +#, fuzzy +msgid "Database connection terminated by administrator" +msgstr "管理员中断数据库连接" #. i18n.T -#: pkg/handler/banner.go:110 +#: pkg/proxy/k8sproxy.go:69 #, fuzzy -msgid "" -"Enter ID number directly login the database, multiple search use // + field, " -"such as: //16" -msgstr "提示:输入数据库ID直接登录,二级搜索使用 // + 字段,如://192" +msgid "Connecting to Kubernetes %s %.1f" +msgstr "开始连接Kubernetes %s %.1f" #. i18n.T -#: pkg/proxy/dbproxy.go:112 +#: pkg/proxy/k8sproxy.go:88 #, fuzzy -msgid "Database connecting to %s %.1f" -msgstr "连接数据库 %s %.1f" +msgid "System user <%s> and kubernetes <%s> protocol are inconsistent." +msgstr "系统用户<%s>和kubernetes<%s>协议不一致" #. i18n.T -#: pkg/proxy/dbproxy.go:131 +#: pkg/proxy/k8sproxy.go:96 #, fuzzy -msgid "System user <%s> and database <%s> protocol are inconsistent." -msgstr "系统用户<%s>和资产<%s>协议不一致" +msgid "%s protocol client not installed." +msgstr "%s 协议的数据库客户端未安装" #. i18n.T -#: pkg/proxy/dbproxy.go:137 -msgid "Database %s protocol client not installed." -msgstr "%s 协议的数据库客户端未安装" +#: pkg/proxy/k8sproxy.go:104 +msgid "You don't have permission login %s" +msgstr "你无权限登陆%s" #. i18n.T -#: pkg/proxy/dbswitch.go:148 -#, fuzzy -msgid "Database connect idle more than %d minutes, disconnect" -msgstr "数据库连接空闲时间超过 %d 分钟,断开连接" +#: pkg/proxy/k8sproxy.go:112 +msgid "You get auth token failed" +msgstr "你获取认证令牌失败" #. i18n.T -#: pkg/proxy/dbswitch.go:155 +#: pkg/proxy/k8sproxy.go:157 #, fuzzy -msgid "Database connection terminated by administrator" -msgstr "管理员中断数据库连接" +msgid "Create k8s session failed" +msgstr "创建Kubernetes会话失败" #. i18n.T -#: pkg/proxy/parser.go:140 -msgid "Command `%s` is forbidden" -msgstr "命令 `%s` 是被禁止的 ..." - #. i18n.T -#: pkg/proxy/proxy.go:146 +#. i18n.T +#: pkg/proxy/k8sproxy.go:195 pkg/proxy/parser.go:150 pkg/proxy/proxy.go:184 msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]" msgstr "复用SSH连接(%s@%s)[连接数量: %d]" #. i18n.T -#: pkg/proxy/proxy.go:164 +#: pkg/proxy/proxy.go:238 msgid "Connecting to %s@%s %.1f" msgstr "开始连接到 %s@%s %.1f" #. i18n.T -#: pkg/proxy/proxy.go:183 +#: pkg/proxy/proxy.go:257 msgid "System user <%s> and asset <%s> protocol are inconsistent." msgstr "系统用户<%s>和资产<%s>协议不一致" #. i18n.T -#: pkg/proxy/proxy.go:189 +#: pkg/proxy/proxy.go:265 msgid "" "Terminal only support protocol ssh/telnet, please use web terminal to access" msgstr "终端仅支持ssh/telnet协议,请使用web终端登录" #. i18n.T -#: pkg/proxy/sessmanager.go:75 +#: pkg/proxy/proxy.go:355 msgid "Connect with api server failed" msgstr "连接API服务失败" #. i18n.T -#: 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:180 -msgid "Terminated by administrator" -msgstr "管理员中断连接" +#: pkg/proxy/proxy.go:404 +#, fuzzy +msgid "Create session failed" +msgstr "创建会话失败" diff --git a/pkg/config/config.go b/pkg/config/config.go index 126217625..9d199b1e1 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -12,6 +12,7 @@ import ( "gopkg.in/yaml.v2" ) +var CipherKey = "JumpServer Cipher Key for KoKo !" type Config struct { AssetListPageSize string `json:"TERMINAL_ASSET_LIST_PAGE_SIZE"` @@ -41,8 +42,7 @@ type Config struct { LogLevel string `yaml:"LOG_LEVEL"` RootPath string `yaml:"ROOT_PATH"` Comment string `yaml:"COMMENT"` - Language string `yaml:"LANG"` - LanguageCode string `yaml:"LANGUAGE_CODE"` // Abandon + LanguageCode string `yaml:"LANGUAGE_CODE"` UploadFailedReplay bool `yaml:"UPLOAD_FAILED_REPLAY_ON_START"` AssetLoadPolicy string `yaml:"ASSET_LOAD_POLICY"` // all ZipMaxSize string `yaml:"ZIP_MAX_SIZE"` @@ -59,12 +59,8 @@ type Config struct { } func (c *Config) EnsureConfigValid() { - // 兼容原来config - if c.LanguageCode != "" && c.Language == "" { - c.Language = c.LanguageCode - } - if c.Language == "" { - c.Language = "zh" + if c.LanguageCode == "" { + c.LanguageCode = "zh" } // 确保至少有一个认证 if !c.PublicKeyAuth && !c.PasswordAuth { diff --git a/pkg/config/init.go b/pkg/config/init.go index c3872de69..cc7b6bbb6 100644 --- a/pkg/config/init.go +++ b/pkg/config/init.go @@ -9,3 +9,5 @@ func Initial(confPath string) { log.Printf("Config file Path: %s\n", confPath) Conf.EnsureConfigValid() } + +var KubectlBanner = "Welcome to JumpServer kubectl, try kubectl --help." \ No newline at end of file diff --git a/pkg/handler/app.go b/pkg/handler/app.go new file mode 100644 index 000000000..b7b3f1b26 --- /dev/null +++ b/pkg/handler/app.go @@ -0,0 +1,100 @@ +package handler + +import "strings" + +const PAGESIZEALL = 0 + +type Application interface { + Name() string + + MoveNextPage() + + MovePrePage() + + Search(key string) + + SearchAgain(key string) + + SearchOrProxy(key string) + +} + +type baseEngine interface { + HasPrev() bool + HasNext() bool + TotalCount() int + TotalPage() int + PageSize() int + CurrentPage() int + CurrentOffSet() int +} + +type pageInfo struct { + pageSize int + totalCount int + + currentOffset int + + totalPage int + currentPage int +} + +func (p *pageInfo) updatePageInfo(pageSiz, totalCount, offset int) { + p.pageSize = pageSiz + p.totalCount = totalCount + p.currentOffset = offset + p.update() +} +func (p *pageInfo) update() { + // 根据 pageSize和total值 更新 totalPage currentPage + if p.pageSize <= 0 { + p.totalPage = 1 + p.currentPage = 1 + return + } + pageSize := p.pageSize + totalCount := p.totalCount + + switch totalCount % pageSize { + case 0: + p.totalPage = totalCount / pageSize + default: + p.totalPage = (totalCount / pageSize) + 1 + } + switch p.currentOffset % pageSize { + case 0: + p.currentPage = p.currentOffset / pageSize + default: + p.currentPage = (p.currentOffset / pageSize) + 1 + } +} + +func (p *pageInfo) TotalCount() int { + return p.totalCount +} + +func (p *pageInfo) TotalPage() int { + return p.totalPage +} +func (p *pageInfo) PageSize() int { + return p.pageSize +} +func (p *pageInfo) CurrentPage() int { + return p.currentPage +} + +func (p *pageInfo) CurrentOffSet() int { + return p.currentOffset +} + +func IsEqualStringSlice(a []string, b []string) bool { + if len(a) != len(b) { + return false + } + for i := 0; i < len(a); i++ { + if !strings.EqualFold(a[i], b[i]) { + return false + } + } + return true +} diff --git a/pkg/handler/app_database.go b/pkg/handler/app_database.go new file mode 100644 index 000000000..d17fcfbc7 --- /dev/null +++ b/pkg/handler/app_database.go @@ -0,0 +1,324 @@ +package handler + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/jumpserver/koko/pkg/common" + "github.com/jumpserver/koko/pkg/i18n" + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/model" + "github.com/jumpserver/koko/pkg/proxy" + "github.com/jumpserver/koko/pkg/service" + "github.com/jumpserver/koko/pkg/utils" +) + +type DatabaseEngine interface { + baseEngine + Retrieve(pageSize, offset int, searches ...string) []model.Database +} + +var ( + _ Application = (*DatabaseApplication)(nil) + _ DatabaseEngine = (*remoteDatabaseEngine)(nil) + _ DatabaseEngine = (*localDatabaseEngine)(nil) +) + +type DatabaseApplication struct { + h *interactiveHandler + + engine DatabaseEngine + + searchKeys []string + + currentResult []model.Database +} + +func (k *DatabaseApplication) Name() string { + return "database" +} + +func (k *DatabaseApplication) MoveNextPage() { + if k.engine.HasNext() { + offset := k.engine.CurrentOffSet() + newPageSize := getPageSize(k.h.term) + k.currentResult = k.engine.Retrieve(newPageSize, offset, k.searchKeys...) + } + k.DisplayCurrentResult() +} + +func (k *DatabaseApplication) MovePrePage() { + if k.engine.HasPrev() { + offset := k.engine.CurrentOffSet() + newPageSize := getPageSize(k.h.term) + start := offset - newPageSize + if start <= 0 { + start = 0 + } + k.currentResult = k.engine.Retrieve(newPageSize, start, k.searchKeys...) + } + k.DisplayCurrentResult() +} + +func (k *DatabaseApplication) Search(key string) { + newPageSize := getPageSize(k.h.term) + k.searchKeys = []string{key} + k.currentResult = k.engine.Retrieve(newPageSize, 0, key) + k.DisplayCurrentResult() + +} + +func (k *DatabaseApplication) SearchAgain(key string) { + k.searchKeys = append(k.searchKeys, key) + newPageSize := getPageSize(k.h.term) + k.currentResult = k.engine.Retrieve(newPageSize, 0, k.searchKeys...) + k.DisplayCurrentResult() +} + +func (k *DatabaseApplication) SearchOrProxy(key string) { + if indexNum, err := strconv.Atoi(key); err == nil && len(k.currentResult) > 0 { + if indexNum > 0 && indexNum <= len(k.currentResult) { + k.ProxyDB(k.currentResult[indexNum-1]) + return + } + } + + newPageSize := getPageSize(k.h.term) + currentResult := k.engine.Retrieve(newPageSize, 0, key) + if len(currentResult) == 1 { + k.ProxyDB(currentResult[0]) + return + } + k.currentResult = currentResult + k.searchKeys = []string{key} + k.DisplayCurrentResult() +} + +func (k *DatabaseApplication) DisplayCurrentResult() { + currentDBS := k.currentResult + term := k.h.term + searchHeader := fmt.Sprintf(i18n.T("Search: %s"), strings.Join(k.searchKeys, " ")) + if len(currentDBS) == 0 { + _, _ = term.Write([]byte(i18n.T("No Databases") + "\n\r")) + utils.IgnoreErrWriteString(term, utils.WrapperString(searchHeader, utils.Green)) + utils.IgnoreErrWriteString(term, utils.CharNewLine) + return + } + + currentPage := k.engine.CurrentPage() + pageSize := k.engine.PageSize() + totalPage := k.engine.TotalPage() + totalCount := k.engine.TotalCount() + + idLabel := i18n.T("ID") + nameLabel := i18n.T("Name") + ipLabel := i18n.T("IP") + dbTypeLabel := i18n.T("DBType") + dbNameLabel := i18n.T("DB Name") + commentLabel := i18n.T("Comment") + + Labels := []string{idLabel, nameLabel, ipLabel, + dbTypeLabel, dbNameLabel, commentLabel} + fields := []string{"ID", "name", "IP", "DBType", "DBName", "comment"} + data := make([]map[string]string, len(currentDBS)) + for i, j := range currentDBS { + row := make(map[string]string) + row["ID"] = strconv.Itoa(i + 1) + row["name"] = j.Name + row["IP"] = j.Host + row["DBType"] = j.DBType + row["DBName"] = j.DBName + + 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, _ := term.GetSize() + + caption := fmt.Sprintf(i18n.T("Page: %d, Count: %d, Total Page: %d, Total Count: %d"), + 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}, + "name": {0, 8, 0}, + "IP": {0, 15, 40}, + "DBType": {0, 8, 0}, + "DBName": {0, 8, 0}, + "comment": {0, 0, 0}, + }, + Data: data, + TotalSize: w, + Caption: caption, + TruncPolicy: common.TruncMiddle, + } + table.Initial() + loginTip := i18n.T("Enter ID number directly login the database, multiple search use // + field, such as: //16") + pageActionTip := i18n.T("Page up: b Page down: n") + actionTip := fmt.Sprintf("%s %s", loginTip, pageActionTip) + + _, _ = term.Write([]byte(utils.CharClear)) + _, _ = term.Write([]byte(table.Display())) + utils.IgnoreErrWriteString(term, utils.WrapperString(actionTip, utils.Green)) + utils.IgnoreErrWriteString(term, utils.CharNewLine) + utils.IgnoreErrWriteString(term, utils.WrapperString(searchHeader, utils.Green)) + utils.IgnoreErrWriteString(term, utils.CharNewLine) +} + +func (k *DatabaseApplication) ProxyDB(dbSelect model.Database) { + systemUsers := service.GetUserDatabaseSystemUsers(k.h.user.ID, dbSelect.ID) + defer k.h.term.SetPrompt("[DB]> ") + systemUserSelect, ok := k.h.chooseSystemUser(systemUsers) + if !ok { + return + } + p := proxy.DBProxyServer{ + UserConn: k.h.sess, + User: k.h.user, + Database: &dbSelect, + SystemUser: &systemUserSelect, + } + k.h.pauseWatchWinSize() + p.Proxy() + k.h.resumeWatchWinSize() + logger.Infof("Request %s: database %s proxy end", k.h.sess.Uuid, dbSelect.Name) +} + +type localDatabaseEngine struct { + data []model.Database + *pageInfo + + cacheLastSearchResult []model.Database + cacheLastSearchKeys []string +} + +func (e *localDatabaseEngine) Retrieve(pageSize, offset int, searches ...string) (databases []model.Database) { + if pageSize <= 0 { + pageSize = PAGESIZEALL + } + if offset < 0 { + offset = 0 + } + + searchResult := e.searchResult(searches...) + var ( + totalDatabase []model.Database + total int + currentOffset int + currentPageSize int + ) + + if offset < len(searchResult) { + totalDatabase = searchResult[offset:] + } + total = len(totalDatabase) + currentPageSize = pageSize + databases = totalDatabase + + if currentPageSize < 0 || currentPageSize == PAGESIZEALL { + currentPageSize = len(totalDatabase) + } + if total > currentPageSize { + databases = totalDatabase[:currentPageSize] + } + currentOffset = offset + len(databases) + e.updatePageInfo(currentPageSize, total, currentOffset) + return +} + +func (e *localDatabaseEngine) searchResult(searches ...string) []model.Database { + sort.Strings(searches) + if len(searches) == 0 { + return e.data + } + if len(searches) == 1 && searches[0] == "" { + return e.data + } + + if IsEqualStringSlice(e.cacheLastSearchKeys, searches) && + e.cacheLastSearchResult != nil { + return e.cacheLastSearchResult + } + e.cacheLastSearchKeys = searches + e.cacheLastSearchResult = searchMatchedDatabases(e.data, searches...) + return e.cacheLastSearchResult +} + +func (e *localDatabaseEngine) HasPrev() bool { + return e.currentPage > 1 +} + +func (e *localDatabaseEngine) HasNext() bool { + return e.currentPage < e.totalPage +} + +type remoteDatabaseEngine struct { + user model.User + + nextUrl string + preUrl string + *pageInfo +} + +func (e *remoteDatabaseEngine) Retrieve(pageSize, offset int, searches ...string) (databases []model.Database) { + resp := service.GetUserPaginationDatabases(e.user.ID, pageSize, offset, searches...) + var ( + total int + currentOffset int + currentPageSize int + ) + e.nextUrl = resp.NextURL + e.preUrl = resp.PreviousURL + databases = resp.Data + total = resp.Total + currentPageSize = pageSize + if currentPageSize < 0 || currentPageSize == PAGESIZEALL { + currentPageSize = len(databases) + } + if len(databases) > currentPageSize { + databases = databases[:currentPageSize] + } + currentOffset = offset + len(databases) + e.updatePageInfo(currentPageSize, total, currentOffset) + return +} + +func (e *remoteDatabaseEngine) HasPrev() bool { + return e.preUrl != "" +} + +func (e *remoteDatabaseEngine) HasNext() bool { + return e.nextUrl != "" +} + +func searchMatchedDatabases(data []model.Database, keys ...string) []model.Database { + matched := make([]model.Database, 0, len(data)) + for i := range data { + db := data[i] + ok := true + contents := []string{strings.ToLower(db.Name), strings.ToLower(db.DBName), + strings.ToLower(db.Host), strings.ToLower(db.Comment)} + + for j := range keys { + if !isSubstring(contents, strings.ToLower(keys[j])) { + ok = false + break + } + } + if ok { + matched = append(matched, db) + } + } + return matched +} diff --git a/pkg/handler/app_k8s.go b/pkg/handler/app_k8s.go new file mode 100644 index 000000000..9b6b904ba --- /dev/null +++ b/pkg/handler/app_k8s.go @@ -0,0 +1,308 @@ +package handler + +import ( + "fmt" + "github.com/jumpserver/koko/pkg/common" + "github.com/jumpserver/koko/pkg/i18n" + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/proxy" + "github.com/jumpserver/koko/pkg/utils" + "strconv" + "strings" + + "github.com/jumpserver/koko/pkg/model" + "github.com/jumpserver/koko/pkg/service" +) + +type K8sEngine interface { + baseEngine + Retrieve(pageSize, offset int, searches ...string) []model.K8sCluster +} + +var ( + _ Application = (*K8sApplication)(nil) + _ K8sEngine = (*remoteK8sEngine)(nil) + _ K8sEngine = (*localK8sEngine)(nil) +) + +type K8sApplication struct { + h *interactiveHandler + + engine K8sEngine + + searchKeys []string + + currentResult []model.K8sCluster +} + +func (k *K8sApplication) Name() string { + return "k8s" +} + +func (k *K8sApplication) MoveNextPage() { + if k.engine.HasNext() { + offset := k.engine.CurrentOffSet() + newPageSize := getPageSize(k.h.term) + k.currentResult = k.engine.Retrieve(newPageSize, offset, k.searchKeys...) + } + k.DisplayCurrentResult() +} + +func (k *K8sApplication) MovePrePage() { + if k.engine.HasPrev() { + offset := k.engine.CurrentOffSet() + newPageSize := getPageSize(k.h.term) + start := offset - newPageSize + if start <= 0 { + start = 0 + } + k.currentResult = k.engine.Retrieve(newPageSize, start, k.searchKeys...) + } + k.DisplayCurrentResult() + +} + +func (k *K8sApplication) Search(key string) { + newPageSize := getPageSize(k.h.term) + k.currentResult = k.engine.Retrieve(newPageSize, 0, key) + k.searchKeys = []string{key} + k.DisplayCurrentResult() +} + +func (k *K8sApplication) SearchAgain(key string) { + k.searchKeys = append(k.searchKeys, key) + newPageSize := getPageSize(k.h.term) + k.currentResult = k.engine.Retrieve(newPageSize, 0, k.searchKeys...) + k.DisplayCurrentResult() +} + +func (k *K8sApplication) SearchOrProxy(key string) { + if indexNum, err := strconv.Atoi(key); err == nil && len(k.currentResult) > 0 { + if indexNum > 0 && indexNum <= len(k.currentResult) { + k.ProxyK8s(k.currentResult[indexNum-1]) + return + } + } + + newPageSize := getPageSize(k.h.term) + currentResult := k.engine.Retrieve(newPageSize, 0, key) + if len(currentResult) == 1 { + k.ProxyK8s(currentResult[0]) + return + } + k.currentResult = currentResult + k.searchKeys = []string{key} + k.DisplayCurrentResult() +} + +func (k *K8sApplication) DisplayCurrentResult() { + currentDBS := k.currentResult + term := k.h.term + searchHeader := fmt.Sprintf(i18n.T("Search: %s"), strings.Join(k.searchKeys, " ")) + if len(currentDBS) == 0 { + _, _ = term.Write([]byte(i18n.T("No kubernetes") + "\n\r")) + utils.IgnoreErrWriteString(term, utils.WrapperString(searchHeader, utils.Green)) + utils.IgnoreErrWriteString(term, utils.CharNewLine) + return + } + + currentPage := k.engine.CurrentPage() + pageSize := k.engine.PageSize() + totalPage := k.engine.TotalPage() + totalCount := k.engine.TotalCount() + + idLabel := i18n.T("ID") + nameLabel := i18n.T("Name") + clusterLabel := i18n.T("Cluster") + commentLabel := i18n.T("Comment") + + Labels := []string{idLabel, nameLabel, clusterLabel, commentLabel} + fields := []string{"ID", "name", "cluster", "comment"} + data := make([]map[string]string, len(currentDBS)) + for i, j := range currentDBS { + row := make(map[string]string) + row["ID"] = strconv.Itoa(i + 1) + row["name"] = j.Name + row["cluster"] = j.Cluster + + 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, _ := term.GetSize() + + caption := fmt.Sprintf(i18n.T("Page: %d, Count: %d, Total Page: %d, Total Count: %d"), + 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}, + "name": {0, 8, 0}, + "cluster": {0, 20, 0}, + "comment": {0, 0, 0}, + }, + Data: data, + TotalSize: w, + Caption: caption, + TruncPolicy: common.TruncMiddle, + } + table.Initial() + + loginTip := i18n.T("Enter ID number directly login the kubernetes, multiple search use // + field, such as: //16") + pageActionTip := i18n.T("Page up: b Page down: n") + actionTip := fmt.Sprintf("%s %s", loginTip, pageActionTip) + _, _ = term.Write([]byte(utils.CharClear)) + _, _ = term.Write([]byte(table.Display())) + utils.IgnoreErrWriteString(term, utils.WrapperString(actionTip, utils.Green)) + utils.IgnoreErrWriteString(term, utils.CharNewLine) + utils.IgnoreErrWriteString(term, utils.WrapperString(searchHeader, utils.Green)) + utils.IgnoreErrWriteString(term, utils.CharNewLine) +} + +func (k *K8sApplication) ProxyK8s(dbSelect model.K8sCluster) { + systemUsers := service.GetUserK8sSystemUsers(k.h.user.ID, dbSelect.ID) + defer k.h.term.SetPrompt("[k8s]> ") + systemUserSelect, ok := k.h.chooseSystemUser(systemUsers) + if !ok { + return + } + p := proxy.K8sProxyServer{ + UserConn: k.h.sess, + User: k.h.user, + Cluster: &dbSelect, + SystemUser: &systemUserSelect, + } + k.h.pauseWatchWinSize() + p.Proxy() + k.h.resumeWatchWinSize() + logger.Infof("Request %s: k8s %s proxy end", k.h.sess.Uuid, dbSelect.Name) +} + +type remoteK8sEngine struct { + user *model.User + + nextUrl string + preUrl string + + *pageInfo +} + +func (e *remoteK8sEngine) Retrieve(pageSize, offset int, searches ...string) (clusters []model.K8sCluster) { + resp := service.GetUserK8sClusters(e.user.ID, pageSize, offset, searches...) + var ( + total int + currentOffset int + currentPageSize int + ) + e.nextUrl = resp.NextURL + e.preUrl = resp.PreviousURL + clusters = resp.Data + total = resp.Total + currentPageSize = pageSize + + if currentPageSize < 0 || currentPageSize == PAGESIZEALL { + currentPageSize = len(clusters) + } + if len(clusters) > currentPageSize { + clusters = clusters[:currentPageSize] + } + currentOffset = offset + len(clusters) + e.updatePageInfo(currentPageSize, total, currentOffset) + return +} + +func (e *remoteK8sEngine) HasPrev() bool { + return e.preUrl != "" +} + +func (e *remoteK8sEngine) HasNext() bool { + return e.nextUrl != "" +} + +type localK8sEngine struct { + data []model.K8sCluster + *pageInfo + + cacheLastSearchResult []model.K8sCluster + cacheLastSearchKeys string +} + +func (e *localK8sEngine) Retrieve(pageSize, offset int, searches ...string) (clusters []model.K8sCluster) { + if pageSize <= 0 { + pageSize = PAGESIZEALL + } + if offset < 0 { + offset = 0 + } + + searchResult := e.searchResult(searches...) + var ( + totalClusters []model.K8sCluster + total int + currentOffset int + currentPageSize int + ) + + if offset < len(searchResult) { + totalClusters = searchResult[offset:] + } + total = len(totalClusters) + currentPageSize = pageSize + if currentPageSize == PAGESIZEALL { + currentPageSize = len(totalClusters) + } + if total > e.pageSize { + clusters = totalClusters[:e.pageSize] + } else { + clusters = totalClusters + } + currentOffset = offset + len(clusters) + e.updatePageInfo(currentPageSize, total, currentOffset) + return +} + +func (e *localK8sEngine) searchResult(searches ...string) []model.K8sCluster { + compareKey := strings.Join(searches, "") + if strings.EqualFold(e.cacheLastSearchKeys, compareKey) && + e.cacheLastSearchResult != nil { + return e.cacheLastSearchResult + } + e.cacheLastSearchKeys = compareKey + switch len(searches) { + case 0: + e.cacheLastSearchResult = e.data + default: + clusters := make([]model.K8sCluster, 0, len(e.data)) + for i := range e.data { + ok := true + for j := range searches { + if !strings.Contains(strings.ToLower(e.data[i].Cluster), + strings.ToLower(searches[j])) { + ok = false + } + } + if ok { + clusters = append(clusters, e.data[i]) + } + } + e.cacheLastSearchResult = clusters + } + return e.cacheLastSearchResult +} + +func (e *localK8sEngine) HasPrev() bool { + return e.currentPage > 1 +} + +func (e *localK8sEngine) HasNext() bool { + return e.currentPage < e.totalPage +} diff --git a/pkg/handler/asset.go b/pkg/handler/asset.go new file mode 100644 index 000000000..c06a16344 --- /dev/null +++ b/pkg/handler/asset.go @@ -0,0 +1,376 @@ +package handler + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "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/proxy" + "github.com/jumpserver/koko/pkg/service" + "github.com/jumpserver/koko/pkg/utils" +) + +type AssetEngine interface { + baseEngine + Retrieve(pageSize, offset int, searches ...string) []model.Asset +} + +var ( + _ Application = (*AssetApplication)(nil) + + _ AssetEngine = (*remoteAssetEngine)(nil) + + _ AssetEngine = (*localAssetEngine)(nil) + + _ AssetEngine = (*remoteNodeAssetEngine)(nil) +) + +type AssetApplication struct { + h *interactiveHandler + + engine AssetEngine + + searchKeys []string + + currentResult []model.Asset +} + +func (k *AssetApplication) Name() string { + return "Asset" +} + +func (k *AssetApplication) MoveNextPage() { + if k.engine.HasNext() { + offset := k.engine.CurrentOffSet() + newPageSize := getPageSize(k.h.term) + currentAssets := k.engine.Retrieve(newPageSize, offset, k.searchKeys...) + k.currentResult = model.AssetList(currentAssets).SortBy(config.GetConf().AssetListSortBy) + } + k.DisplayCurrentResult() +} + +func (k *AssetApplication) MovePrePage() { + if k.engine.HasPrev() { + offset := k.engine.CurrentOffSet() + newPageSize := getPageSize(k.h.term) + start := offset - newPageSize + if start <= 0 { + start = 0 + } + currentAssets := k.engine.Retrieve(newPageSize, start, k.searchKeys...) + k.currentResult = model.AssetList(currentAssets).SortBy(config.GetConf().AssetListSortBy) + } + k.DisplayCurrentResult() +} + +func (k *AssetApplication) Search(key string) { + newPageSize := getPageSize(k.h.term) + k.searchKeys = []string{key} + currentAssets := k.engine.Retrieve(newPageSize, 0, key) + k.currentResult = model.AssetList(currentAssets).SortBy(config.GetConf().AssetListSortBy) + k.DisplayCurrentResult() +} + +func (k *AssetApplication) SearchAgain(key string) { + k.searchKeys = append(k.searchKeys, key) + newPageSize := getPageSize(k.h.term) + currentAssets := k.engine.Retrieve(newPageSize, 0, k.searchKeys...) + k.currentResult = model.AssetList(currentAssets).SortBy(config.GetConf().AssetListSortBy) + k.DisplayCurrentResult() +} + +func (k *AssetApplication) SearchOrProxy(key string) { + if indexNum, err := strconv.Atoi(key); err == nil && len(k.currentResult) > 0 { + if indexNum > 0 && indexNum <= len(k.currentResult) { + k.proxyAsset(k.currentResult[indexNum-1]) + return + } + } + + newPageSize := getPageSize(k.h.term) + currentResult := k.engine.Retrieve(newPageSize, 0, key) + if len(currentResult) == 1 { + k.proxyAsset(currentResult[0]) + return + } + k.currentResult = currentResult + k.searchKeys = []string{key} + k.DisplayCurrentResult() +} + +func (k *AssetApplication) DisplayCurrentResult() { + currentAssets := k.currentResult + term := k.h.term + searchHeader := fmt.Sprintf(i18n.T("Search: %s"), strings.Join(k.searchKeys, " ")) + + if len(currentAssets) == 0 { + noAssets := i18n.T("No Assets") + switch v := k.engine.(type) { + case *remoteNodeAssetEngine: + noAssets = fmt.Sprintf(i18n.T("%s node has no assets"), v.node.Name) + } + utils.IgnoreErrWriteString(term, utils.WrapperString(noAssets, utils.Red)) + utils.IgnoreErrWriteString(term, utils.CharNewLine) + utils.IgnoreErrWriteString(term, utils.WrapperString(searchHeader, utils.Green)) + utils.IgnoreErrWriteString(term, utils.CharNewLine) + return + } + + currentPage := k.engine.CurrentPage() + pageSize := k.engine.PageSize() + totalPage := k.engine.TotalPage() + totalCount := k.engine.TotalCount() + + idLabel := i18n.T("ID") + hostLabel := i18n.T("Hostname") + ipLabel := i18n.T("IP") + commentLabel := i18n.T("Comment") + + Labels := []string{idLabel, hostLabel, ipLabel, commentLabel} + fields := []string{"ID", "hostname", "IP", "comment"} + data := make([]map[string]string, len(currentAssets)) + for i, j := range currentAssets { + 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, _ := term.GetSize() + caption := fmt.Sprintf(i18n.T("Page: %d, Count: %d, Total Page: %d, Total Count: %d"), + 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() + 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") + actionTip := fmt.Sprintf("%s %s", loginTip, pageActionTip) + + _, _ = term.Write([]byte(utils.CharClear)) + _, _ = term.Write([]byte(table.Display())) + utils.IgnoreErrWriteString(term, utils.WrapperString(actionTip, utils.Green)) + utils.IgnoreErrWriteString(term, utils.CharNewLine) + utils.IgnoreErrWriteString(term, utils.WrapperString(searchHeader, utils.Green)) + utils.IgnoreErrWriteString(term, utils.CharNewLine) +} + +func (k *AssetApplication) proxyAsset(assetSelect model.Asset) { + systemUsers := service.GetUserAssetSystemUsers(k.h.user.ID, assetSelect.ID) + defer k.h.term.SetPrompt("[Host]> ") + systemUserSelect, ok := k.h.chooseSystemUser(systemUsers) + if !ok { + return + } + k.proxy(assetSelect, systemUserSelect) +} + +func (k *AssetApplication) proxy(assetSelect model.Asset, systemUserSelect model.SystemUser) { + p := proxy.ProxyServer{ + UserConn: k.h.sess, + User: k.h.user, + Asset: &assetSelect, + SystemUser: &systemUserSelect, + } + k.h.pauseWatchWinSize() + p.Proxy() + k.h.resumeWatchWinSize() + logger.Infof("Request %s: asset %s proxy end", k.h.sess.Uuid, assetSelect.Hostname) +} + +type localAssetEngine struct { + data []model.Asset + *pageInfo + + cacheLastSearchResult []model.Asset + cacheLastSearchKeys []string +} + +func (e *localAssetEngine) Retrieve(pageSize, offset int, searches ...string) (Assets []model.Asset) { + if pageSize <= 0 { + pageSize = PAGESIZEALL + } + if offset < 0 { + offset = 0 + } + + searchResult := e.searchResult(searches...) + var ( + totalAsset []model.Asset + total int + currentOffset int + currentPageSize int + ) + + if offset < len(searchResult) { + totalAsset = searchResult[offset:] + } + total = len(totalAsset) + currentPageSize = pageSize + if currentPageSize == PAGESIZEALL { + currentPageSize = len(totalAsset) + } + if total > currentPageSize { + Assets = totalAsset[:currentPageSize] + } else { + Assets = totalAsset + } + currentOffset = offset + len(Assets) + e.updatePageInfo(currentPageSize, total, currentOffset) + return +} + +func (e *localAssetEngine) searchResult(searches ...string) []model.Asset { + if len(searches) == 0 { + return e.data + } + if len(searches) == 1 && searches[0] == "" { + return e.data + } + sort.Strings(searches) + if IsEqualStringSlice(e.cacheLastSearchKeys, searches) && + e.cacheLastSearchResult != nil { + return e.cacheLastSearchResult + } + e.cacheLastSearchKeys = searches + e.cacheLastSearchResult = searchMatchedAssets(e.data, searches...) + return e.cacheLastSearchResult +} + +func (e *localAssetEngine) HasPrev() bool { + return e.currentPage > 1 +} + +func (e *localAssetEngine) HasNext() bool { + return e.currentPage < e.totalPage +} + +type remoteAssetEngine struct { + user *model.User + *pageInfo + + nextUrl string + preUrl string +} + +func (e *remoteAssetEngine) Retrieve(pageSize, offset int, searches ...string) (Assets []model.Asset) { + resp := service.GetUserAssets(e.user.ID, pageSize, offset, searches...) + var ( + total int + currentOffset int + currentPageSize int + ) + e.nextUrl = resp.NextURL + e.preUrl = resp.PreviousURL + Assets = resp.Data + total = resp.Total + currentPageSize = pageSize + + if currentPageSize < 0 || currentPageSize == PAGESIZEALL { + currentPageSize = len(Assets) + } + if len(Assets) > currentPageSize { + Assets = Assets[:currentPageSize] + } + currentOffset = offset + len(Assets) + e.updatePageInfo(currentPageSize, total, currentOffset) + return +} + +func (e *remoteAssetEngine) HasPrev() bool { + return e.preUrl != "" +} + +func (e *remoteAssetEngine) HasNext() bool { + return e.nextUrl != "" +} + +type remoteNodeAssetEngine struct { + user *model.User + node model.Node + nextUrl string + preUrl string + *pageInfo +} + +func (e *remoteNodeAssetEngine) Retrieve(pageSize, offset int, searches ...string) (Assets []model.Asset) { + resp := service.GetUserNodePaginationAssets(e.user.ID, e.node.ID, pageSize, offset, searches...) + var ( + total int + currentOffset int + currentPageSize int + ) + e.nextUrl = resp.NextURL + e.preUrl = resp.PreviousURL + Assets = resp.Data + total = resp.Total + currentPageSize = pageSize + + if currentPageSize < 0 || currentPageSize == PAGESIZEALL { + currentPageSize = len(Assets) + } + + if len(Assets) > currentPageSize { + Assets = Assets[:currentPageSize] + } + currentOffset = offset + len(Assets) + e.updatePageInfo(currentPageSize, total, currentOffset) + return +} + +func (e *remoteNodeAssetEngine) HasPrev() bool { + return e.preUrl != "" +} + +func (e *remoteNodeAssetEngine) HasNext() bool { + return e.nextUrl != "" +} + +func searchMatchedAssets(data []model.Asset, keys ...string) []model.Asset { + matched := make([]model.Asset, 0, len(data)) + for i := range data { + asset := data[i] + ok := true + contents := []string{strings.ToLower(asset.Hostname), + strings.ToLower(asset.IP), strings.ToLower(asset.Comment)} + + for j := range keys { + if !isSubstring(contents, strings.ToLower(keys[j])) { + ok = false + break + } + } + if ok { + matched = append(matched, asset) + } + } + return matched +} diff --git a/pkg/handler/assetpaginator.go b/pkg/handler/assetpaginator.go deleted file mode 100644 index c89fa9b92..000000000 --- a/pkg/handler/assetpaginator.go +++ /dev/null @@ -1,507 +0,0 @@ -package handler - -import ( - "sync" - - "github.com/jumpserver/koko/pkg/model" - "github.com/jumpserver/koko/pkg/service" -) - -type Paginator interface { - HasPrev() bool - HasNext() bool - CurrentPage() int - TotalCount() int - TotalPage() int - PageSize() int - SetPageSize(size int) -} - -type AssetPaginator interface { - Paginator - RetrievePageData(pageIndex int) model.AssetList - SearchAsset(key string) model.AssetList - SearchAgain(key string) model.AssetList - Name() string - SearchKeys() []string -} - -func NewRemoteAssetPaginator(user model.User, pageSize int) AssetPaginator { - p := remoteAssetsPaginator{ - user: user, - pageSize: pageSize, - currentOffset: 0, - currentPage: 1, - search: make([]string, 0, 4), - lock: new(sync.RWMutex), - } - return &p -} - -func NewLocalAssetPaginator(data model.AssetList, pageSize int) AssetPaginator { - p := localAssetsPaginator{ - allData: data, - currentData: data, - pageSize: pageSize, - currentOffset: 0, - currentPage: 1, - search: make([]string, 0, 4), - lock: new(sync.RWMutex), - } - return &p -} - -func NewNodeAssetPaginator(user model.User, node model.Node, pageSize int) AssetPaginator { - p := nodeAssetsPaginator{ - user: user, - node: node, - pageSize: pageSize, - currentOffset: 0, - currentPage: 1, - lock: new(sync.RWMutex), - } - return &p -} - -type remoteAssetsPaginator struct { - user model.User - pageSize int - - lock *sync.RWMutex - currentOffset int - search []string - - currentData model.AssetList - totalPage int - currentPage int - totalCount int - - preUrl string - nextUrl string -} - -func (r *remoteAssetsPaginator) HasPrev() bool { - r.lock.RLock() - defer r.lock.RUnlock() - return r.preUrl != "" -} - -func (r *remoteAssetsPaginator) HasNext() bool { - r.lock.RLock() - defer r.lock.RUnlock() - return r.nextUrl != "" -} - -func (r *remoteAssetsPaginator) CurrentPage() int { - r.lock.RLock() - defer r.lock.RUnlock() - return r.currentPage -} - -func (r *remoteAssetsPaginator) TotalCount() int { - r.lock.RLock() - defer r.lock.RUnlock() - return r.totalCount -} - -func (r *remoteAssetsPaginator) TotalPage() int { - r.lock.RLock() - defer r.lock.RUnlock() - return r.totalPage -} - -func (r *remoteAssetsPaginator) PageSize() int { - r.lock.RLock() - defer r.lock.RUnlock() - if r.pageSize == 0 { - // size 0, 则获取全部资产 - return r.totalCount - } - return r.pageSize -} - -func (r *remoteAssetsPaginator) SetPageSize(size int) { - r.lock.Lock() - defer r.lock.Unlock() - if size < 0 { - // size 0, 则获取全部资产 - size = 0 - } - if r.pageSize == size { - return - } - r.pageSize = size -} - -func (r *remoteAssetsPaginator) RetrievePageData(pageIndex int) model.AssetList { - r.lock.Lock() - defer r.lock.Unlock() - return r.retrievePageDta(pageIndex) -} - -func (r *remoteAssetsPaginator) SearchAsset(key string) model.AssetList { - r.lock.Lock() - defer r.lock.Unlock() - r.search = r.search[:0] - r.search = append(r.search, key) - r.currentPage = 1 - r.currentOffset = 0 - return r.retrievePageDta(1) -} - -func (r *remoteAssetsPaginator) SearchAgain(key string) model.AssetList { - r.lock.Lock() - defer r.lock.Unlock() - r.search = append(r.search, key) - r.currentPage = 1 - r.currentOffset = 0 - return r.retrievePageDta(1) -} - -func (r *remoteAssetsPaginator) Name() string { - return "remote" -} - -func (r *remoteAssetsPaginator) SearchKeys() []string { - return r.search -} - -func (r *remoteAssetsPaginator) retrievePageDta(pageIndex int) model.AssetList { - offsetPage := pageIndex - r.currentPage - totalOffset := offsetPage * r.pageSize - - r.currentOffset += totalOffset - - if r.pageSize == 0 || r.currentOffset < 0 || r.pageSize >= r.totalCount { - r.currentOffset = 0 - } - res := service.GetUserAssets(r.user.ID, r.pageSize, r.currentOffset, r.search...) - - // update page info data, - r.totalCount = res.Total - r.nextUrl = res.NextURL - r.preUrl = res.PreviousURL - r.currentData = res.Data - r.updatePageInfo() - return res.Data -} - -func (r *remoteAssetsPaginator) updatePageInfo() { - switch r.pageSize { - case 0: - r.totalPage = 1 - r.currentPage = 1 - default: - pageSize := r.pageSize - totalCount := r.totalCount - - switch totalCount % pageSize { - case 0: - r.totalPage = totalCount / pageSize - default: - r.totalPage = (totalCount / pageSize) + 1 - } - currentOffset := r.currentOffset + len(r.currentData) - - switch currentOffset % pageSize { - case 0: - r.currentPage = currentOffset / pageSize - default: - r.currentPage = (currentOffset / pageSize) + 1 - } - } -} - -type localAssetsPaginator struct { - allData model.AssetList - - currentData model.AssetList - - currentPage int - - pageSize int - totalPage int - - currentOffset int - - search []string - lock *sync.RWMutex - - currentResult model.AssetList -} - -func (l *localAssetsPaginator) Name() string { - return "local" -} - -func (l *localAssetsPaginator) SearchKeys() []string { - return l.search -} - -func (l *localAssetsPaginator) HasPrev() bool { - l.lock.RLock() - defer l.lock.RUnlock() - return l.currentPage > 1 -} - -func (l *localAssetsPaginator) HasNext() bool { - l.lock.RLock() - defer l.lock.RUnlock() - return l.currentPage < l.totalPage -} - -func (l *localAssetsPaginator) CurrentPage() int { - l.lock.RLock() - defer l.lock.RUnlock() - return l.currentPage -} - -func (l *localAssetsPaginator) TotalCount() int { - l.lock.RLock() - defer l.lock.RUnlock() - return len(l.currentData) -} - -func (l *localAssetsPaginator) TotalPage() int { - l.lock.RLock() - defer l.lock.RUnlock() - return l.totalPage -} - -func (l *localAssetsPaginator) PageSize() int { - l.lock.RLock() - defer l.lock.RUnlock() - return l.pageSize -} - -func (l *localAssetsPaginator) 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 *localAssetsPaginator) RetrievePageData(pageIndex int) model.AssetList { - l.lock.Lock() - defer l.lock.Unlock() - return l.retrievePageData(pageIndex) -} - -func (l *localAssetsPaginator) SearchAsset(key string) model.AssetList { - l.lock.Lock() - defer l.lock.Unlock() - l.search = l.search[:0] - l.search = append(l.search, key) - l.currentData = searchFromLocalAssets(l.allData, key) - l.currentPage = 1 - l.currentOffset = 0 - return l.retrievePageData(1) -} - -func (l *localAssetsPaginator) SearchAgain(key string) model.AssetList { - l.lock.Lock() - defer l.lock.Unlock() - l.currentData = searchFromLocalAssets(l.currentData, key) - l.search = append(l.search, key) - l.currentPage = 1 - l.currentOffset = 0 - return l.retrievePageData(1) -} - -func (l *localAssetsPaginator) retrievePageData(pageIndex int) model.AssetList { - 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 *localAssetsPaginator) 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 - } -} - -type nodeAssetsPaginator struct { - user model.User - node model.Node - - currentPage int - pageSize int - totalPage int - totalCount int - search []string - - lock *sync.RWMutex - - currentData model.AssetList - - preUrl string - nextUrl string - currentOffset int -} - -func (n *nodeAssetsPaginator) Name() string { - return n.node.Name -} - -func (n *nodeAssetsPaginator) SearchKeys() []string { - return n.search -} - -func (n *nodeAssetsPaginator) HasPrev() bool { - n.lock.RLock() - defer n.lock.RUnlock() - return n.preUrl != "" -} - -func (n *nodeAssetsPaginator) HasNext() bool { - n.lock.RLock() - defer n.lock.RUnlock() - return n.nextUrl != "" -} - -func (n *nodeAssetsPaginator) CurrentPage() int { - n.lock.RLock() - defer n.lock.RUnlock() - return n.currentPage -} - -func (n *nodeAssetsPaginator) TotalCount() int { - n.lock.RLock() - defer n.lock.RUnlock() - return n.totalCount -} - -func (n *nodeAssetsPaginator) TotalPage() int { - n.lock.RLock() - defer n.lock.RUnlock() - return n.totalPage -} - -func (n *nodeAssetsPaginator) PageSize() int { - n.lock.RLock() - defer n.lock.RUnlock() - if n.pageSize == 0 { - // size 0, 则获取全部资产 - return n.totalCount - } - return n.pageSize -} - -func (n *nodeAssetsPaginator) SetPageSize(size int) { - n.lock.Lock() - defer n.lock.Unlock() - if size < 0 { - // size 0, 则获取全部资产 - size = 0 - } - if n.pageSize == size { - return - } - n.pageSize = size -} - -func (n *nodeAssetsPaginator) RetrievePageData(pageIndex int) model.AssetList { - n.lock.Lock() - defer n.lock.Unlock() - return n.retrievePageData(pageIndex) -} - -func (n *nodeAssetsPaginator) SearchAsset(key string) model.AssetList { - n.lock.Lock() - defer n.lock.Unlock() - n.search = n.search[:0] - n.search = append(n.search, key) - n.currentPage = 1 - n.currentOffset = 0 - return n.RetrievePageData(1) -} - -func (n *nodeAssetsPaginator) SearchAgain(key string) model.AssetList { - n.lock.Lock() - defer n.lock.Unlock() - n.search = append(n.search, key) - n.currentPage = 1 - n.currentOffset = 0 - return n.retrievePageData(1) -} - -func (n *nodeAssetsPaginator) retrievePageData(pageIndex int) model.AssetList { - offsetPage := pageIndex - n.currentPage - totalOffset := offsetPage * n.pageSize - - n.currentOffset += totalOffset - - if n.pageSize == 0 || n.currentOffset < 0 || n.pageSize >= n.totalCount { - n.currentOffset = 0 - } - res := service.GetUserNodePaginationAssets(n.user.ID, n.node.ID, - n.pageSize, n.currentOffset, n.search...) - - n.totalCount = res.Total - n.nextUrl = res.NextURL - n.preUrl = res.PreviousURL - n.currentData = res.Data - n.updatePageInfo() - return res.Data -} - -func (n *nodeAssetsPaginator) updatePageInfo() { - switch n.pageSize { - case 0: - n.totalPage = 1 - n.currentPage = 1 - default: - pageSize := n.pageSize - totalCount := n.totalCount - - switch totalCount % pageSize { - case 0: - n.totalPage = totalCount / pageSize - default: - n.totalPage = (totalCount / pageSize) + 1 - } - currentOffset := n.currentOffset + len(n.currentData) - switch currentOffset % pageSize { - case 0: - n.currentPage = currentOffset / pageSize - default: - n.currentPage = (currentOffset / pageSize) + 1 - } - } -} diff --git a/pkg/handler/banner.go b/pkg/handler/banner.go index c2175d2d1..96c64ff16 100644 --- a/pkg/handler/banner.go +++ b/pkg/handler/banner.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io" - "sync" "text/template" "github.com/jumpserver/koko/pkg/config" @@ -43,16 +42,17 @@ func (mi *MenuItem) Text() string { type Menu []MenuItem func Initial() { - defaultTitle = utils.WrapperTitle(i18n.T("Welcome to use Jumpserver open source fortress system")) + defaultTitle = utils.WrapperTitle(i18n.T("Welcome to use JumpServer open source fortress system")) menu = Menu{ {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")}, + {id: 6, instruct: "k", helpText: i18n.T("display the kubernetes that you have permission")}, + {id: 7, instruct: "r", helpText: i18n.T("refresh your assets and nodes")}, + {id: 8, instruct: "h", helpText: i18n.T("print help")}, + {id: 9, instruct: "q", helpText: i18n.T("exit")}, } } @@ -80,35 +80,3 @@ func displayBanner(sess io.ReadWriter, user string) { utils.IgnoreErrWriteString(sess, v.Text()) } } - -var i18nMap map[string]string -var i18nOnce sync.Once - -func getI18nFromMap(name string) string { - i18nOnce.Do(func() { - i18nMap = map[string]string{ - "ID": i18n.T("ID"), - "Hostname": i18n.T("hostname"), - "IP": i18n.T("IP"), - "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("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"), - "SelectUserTip": i18n.T("Tips: Enter system user ID and directly login the asset [ %s(%s) ]"), - "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] -} diff --git a/pkg/handler/dbpaginator.go b/pkg/handler/dbpaginator.go deleted file mode 100644 index 527ac44cf..000000000 --- a/pkg/handler/dbpaginator.go +++ /dev/null @@ -1,187 +0,0 @@ -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, - currentOffset: 0, - currentPage: 1, - search: make([]string, 0, 4), - lock: new(sync.RWMutex), - } - p.SetPageSize(pageSize) - 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 -} diff --git a/pkg/handler/dispatch.go b/pkg/handler/dispatch.go index f2609c434..5545574a3 100644 --- a/pkg/handler/dispatch.go +++ b/pkg/handler/dispatch.go @@ -6,18 +6,17 @@ import ( "strconv" "strings" - "github.com/jumpserver/koko/pkg/common" - "github.com/jumpserver/koko/pkg/config" "github.com/jumpserver/koko/pkg/exchange" + "github.com/jumpserver/koko/pkg/i18n" "github.com/jumpserver/koko/pkg/logger" "github.com/jumpserver/koko/pkg/model" - "github.com/jumpserver/koko/pkg/proxy" "github.com/jumpserver/koko/pkg/service" "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) + var currentApp Application for { line, err := h.term.ReadLine() if err != nil { @@ -29,495 +28,168 @@ func (h *interactiveHandler) Dispatch() { case 0, 1: switch strings.ToLower(line) { case "p": - h.resetPaginator() + currentApp = h.getAssetApp() + currentApp.Search("") + continue case "b": - if h.assetPaginator != nil { - h.movePrePage() - break - } - if h.dbPaginator != nil { - h.moveDBPrePage() - break - } - if ok := h.searchOrProxy(line); ok { + if currentApp != nil { + currentApp.MovePrePage() continue } case "d": - h.assetPaginator = nil - h.dbPaginator = h.getDatabasePaginator() - h.currentDBData = h.dbPaginator.RetrievePageData(1) + currentApp = h.getDatabaseApp() + currentApp.Search("") + continue case "n": - if h.assetPaginator != nil { - h.moveNextPage() - break - } - if h.dbPaginator != nil { - h.moveDBNextPage() - break - } - if ok := h.searchOrProxy(line); ok { + if currentApp != nil { + currentApp.MoveNextPage() continue } case "": - if h.assetPaginator != nil { - h.moveNextPage() - } else if h.dbPaginator != nil { - h.moveDBNextPage() + if currentApp != nil { + currentApp.MoveNextPage() } else { - h.resetPaginator() + currentApp = h.getAssetApp() + currentApp.Search("") } + continue case "g": - h.displayNodeTree() + h.displayNodeTree(h.nodes) continue case "h": h.displayBanner() + currentApp = nil continue case "r": h.refreshAssetsAndNodesData() continue case "q": - logger.Debugf("user %s enter to exit", h.user.Name) + logger.Infof("user %s enter %s to exit", h.user.Name, line) return - default: - if ok := h.searchOrProxy(line); ok { - continue - } + case "k": + currentApp = h.getK8sApp() + currentApp.Search("") + continue } default: switch { case line == "exit", line == "quit": - logger.Debugf("user %s enter to exit", h.user.Name) + logger.Infof("user %s enter %s to exit", h.user.Name, line) return case strings.Index(line, "/") == 0: if strings.Index(line[1:], "/") == 0 { line = strings.TrimSpace(line[2:]) - h.searchAssetsAgain(line) - break + if currentApp != nil { + currentApp.SearchAgain(line) + continue + } } line = strings.TrimSpace(line[1:]) - h.searchAssetAndDisplay(line) + if currentApp == nil { + currentApp = h.getAssetApp() + } + currentApp.Search(line) + continue 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 + <-h.firstLoadDone + if num >= 0 && num <= len(h.nodes) { + currentApp = h.getNodeAssetApp(h.nodes[num-1]) + currentApp.Search("") + continue } } - if ok := h.searchOrProxy(line); ok { - continue - } case strings.Index(line, "join") == 0: roomID := strings.TrimSpace(strings.TrimPrefix(line, "join")) JoinRoom(h, roomID) - default: - if ok := h.searchOrProxy(line); ok { - continue - } - } - } - if h.dbPaginator != nil { - h.displayPageDatabase() - } - if h.assetPaginator != nil { - h.displayPageAssets() - } - - } -} - -func (h *interactiveHandler) resetPaginator() { - h.dbPaginator = nil - h.currentDBData = nil - 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, " ") + if currentApp == nil { + currentApp = h.getAssetApp() } - default: - header = fmt.Sprintf("%s %s", h.assetPaginator.Name(), strings.Join(keys, " ")) + currentApp.SearchOrProxy(line) } - 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 h.dbPaginator != nil { - if indexNum, err := strconv.Atoi(key); err == nil && len(h.currentDBData) > 0 { - if indexNum > 0 && indexNum <= len(h.currentDBData) { - dbSelected := h.currentDBData[indexNum-1] - h.ProxyDB(dbSelected) - h.dbPaginator = nil - h.currentDBData = nil - return true - } - } - if data := h.dbPaginator.SearchAsset(key); len(data) == 1 { - h.ProxyDB(data[0]) - h.dbPaginator = nil - h.currentDBData = nil - return true - } else { - h.currentDBData = data - } - return false - } - 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.currentDBData = nil - h.dbPaginator = nil - h.currentData = h.searchAssets(key) -} - -func (h *interactiveHandler) searchAssetsAgain(key string) { - if h.dbPaginator != nil { - h.currentDBData = h.dbPaginator.SearchAgain(key) - return - } - 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")) +func (h *interactiveHandler) displayNodeTree(nodes model.NodeList) { + tree := ConstructAssetNodeTree(nodes) + _, _ = io.WriteString(h.term, "\n\r"+i18n.T("Node: [ ID.Name(Asset amount) ]")) _, _ = io.WriteString(h.term, tree.String()) - _, err := io.WriteString(h.term, getI18nFromMap("NodeEndTip")+"\n\r") + _, err := io.WriteString(h.term, i18n.T("Tips: Enter g+NodeID to display the host under the node, such as g1")+"\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 { +func (h *interactiveHandler) getAssetApp() Application { + var eng AssetEngine switch h.assetLoadPolicy { case "all": <-h.firstLoadDone - return NewLocalAssetPaginator(h.allAssets, getPageSize(h.term)) + eng = &localAssetEngine{ + data: h.allAssets, + pageInfo: &pageInfo{}, + } default: - } - return NewRemoteAssetPaginator(*h.user, getPageSize(h.term)) -} - -func (h *interactiveHandler) getNodeAssetPaginator(node model.Node) AssetPaginator { - return NewNodeAssetPaginator(*h.user, node, getPageSize(h.term)) -} - -func (h *interactiveHandler) getDatabasePaginator() DatabasePaginator { - dbs := service.GetUserDatabases(h.user.ID) - return NewLocalDatabasePaginator(dbs, getPageSize(h.term)) -} - -func (h *interactiveHandler) displayPageDatabase() { - if len(h.currentDBData) == 0 { - _, _ = h.term.Write([]byte(getI18nFromMap("NoDatabases") + "\n\r")) - h.dbPaginator = nil - return - } - Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Name"), - getI18nFromMap("IP"), getI18nFromMap("DBType"), - getI18nFromMap("DBName"), getI18nFromMap("Comment")} - fields := []string{"ID", "name", "IP", "DBType", "DBName", "comment"} - data := make([]map[string]string, len(h.currentDBData)) - for i, j := range h.currentDBData { - row := make(map[string]string) - row["ID"] = strconv.Itoa(i + 1) - row["name"] = j.Name - row["IP"] = j.Host - row["DBType"] = j.DBType - row["DBName"] = j.DBName - - 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), " ", ",")) + eng = &remoteAssetEngine{ + user: h.user, + pageInfo: &pageInfo{}, } - row["comment"] = strings.Join(comments, "|") - data[i] = row } - w, _ := h.term.GetSize() - - currentPage := h.dbPaginator.CurrentPage() - pageSize := h.dbPaginator.PageSize() - totalPage := h.dbPaginator.TotalPage() - totalCount := h.dbPaginator.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}, - "name": {0, 8, 0}, - "IP": {0, 15, 40}, - "DBType": {0, 8, 0}, - "DBName": {0, 8, 0}, - "comment": {0, 0, 0}, - }, - Data: data, - TotalSize: w, - Caption: caption, - TruncPolicy: common.TruncMiddle, + app := AssetApplication{ + h: h, + engine: eng, + searchKeys: make([]string, 0), } - table.Initial() - header := getI18nFromMap("All") - keys := h.dbPaginator.SearchKeys() - switch h.dbPaginator.Name() { - case "local", "remote": - if len(keys) != 0 { - header = strings.Join(keys, " ") - } - default: - header = fmt.Sprintf("%s %s", h.dbPaginator.Name(), strings.Join(keys, " ")) - } - searchHeader := fmt.Sprintf(getI18nFromMap("SearchTip"), header) - actionTip := fmt.Sprintf("%s %s", getI18nFromMap("DBLoginTip"), 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) + h.term.SetPrompt("[Host]> ") + return &app } -func (h *interactiveHandler) moveDBPrePage() { - if h.dbPaginator == nil || !h.dbPaginator.HasPrev() { - return +func (h *interactiveHandler) getNodeAssetApp(node model.Node) Application { + eng := &remoteNodeAssetEngine{ + user: h.user, + node: node, + pageInfo: &pageInfo{}, } - h.dbPaginator.SetPageSize(getPageSize(h.term)) - prePage := h.dbPaginator.CurrentPage() - 1 - h.currentDBData = h.dbPaginator.RetrievePageData(prePage) -} - -func (h *interactiveHandler) moveDBNextPage() { - if h.dbPaginator == nil || !h.dbPaginator.HasNext() { - return + app := AssetApplication{ + h: h, + engine: eng, + searchKeys: make([]string, 0), } - h.dbPaginator.SetPageSize(getPageSize(h.term)) - prePage := h.dbPaginator.CurrentPage() + 1 - h.currentDBData = h.dbPaginator.RetrievePageData(prePage) + h.term.SetPrompt("[Host]> ") + return &app } -func (h *interactiveHandler) ProxyDB(dbSelect model.Database) { - systemUsers := service.GetUserDatabaseSystemUsers(h.user.ID, dbSelect.ID) - systemUserSelect, ok := h.chooseDBSystemUser(dbSelect, systemUsers) - if !ok { - return +func (h *interactiveHandler) getDatabaseApp() Application { + allDBs := service.GetUserDatabases(h.user.ID) + eng := &localDatabaseEngine{ + data: allDBs, + pageInfo: &pageInfo{}, } - p := proxy.DBProxyServer{ - UserConn: h.sess, - User: h.user, - Database: &dbSelect, - SystemUser: &systemUserSelect, + app := DatabaseApplication{ + h: h, + engine: eng, + searchKeys: make([]string, 0), } - h.pauseWatchWinSize() - p.Proxy() - logger.Infof("Request %s: database %s proxy end", h.sess.Uuid, dbSelect.Name) - h.resumeWatchWinSize() + h.term.SetPrompt("[DB]> ") + return &app } -func (h *interactiveHandler) chooseDBSystemUser(dbAsset model.Database, - systemUsers []model.SystemUser) (systemUser model.SystemUser, ok bool) { - - length := len(systemUsers) - switch length { - case 0: - return model.SystemUser{}, false - case 1: - return systemUsers[0], true - default: - } - displaySystemUsers := selectHighestPrioritySystemUsers(systemUsers) - if len(displaySystemUsers) == 1 { - return displaySystemUsers[0], true - } - - Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Name"), getI18nFromMap("Username")} - fields := []string{"ID", "Name", "Username"} - - data := make([]map[string]string, len(displaySystemUsers)) - for i, j := range displaySystemUsers { - row := make(map[string]string) - row["ID"] = strconv.Itoa(i + 1) - row["Name"] = j.Name - row["Username"] = j.Username - data[i] = row +func (h *interactiveHandler) getK8sApp() Application { + eng := &remoteK8sEngine{ + user: h.user, + pageInfo: &pageInfo{}, } - w, _ := h.term.GetSize() - table := common.WrapperTable{ - Fields: fields, - Labels: Labels, - FieldsSize: map[string][3]int{ - "ID": {0, 0, 5}, - "Name": {0, 8, 0}, - "Username": {0, 10, 0}, - }, - Data: data, - TotalSize: w, - TruncPolicy: common.TruncMiddle, - } - table.Initial() - - h.term.SetPrompt("ID> ") - defer h.term.SetPrompt("Opt> ") - selectUserTip := fmt.Sprintf(getI18nFromMap("SelectUserTip"), dbAsset.Name, dbAsset.Host) - for { - utils.IgnoreErrWriteString(h.term, table.Display()) - utils.IgnoreErrWriteString(h.term, selectUserTip) - utils.IgnoreErrWriteString(h.term, getI18nFromMap("BackTip")) - utils.IgnoreErrWriteString(h.term, "\r\n") - line, err := h.term.ReadLine() - if err != nil { - return - } - line = strings.TrimSpace(line) - switch strings.ToLower(line) { - case "q", "b", "quit", "exit", "back": - return - } - if num, err := strconv.Atoi(line); err == nil { - if num > 0 && num <= len(displaySystemUsers) { - return displaySystemUsers[num-1], true - } - } + app := K8sApplication{ + h: h, + engine: eng, + searchKeys: make([]string, 0), } + h.term.SetPrompt("[k8s]> ") + return &app } func (h *interactiveHandler) CheckShareRoomWritePerm(shareRoomID string) bool { diff --git a/pkg/handler/session.go b/pkg/handler/session.go index 9e28c134f..4a5948b6b 100644 --- a/pkg/handler/session.go +++ b/pkg/handler/session.go @@ -1,7 +1,6 @@ package handler import ( - "context" "fmt" "io" "strconv" @@ -13,9 +12,9 @@ import ( "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/proxy" "github.com/jumpserver/koko/pkg/service" "github.com/jumpserver/koko/pkg/utils" ) @@ -56,22 +55,13 @@ type interactiveHandler struct { term *utils.Terminal winWatchChan chan bool - assetSelect *model.Asset - systemUserSelect *model.SystemUser - nodes model.NodeList + nodes model.NodeList allAssets []model.Asset - firstLoadDone chan struct{} assetLoadPolicy string - currentSortedData []model.Asset - currentData []model.Asset - - assetPaginator AssetPaginator - - dbPaginator DatabasePaginator - currentDBData []model.Database + firstLoadDone chan struct{} } func (h *interactiveHandler) Initial() { @@ -96,6 +86,7 @@ func (h *interactiveHandler) firstLoadData() { } func (h *interactiveHandler) displayBanner() { + h.term.SetPrompt("Opt> ") displayBanner(h.sess, h.user.Name) } @@ -154,12 +145,13 @@ func (h *interactiveHandler) resumeWatchWinSize() { h.winWatchChan <- true } -func (h *interactiveHandler) chooseSystemUser(asset model.Asset, - systemUsers []model.SystemUser) (systemUser model.SystemUser, ok bool) { +func (h *interactiveHandler) chooseSystemUser(systemUsers []model.SystemUser) (systemUser model.SystemUser, ok bool) { length := len(systemUsers) switch length { case 0: + warningInfo := i18n.T("No system user found.") + _, _ = io.WriteString(h.term, warningInfo+"\n\r") return model.SystemUser{}, false case 1: return systemUsers[0], true @@ -170,7 +162,11 @@ func (h *interactiveHandler) chooseSystemUser(asset model.Asset, return displaySystemUsers[0], true } - Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Name"), getI18nFromMap("Username")} + idLabel := i18n.T("ID") + nameLabel := i18n.T("Name") + usernameLabel := i18n.T("Username") + + labels := []string{idLabel, nameLabel, usernameLabel} fields := []string{"ID", "Name", "Username"} data := make([]map[string]string, len(displaySystemUsers)) @@ -184,7 +180,7 @@ func (h *interactiveHandler) chooseSystemUser(asset model.Asset, w, _ := h.term.GetSize() table := common.WrapperTable{ Fields: fields, - Labels: Labels, + Labels: labels, FieldsSize: map[string][3]int{ "ID": {0, 0, 5}, "Name": {0, 8, 0}, @@ -197,13 +193,14 @@ func (h *interactiveHandler) chooseSystemUser(asset model.Asset, table.Initial() h.term.SetPrompt("ID> ") - defer h.term.SetPrompt("Opt> ") - selectUserTip := fmt.Sprintf(getI18nFromMap("SelectUserTip"), asset.Hostname, asset.IP) + selectTip := i18n.T("Tips: Enter system user ID and directly login") + backTip := i18n.T("Back: B/b") for { utils.IgnoreErrWriteString(h.term, table.Display()) - utils.IgnoreErrWriteString(h.term, selectUserTip) - utils.IgnoreErrWriteString(h.term, getI18nFromMap("BackTip")) - utils.IgnoreErrWriteString(h.term, "\r\n") + utils.IgnoreErrWriteString(h.term, selectTip) + utils.IgnoreErrWriteString(h.term, utils.CharNewLine) + utils.IgnoreErrWriteString(h.term, backTip) + utils.IgnoreErrWriteString(h.term, utils.CharNewLine) line, err := h.term.ReadLine() if err != nil { return @@ -229,12 +226,10 @@ func (h *interactiveHandler) refreshAssetsAndNodesData() { _ = service.ForceRefreshUserPemAssets(h.user.ID) } h.loadUserNodes("2") - _, err := io.WriteString(h.term, getI18nFromMap("RefreshDone")+"\n\r") + _, err := io.WriteString(h.term, i18n.T("Refresh done")+"\n\r") if err != nil { logger.Error("refresh Assets Nodes err:", err) } - h.assetPaginator = nil - h.dbPaginator = nil } func (h *interactiveHandler) loadUserNodes(cachePolicy string) { @@ -245,30 +240,6 @@ func (h *interactiveHandler) loadAllAssets() { h.allAssets = service.GetUserAllAssets(h.user.ID) } -func (h *interactiveHandler) ProxyAsset(assetSelect model.Asset) { - systemUsers := service.GetUserAssetSystemUsers(h.user.ID, assetSelect.ID) - systemUserSelect, ok := h.chooseSystemUser(assetSelect, systemUsers) - if !ok { - return - } - h.systemUserSelect = &systemUserSelect - h.assetSelect = &assetSelect - h.Proxy(context.Background()) -} - -func (h *interactiveHandler) Proxy(ctx context.Context) { - p := proxy.ProxyServer{ - UserConn: h.sess, - User: h.user, - Asset: h.assetSelect, - SystemUser: h.systemUserSelect, - } - 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 { model.SortAssetNodesByKey(assetNodes) var treeMap = map[string]treeprint.Tree{} @@ -327,19 +298,6 @@ func selectHighestPrioritySystemUsers(systemUsers []model.SystemUser) []model.Sy return result } -func searchFromLocalAssets(assets model.AssetList, key string) []model.Asset { - displayAssets := make([]model.Asset, 0, len(assets)) - key = strings.ToLower(key) - for _, assetValue := range assets { - contents := []string{strings.ToLower(assetValue.Hostname), - strings.ToLower(assetValue.IP), strings.ToLower(assetValue.Comment)} - if isSubstring(contents, key) { - displayAssets = append(displayAssets, assetValue) - } - } - return displayAssets -} - func getPageSize(term *utils.Terminal) int { var ( pageSize int @@ -352,7 +310,7 @@ func getPageSize(term *utils.Terminal) int { case "auto": pageSize = height - minHeight case "all": - return 0 + return PAGESIZEALL default: if value, err := strconv.Atoi(conf.AssetListPageSize); err == nil { pageSize = value diff --git a/pkg/httpd/sftpvolume.go b/pkg/httpd/sftpvolume.go index 0940525cd..bcfb7deb7 100644 --- a/pkg/httpd/sftpvolume.go +++ b/pkg/httpd/sftpvolume.go @@ -321,9 +321,12 @@ func (u *UserVolume) RootFileDir() elfinder.FileDir { size int64 ) tz := time.Now().UnixNano() + readPem := byte(1) + writePem := byte(0) if fInfo, err := u.UserSftp.Stat(u.basePath); err == nil { size = fInfo.Size() tz = fInfo.ModTime().Unix() + readPem, writePem = elfinder.ReadWritePem(fInfo.Mode()) } var rest elfinder.FileDir rest.Name = u.Homename @@ -332,7 +335,7 @@ func (u *UserVolume) RootFileDir() elfinder.FileDir { rest.Volumeid = u.Uuid rest.Mime = "directory" rest.Dirs = 1 - rest.Read, rest.Write = 1, 1 + rest.Read, rest.Write = readPem, writePem rest.Locked = 1 rest.Ts = tz return rest diff --git a/pkg/httpd/websshws.go b/pkg/httpd/websshws.go index 5250e9de2..e81cac175 100644 --- a/pkg/httpd/websshws.go +++ b/pkg/httpd/websshws.go @@ -106,6 +106,7 @@ func OnHostHandler(ns *neffos.NSConn, msg neffos.Message) (err error) { userConn.SendRoomEvent(neffos.Marshal(emitMsg)) var databaseAsset model.Database var asset model.Asset + var k8sCluster model.K8sCluster systemUser := service.GetSystemUser(systemUserID) var connectName string @@ -120,6 +121,16 @@ func OnHostHandler(ns *neffos.NSConn, msg neffos.Message) (err error) { return errors.New("no found database or systemUser") } connectName = databaseAsset.Name + case "k8s": + k8sCluster = service.GetK8sCluster(assetID) + if k8sCluster.ID == "" || systemUser.ID == "" { + msg := "No k8s id or system user id found, exit" + logger.Error(msg) + dataMsg := DataMsg{Room: roomID, Data: msg} + userConn.SendDataEvent(neffos.Marshal(dataMsg)) + return errors.New("no found k8s or systemUser") + } + connectName = k8sCluster.Name default: asset = service.GetAsset(assetID) if asset.ID == "" || systemUser.ID == "" { @@ -149,6 +160,13 @@ func OnHostHandler(ns *neffos.NSConn, msg neffos.Message) (err error) { Database: &databaseAsset, SystemUser: &systemUser, } + case "k8s": + proxySrv = &proxy.K8sProxyServer{ + UserConn: client, + User: userConn.User, + Cluster: &k8sCluster, + SystemUser: &systemUser, + } default: proxySrv = &proxy.ProxyServer{ UserConn: client, User: userConn.User, diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index e0448a109..8fd7d1cc1 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -12,10 +12,10 @@ import ( func Initial()() { cf := config.GetConf() localePath := path.Join(cf.RootPath, "locale") - if strings.HasPrefix(cf.Language, "zh") { - gotext.Configure(localePath, "zh_CN", "koko") - } else { + if strings.HasPrefix(strings.ToLower(cf.LanguageCode), "en") { gotext.Configure(localePath, "en_US", "koko") + } else { + gotext.Configure(localePath, "zh_CN", "koko") } } diff --git a/pkg/koko/koko.go b/pkg/koko/koko.go index 17e62bf64..f3aa2fec4 100644 --- a/pkg/koko/koko.go +++ b/pkg/koko/koko.go @@ -17,7 +17,7 @@ import ( "github.com/jumpserver/koko/pkg/sshd" ) -const Version = "2.0.0" +var Version = "unknown" type Coco struct { } diff --git a/pkg/model/assets.go b/pkg/model/assets.go index c4e2f1738..5c81a8386 100644 --- a/pkg/model/assets.go +++ b/pkg/model/assets.go @@ -236,6 +236,7 @@ type SystemUser struct { OrgId string `json:"org_id"` OrgName string `json:"org_name"` UsernameSameWithUser bool `json:"username_same_with_user"` + Token string `json:"token"` } type SystemUserAuthInfo struct { @@ -246,6 +247,7 @@ type SystemUserAuthInfo struct { LoginMode string `json:"login_mode"` Password string `json:"password"` PrivateKey string `json:"private_key"` + Token string `json:"token"` } type systemUserSortBy func(user1, user2 *SystemUser) bool diff --git a/pkg/model/database.go b/pkg/model/database.go index 97217a149..82ddcd795 100644 --- a/pkg/model/database.go +++ b/pkg/model/database.go @@ -16,3 +16,10 @@ type Database struct { func (db Database) String() string { return fmt.Sprintf("%s://%s:%d/%s", db.DBType, db.Host, db.Port, db.DBName) } + +type DatabasesPaginationResponse struct { + Total int `json:"count"` + NextURL string `json:"next"` + PreviousURL string `json:"previous"` + Data []Database `json:"results"` +} diff --git a/pkg/model/k8s.go b/pkg/model/k8s.go new file mode 100644 index 000000000..0c4f431e9 --- /dev/null +++ b/pkg/model/k8s.go @@ -0,0 +1,17 @@ +package model + +type K8sCluster struct { + ID string `json:"id"` + Name string `json:"name"` + Cluster string `json:"cluster"` + Comment string `json:"comment"` + Type string `json:"type"` // k8s + OrgID string `json:"org_id"` +} + +type K8sClustersPaginationResponse struct { + Total int `json:"count"` + NextURL string `json:"next"` + PreviousURL string `json:"previous"` + Data []K8sCluster `json:"results"` +} diff --git a/pkg/proxy/commonswitch.go b/pkg/proxy/commonswitch.go new file mode 100644 index 000000000..8fbdd3cf7 --- /dev/null +++ b/pkg/proxy/commonswitch.go @@ -0,0 +1,299 @@ +package proxy + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/gliderlabs/ssh" + uuid "github.com/satori/go.uuid" + + "github.com/jumpserver/koko/pkg/common" + "github.com/jumpserver/koko/pkg/config" + "github.com/jumpserver/koko/pkg/exchange" + "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" +) + +func NewCommonSwitch(p proxyEngine) *commonSwitch { + ctx, cancel := context.WithCancel(context.Background()) + c := commonSwitch{ + ID: uuid.NewV4().String(), + DateStart: common.CurrentUTCTime(), + MaxIdleTime: config.GetConf().MaxIdleTime, + ctx: ctx, + cancel: cancel, + p: p, + } + return &c +} + +type commonSwitch struct { + ID string + DateStart string + DateEnd string + finished bool + + isConnected bool + + MaxIdleTime time.Duration + + ctx context.Context + cancel context.CancelFunc + + p proxyEngine +} + +func (s *commonSwitch) Terminate() { + select { + case <-s.ctx.Done(): + return + default: + } + s.cancel() + logger.Infof("Session[%s] receive terminate task from admin", s.ID) +} + +func (s *commonSwitch) SessionID() string { + return s.ID +} + +func (s *commonSwitch) recordCommand(cmdRecordChan chan [3]string) { + // 命令记录 + cmdRecorder := NewCommandRecorder(s.ID) + for command := range cmdRecordChan { + if command[0] == "" { + continue + } + cmd := s.generateCommandResult(command) + cmdRecorder.Record(cmd) + } + // 关闭命令记录 + cmdRecorder.End() +} + +// generateCommandResult 生成命令结果 +func (s *commonSwitch) generateCommandResult(command [3]string) *model.Command { + var input string + var output string + var riskLevel int64 + 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] + } + + switch command[2] { + case model.HighRiskFlag: + riskLevel = 5 + default: + riskLevel = 0 + } + return s.p.GenerateRecordCommand(s, input, output, riskLevel) +} + +// postBridge 桥接结束以后执行操作 +func (s *commonSwitch) postBridge() { + s.DateEnd = common.CurrentUTCTime() + s.finished = true +} +func (s *commonSwitch) MapData() map[string]interface{} { + return s.p.MapData(s) +} + +// Bridge 桥接两个链接 +func (s *commonSwitch) Bridge(userConn UserConnection, srvConn srvconn.ServerConnection) (err error) { + var ( + //ParseEngine Parser + replayRecorder ReplyRecorder + + userInChan chan []byte + srvInChan chan []byte + done chan struct{} + ) + s.isConnected = true + //ParseEngine = newParser(s.ID) + parser := s.p.NewParser(s) + logger.Infof("Conn[%s] create ParseEngine success", userConn.ID()) + replayRecorder = NewReplyRecord(s.ID) + logger.Infof("Conn[%s] create replay success", userConn.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() + }() + + // 记录命令 + cmdChan := parser.CommandRecordChan() + go s.recordCommand(cmdChan) + 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() + ex := exchange.GetExchange() + roomChan := make(chan model.RoomMessage) + sub := ex.CreateRoom(roomChan, s.ID) + logger.Infof("Conn[%s] create exchange room success", userConn.ID()) + defer ex.DestroyRoom(sub) + go s.loopReadFromRoom(done, roomChan, userInChan) + defer sub.Publish(model.RoomMessage{Event: model.ExitEvent}) + logger.Infof("Conn[%s] start session %s bridge loop", userConn.ID(), s.ID) + for { + select { + // 检测是否超过最大空闲时间 + case <-tick.C: + now := time.Now() + outTime := lastActiveTime.Add(maxIdleTime) + if !now.After(outTime) { + continue + } + msg := fmt.Sprintf(i18n.T("Connect idle more than %d minutes, disconnect"), s.MaxIdleTime) + logger.Infof("Session[%s] idle more than %d minutes, disconnect", s.ID, s.MaxIdleTime) + msg = utils.WrapperWarn(msg) + utils.IgnoreErrWriteString(userConn, "\n\r"+msg) + sub.Publish(model.RoomMessage{Event: model.MaxIdleEvent, Body: []byte("\n\r" + msg)}) + logger.Debugf("Session[%s] published MaxIdleEvent", s.ID) + return + // 手动结束 + 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) + sub.Publish(model.RoomMessage{Event: model.AdminTerminateEvent, Body: []byte("\n\r" + msg)}) + logger.Debugf("Session[%s] published AdminTerminateEvent", s.ID) + return + // 监控窗口大小变化 + case win, ok := <-winCh: + if !ok { + return + } + _ = srvConn.SetWinSize(win.Width, win.Height) + logger.Infof("Session[%s] Window server change: %d*%d", + s.ID, win.Width, win.Height) + p, _ := json.Marshal(win) + msg := model.RoomMessage{ + Event: model.WindowsEvent, + Body: p, + } + sub.Publish(msg) + // 经过parse处理的server数据,发给user + case p, ok := <-srvOutChan: + if !ok { + return + } + nw, _ := userConn.Write(p) + if parser.NeedRecord() { + replayRecorder.Record(p[:nw]) + } + msg := model.RoomMessage{ + Event: model.DataEvent, + Body: p[:nw], + } + sub.Publish(msg) + // 经过parse处理的user数据,发给server + case p, ok := <-userOutChan: + if !ok { + return + } + _, err = srvConn.Write(p) + sub.Publish(model.RoomMessage{ + Event: model.PingEvent, + }) + } + lastActiveTime = time.Now() + } +} + +func (s *commonSwitch) 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 *commonSwitch) 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 *commonSwitch) 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: + break loop + case inChan <- buf[:nr]: + } + } + if err != nil { + break + } + } + close(inChan) +} + +func (s *commonSwitch) loopReadFromRoom(done chan struct{}, roomMsgChan <-chan model.RoomMessage, inChan chan<- []byte) { + defer logger.Infof("Session[%s] stop receive event from room", s.ID) + for { + select { + case <-done: + logger.Infof("Session[%s] stop loop read from room by done", s.ID) + return + case roomMsg, ok := <-roomMsgChan: + if !ok { + logger.Infof("Session[%s] stop loop read from room by close room channel", s.ID) + return + } + switch roomMsg.Event { + case model.DataEvent: + select { + case inChan <- roomMsg.Body: + case <-done: + logger.Infof("Session[%s] stop loop read from room by done", + s.ID) + return + } + case model.LogoutEvent, model.MaxIdleEvent, model.AdminTerminateEvent, model.ExitEvent: + logger.Infof("Session[%s] stop loop read from room by event %s", s.ID, roomMsg.Event) + return + case model.WindowsEvent: + var win ssh.Window + _ = json.Unmarshal(roomMsg.Body, &win) + logger.Infof("Session[%s] room windows change event height*width %d*%d", + s.ID, win.Height, win.Width) + } + + } + } +} diff --git a/pkg/proxy/dbparser.go b/pkg/proxy/dbparser.go index 52f1af128..146423d81 100644 --- a/pkg/proxy/dbparser.go +++ b/pkg/proxy/dbparser.go @@ -16,6 +16,8 @@ const ( DBOutputParserName = "DB Output parser" ) +var _ ParseEngine = (*DBParser)(nil) + func newDBParser(id string) DBParser { dbParser := DBParser{ id: id, @@ -215,3 +217,11 @@ func (p *DBParser) sendCommandRecord() { p.output = "" } } + +func (p *DBParser) NeedRecord() bool { + return true +} + +func (p *DBParser) CommandRecordChan() chan [3]string { + return p.cmdRecordChan +} diff --git a/pkg/proxy/dbproxy.go b/pkg/proxy/dbproxy.go index f1c0dcba1..8aec599cb 100644 --- a/pkg/proxy/dbproxy.go +++ b/pkg/proxy/dbproxy.go @@ -1,7 +1,9 @@ package proxy import ( + "bytes" "fmt" + "os/exec" "strings" "time" @@ -14,6 +16,8 @@ import ( "github.com/jumpserver/koko/pkg/utils" ) +var _ proxyEngine = (*DBProxyServer)(nil) + type DBProxyServer struct { UserConn UserConnection User *model.User @@ -74,7 +78,7 @@ func (p *DBProxyServer) checkProtocolMatch() bool { func (p *DBProxyServer) checkProtocolClientInstalled() bool { switch strings.ToLower(p.Database.DBType) { case "mysql": - return utils.IsInstalledMysqlClient() + return IsInstalledMysqlClient() } return false @@ -112,7 +116,7 @@ func (p *DBProxyServer) getServerConn() (srvConn srvconn.ServerConnection, err e // 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) + msg := fmt.Sprintf(i18n.T("Connecting to Database %s %.1f"), p.Database, delay) utils.IgnoreErrWriteString(p.UserConn, msg) for int(delay) < int(delayDuration/time.Second) { select { @@ -196,13 +200,16 @@ func (p *DBProxyServer) Proxy() { } logger.Infof("Conn[%s] checking pre requisite success", p.UserConn.ID()) // 创建Session - sw, err := CreateDBSession(p) - if err != nil { - logger.Errorf("Conn[%s] create session failed: %s", p.UserConn.ID(), err) + sw, ok := CreateCommonSwitch(p) + if !ok { + msg := i18n.T("Create database session failed") + msg = utils.WrapperWarn(msg) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Error(msg) return } logger.Infof("Conn[%s] create database session %s success", p.UserConn.ID(), sw.ID) - defer RemoveDBSession(sw) + defer RemoveCommonSwitch(sw) srvConn, err := p.getServerConn() // 连接后端服务器失败 if err != nil { @@ -215,3 +222,70 @@ func (p *DBProxyServer) Proxy() { logger.Infof("Conn[%s] end database session %s bridge", p.UserConn.ID(), sw.ID) } + +func (p *DBProxyServer) GenerateRecordCommand(s *commonSwitch, input, output string, + riskLevel int64) *model.Command { + return &model.Command{ + SessionID: s.ID, + OrgID: p.Database.OrgID, + Input: input, + Output: output, + User: fmt.Sprintf("%s (%s)", p.User.Name, p.User.Username), + Server: p.Database.Name, + SystemUser: p.SystemUser.Username, + Timestamp: time.Now().Unix(), + RiskLevel: riskLevel, + } +} + +func (p *DBProxyServer) NewParser(s *commonSwitch) ParseEngine { + dbParser := newDBParser(s.ID) + msg := i18n.T("Create database session failed") + if cmdRules, err := service.GetSystemUserFilterRules(p.SystemUser.ID); err == nil { + dbParser.SetCMDFilterRules(cmdRules) + } else { + msg = utils.WrapperWarn(msg) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Error(msg + err.Error()) + } + return &dbParser +} + +func (p *DBProxyServer) MapData(s *commonSwitch) map[string]interface{} { + var dataEnd interface{} + if s.DateEnd != "" { + dataEnd = s.DateEnd + } + return map[string]interface{}{ + "id": s.ID, + "user": fmt.Sprintf("%s (%s)", p.User.Name, p.User.Username), + "asset": p.Database.Name, + "org_id": p.Database.OrgID, + "login_from": p.UserConn.LoginFrom(), + "system_user": p.SystemUser.Username, + "protocol": p.SystemUser.Protocol, + "remote_addr": p.UserConn.RemoteAddr(), + "is_finished": s.finished, + "date_start": s.DateStart, + "date_end": dataEnd, + "user_id": p.User.ID, + "asset_id": p.Database.ID, + "system_user_id": p.SystemUser.ID, + "is_success": s.isConnected, + } +} + +func IsInstalledMysqlClient() bool { + checkLine := "mysql -V" + cmd := exec.Command("bash", "-c", checkLine) + out, err := cmd.CombinedOutput() + if err != nil && len(out) == 0 { + logger.Errorf("Check mysql client installed failed: %s", err, out) + return false + } + if bytes.HasPrefix(out, []byte("mysql")) { + return true + } + logger.Errorf("Check mysql client installed failed: %s", out) + return false +} diff --git a/pkg/proxy/interface.go b/pkg/proxy/interface.go new file mode 100644 index 000000000..bbad2f2e5 --- /dev/null +++ b/pkg/proxy/interface.go @@ -0,0 +1,23 @@ +package proxy + +import ( + "github.com/jumpserver/koko/pkg/model" +) + +type proxyEngine interface { + GenerateRecordCommand(s *commonSwitch, input, output string, riskLevel int64) *model.Command + + NewParser(s *commonSwitch) ParseEngine + + MapData(s *commonSwitch) map[string]interface{} +} + +type ParseEngine interface { + ParseStream(userInChan, srvInChan <-chan []byte) (userOut, srvOut <-chan []byte) + + Close() + + NeedRecord() bool + + CommandRecordChan() chan [3]string // [3]string{command, out, flag} +} diff --git a/pkg/proxy/k8sproxy.go b/pkg/proxy/k8sproxy.go new file mode 100644 index 000000000..c2c0c0647 --- /dev/null +++ b/pkg/proxy/k8sproxy.go @@ -0,0 +1,251 @@ +package proxy + +import ( + "encoding/json" + "errors" + "fmt" + "os/exec" + "strings" + "time" + + "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" +) + +var _ proxyEngine = (*K8sProxyServer)(nil) + +type K8sProxyServer struct { + UserConn UserConnection + User *model.User + Cluster *model.K8sCluster + SystemUser *model.SystemUser +} + +func (p *K8sProxyServer) checkProtocolMatch() bool { + return strings.EqualFold(p.Cluster.Type, p.SystemUser.Protocol) +} + +func (p *K8sProxyServer) checkProtocolClientInstalled() bool { + switch strings.ToLower(p.Cluster.Type) { + case "k8s": + return IsInstalledKubectlClient() + } + return false +} + +// validatePermission 检查是否有权限连接 +func (p *K8sProxyServer) validatePermission() bool { + return service.ValidateUserK8sPermission(p.User.ID, p.Cluster.ID, p.SystemUser.ID) +} + +// getSSHConn 获取ssh连接 +func (p *K8sProxyServer) getK8sConConn() (srvConn *srvconn.K8sCon, err error) { + srvConn = srvconn.NewK8sCon( + srvconn.K8sToken(p.SystemUser.Token), + srvconn.K8sClusterServer(p.Cluster.Cluster), + srvconn.K8sUsername(p.SystemUser.Username), + srvconn.K8sSkipTls(true), + ) + err = srvConn.Connect() + return +} + +// getServerConn 获取获取server连接 +func (p *K8sProxyServer) getServerConn() (srvConn srvconn.ServerConnection, err error) { + done := make(chan struct{}) + defer func() { + utils.IgnoreErrWriteString(p.UserConn, "\r\n") + close(done) + }() + go p.sendConnectingMsg(done) + return p.getK8sConConn() +} + +// sendConnectingMsg 发送连接信息 +func (p *K8sProxyServer) sendConnectingMsg(done chan struct{}) { + delay := 0.1 + msg := fmt.Sprintf(i18n.T("Connecting to Kubernetes %s %.1f"), p.Cluster.Cluster, delay) + utils.IgnoreErrWriteString(p.UserConn, msg) + for { + 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 *K8sProxyServer) preCheckRequisite() (ok bool) { + if !p.checkProtocolMatch() { + msg := utils.WrapperWarn(i18n.T("System user <%s> and kubernetes <%s> protocol are inconsistent.")) + msg = fmt.Sprintf(msg, p.SystemUser.Username, p.Cluster.Type) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Errorf("Conn[%s] checking protocol matched failed: %s", p.UserConn.ID(), msg) + return + } + logger.Infof("Conn[%s] System user and k8s protocol matched", p.UserConn.ID()) + if !p.checkProtocolClientInstalled() { + msg := utils.WrapperWarn(i18n.T("%s protocol client not installed.")) + msg = fmt.Sprintf(msg, p.Cluster.Type) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Errorf("Conn[%s] %s", p.UserConn.ID(), msg) + return + } + logger.Infof("Conn[%s] System user protocol %s supported", p.UserConn.ID(), p.SystemUser.Protocol) + if !p.validatePermission() { + msg := utils.WrapperWarn(i18n.T("You don't have permission login %s")) + msg = fmt.Sprintf(msg, p.Cluster.Cluster) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Errorf("Conn[%s] get k8s %s permission failed", p.UserConn.ID(), p.Cluster.Cluster) + return + } + logger.Infof("Conn[%s] has permission to access k8s %s", p.UserConn.ID(), p.Cluster.Cluster) + if err := p.checkRequiredAuth(); err != nil { + msg := utils.WrapperWarn(i18n.T("You get auth token failed")) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Errorf("Conn[%s] get k8s %s auth info failed: %s", p.UserConn.ID(), p.Cluster.Cluster, err) + return + } + return true +} + +func (p *K8sProxyServer) checkRequiredAuth() error { + info := service.GetUserK8sAuthToken(p.SystemUser.ID) + if info.Token == "" { + return errors.New("no auth token") + } + p.SystemUser.Token = info.Token + logger.Infof("Conn[%s] get k8s %s auth info from JMS core success", + p.UserConn.ID(), p.Cluster.Cluster) + return nil +} + +// sendConnectErrorMsg 发送连接错误消息 +func (p *K8sProxyServer) sendConnectErrorMsg(err error) { + msg := fmt.Sprintf("Connect K8s %s error: %s\r\n", p.Cluster.Cluster, err) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Error(msg) + token := p.SystemUser.Token + if token != "" { + tokenLen := len(token) + showLen := tokenLen / 2 + hiddenLen := tokenLen - showLen + msg2 := fmt.Sprintf("Try token: %s", token[:showLen]+strings.Repeat("*", hiddenLen)) + logger.Errorf(msg2) + } +} + +// Proxy 代理 +func (p *K8sProxyServer) Proxy() { + if !p.preCheckRequisite() { + logger.Errorf("Conn[%s] Check requisite failed", p.UserConn.ID()) + return + } + logger.Infof("Conn[%s] checking pre requisite success", p.UserConn.ID()) + // 创建Session + sw, ok := CreateCommonSwitch(p) + logger.Info("Create Common Switch", ok) + if !ok { + msg := i18n.T("Create k8s session failed") + msg = utils.WrapperWarn(msg) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Error(msg) + return + } + logger.Infof("Conn[%s] create k8s session %s success", p.UserConn.ID(), sw.ID) + defer RemoveCommonSwitch(sw) + srvConn, err := p.getServerConn() + // 连接后端服务器失败 + if err != nil { + logger.Errorf("Conn[%s] create k8s conn failed: %s", p.UserConn.ID(), err) + p.sendConnectErrorMsg(err) + return + } + logger.Infof("Conn[%s] get k8s conn success", p.UserConn.ID()) + _ = sw.Bridge(p.UserConn, srvConn) + logger.Infof("Conn[%s] end k8s session %s bridge", p.UserConn.ID(), sw.ID) + +} + +func (p *K8sProxyServer) GenerateRecordCommand(s *commonSwitch, input, output string, + riskLevel int64) *model.Command { + return &model.Command{ + SessionID: s.ID, + OrgID: p.Cluster.OrgID, + Input: input, + Output: output, + User: fmt.Sprintf("%s (%s)", p.User.Name, p.User.Username), + Server: fmt.Sprintf("%s (%s)", p.Cluster.Name, p.Cluster.Cluster), + SystemUser: fmt.Sprintf("%s (%s)", p.SystemUser.Name, p.SystemUser.Username), + Timestamp: time.Now().Unix(), + RiskLevel: riskLevel, + } +} + +func (p *K8sProxyServer) NewParser(s *commonSwitch) ParseEngine { + shellParser := newParser(s.ID) + msg := i18n.T("Create k8s session failed") + if cmdRules, err := service.GetSystemUserFilterRules(p.SystemUser.ID); err == nil { + shellParser.SetCMDFilterRules(cmdRules) + } else { + msg = utils.WrapperWarn(msg) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Error(msg + err.Error()) + } + return &shellParser +} + +func (p *K8sProxyServer) MapData(s *commonSwitch) map[string]interface{} { + var dataEnd interface{} + if s.DateEnd != "" { + dataEnd = s.DateEnd + } + return map[string]interface{}{ + "id": s.ID, + "user": fmt.Sprintf("%s (%s)", p.User.Name, p.User.Username), + "asset": p.Cluster.Cluster, + "org_id": p.Cluster.OrgID, + "login_from": p.UserConn.LoginFrom(), + "system_user": fmt.Sprintf("%s (%s)", p.SystemUser.Name, p.SystemUser.Username), + "protocol": p.SystemUser.Protocol, + "remote_addr": p.UserConn.RemoteAddr(), + "is_finished": s.finished, + "date_start": s.DateStart, + "date_end": dataEnd, + "user_id": p.User.ID, + "asset_id": p.Cluster.ID, + "system_user_id": p.SystemUser.ID, + "is_success": s.isConnected, + } +} + +func IsInstalledKubectlClient() bool { + checkLine := "kubectl version --client -o json" + cmd := exec.Command("bash", "-c", checkLine) + out, err := cmd.CombinedOutput() + if err != nil && len(out) == 0 { + logger.Errorf("Check kubectl client failed %s", err, out) + return false + } + var result map[string]interface{} + err = json.Unmarshal(out, &result) + if err != nil { + logger.Errorf("Check kubectl client failed %s %s", err, out) + return false + } + if _, ok := result["clientVersion"]; ok { + return true + } + logger.Errorf("Check kubectl client failed: %s", out) + return false +} diff --git a/pkg/proxy/parser.go b/pkg/proxy/parser.go index ddc9c348b..2ac32c0b3 100644 --- a/pkg/proxy/parser.go +++ b/pkg/proxy/parser.go @@ -12,10 +12,6 @@ import ( ) var ( - // Todo: Vim过滤依然存在问题 - vimEnterMark = []byte("\x1b[?25l\x1b[37;1H\x1b[1m") - vimExitMark = []byte("\x1b[37;1H\x1b[K\x1b") - zmodemRecvStartMark = []byte("rz waiting to receive.**\x18B0100") zmodemSendStartMark = []byte("**\x18B00000000000000") zmodemCancelMark = []byte("\x18\x18\x18\x18\x18") @@ -24,6 +20,20 @@ var ( zmodemStateRecv = "recv" charEnter = []byte("\r") + + enterMarks = [][]byte{ + []byte("\x1b[?1049h"), + []byte("\x1b[?1048h"), + []byte("\x1b[?1047h"), + []byte("\x1b[?47h"), + } + + exitMarks = [][]byte{ + []byte("\x1b[?1049l"), + []byte("\x1b[?1048l"), + []byte("\x1b[?1047l"), + []byte("\x1b[?47l"), + } ) const ( @@ -31,6 +41,8 @@ const ( CommandOutputParserName = "Command Output parser" ) +var _ ParseEngine = (*Parser)(nil) + func newParser(sid string) Parser { parser := Parser{id: sid} parser.initial() @@ -201,11 +213,11 @@ func (p *Parser) parseZmodemState(b []byte) { // parseVimState 解析vim的状态,处于vim状态中,里面输入的命令不再记录 func (p *Parser) parseVimState(b []byte) { - if p.zmodemState == "" && !p.inVimState && bytes.Contains(b, vimEnterMark) { + if p.zmodemState == "" && !p.inVimState && IsEditEnterMode(b) { p.inVimState = true logger.Debug("In vim state: true") } - if p.zmodemState == "" && p.inVimState && bytes.Contains(b, vimExitMark) { + if p.zmodemState == "" && p.inVimState && IsEditExitMode(b) { p.inVimState = false logger.Debug("In vim state: false") } @@ -282,3 +294,28 @@ func (p *Parser) sendCommandRecord() { p.output = "" } } + +func (p *Parser) NeedRecord() bool { + return !p.IsInZmodemRecvState() +} + +func (p *Parser) CommandRecordChan() chan [3]string { + return p.cmdRecordChan +} + +func IsEditEnterMode(p []byte) bool { + return matchMark(p, enterMarks) +} + +func IsEditExitMode(p []byte) bool { + return matchMark(p, exitMarks) +} + +func matchMark(p []byte, marks [][]byte) bool { + for _, item := range marks { + if bytes.Contains(p, item) { + return true + } + } + return false +} diff --git a/pkg/proxy/parsercmd.go b/pkg/proxy/parsercmd.go index e003998e2..a4f3c352a 100644 --- a/pkg/proxy/parsercmd.go +++ b/pkg/proxy/parsercmd.go @@ -35,7 +35,7 @@ func (cp *CmdParser) WriteData(p []byte) (int, error) { } func (cp *CmdParser) Close() error { - logger.Infof("session ID: %s, parser name: %s Close", cp.id, cp.name) + logger.Infof("session ID: %s, ParseEngine name: %s Close", cp.id, cp.name) return nil } diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 5d03a2ba8..4edfa1980 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -15,6 +15,8 @@ import ( "github.com/jumpserver/koko/pkg/utils" ) +var _ proxyEngine = (*ProxyServer)(nil) + type ProxyServer struct { UserConn UserConnection User *model.User @@ -348,16 +350,19 @@ func (p *ProxyServer) Proxy() { } logger.Infof("Conn[%s] checking pre requisite success", p.UserConn.ID()) // 创建Session - sw, err := CreateSession(p) - if err != nil { + sw, ok := CreateCommonSwitch(p) + if !ok { + msg := i18n.T("Connect with api server failed") if p.cacheSSHConnection != nil { _ = p.cacheSSHConnection.Close() } - logger.Errorf("Conn[%s] create session failed: %s", p.UserConn.ID(), err) + msg = utils.WrapperWarn(msg) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Errorf("Conn[%s] submit session %s to core server err: %s", p.UserConn.ID(), msg) return } logger.Infof("Conn[%s] create session %s success", p.UserConn.ID(), sw.ID) - defer RemoveSession(sw) + defer RemoveCommonSwitch(sw) srvConn, err := p.getServerConn() // 连接后端服务器失败 if err != nil { @@ -369,3 +374,56 @@ func (p *ProxyServer) Proxy() { _ = sw.Bridge(p.UserConn, srvConn) logger.Infof("Conn[%s] end session %s bridge", p.UserConn.ID(), sw.ID) } + +func (p *ProxyServer) MapData(s *commonSwitch) map[string]interface{} { + var dataEnd interface{} + if s.DateEnd != "" { + dataEnd = s.DateEnd + } + return map[string]interface{}{ + "id": s.ID, + "user": fmt.Sprintf("%s (%s)", p.User.Name, p.User.Username), + "asset": p.Asset.Hostname, + "org_id": p.Asset.OrgID, + "login_from": p.UserConn.LoginFrom(), + "system_user": p.SystemUser.Username, + "protocol": p.SystemUser.Protocol, + "remote_addr": p.UserConn.RemoteAddr(), + "is_finished": s.finished, + "date_start": s.DateStart, + "date_end": dataEnd, + "user_id": p.User.ID, + "asset_id": p.Asset.ID, + "system_user_id": p.SystemUser.ID, + "is_success": s.isConnected, + } +} + +func (p *ProxyServer) NewParser(s *commonSwitch) ParseEngine { + shellParser := newParser(s.ID) + msg := i18n.T("Create session failed") + if cmdRules, err := service.GetSystemUserFilterRules(p.SystemUser.ID); err == nil { + logger.Infof("Conn[%s] get command filter rules success", p.UserConn.ID()) + shellParser.SetCMDFilterRules(cmdRules) + } else { + msg = utils.WrapperWarn(msg) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Error(msg + err.Error()) + } + return &shellParser +} + +func (p *ProxyServer) GenerateRecordCommand(s *commonSwitch, input, output string, + riskLevel int64) *model.Command { + return &model.Command{ + SessionID: s.ID, + OrgID: p.Asset.OrgID, + Input: input, + Output: output, + User: fmt.Sprintf("%s (%s)", p.User.Name, p.User.Username), + Server: p.Asset.Hostname, + SystemUser: p.SystemUser.Username, + Timestamp: time.Now().Unix(), + RiskLevel: riskLevel, + } +} diff --git a/pkg/proxy/sessmanager.go b/pkg/proxy/sessmanager.go index 46c5b4f16..be1e18472 100644 --- a/pkg/proxy/sessmanager.go +++ b/pkg/proxy/sessmanager.go @@ -1,15 +1,12 @@ package proxy import ( - "errors" "sync" "time" - "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/utils" ) var sessionMap = make(map[string]Session) @@ -51,14 +48,6 @@ func GetAliveSessions() []string { return sids } -func RemoveSession(sw *SwitchSession) { - lock.Lock() - defer lock.Unlock() - delete(sessionMap, sw.ID) - data := sw.MapData() - finishSession(data) - logger.Infof("Session %s has finished", sw.ID) -} func AddSession(sw Session) { lock.Lock() @@ -66,34 +55,6 @@ func AddSession(sw Session) { sessionMap[sw.SessionID()] = sw } -func CreateSession(p *ProxyServer) (sw *SwitchSession, err error) { - // 创建Session - sw = NewSwitchSession(p) - // Post到Api端 - data := sw.MapData() - ok := postSession(data) - msg := i18n.T("Connect with api server failed") - if !ok { - msg = utils.WrapperWarn(msg) - utils.IgnoreErrWriteString(p.UserConn, msg) - logger.Errorf("Conn[%s] submit session %s to core server err: %s", p.UserConn.ID(), msg) - return sw, errors.New("connect api server failed") - } - logger.Infof("Conn[%s] submit session %s to core server success", p.UserConn.ID(), sw.ID) - // 获取系统用户的过滤规则,并设置 - cmdRules, err := service.GetSystemUserFilterRules(p.SystemUser.ID) - if err != nil { - msg = utils.WrapperWarn(msg) - utils.IgnoreErrWriteString(p.UserConn, msg) - logger.Errorf("Conn[%s] get filter rules from core server err: %s", - p.UserConn.ID(), err) - return sw, errors.New("connect api server failed") - } - logger.Infof("Conn[%s] get filter rules from core server success", p.UserConn.ID()) - sw.SetFilterRules(cmdRules) - AddSession(sw) - return -} func postSession(data map[string]interface{}) bool { for i := 0; i < 5; i++ { @@ -109,40 +70,19 @@ func finishSession(data map[string]interface{}) { service.FinishSession(data) } -func CreateDBSession(p *DBProxyServer) (sw *DBSwitchSession, err error) { - // 创建Session - sw = &DBSwitchSession{ - p: p, - } - sw.Initial() - logger.Infof("Conn[%s] create DB session %s", p.UserConn.ID(), sw.ID) - 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") - } - logger.Infof("Conn[%s] submit DB session %s to server success", p.UserConn.ID(), sw.ID) - cmdRules, err := service.GetSystemUserFilterRules(p.SystemUser.ID) - if err != nil { - msg = utils.WrapperWarn(msg) - utils.IgnoreErrWriteString(p.UserConn, msg) - logger.Error(msg + err.Error()) - return sw, errors.New("connect api server failed") +func CreateCommonSwitch(p proxyEngine) (s *commonSwitch, ok bool) { + s = NewCommonSwitch(p) + ok = postSession(s.MapData()) + if ok { + AddSession(s) } - logger.Infof("Conn[%s] get filter rules success", p.UserConn.ID()) - sw.SetFilterRules(cmdRules) - AddSession(sw) - return + return s, ok } -func RemoveDBSession(sw *DBSwitchSession) { +func RemoveCommonSwitch(s *commonSwitch) { lock.Lock() defer lock.Unlock() - delete(sessionMap, sw.ID) - finishSession(sw.MapData()) - logger.Infof("DB Session %s has finished", sw.ID) + delete(sessionMap, s.ID) + finishSession(s.MapData()) + logger.Infof("Session %s has finished", s.ID) } diff --git a/pkg/service/database.go b/pkg/service/database.go index 66635e8b9..5c49947f0 100644 --- a/pkg/service/database.go +++ b/pkg/service/database.go @@ -2,6 +2,7 @@ package service import ( "fmt" + "strconv" "github.com/jumpserver/koko/pkg/logger" "github.com/jumpserver/koko/pkg/model" @@ -34,7 +35,6 @@ func GetSystemUserDatabaseAuthInfo(systemUserID string) (info model.SystemUserAu return } - func GetDatabase(dbID string) (res model.Database) { Url := fmt.Sprintf(DatabaseDetailURL, dbID) _, err := authClient.Get(Url, &res) @@ -42,4 +42,36 @@ func GetDatabase(dbID string) (res model.Database) { logger.Errorf("Get User databases err: %s", err) } return -} \ No newline at end of file +} + +func GetUserPaginationDatabases(userID string, pageSize, offset int, searches ...string) (resp model.DatabasesPaginationResponse) { + 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": searches[i], + }) + } + params := map[string]string{ + "limit": strconv.Itoa(pageSize), + "offset": strconv.Itoa(offset), + } + paramsArray = append(paramsArray, params) + Url := fmt.Sprintf(DatabaseAPPURL, userID) + var err error + if pageSize > 0 { + _, err = authClient.Get(Url, &resp, paramsArray...) + } else { + var data []model.Database + _, err = authClient.Get(Url, &data, paramsArray...) + resp.Data = data + resp.Total = len(data) + } + + if err != nil { + logger.Errorf("Get User databases err: %s", err) + } + return +} diff --git a/pkg/service/k8s.go b/pkg/service/k8s.go new file mode 100644 index 000000000..a102cdd16 --- /dev/null +++ b/pkg/service/k8s.go @@ -0,0 +1,86 @@ +package service + +import ( + "fmt" + "strconv" + + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/model" +) + +func ValidateUserK8sPermission(userID, clusterID, systemUserID string) bool { + payload := map[string]string{ + "user_id": userID, + "k8s_app_id": clusterID, + "system_user_id": systemUserID, + } + Url := ValidateUserK8sPermissionURL + var res struct { + Msg bool `json:"msg"` + } + _, err := authClient.Get(Url, &res, payload) + if err != nil { + logger.Error(err) + return false + } + + return res.Msg +} + +func GetUserK8sClusters(userID string, pageSize, offset int, searches ...string) (resp model.K8sClustersPaginationResponse) { + 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": searches[i], + }) + } + params := map[string]string{ + "limit": strconv.Itoa(pageSize), + "offset": strconv.Itoa(offset), + } + paramsArray = append(paramsArray, params) + Url := fmt.Sprintf(K8sPemClustersURL, userID) + var err error + if pageSize > 0 { + _, err = authClient.Get(Url, &resp, paramsArray...) + } else { + var data []model.K8sCluster + _, err = authClient.Get(Url, &data, paramsArray...) + resp.Data = data + resp.Total = len(data) + } + if err != nil { + logger.Error("Get user K8s Clusters error: ", err) + } + return +} + +func GetUserK8sSystemUsers(userID, k8sId string) (sysUsers []model.SystemUser) { + Url := fmt.Sprintf(K8sSystemUsersURL, userID, k8sId) + _, err := authClient.Get(Url, &sysUsers) + if err != nil { + logger.Error("Get user k8s system users error: ", err) + } + return +} + +func GetUserK8sAuthToken(systemUserID string) (info model.SystemUserAuthInfo) { + Url := fmt.Sprintf(SystemUserAuthURL, systemUserID) + _, err := authClient.Get(Url, &info) + if err != nil { + logger.Errorf("Get system user %s auth info failed", systemUserID) + } + return +} + +func GetK8sCluster(k8sId string) (res model.K8sCluster) { + Url := fmt.Sprintf(K8sClusterDetailURL, k8sId) + _, err := authClient.Get(Url, &res) + if err != nil { + logger.Errorf("Get User k8s err: %s", err) + } + return +} diff --git a/pkg/service/urls.go b/pkg/service/urls.go index cb0b51571..e0b3c9fc9 100644 --- a/pkg/service/urls.go +++ b/pkg/service/urls.go @@ -29,16 +29,9 @@ const ( UserNodesListURL = "/api/v1/perms/users/%s/nodes/" UserNodeAssetsListURL = "/api/v1/perms/users/%s/nodes/%s/assets/" ValidateUserAssetPermissionURL = "/api/v1/perms/asset-permissions/user/validate/" //0不使用缓存 1 使用缓存 2 刷新缓存 -) - -// 1.5.3 -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/" @@ -66,6 +59,16 @@ const ( JoinRoomValidateURL = "/api/v1/terminal/sessions/join/validate/" ) -const ( +const ( AssetPlatFormURL = "/api/v1/assets/assets/%s/platform/" -) \ No newline at end of file +) + +const ( + ValidateUserK8sPermissionURL = "/api/v1/perms/k8s-app-permissions/user/validate/" + + K8sPemClustersURL = "/api/v1/perms/users/%s/k8s-apps/" //数据库app + + K8sSystemUsersURL = "/api/v1/perms/users/%s/k8s-apps/%s/system-users/" + + K8sClusterDetailURL = "/api/v1/applications/k8s-apps/%s/" +) diff --git a/pkg/srvconn/k8s.go b/pkg/srvconn/k8s.go new file mode 100644 index 000000000..156355e7d --- /dev/null +++ b/pkg/srvconn/k8s.go @@ -0,0 +1,211 @@ +package srvconn + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "syscall" + + "github.com/creack/pty" + + "github.com/jumpserver/koko/pkg/config" + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/utils" +) + +var ( + InValidToken = errors.New("invalid token") + + _ ServerConnection = (*K8sCon)(nil) +) + +const ( + k8sInitFilename = "init-kubectl.sh" + + checkTokenCommand = `kubectl --insecure-skip-tls-verify=%s --token=%s --server=%s auth can-i get pods` +) + +func isValidK8sUserToken(o *k8sOptions) bool { + skipVerifyTls := "true" + token := o.Token + server := o.ClusterServer + if !o.IsSkipTls { + skipVerifyTls = "false" + } + c := exec.Command("bash", "-c", + fmt.Sprintf(checkTokenCommand, skipVerifyTls, token, server)) + out, err := c.CombinedOutput() + if err != nil { + logger.Info(err) + } + result := strings.TrimSpace(string(out)) + switch strings.ToLower(result) { + case "yes", "no": + logger.Info("K8sCon check token success") + return true + } + logger.Errorf("K8sCon check token err: %s", result) + return false +} + +func NewK8sCon(ops ...K8sOption) *K8sCon { + args := &k8sOptions{ + Username: os.Getenv("USER"), + ClusterServer: "https://127.0.0.1:8443", + Token: "", + IsSkipTls: true, + ExtraEnv: map[string]string{}, + } + for _, setter := range ops { + setter(args) + } + return &K8sCon{options: args} +} + +type K8sCon struct { + options *k8sOptions + ptyFD *os.File + onceClose sync.Once + cmd *exec.Cmd +} + +func (c *K8sCon) Connect() (err error) { + if !isValidK8sUserToken(c.options) { + return InValidToken + } + cmd, ptyFD, err := connectK8s(c) + go func() { + err = cmd.Wait() + if err != nil { + logger.Errorf("K8sCon command exit err: %s", err) + } + if ptyFD != nil { + _ = ptyFD.Close() + } + logger.Info("K8sCon connect closed.") + var wstatus syscall.WaitStatus + _, err = syscall.Wait4(-1, &wstatus, 0, nil) + }() + if err != nil { + logger.Errorf("K8sCon pty start err: %s", err) + return fmt.Errorf("K8sCon start local pty err: %s", err) + } + + logger.Infof("Connect K8s cluster server %s success ", c.options.ClusterServer) + c.cmd = cmd + c.ptyFD = ptyFD + return +} + +func (c *K8sCon) Read(p []byte) (int, error) { + return c.ptyFD.Read(p) +} + +func (c *K8sCon) Write(p []byte) (int, error) { + return c.ptyFD.Write(p) +} + +func (c *K8sCon) SetWinSize(w, h int) error { + win := pty.Winsize{ + Rows: uint16(h), + Cols: uint16(w), + } + logger.Infof("K8sCon conn windows size change %d*%d", h, w) + return pty.Setsize(c.ptyFD, &win) +} + +func (c *K8sCon) Protocol() string { + return "k8s" +} + +func (c *K8sCon) Close() (err error) { + c.onceClose.Do(func() { + if c.ptyFD == nil { + return + } + _ = c.ptyFD.Close() + err = c.cmd.Process.Signal(os.Kill) + }) + return +} + +type k8sOptions struct { + ClusterServer string // https://172.16.10.51:8443 + Username string // user 系统用户名 + Token string // 授权token + IsSkipTls bool + ExtraEnv map[string]string +} + +func (o *k8sOptions) Env() []string { + token, err := utils.Encrypt(o.Token, config.CipherKey) + if err != nil { + logger.Errorf("Encrypt k8s token err: %s", err) + token = o.Token + } + skipTls := "true" + if !o.IsSkipTls { + skipTls = "false" + } + return []string{ + fmt.Sprintf("KUBECTL_USER=%s", o.Username), + fmt.Sprintf("KUBECTL_CLUSTER=%s", o.ClusterServer), + fmt.Sprintf("KUBECTL_INSECURE_SKIP_TLS_VERIFY=%s", skipTls), + fmt.Sprintf("K8S_ENCRYPTED_TOKEN=%s", token), + fmt.Sprintf("WELCOME_BANNER=%s", config.KubectlBanner), + } +} +func connectK8s(con *K8sCon) (cmd *exec.Cmd, ptyFD *os.File, err error) { + return connectK8sWithNamespace(con.options.Env()) +} + +func connectK8sWithNamespace(envs []string) (cmd *exec.Cmd, ptyFD *os.File, err error) { + pwd, _ := os.Getwd() + shPath := filepath.Join(pwd, k8sInitFilename) + args := []string{ + "--fork", + "--pid", + "--mount-proc", + shPath, + } + cmd = exec.Command("unshare", args...) + cmd.Env = envs + ptyFD, err = pty.Start(cmd) + return +} + +type K8sOption func(*k8sOptions) + +func K8sUsername(username string) K8sOption { + return func(args *k8sOptions) { + args.Username = username + } +} + +func K8sToken(token string) K8sOption { + return func(args *k8sOptions) { + args.Token = token + } +} + +func K8sClusterServer(clusterServer string) K8sOption { + return func(args *k8sOptions) { + args.ClusterServer = clusterServer + } +} + +func K8sExtraEnvs(envs map[string]string) K8sOption { + return func(args *k8sOptions) { + args.ExtraEnv = envs + } +} + +func K8sSkipTls(isSkipTls bool) K8sOption { + return func(args *k8sOptions) { + args.IsSkipTls = isSkipTls + } +} diff --git a/pkg/srvconn/mysqlconn.go b/pkg/srvconn/mysqlconn.go index e7af8ed28..1a64d0e2f 100644 --- a/pkg/srvconn/mysqlconn.go +++ b/pkg/srvconn/mysqlconn.go @@ -34,7 +34,8 @@ mount -t tmpfs -o size=10M tmpfs /nonexistent cd /nonexistent export HOME=/nonexistent export TMPDIR=/nonexistent -exec su -s /bin/bash --command="mysql --default-character-set=utf8 --user=${USERNAME} --host=${HOSTNAME} --port=${PORT} --password ${DATABASE}" nobody +export LANG=en_US.UTF-8 +exec su -s /bin/bash --command="mysql --user=${USERNAME} --host=${HOSTNAME} --port=${PORT} --password ${DATABASE}" nobody ` var mysqlOnce sync.Once @@ -249,7 +250,6 @@ type SqlOptions struct { func (opts *SqlOptions) CommandArgs() []string { return []string{ - "--default-character-set=utf8", fmt.Sprintf("--user=%s", opts.Username), fmt.Sprintf("--host=%s", opts.Host), fmt.Sprintf("--port=%d", opts.Port), diff --git a/pkg/srvconn/sftpconn.go b/pkg/srvconn/sftpconn.go index 8e9292176..2be0c0ef7 100644 --- a/pkg/srvconn/sftpconn.go +++ b/pkg/srvconn/sftpconn.go @@ -207,7 +207,7 @@ func (u *UserSftpConn) Name() string { func (u *UserSftpConn) Size() int64 { return 0 } func (u *UserSftpConn) Mode() os.FileMode { - return os.ModePerm | os.ModeDir + return os.FileMode(0444) | os.ModeDir } func (u *UserSftpConn) ModTime() time.Time { return u.modeTime } diff --git a/pkg/srvconn/sftpfile.go b/pkg/srvconn/sftpfile.go index 757111361..e7abd610b 100644 --- a/pkg/srvconn/sftpfile.go +++ b/pkg/srvconn/sftpfile.go @@ -39,7 +39,7 @@ func (sd *SearchResultDir) Name() string { func (sd *SearchResultDir) Size() int64 { return 0 } func (sd *SearchResultDir) Mode() os.FileMode { - return os.ModePerm | os.ModeDir + return os.FileMode(0444) | os.ModeDir } func (sd *SearchResultDir) ModTime() time.Time { return sd.modeTime } @@ -92,7 +92,7 @@ func (nd *NodeDir) Name() string { func (nd *NodeDir) Size() int64 { return 0 } func (nd *NodeDir) Mode() os.FileMode { - return os.ModePerm | os.ModeDir + return os.FileMode(0444) | os.ModeDir } func (nd *NodeDir) ModTime() time.Time { return nd.modeTime } @@ -248,7 +248,10 @@ func (ad *AssetDir) Name() string { func (ad *AssetDir) Size() int64 { return 0 } func (ad *AssetDir) Mode() os.FileMode { - return os.ModePerm | os.ModeDir + if len(ad.suMaps) > 1 { + return os.FileMode(0444) | os.ModeDir + } + return os.FileMode(0644) | os.ModeDir } func (ad *AssetDir) ModTime() time.Time { return ad.modeTime } diff --git a/pkg/utils/aes.go b/pkg/utils/aes.go new file mode 100644 index 000000000..3957bb75c --- /dev/null +++ b/pkg/utils/aes.go @@ -0,0 +1,91 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "io" +) + +func Encrypt(src, cipherKey string) (dst string, err error) { + var encryptResult []byte + encryptResult, err = aseGcmEncrypt([]byte(src), cipherKey) + if err != nil { + return + } + dst = encodeBase64(encryptResult) + return +} + +func Decrypt(src string, cipherKey string) (dst string, err error) { + var ( + encryptResult []byte + decryptResult []byte + ) + encryptResult, err = decodeBase64(src) + if err != nil { + return + } + decryptResult, err = aseGcmDecrypt(encryptResult, cipherKey) + if err != nil { + return + } + dst = string(decryptResult) + return +} + +var ErrGcmSize = errors.New("cipher: incorrect size given to GCM") + +func encodeBase64(src []byte) string { + return base64.StdEncoding.EncodeToString(src) +} + +func decodeBase64(src string) ([]byte, error) { + return base64.StdEncoding.DecodeString(src) +} + +func aseGcmEncrypt(plainText []byte, cipherKey string) (result []byte, err error) { + var ( + aesBlock cipher.Block + gcm cipher.AEAD + ) + aesBlock, err = aes.NewCipher([]byte(cipherKey)) + if err != nil { + return + } + gcm, err = cipher.NewGCM(aesBlock) + if err != nil { + return + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return + } + result = gcm.Seal(nonce, nonce, plainText, nil) + return +} + +func aseGcmDecrypt(encryptText []byte, cipherKey string) (result []byte, err error) { + var ( + aesBlock cipher.Block + gcm cipher.AEAD + ) + aesBlock, err = aes.NewCipher([]byte(cipherKey)) + if err != nil { + return + } + gcm, err = cipher.NewGCM(aesBlock) + if err != nil { + return + } + nonceSize := gcm.NonceSize() + if len(encryptText) < nonceSize { + err = ErrGcmSize + return + } + nonce, cipherText := encryptText[:nonceSize], encryptText[nonceSize:] + return gcm.Open(nil, nonce, cipherText, nil) +} diff --git a/pkg/utils/aes_test.go b/pkg/utils/aes_test.go new file mode 100644 index 000000000..e344c5488 --- /dev/null +++ b/pkg/utils/aes_test.go @@ -0,0 +1,25 @@ +package utils + +import ( + "testing" +) + +func TestDecrypt(t *testing.T) { + var cipherKey = "JumpServer Cipher Key for KoKo !" + text := "JumpServer Token Value" + t.Log("Encrypt Text: ", text) + dst, err := Encrypt(text, cipherKey) + if err != nil { + t.Fatal(err) + } + t.Logf("Encrypt '%s' to '%s'", text, dst) + decryptResult, err := Decrypt(dst, cipherKey) + if err != nil { + t.Fatal(err) + } + t.Logf("Decrypt '%s' to '%s'", dst, decryptResult) + if decryptResult != text { + t.Fatalf("Decrypt %s error: %s\n", text, decryptResult) + } + +} diff --git a/pkg/utils/database.go b/pkg/utils/database.go deleted file mode 100644 index 85540e9c8..000000000 --- a/pkg/utils/database.go +++ /dev/null @@ -1,23 +0,0 @@ -package utils - -import ( - "os/exec" - "os/user" -) - -func IsInstalledMysqlClient() bool { - if mysqlPath, err := exec.LookPath("mysql"); err == nil { - cmd := exec.Command(mysqlPath, "-V") - if err = cmd.Run(); err == nil { - return true - } - } - return false -} - -func IsUserExist(username string) bool { - if _, err := user.Lookup(username); err == nil { - return true - } - return false -} diff --git a/pkg/utils/util_test.go b/pkg/utils/util_test.go index 8641a531b..eccc69843 100644 --- a/pkg/utils/util_test.go +++ b/pkg/utils/util_test.go @@ -19,3 +19,15 @@ func TestWrapperString(t *testing.T) { s4 := WrapperWarn(s) fmt.Println(s4) } + +func TestIsInstalledKubectlClient(t *testing.T) { + if ok := IsInstalledKubectlClient(); !ok { + t.Fatal(ok) + } +} + +func TestIsInstalledMysqlClient(t *testing.T) { + if ok := IsInstalledMysqlClient(); !ok { + t.Fatal(ok) + } +} diff --git a/utils/build.sh b/utils/build.sh index 1ad3d8967..91aabbf23 100644 --- a/utils/build.sh +++ b/utils/build.sh @@ -1,5 +1,4 @@ -#!/bin/bash -# +#!/bin/sh # 该build基于 golang:1.12-alpine utils_dir=$(pwd) project_dir=$(dirname "$utils_dir") @@ -22,33 +21,35 @@ function install_git() { && apk add git } - -if [[ $(uname) == 'Darwin' ]];then - alias sedi="sed -i ''" -else - alias sedi='sed -i' -fi - - # 安装依赖包 command -v git || install_git - +kokoVersion='unknown' +goVersion="$(go version)" +gitHash="$(git rev-parse HEAD)" +buildStamp="$(date -u '+%Y-%m-%d %I:%M:%S%p')" +set +x +cipherKey="$(head -c 100 /dev/urandom | base64 | head -c 32)" # 修改版本号文件 if [[ -n "${VERSION-}" ]]; then - sedi "s@Version = .*@Version = \"${VERSION}\"@g" "${project_dir}/pkg/koko/koko.go" || exit 2 + kokoVersion="${VERSION}" fi - +goldflags="-X 'main.Buildstamp=$buildStamp' -X 'main.Githash=$gitHash' -X 'main.Goversion=$goVersion' -X 'github.com/jumpserver/koko/pkg/koko.Version=$kokoVersion' -X 'github.com/jumpserver/koko/pkg/config.CipherKey=$cipherKey'" +kubectlflags="-X 'github.com/jumpserver/koko/pkg/config.CipherKey=$cipherKey'" # 下载依赖模块并构建 cd .. && go mod download || exit 3 -cd cmd && CGO_ENABLED=0 GOOS="$OS" GOARCH="$ARCH" go build -ldflags "-X 'main.Buildstamp=`date -u '+%Y-%m-%d %I:%M:%S%p'`' -X 'main.Githash=`git rev-parse HEAD`' -X 'main.Goversion=`go version`'" -o koko koko.go || exit 4 +cd cmd && CGO_ENABLED=0 GOOS="$OS" GOARCH="$ARCH" go build -ldflags "$goldflags" -o koko koko.go || exit 4 +CGO_ENABLED=0 GOOS="$OS" GOARCH="$ARCH" go build -ldflags "$kubectlflags" -o kubectl kubectl.go || exit 4 +set -x # 打包 rm -rf "${release_dir:?}/*" to_dir="${release_dir}/koko" mkdir -p "${to_dir}" -for i in koko static templates locale config_example.yml;do +cp -r "${utils_dir}/init-kubectl.sh" "${to_dir}" + +for i in koko kubectl static templates locale config_example.yml;do cp -r $i "${to_dir}" done diff --git a/tools/coredump.sh b/utils/coredump.sh similarity index 100% rename from tools/coredump.sh rename to utils/coredump.sh diff --git a/utils/init-kubectl.sh b/utils/init-kubectl.sh new file mode 100644 index 000000000..9f27746a1 --- /dev/null +++ b/utils/init-kubectl.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -e + +if [ "${WELCOME_BANNER}" ]; then + echo ${WELCOME_BANNER} +fi + +mkdir -p /nonexistent +mount -t tmpfs -o size=10M tmpfs /nonexistent +cd /nonexistent +touch .bashrc +echo 'PS1="# "' >> .bashrc +echo "export TERM=xterm" >> .bashrc +echo "source /usr/share/bash-completion/bash_completion" >> .bashrc +echo 'source /opt/kubectl-aliases/.kubectl_aliases' >> .bashrc +echo 'source <(kubectl completion bash)' >> .bashrc +echo 'complete -F __start_kubectl k' >> .bashrc +mkdir -p .kube + +export HOME=/nonexistent + +echo `rawkubectl config set-credentials JumpServer-user` > /dev/null 2>&1 +echo `rawkubectl config set-cluster kubernetes --server=${KUBECTL_CLUSTER}` > /dev/null 2>&1 +echo `rawkubectl config set-context kubernetes --cluster=kubernetes --user=JumpServer-user` > /dev/null 2>&1 +echo `rawkubectl config use-context kubernetes` > /dev/null 2>&1 + +if [ ${KUBECTL_INSECURE_SKIP_TLS_VERIFY} == "true" ];then + { + clusters=`rawkubectl config get-clusters | tail -n +2` + for s in ${clusters[@]}; do + { + echo `rawkubectl config set-cluster ${s} --insecure-skip-tls-verify=true` > /dev/null 2>&1 + echo `rawkubectl config unset clusters.${s}.certificate-authority-data` > /dev/null 2>&1 + } || { + echo err > /dev/null 2>&1 + } + done + } || { + echo err > /dev/null 2>&1 + } +fi + +chown -R nobody:nogroup .kube + +export TMPDIR=/nonexistent + +exec su -s /bin/bash nobody \ No newline at end of file diff --git a/tools/message.sh b/utils/message.sh similarity index 100% rename from tools/message.sh rename to utils/message.sh