diff --git a/.gitignore b/.gitignore index 25851cf3e..c22f11649 100644 --- a/.gitignore +++ b/.gitignore @@ -24,10 +24,8 @@ release !config_example.yml .DS_Store -package-lock.json -node_modules - - +ui/node_modules/ +ui/dist/ # local env files .env.local @@ -48,7 +46,5 @@ pnpm-debug.log* *.sln *.sw? demo -assets/ -ui/dist/favicon.ico -ui/dist/index.html + docker-compose.yaml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3b0cec743..ce20bbf19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM jumpserver/koko-base:20240916_022847 AS stage-build +FROM jumpserver/koko-base:20241011_023915 AS stage-build + WORKDIR /opt/koko ARG TARGETARCH COPY . . @@ -12,9 +13,9 @@ RUN yarn build WORKDIR /opt/koko RUN make build -s \ && set -x && ls -al . \ - && mv /opt/koko/build/koko-linux-${TARGETARCH} /opt/koko/koko \ - && mv /opt/koko/build/helm-linux-${TARGETARCH} /opt/koko/bin/helm \ - && mv /opt/koko/build/kubectl-linux-${TARGETARCH} /opt/koko/bin/kubectl + && mv /opt/koko/build/koko /opt/koko/koko \ + && mv /opt/koko/build/helm /opt/koko/bin/helm \ + && mv /opt/koko/build/kubectl /opt/koko/bin/kubectl RUN mkdir /opt/koko/release \ && mv /opt/koko/locale /opt/koko/release \ diff --git a/Dockerfile-base b/Dockerfile-base index 73d7a4774..413416793 100644 --- a/Dockerfile-base +++ b/Dockerfile-base @@ -21,7 +21,8 @@ WORKDIR /opt ARG HELM_VERSION=v3.14.3 ARG KUBECTL_VERSION=v1.29.3 ARG CHECK_VERSION=v1.0.3 -ARG USQL_VERSION=v0.0.3 +ARG USQL_VERSION=v0.0.4 + RUN set -ex \ && mkdir -p /opt/koko/bin \ && wget -O kubectl.tar.gz https://dl.k8s.io/${KUBECTL_VERSION}/kubernetes-client-linux-${TARGETARCH}.tar.gz \ diff --git a/go.mod b/go.mod index cf83ae421..2dc4b7e4c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( github.com/Azure/azure-storage-blob-go v0.6.0 - github.com/LeeEirc/elfinder v0.0.14 + github.com/LeeEirc/elfinder v0.0.15 github.com/LeeEirc/httpsig v1.2.1 github.com/LeeEirc/tclientlib v0.0.3-0.20230803101925-fb52a90cb08d github.com/LeeEirc/terminalparser v0.0.0-20220328021224-de16b7643ea4 diff --git a/go.sum b/go.sum index 668d30db3..136e7addb 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/LeeEirc/elfinder v0.0.14 h1:6ObxwIoC5zmrnKArUU5Mz++/T3lzgl1Ja0pS1Smd3j4= -github.com/LeeEirc/elfinder v0.0.14/go.mod h1:d1bMAAydkZSBxSN/EuQjBg6B0xcPP3boHuYEpzEHYTs= +github.com/LeeEirc/elfinder v0.0.15 h1:ZnBJqkcbyt6zUgcGzhPHwsa88k0lhbNOa5rVsoJTG9s= +github.com/LeeEirc/elfinder v0.0.15/go.mod h1:d1bMAAydkZSBxSN/EuQjBg6B0xcPP3boHuYEpzEHYTs= github.com/LeeEirc/httpsig v1.2.1 h1:GGmCc2Bug3KeCchlZHwrfyjyAnw+JlzMjKDobPypirs= github.com/LeeEirc/httpsig v1.2.1/go.mod h1:aoLZLXCSNDgkzsH2sGLWn3hlVbF+Voe8fCArxLt9nWA= github.com/LeeEirc/tclientlib v0.0.3-0.20230803101925-fb52a90cb08d h1:4qUSGc/34IALiDs2kBrjbCKfx7zvAt16K+gTRzNN8Fo= diff --git a/locale/en_US/LC_MESSAGES/koko.po b/locale/en_US/LC_MESSAGES/koko.po index cf1bb6c12..6d6c0fc15 100644 --- a/locale/en_US/LC_MESSAGES/koko.po +++ b/locale/en_US/LC_MESSAGES/koko.po @@ -362,7 +362,7 @@ msgstr "" #. i18n.T #. i18n.T -#: pkg/handler/server_ssh.go:662 pkg/handler/server_ssh.go:666 +#: pkg/handler/server_ssh.go:680 pkg/handler/server_ssh.go:684 msgid "No found asset" msgstr "" @@ -370,43 +370,44 @@ msgstr "" #. i18n.T #. i18n.T #. lang.T -#: pkg/handler/server_ssh.go:695 pkg/handler/server_ssh.go:700 -#: pkg/handler/server_ssh.go:718 pkg/proxy/parser.go:226 +#: pkg/handler/server_ssh.go:713 pkg/handler/server_ssh.go:718 +#: pkg/handler/server_ssh.go:736 pkg/proxy/parser.go:230 msgid "have no permission to upload file" msgstr "" #. lang.T -#: pkg/proxy/parser.go:262 +#: pkg/proxy/parser.go:266 msgid "" "The command you executed is risky and an alert notification will be sent to " "the administrator. Do you want to continue?[Y/N]" msgstr "" #. lang.T -#: pkg/proxy/parser.go:278 -msgid "the reviewers will confirm. continue or not [Y/n]" +#: pkg/proxy/parser.go:282 +msgid "The command '%s' requires review. Continue or not [Y/n]?" msgstr "" #. lang.T #. lang.T #. lang.T -#: pkg/proxy/parser.go:298 pkg/proxy/parser.go:305 pkg/proxy/parser.go:453 +#: pkg/proxy/parser.go:302 pkg/proxy/parser.go:309 pkg/proxy/parser.go:463 msgid "Command `%s` is forbidden" msgstr "" #. lang.T -#: pkg/proxy/parser.go:524 +#: pkg/proxy/parser.go:534 msgid "have no permission to download file" msgstr "" #. lang.T -#: pkg/proxy/parser.go:598 +#: pkg/proxy/parser.go:608 msgid "" -"Please waiting for the reviewers to confirm command `%s`, cancel by CTRL+C or CTRL+D." +"Please waiting for the reviewers to confirm command `%s`, cancel by CTRL+C " +"or CTRL+D." msgstr "" #. lang.T -#: pkg/proxy/parser.go:608 +#: pkg/proxy/parser.go:618 msgid "" "Need ticket confirm to execute command, already send email to the reviewers" msgstr "" @@ -415,7 +416,7 @@ msgstr "" #. lang.T #. lang.T #. lang.T -#: pkg/proxy/parser.go:609 pkg/proxy/parser.go:610 pkg/proxy/server.go:58 +#: pkg/proxy/parser.go:619 pkg/proxy/parser.go:620 pkg/proxy/server.go:58 #: pkg/proxy/server.go:62 msgid "" "HandleTask does not support protocol %s, please use web terminal to access" @@ -427,45 +428,45 @@ msgid "Account <%s> and asset <%s> protocol are inconsistent." msgstr "" #. lang.T -#: pkg/proxy/server.go:100 +#: pkg/proxy/server.go:101 msgid "You don't have permission login %s" msgstr "" #. lang.T -#: pkg/proxy/server.go:336 +#: pkg/proxy/server.go:337 msgid "You get auth token failed" msgstr "" #. lang.T -#: pkg/proxy/server.go:347 +#: pkg/proxy/server.go:348 msgid "Get auth password failed" msgstr "" #. lang.T #. lang.T -#: pkg/proxy/server.go:363 pkg/proxy/server.go:420 +#: pkg/proxy/server.go:364 pkg/proxy/server.go:421 msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]" msgstr "" #. lang.T -#: pkg/proxy/server.go:676 +#: pkg/proxy/server.go:677 msgid "Switched to %s" msgstr "" #. lang.T #. lang.T -#: pkg/proxy/server.go:773 pkg/proxy/server.go:918 +#: pkg/proxy/server.go:774 pkg/proxy/server.go:919 msgid "Connect with api server failed" msgstr "" #. lang.T -#: pkg/proxy/server.go:963 +#: pkg/proxy/server.go:968 msgid "Start domain gateway failed %s" msgstr "" #. lang.T #. lang.T -#: pkg/proxy/server.go:971 pkg/proxy/server_options.go:108 +#: pkg/proxy/server.go:976 pkg/proxy/server_options.go:108 msgid "Manual" msgstr "" @@ -495,18 +496,18 @@ msgid "Connecting to Kubernetes %s container %s" msgstr "" #. lang.T -#: pkg/proxy/switch.go:285 +#: pkg/proxy/switch.go:325 msgid "Session max time reached, disconnect" msgstr "" #. lang.T #. lang.T -#: pkg/proxy/switch.go:296 pkg/proxy/switch.go:305 +#: pkg/proxy/switch.go:336 pkg/proxy/switch.go:345 msgid "Permission has expired, disconnect" msgstr "" #. lang.T -#: pkg/proxy/switch.go:317 +#: pkg/proxy/switch.go:357 msgid "Terminated by admin %s" msgstr "" diff --git a/locale/ja_JP/LC_MESSAGES/koko.po b/locale/ja_JP/LC_MESSAGES/koko.po index 193ba80ee..b70addd44 100644 --- a/locale/ja_JP/LC_MESSAGES/koko.po +++ b/locale/ja_JP/LC_MESSAGES/koko.po @@ -375,7 +375,7 @@ msgstr "%s の一意のアセットでなければなりません" #. i18n.T #. i18n.T -#: pkg/handler/server_ssh.go:662 pkg/handler/server_ssh.go:666 +#: pkg/handler/server_ssh.go:680 pkg/handler/server_ssh.go:684 #, fuzzy msgid "No found asset" msgstr "一致したアセット %s が見つかりません" @@ -384,44 +384,48 @@ msgstr "一致したアセット %s が見つかりません" #. i18n.T #. i18n.T #. lang.T -#: pkg/handler/server_ssh.go:695 pkg/handler/server_ssh.go:700 -#: pkg/handler/server_ssh.go:718 pkg/proxy/parser.go:226 +#: pkg/handler/server_ssh.go:713 pkg/handler/server_ssh.go:718 +#: pkg/handler/server_ssh.go:736 pkg/proxy/parser.go:230 msgid "have no permission to upload file" msgstr "ファイルをアップロードする権限がない" #. lang.T -#: pkg/proxy/parser.go:262 +#: pkg/proxy/parser.go:266 msgid "" "The command you executed is risky and an alert notification will be sent to " "the administrator. Do you want to continue?[Y/N]" -msgstr "接続に失敗しました。データベース接続設定が正しいか確認してください[Y/N]" +msgstr "" +"接続に失敗しました。データベース接続設定が正しいか確認してください[Y/N]" #. lang.T -#: pkg/proxy/parser.go:278 -msgid "the reviewers will confirm. continue or not [Y/n]" +#: pkg/proxy/parser.go:282 +#, fuzzy +msgid "The command '%s' requires review. Continue or not [Y/n]?" msgstr "レビューアが確認します。継続するかどうか [Y/n]" #. lang.T #. lang.T #. lang.T -#: pkg/proxy/parser.go:298 pkg/proxy/parser.go:305 pkg/proxy/parser.go:453 +#: pkg/proxy/parser.go:302 pkg/proxy/parser.go:309 pkg/proxy/parser.go:463 msgid "Command `%s` is forbidden" msgstr "命令 `%s` は禁止されています" #. lang.T -#: pkg/proxy/parser.go:524 +#: pkg/proxy/parser.go:534 msgid "have no permission to download file" msgstr "ファイルをダウンロードする権限がない" #. lang.T -#: pkg/proxy/parser.go:598 +#: pkg/proxy/parser.go:608 msgid "" -"Please waiting for the reviewers to confirm command `%s`, cancel by CTRL+C or CTRL+D." +"Please waiting for the reviewers to confirm command `%s`, cancel by CTRL+C " +"or CTRL+D." msgstr "" -"レビュー担当者がコマンド `%s` をレビューするまでお待ちください。キャンセルするには、CTRL+C または CTRL+D を押してください。" +"レビュー担当者がコマンド `%s` をレビューするまでお待ちください。キャンセルす" +"るには、CTRL+C または CTRL+D を押してください。" #. lang.T -#: pkg/proxy/parser.go:608 +#: pkg/proxy/parser.go:618 msgid "" "Need ticket confirm to execute command, already send email to the reviewers" msgstr "" @@ -432,7 +436,7 @@ msgstr "" #. lang.T #. lang.T #. lang.T -#: pkg/proxy/parser.go:609 pkg/proxy/parser.go:610 pkg/proxy/server.go:58 +#: pkg/proxy/parser.go:619 pkg/proxy/parser.go:620 pkg/proxy/server.go:58 #: pkg/proxy/server.go:62 #, fuzzy msgid "" @@ -448,45 +452,45 @@ msgid "Account <%s> and asset <%s> protocol are inconsistent." msgstr "システムユーザ <%s> と資産 <%s> のプロトコルが一致しない" #. lang.T -#: pkg/proxy/server.go:100 +#: pkg/proxy/server.go:101 msgid "You don't have permission login %s" msgstr "ログイン権限がありません %s" #. lang.T -#: pkg/proxy/server.go:336 +#: pkg/proxy/server.go:337 msgid "You get auth token failed" msgstr "認証トークンの取得に失敗しました" #. lang.T -#: pkg/proxy/server.go:347 +#: pkg/proxy/server.go:348 msgid "Get auth password failed" msgstr "認証トークンの取得に失敗しました" #. lang.T #. lang.T -#: pkg/proxy/server.go:363 pkg/proxy/server.go:420 +#: pkg/proxy/server.go:364 pkg/proxy/server.go:421 msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]" msgstr "SSH接続の再利用 (%s@%s) [接続数: %d]" #. lang.T -#: pkg/proxy/server.go:676 +#: pkg/proxy/server.go:677 msgid "Switched to %s" msgstr "%s に切り替え" #. lang.T #. lang.T -#: pkg/proxy/server.go:773 pkg/proxy/server.go:918 +#: pkg/proxy/server.go:774 pkg/proxy/server.go:919 msgid "Connect with api server failed" msgstr "APIサーバーとの接続に失敗しました" #. lang.T -#: pkg/proxy/server.go:963 +#: pkg/proxy/server.go:968 msgid "Start domain gateway failed %s" msgstr "ドメインゲートウェイの開始に失敗した %s" #. lang.T #. lang.T -#: pkg/proxy/server.go:971 pkg/proxy/server_options.go:108 +#: pkg/proxy/server.go:976 pkg/proxy/server_options.go:108 #, fuzzy msgid "Manual" msgstr "マニュアル" @@ -518,19 +522,19 @@ msgid "Connecting to Kubernetes %s container %s" msgstr "Kubernetes %s コンテナー %s への接続" #. lang.T -#: pkg/proxy/switch.go:285 +#: pkg/proxy/switch.go:325 #, fuzzy msgid "Session max time reached, disconnect" msgstr "セッションの最大時間に達しました。" #. lang.T #. lang.T -#: pkg/proxy/switch.go:296 pkg/proxy/switch.go:305 +#: pkg/proxy/switch.go:336 pkg/proxy/switch.go:345 msgid "Permission has expired, disconnect" msgstr "権限の有効期限が切れました。" #. lang.T -#: pkg/proxy/switch.go:317 +#: pkg/proxy/switch.go:357 msgid "Terminated by admin %s" msgstr "%s管理者が接続を終了" diff --git a/locale/zh_CN/LC_MESSAGES/koko.po b/locale/zh_CN/LC_MESSAGES/koko.po index e33fabb78..6bafff96e 100644 --- a/locale/zh_CN/LC_MESSAGES/koko.po +++ b/locale/zh_CN/LC_MESSAGES/koko.po @@ -389,7 +389,7 @@ msgstr "必须是自动登录账号 %s" #. i18n.T #. i18n.T -#: pkg/handler/server_ssh.go:662 pkg/handler/server_ssh.go:666 +#: pkg/handler/server_ssh.go:680 pkg/handler/server_ssh.go:684 #, fuzzy msgid "No found asset" msgstr "未发现匹配的资产 %s" @@ -398,44 +398,46 @@ msgstr "未发现匹配的资产 %s" #. i18n.T #. i18n.T #. lang.T -#: pkg/handler/server_ssh.go:695 pkg/handler/server_ssh.go:700 -#: pkg/handler/server_ssh.go:718 pkg/proxy/parser.go:226 +#: pkg/handler/server_ssh.go:713 pkg/handler/server_ssh.go:718 +#: pkg/handler/server_ssh.go:736 pkg/proxy/parser.go:230 msgid "have no permission to upload file" msgstr "无权限上传文件" #. lang.T -#: pkg/proxy/parser.go:262 +#: pkg/proxy/parser.go:266 msgid "" "The command you executed is risky and an alert notification will be sent to " "the administrator. Do you want to continue?[Y/N]" msgstr "您执行的命令存在风险,告警通知将发送给管理员。是否继续?[Y/N]" #. lang.T -#: pkg/proxy/parser.go:278 -msgid "the reviewers will confirm. continue or not [Y/n]" -msgstr "需要审核人复核,是否继续 [Y/N]" +#: pkg/proxy/parser.go:282 +#, fuzzy +msgid "The command '%s' requires review. Continue or not [Y/n]?" +msgstr "命令 %s 需要复核,是否继续?[Y/N]" #. lang.T #. lang.T #. lang.T -#: pkg/proxy/parser.go:298 pkg/proxy/parser.go:305 pkg/proxy/parser.go:453 +#: pkg/proxy/parser.go:302 pkg/proxy/parser.go:309 pkg/proxy/parser.go:463 msgid "Command `%s` is forbidden" msgstr "命令 `%s` 是被禁止的 ..." #. lang.T -#: pkg/proxy/parser.go:524 +#: pkg/proxy/parser.go:534 msgid "have no permission to download file" msgstr "无权限下载文件" #. lang.T -#: pkg/proxy/parser.go:598 +#: pkg/proxy/parser.go:608 #, fuzzy msgid "" -"Please waiting for the reviewers to confirm command `%s`, cancel by CTRL+C or CTRL+D." +"Please waiting for the reviewers to confirm command `%s`, cancel by CTRL+C " +"or CTRL+D." msgstr "请等待审核人复核命令 `%s`,取消按 CTRL+C 或 CTRL+D。" #. lang.T -#: pkg/proxy/parser.go:608 +#: pkg/proxy/parser.go:618 #, fuzzy msgid "" "Need ticket confirm to execute command, already send email to the reviewers" @@ -445,7 +447,7 @@ msgstr "需要工单命令执行复核,已发邮件通知审核人" #. lang.T #. lang.T #. lang.T -#: pkg/proxy/parser.go:609 pkg/proxy/parser.go:610 pkg/proxy/server.go:58 +#: pkg/proxy/parser.go:619 pkg/proxy/parser.go:620 pkg/proxy/server.go:58 #: pkg/proxy/server.go:62 #, fuzzy msgid "" @@ -459,47 +461,47 @@ msgid "Account <%s> and asset <%s> protocol are inconsistent." msgstr "系统用户<%s>和资产<%s>协议不一致" #. lang.T -#: pkg/proxy/server.go:100 +#: pkg/proxy/server.go:101 msgid "You don't have permission login %s" msgstr "你无权限登陆%s" #. lang.T -#: pkg/proxy/server.go:336 +#: pkg/proxy/server.go:337 msgid "You get auth token failed" msgstr "你获取认证令牌失败" #. lang.T -#: pkg/proxy/server.go:347 +#: pkg/proxy/server.go:348 #, fuzzy msgid "Get auth password failed" msgstr "你获取认证令牌失败" #. lang.T #. lang.T -#: pkg/proxy/server.go:363 pkg/proxy/server.go:420 +#: pkg/proxy/server.go:364 pkg/proxy/server.go:421 msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]" msgstr "复用SSH连接(%s@%s)[连接数量: %d]" #. lang.T -#: pkg/proxy/server.go:676 +#: pkg/proxy/server.go:677 msgid "Switched to %s" msgstr "已切换至%s" #. lang.T #. lang.T -#: pkg/proxy/server.go:773 pkg/proxy/server.go:918 +#: pkg/proxy/server.go:774 pkg/proxy/server.go:919 msgid "Connect with api server failed" msgstr "连接API服务失败" #. lang.T -#: pkg/proxy/server.go:963 +#: pkg/proxy/server.go:968 #, fuzzy msgid "Start domain gateway failed %s" msgstr "启动数据库网关失败%s" #. lang.T #. lang.T -#: pkg/proxy/server.go:971 pkg/proxy/server_options.go:108 +#: pkg/proxy/server.go:976 pkg/proxy/server_options.go:108 #, fuzzy msgid "Manual" msgstr "手动账号" @@ -535,19 +537,19 @@ msgid "Connecting to Kubernetes %s container %s" msgstr "开始连接Kubernetes %s 容器 %s" #. lang.T -#: pkg/proxy/switch.go:285 +#: pkg/proxy/switch.go:325 #, fuzzy msgid "Session max time reached, disconnect" msgstr "会话超过最大连接时间,断开连接" #. lang.T #. lang.T -#: pkg/proxy/switch.go:296 pkg/proxy/switch.go:305 +#: pkg/proxy/switch.go:336 pkg/proxy/switch.go:345 msgid "Permission has expired, disconnect" msgstr "授权已过期,断开连接" #. lang.T -#: pkg/proxy/switch.go:317 +#: pkg/proxy/switch.go:357 #, fuzzy msgid "Terminated by admin %s" msgstr "%s 管理员终断连接" diff --git a/locale/zh_Hant/LC_MESSAGES/koko.po b/locale/zh_Hant/LC_MESSAGES/koko.po index 4481cd95b..6255c2313 100644 --- a/locale/zh_Hant/LC_MESSAGES/koko.po +++ b/locale/zh_Hant/LC_MESSAGES/koko.po @@ -7,7 +7,6 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: xgotext\n" - #. lang.T #: pkg/handler/app_database.go:11 msgid "No Databases" @@ -37,69 +36,69 @@ msgid "Page up: b\tPage down: n" msgstr "上一頁:b 下一頁:n" #. lang.T -#: pkg/handler/asset.go:41 +#: pkg/handler/asset.go:43 msgid "No Assets" msgstr "沒有資產" #. lang.T -#: pkg/handler/asset.go:53 +#: pkg/handler/asset.go:55 msgid "ID" msgstr "ID" #. lang.T -#: pkg/handler/asset.go:54 +#: pkg/handler/asset.go:56 msgid "Name" msgstr "名稱" #. lang.T -#: pkg/handler/asset.go:55 +#: pkg/handler/asset.go:57 msgid "Address" msgstr "地址" #. lang.T -#: pkg/handler/asset.go:56 +#: pkg/handler/asset.go:58 msgid "Platform" msgstr "平台" #. lang.T -#: pkg/handler/asset.go:57 +#: pkg/handler/asset.go:59 msgid "Organization" msgstr "組織" #. lang.T -#: pkg/handler/asset.go:58 +#: pkg/handler/asset.go:60 #, fuzzy msgid "Comment" msgstr "備註" #. lang.T -#: pkg/handler/asset.go:155 +#: pkg/handler/asset.go:174 #, fuzzy msgid "%s protocol client not installed." msgstr "%s 協議的用戶端未安裝" #. lang.T -#: pkg/handler/asset.go:158 +#: pkg/handler/asset.go:177 #, fuzzy msgid "" "Terminal does not support protocol %s, please use web terminal to access" msgstr "該終端不支持 %s 協議,請使用web終端登入" #. lang.T -#: pkg/handler/asset.go:183 +#: pkg/handler/asset.go:211 #, fuzzy msgid "Core API failed" msgstr "Core API 發生錯誤" #. lang.T -#: pkg/handler/asset.go:189 +#: pkg/handler/asset.go:217 #, fuzzy msgid "ACL reject" msgstr "本次登入已拒絕,原因是訪問控制策略的限制" #. lang.T #. lang.T -#: pkg/handler/asset.go:203 pkg/handler/asset.go:220 +#: pkg/handler/asset.go:231 pkg/handler/asset.go:248 msgid "get connect token err" msgstr "獲取 connect token 錯誤" @@ -109,120 +108,121 @@ msgid "%s node has no assets" msgstr "%s節點沒有資產" #. lang.T -#: pkg/handler/banner.go:29 +#: pkg/handler/banner.go:30 #, fuzzy msgid "Welcome to use JumpServer open source fortress system" msgstr "歡迎使用JumpServer開源堡壘機系統" #. lang.T -#: pkg/handler/banner.go:31 +#: pkg/handler/banner.go:32 msgid "part IP, Hostname, Comment" msgstr "部分IP,主機名,備註" #. lang.T -#: pkg/handler/banner.go:31 +#: pkg/handler/banner.go:32 #, fuzzy msgid "to search login if unique" msgstr "搜索登入(如果唯一)" #. lang.T -#: pkg/handler/banner.go:32 +#: pkg/handler/banner.go:33 msgid "/ + IP, Hostname, Comment" msgstr "/ + IP,主機名,備註" #. lang.T -#: pkg/handler/banner.go:32 +#: pkg/handler/banner.go:33 #, fuzzy msgid "to search, such as: /192.168" msgstr "搜索,如:/192.168" #. lang.T -#: pkg/handler/banner.go:33 +#: pkg/handler/banner.go:34 #, fuzzy msgid "display the assets you have permission" msgstr "顯示您有權限的資產" #. lang.T -#: pkg/handler/banner.go:34 +#: pkg/handler/banner.go:35 msgid "display the node that you have permission" msgstr "顯示您有權限的節點" #. lang.T -#: pkg/handler/banner.go:35 +#: pkg/handler/banner.go:36 #, fuzzy msgid "display the hosts that you have permission" msgstr "顯示您有權限的主機" #. lang.T -#: pkg/handler/banner.go:36 +#: pkg/handler/banner.go:37 #, fuzzy msgid "display the databases that you have permission" msgstr "顯示您有權限的資料庫" #. lang.T -#: pkg/handler/banner.go:37 +#: pkg/handler/banner.go:38 #, fuzzy msgid "display the kubernetes that you have permission" msgstr "顯示您有權限的Kubernetes" #. lang.T -#: pkg/handler/banner.go:38 +#: pkg/handler/banner.go:39 msgid "refresh your assets and nodes" msgstr "刷新最新的機器和節點資訊" #. lang.T -#: pkg/handler/banner.go:39 +#: pkg/handler/banner.go:40 msgid "Chinese-English-Japanese switch" msgstr "中文-English-日本語語言切換" #. lang.T -#: pkg/handler/banner.go:40 +#: pkg/handler/banner.go:41 msgid "print help" msgstr "顯示幫助" #. lang.T -#: pkg/handler/banner.go:41 +#: pkg/handler/banner.go:42 msgid "exit" msgstr "退出" #. lang.T -#: pkg/handler/banner.go:59 -msgid "\t%d) Enter {{.GreenBoldColor}}%s{{.ColorEnd}} to %s.%s" +#: pkg/handler/banner.go:60 +#, fuzzy +msgid "\t%2d) Enter {{.GreenBoldColor}}%s{{.ColorEnd}} to %s.%s" msgstr "\t%d) 輸入 {{.GreenBoldColor}}%s{{.ColorEnd}} 進行%s.%s" #. lang.T -#: pkg/handler/banner.go:79 +#: pkg/handler/banner.go:85 msgid "Announcement: " msgstr "公告:" #. lang.T -#: pkg/handler/direct_handler.go:247 +#: pkg/handler/direct_handler.go:272 msgid "No Account found." msgstr "未發現帳號" #. lang.T #. lang.T #. lang.T -#: pkg/handler/direct_handler.go:257 pkg/handler/direct_handler.go:258 -#: pkg/handler/direct_handler.go:259 +#: pkg/handler/direct_handler.go:282 pkg/handler/direct_handler.go:283 +#: pkg/handler/direct_handler.go:284 #, fuzzy msgid "Username" msgstr "使用者名稱" #. lang.T -#: pkg/handler/direct_handler.go:288 +#: pkg/handler/direct_handler.go:313 #, fuzzy msgid "Tips: Enter asset[%s] account ID" msgstr "提示:輸入資產[%s]的帳號ID" #. lang.T -#: pkg/handler/direct_handler.go:289 +#: pkg/handler/direct_handler.go:314 msgid "Back: B/b" msgstr "返回:B/b" #. lang.T #. lang.T -#: pkg/handler/direct_handler.go:324 pkg/handler/direct_handler.go:325 +#: pkg/handler/direct_handler.go:349 pkg/handler/direct_handler.go:350 #, fuzzy msgid "Hostname" msgstr "主機名" @@ -230,14 +230,14 @@ msgstr "主機名" #. lang.T #. lang.T #. lang.T -#: pkg/handler/direct_handler.go:326 pkg/handler/direct_handler.go:327 -#: pkg/handler/direct_handler.go:356 +#: pkg/handler/direct_handler.go:351 pkg/handler/direct_handler.go:352 +#: pkg/handler/direct_handler.go:381 msgid "select one asset to login" msgstr "選擇其中一個資產登入" #. lang.T #. lang.T -#: pkg/handler/direct_handler.go:370 pkg/handler/direct_handler.go:375 +#: pkg/handler/direct_handler.go:395 pkg/handler/direct_handler.go:400 msgid "not found matched username %s" msgstr "未發現匹配的使用者名稱 %s" @@ -246,8 +246,8 @@ msgstr "未發現匹配的使用者名稱 %s" #. lang.T #. lang.T #. lang.T -#: pkg/handler/direct_handler.go:399 pkg/handler/direct_handler.go:405 -#: pkg/handler/direct_handler.go:419 pkg/handler/direct_handler.go:435 +#: pkg/handler/direct_handler.go:433 pkg/handler/direct_handler.go:439 +#: pkg/handler/direct_handler.go:453 pkg/handler/direct_handler.go:469 #: pkg/handler/dispatch.go:139 msgid "Node: [ ID.Name(Asset amount) ]" msgstr "節點:[ ID.名稱(資產數量) ]" @@ -370,61 +370,74 @@ msgid "Search: %s" msgstr "搜索:%s" #. i18n.T -#: pkg/handler/server_ssh.go:199 +#: pkg/handler/server_ssh.go:222 msgid "Must be unique asset for %s" msgstr "必須是唯一的資產 %s" #. i18n.T #. i18n.T -#: pkg/handler/server_ssh.go:206 pkg/handler/server_ssh.go:228 +#: pkg/handler/server_ssh.go:229 pkg/handler/server_ssh.go:251 #, fuzzy msgid "Must be unique account for %s" msgstr "必須是唯一的帳號 %s" #. i18n.T -#: pkg/handler/server_ssh.go:236 +#: pkg/handler/server_ssh.go:259 #, fuzzy msgid "Must be auto login account for %s" msgstr "必須是自動登入帳號 %s" #. i18n.T #. i18n.T -#: pkg/handler/server_ssh.go:585 pkg/handler/server_ssh.go:589 +#: pkg/handler/server_ssh.go:680 pkg/handler/server_ssh.go:684 #, fuzzy msgid "No found asset" msgstr "未發現匹配的資產 %s" +#. i18n.T +#. i18n.T +#. i18n.T #. lang.T -#: pkg/proxy/parser.go:215 +#: pkg/handler/server_ssh.go:713 pkg/handler/server_ssh.go:718 +#: pkg/handler/server_ssh.go:736 pkg/proxy/parser.go:230 msgid "have no permission to upload file" msgstr "無權限上傳文件" #. lang.T -#: pkg/proxy/parser.go:250 -msgid "the reviewers will confirm. continue or not [Y/n]" -msgstr "需要審核人覆核,是否繼續 [Y/N]" +#: pkg/proxy/parser.go:266 +msgid "" +"The command you executed is risky and an alert notification will be sent to " +"the administrator. Do you want to continue?[Y/N]" +msgstr "您執行的指令存在風險,警告通知將發送給管理員。是否繼續?[Y/N]" + +#. lang.T +#: pkg/proxy/parser.go:282 +#, fuzzy +msgid "The command '%s' requires review. Continue or not [Y/n]?" +msgstr "命令 `%s` 需要複核,是否繼續?[Y/N]" #. lang.T #. lang.T #. lang.T -#: pkg/proxy/parser.go:270 pkg/proxy/parser.go:277 pkg/proxy/parser.go:410 +#: pkg/proxy/parser.go:302 pkg/proxy/parser.go:309 pkg/proxy/parser.go:463 msgid "Command `%s` is forbidden" msgstr "命令 `%s` 是被禁止的 ..." #. lang.T -#: pkg/proxy/parser.go:481 +#: pkg/proxy/parser.go:534 msgid "have no permission to download file" msgstr "無權限下載文件" #. lang.T -#: pkg/proxy/parser.go:555 +#: pkg/proxy/parser.go:608 #, fuzzy msgid "" -"Please waiting for the reviewers to confirm command `%s`, cancel by CTRL+C or CTRL+D." +"Please waiting for the reviewers to confirm command `%s`, cancel by CTRL+C " +"or CTRL+D." msgstr "請等待審核人覆核命令 `%s`,取消按 CTRL+C 或 CTRL+D。" #. lang.T -#: pkg/proxy/parser.go:565 +#: pkg/proxy/parser.go:618 #, fuzzy msgid "" "Need ticket confirm to execute command, already send email to the reviewers" @@ -434,70 +447,61 @@ msgstr "需要工單命令執行覆核,已發郵件通知審核人" #. lang.T #. lang.T #. lang.T -#: pkg/proxy/parser.go:566 pkg/proxy/parser.go:567 pkg/proxy/server.go:56 -#: pkg/proxy/server.go:60 +#: pkg/proxy/parser.go:619 pkg/proxy/parser.go:620 pkg/proxy/server.go:58 +#: pkg/proxy/server.go:62 #, fuzzy msgid "" "HandleTask does not support protocol %s, please use web terminal to access" msgstr "該終端不支持 %s 協議,請使用web終端登入" #. lang.T -#: pkg/proxy/server.go:68 +#: pkg/proxy/server.go:70 #, fuzzy msgid "Account <%s> and asset <%s> protocol are inconsistent." msgstr "系統用戶<%s>和資產<%s>協議不一致" #. lang.T -#: pkg/proxy/server.go:98 +#: pkg/proxy/server.go:101 msgid "You don't have permission login %s" msgstr "你無權限登入 %s" #. lang.T -#: pkg/proxy/server.go:359 +#: pkg/proxy/server.go:337 msgid "You get auth token failed" msgstr "你獲取認證令牌失敗" #. lang.T -#: pkg/proxy/server.go:369 -#, fuzzy -msgid "Get auth username failed" -msgstr "你獲取認證令牌失敗" - -#. lang.T -#: pkg/proxy/server.go:374 +#: pkg/proxy/server.go:348 #, fuzzy msgid "Get auth password failed" msgstr "你獲取認證令牌失敗" #. lang.T #. lang.T -#. lang.T -#. lang.T -#: pkg/proxy/server.go:380 pkg/proxy/server.go:386 pkg/proxy/server.go:401 -#: pkg/proxy/server.go:458 +#: pkg/proxy/server.go:364 pkg/proxy/server.go:421 msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]" msgstr "復用SSH連接(%s@%s)[連接數量: %d]" #. lang.T -#: pkg/proxy/server.go:740 +#: pkg/proxy/server.go:677 msgid "Switched to %s" msgstr "已切換至%s" #. lang.T #. lang.T -#: pkg/proxy/server.go:837 pkg/proxy/server.go:1007 +#: pkg/proxy/server.go:774 pkg/proxy/server.go:919 msgid "Connect with api server failed" msgstr "連接API服務失敗" #. lang.T -#: pkg/proxy/server.go:1052 +#: pkg/proxy/server.go:968 #, fuzzy msgid "Start domain gateway failed %s" msgstr "啟動資料庫網關失敗%s" #. lang.T #. lang.T -#: pkg/proxy/server.go:1060 pkg/proxy/server_options.go:108 +#: pkg/proxy/server.go:976 pkg/proxy/server_options.go:108 #, fuzzy msgid "Manual" msgstr "手動帳號" @@ -533,19 +537,19 @@ msgid "Connecting to Kubernetes %s container %s" msgstr "開始連接Kubernetes %s 容器 %s" #. lang.T -#: pkg/proxy/switch.go:290 +#: pkg/proxy/switch.go:325 #, fuzzy msgid "Session max time reached, disconnect" msgstr "會話超過最大連接時間,斷開連接" #. lang.T #. lang.T -#: pkg/proxy/switch.go:300 pkg/proxy/switch.go:308 +#: pkg/proxy/switch.go:336 pkg/proxy/switch.go:345 msgid "Permission has expired, disconnect" msgstr "授權已過期,斷開連接" #. lang.T -#: pkg/proxy/switch.go:319 +#: pkg/proxy/switch.go:357 #, fuzzy msgid "Terminated by admin %s" msgstr "%s 管理員中斷連接" @@ -576,6 +580,10 @@ msgstr "網路不通(路由不通)" msgid "network is unreachable" msgstr "網路不通(網路不可達)" +#, fuzzy +#~ msgid "Get auth username failed" +#~ msgstr "你獲取認證令牌失敗" + #, fuzzy #~ msgid "Protocols" #~ msgstr "協議" diff --git a/pkg/common/utils.go b/pkg/common/utils.go index 608a4e847..5488bb6f7 100644 --- a/pkg/common/utils.go +++ b/pkg/common/utils.go @@ -2,9 +2,11 @@ package common import ( "compress/gzip" + "fmt" "io" "net/netip" "os" + "sync" "time" "unsafe" ) @@ -92,3 +94,57 @@ func CompareIP(ipA, ipB string) bool { } return addrA.Less(addrB) } + +func ChunkedFileTransfer(fd io.WriterAt, readerAt io.ReaderAt, offset, fileSize int64) error { + chunkSize := int64(64 * 1024) + maxConcurrent := 200 + + var wg sync.WaitGroup + chunkCount := int(fileSize / chunkSize) + if fileSize%chunkSize != 0 { + chunkCount++ + } + + errChan := make(chan error, chunkCount) + sem := make(chan struct{}, maxConcurrent) + for i := 0; i < chunkCount; i++ { + wg.Add(1) + sem <- struct{}{} + + go func(chunkIndex int) { + defer wg.Done() + defer func() { <-sem }() + + start := int64(chunkIndex) * chunkSize + end := start + chunkSize + if end > fileSize { + end = fileSize + } + + buf := make([]byte, end-start) + _, err := readerAt.ReadAt(buf, start) + if err != nil && err != io.EOF { + errChan <- fmt.Errorf("failed to read chunk %d: %v", chunkIndex, err) + return + } + + _, err = fd.WriteAt(buf, offset+start) + if err != nil { + errChan <- fmt.Errorf("failed to write chunk %d: %v", chunkIndex, err) + return + } + + }(i) + } + + wg.Wait() + close(errChan) + + for err := range errChan { + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 00ce38407..3020fd7b7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -64,6 +64,8 @@ type Config struct { SshMaxSessions int `mapstructure:"SSH_MAX_SESSIONS"` + DisableInputAsCommand bool `mapstructure:"DISABLE_INPUT_AS_COMMAND"` + RootPath string DataFolderPath string LogDirPath string @@ -76,7 +78,7 @@ type Config struct { func (c *Config) EnsureConfigValid() { if c.LanguageCode == "" { - c.LanguageCode = "zh" + c.LanguageCode = "en" } } @@ -133,6 +135,7 @@ func getDefaultConfig() Config { ReplayFolderPath: replayFolderPath, FTPFileFolderPath: ftpFileFolderPath, CertsFolderPath: CertsFolderPath, + LanguageCode: "en", Comment: "KOKO", UploadFailedReplay: true, diff --git a/pkg/exchange/message.go b/pkg/exchange/message.go index 5aab69dcc..2a5b05928 100644 --- a/pkg/exchange/message.go +++ b/pkg/exchange/message.go @@ -46,6 +46,9 @@ const ( ActionEvent = "Action" + PermExpiredEvent = "PermExpired" + PermValidEvent = "PermValid" + ShareRemoveUser = "Share_REMOVE_USER" ) diff --git a/pkg/handler/server_ssh.go b/pkg/handler/server_ssh.go index 0889c82d9..ed51a6bc8 100644 --- a/pkg/handler/server_ssh.go +++ b/pkg/handler/server_ssh.go @@ -417,7 +417,17 @@ func (s *Server) proxyAssetCommand(sess ssh.Session, sshClient *srvconn.SSHClien switch task.Name { case model.TaskKillSession: cancel() + logger.Infof("User %s end command request %s as task kill_session", + tokenInfo.User.String(), sshClient) return nil + case model.TaskPermExpired: + cancel() + logger.Infof("User %s end command request %s as task permission has expired", + tokenInfo.User.String(), sshClient) + return nil + case model.TaskPermValid: + return nil + } return fmt.Errorf("ssh proxy not support task: %s", task.Name) }) @@ -534,7 +544,15 @@ func (s *Server) proxyVscodeShell(sess ssh.Session, vsReq *vscodeReq, sshClient switch task.Name { case model.TaskKillSession: cancel() + logger.Infof("User %s end vscode request %s as task kill_session", vsReq.user, sshClient) return nil + case model.TaskPermExpired: + cancel() + logger.Infof("User %s end vscode request %s as permission has expired", vsReq.user, sshClient) + return nil + case model.TaskPermValid: + return nil + } return fmt.Errorf("ssh proxy not support task: %s", task.Name) }) diff --git a/pkg/handler/server_ssh_forward.go b/pkg/handler/server_ssh_forward.go index f037c8ffc..e0790105d 100644 --- a/pkg/handler/server_ssh_forward.go +++ b/pkg/handler/server_ssh_forward.go @@ -93,6 +93,13 @@ func (s *Server) HandleSSHRequest(ctx ssh.Context, srv *ssh.Server, req *gossh.R switch task.Name { case model.TaskKillSession: cancel() + logger.Info("ide session killed as task kill session") + return nil + case model.TaskPermExpired: + cancel() + logger.Info("ide session killed as task perm expired") + return nil + case model.TaskPermValid: return nil } return fmt.Errorf("ssh proxy not support task: %s", task.Name) diff --git a/pkg/handler/sftp.go b/pkg/handler/sftp.go index 14ffc0731..a340dc638 100644 --- a/pkg/handler/sftp.go +++ b/pkg/handler/sftp.go @@ -94,9 +94,16 @@ func (s *SftpHandler) Fileread(r *sftp.Request) (io.ReaderAt, error) { if err != nil { return nil, err } - if err1 := s.recorder.RecordChunkRead(f.FTPLog, f); err1 != nil { + + fileInfo, err := f.Stat() + if err != nil { + return nil, err + } + + if err1 := s.recorder.ChunkedRecord(f.FTPLog, f, 0, fileInfo.Size()); err1 != nil { logger.Errorf("Record file %s err: %s", r.Filepath, err1) } + // 重置文件指针 _, _ = f.Seek(0, io.SeekStart) go func() { diff --git a/pkg/httpd/client.go b/pkg/httpd/client.go index 59d8639fa..3639bb7c7 100644 --- a/pkg/httpd/client.go +++ b/pkg/httpd/client.go @@ -4,11 +4,12 @@ import ( "bytes" "context" "encoding/json" - "github.com/gliderlabs/ssh" "io" "sync" "time" + "github.com/gliderlabs/ssh" + "github.com/jumpserver/koko/pkg/exchange" "github.com/jumpserver/koko/pkg/logger" ) @@ -162,6 +163,14 @@ func (c *Client) HandleRoomEvent(event string, roomMsg *exchange.RoomMessage) { msgType = TerminalSessionResume msgData = string(roomMsg.Body) logger.Debugf("Resume terminal session : %+v", roomMsg) + case exchange.PermValidEvent: + msgType = TerminalPermValid + msgData = string(roomMsg.Body) + logger.Debugf("Terminal perm is valid : %+v", roomMsg) + case exchange.PermExpiredEvent: + msgType = TerminalPermExpired + msgData = string(roomMsg.Body) + logger.Debugf("Terminal perm is expired : %+v", roomMsg) default: logger.Infof("unsupported room msg %+v", roomMsg) return diff --git a/pkg/httpd/message.go b/pkg/httpd/message.go index 3599f2900..6892ef177 100644 --- a/pkg/httpd/message.go +++ b/pkg/httpd/message.go @@ -43,6 +43,9 @@ const ( TerminalSessionPause = "TERMINAL_SESSION_PAUSE" TerminalSessionResume = "TERMINAL_SESSION_RESUME" + TerminalPermValid = "TERMINAL_PERM_VALID" + TerminalPermExpired = "TERMINAL_PERM_EXPIRED" + TerminalShare = "TERMINAL_SHARE" TerminalShareJoin = "TERMINAL_SHARE_JOIN" TerminalShareLeave = "TERMINAL_SHARE_LEAVE" diff --git a/pkg/httpd/sftpvolume.go b/pkg/httpd/sftpvolume.go index 3028bc393..adcce70f0 100644 --- a/pkg/httpd/sftpvolume.go +++ b/pkg/httpd/sftpvolume.go @@ -12,6 +12,7 @@ import ( "github.com/LeeEirc/elfinder" "github.com/pkg/sftp" + "github.com/jumpserver/koko/pkg/common" "github.com/jumpserver/koko/pkg/jms-sdk-go/model" "github.com/jumpserver/koko/pkg/jms-sdk-go/service" "github.com/jumpserver/koko/pkg/logger" @@ -222,21 +223,29 @@ func (u *UserVolume) Parents(path string, dep int) []elfinder.FileDir { return dirs } -func (u *UserVolume) GetFile(path string) (reader io.ReadCloser, err error) { +func (u *UserVolume) GetFile(path string) (fileData elfinder.FileData, err error) { logger.Debug("GetFile path: ", path) + var rest elfinder.FileData sf, err := u.UserSftp.Open(filepath.Join(u.basePath, TrimPrefix(path))) if err != nil { - return nil, err + return rest, err + } + + fileInfo, err := sf.Stat() + if err != nil { + return rest, err } - if err1 := u.recorder.Record(sf.FTPLog, sf); err1 != nil { + + if err1 := u.recorder.ChunkedRecord(sf.FTPLog, sf, 0, fileInfo.Size()); err1 != nil { logger.Errorf("Record file err: %s", err1) } _, _ = sf.Seek(0, io.SeekStart) // 屏蔽 sftp*File 的 WriteTo 方法,防止调用 sftp stat 命令 - return &fileReader{sf}, nil + fileData = elfinder.FileData{Reader: sf, Size: fileInfo.Size()} + return fileData, nil } -func (u *UserVolume) UploadFile(dirPath, uploadPath, filename string, reader io.Reader) (elfinder.FileDir, error) { +func (u *UserVolume) UploadFile(dirPath, uploadPath, filename string, reader io.Reader, totalSize int64) (elfinder.FileDir, error) { var path string switch { case strings.Contains(uploadPath, filename): @@ -254,11 +263,17 @@ func (u *UserVolume) UploadFile(dirPath, uploadPath, filename string, reader io. return rest, err } defer fd.Close() - if err1 := u.recorder.Record(fd.FTPLog, reader); err1 != nil { + + readerAt, ok := reader.(io.ReaderAt) + if !ok { + return rest, fmt.Errorf("the provided reader does not implement io.ReaderAt") + } + + if err1 := u.recorder.ChunkedRecord(fd.FTPLog, readerAt, 0, totalSize); err1 != nil { logger.Errorf("Record file err: %s", err1) } - _, _ = reader.(io.Seeker).Seek(0, io.SeekStart) - _, err = io.Copy(fd, reader) + + err = common.ChunkedFileTransfer(fd, readerAt, 0, totalSize) if err != nil { return rest, err } @@ -297,11 +312,20 @@ func (u *UserVolume) UploadChunk(cid int, dirPath, uploadPath, filename string, u.ftpLogMap[cid] = ftpLog u.lock.Unlock() } - if err2 := u.recorder.RecordChunkRead(ftpLog, reader); err2 != nil { + + fileSize := rangeData.Length + offset := rangeData.Offset + readerAt, ok := reader.(io.ReaderAt) + if !ok { + return fmt.Errorf("the provided reader does not implement io.ReaderAt") + } + + if err2 := u.recorder.ChunkedRecord(ftpLog, readerAt, offset, fileSize); err2 != nil { logger.Errorf("Record file err: %s", err2) } - _, _ = reader.(io.Seeker).Seek(0, io.SeekStart) - _, err = io.Copy(fd, reader) + + err = common.ChunkedFileTransfer(fd, readerAt, offset, fileSize) + if err != nil { _ = fd.Close() u.lock.Lock() @@ -353,12 +377,20 @@ func (u *UserVolume) MakeFile(dir, newFilename string) (elfinder.FileDir, error) path := filepath.Join(dir, newFilename) var rest elfinder.FileDir fd, err := u.UserSftp.Create(filepath.Join(u.basePath, path)) + if err != nil { return rest, err } - if err1 := u.recorder.Record(fd.FTPLog, fd); err1 != nil { + + fileInfo, err := fd.Stat() + if err != nil { + return rest, err + } + + if err1 := u.recorder.ChunkedRecord(fd.FTPLog, fd, 0, fileInfo.Size()); err1 != nil { logger.Errorf("Record file err: %s", err1) } + _, _ = fd.Seek(0, io.SeekStart) _ = fd.Close() res, err := u.UserSftp.Stat(filepath.Join(u.basePath, path)) @@ -393,7 +425,9 @@ func (u *UserVolume) Remove(path string) error { return u.UserSftp.Remove(filepath.Join(u.basePath, path)) } -func (u *UserVolume) Paste(dir, filename, suffix string, reader io.ReadCloser) (elfinder.FileDir, error) { +func (u *UserVolume) Paste(dir, filename, suffix string, fileData elfinder.FileData) (elfinder.FileDir, error) { + reader := fileData.Reader + totalSize := fileData.Size defer reader.Close() var rest elfinder.FileDir path := filepath.Join(dir, filename) @@ -407,7 +441,13 @@ func (u *UserVolume) Paste(dir, filename, suffix string, reader io.ReadCloser) ( return rest, err } defer fd.Close() - _, err = io.Copy(fd, reader) + + readerAt, ok := reader.(io.ReaderAt) + if !ok { + return rest, fmt.Errorf("the provided reader does not implement io.ReaderAt") + } + + err = common.ChunkedFileTransfer(fd, readerAt, 0, totalSize) if err != nil { return rest, err } @@ -488,19 +528,3 @@ func hashPath(id, path string) string { func TrimPrefix(path string) string { return strings.TrimPrefix(path, "/") } - -var ( - _ io.ReadCloser = (*fileReader)(nil) -) - -type fileReader struct { - read io.ReadCloser -} - -func (f *fileReader) Read(p []byte) (nr int, err error) { - return f.read.Read(p) -} - -func (f *fileReader) Close() error { - return f.read.Close() -} diff --git a/pkg/jms-sdk-go/model/session.go b/pkg/jms-sdk-go/model/session.go index 2b9a9ff72..fe982b2fc 100644 --- a/pkg/jms-sdk-go/model/session.go +++ b/pkg/jms-sdk-go/model/session.go @@ -57,6 +57,8 @@ type Session struct { AccountID string `json:"account_id"` Type LabelField `json:"type"` ErrReason LabelField `json:"error_reason,omitempty"` + TokenId string `json:"token_id,omitempty"` + LangCode string `json:"lang_code,omitempty"` } type ReplayVersion string diff --git a/pkg/jms-sdk-go/model/terminal.go b/pkg/jms-sdk-go/model/terminal.go index 22439df1f..119f9bd5a 100644 --- a/pkg/jms-sdk-go/model/terminal.go +++ b/pkg/jms-sdk-go/model/terminal.go @@ -33,11 +33,10 @@ type Terminal struct { } type TerminalTask struct { - ID string `json:"id"` - Name string `json:"name"` - Args string `json:"args"` - Kwargs TaskKwargs `json:"kwargs"` - IsFinished bool + ID string `json:"id"` + Name string `json:"name"` + Args string `json:"args"` + Kwargs TaskKwargs `json:"kwargs"` } const ( @@ -45,6 +44,11 @@ const ( TaskLockSession = "lock_session" TaskUnlockSession = "unlock_session" + + // TaskPermExpired TaskPermValid 非 api 数据,仅用于内部处理 + + TaskPermExpired = "perm_expired" + TaskPermValid = "perm_valid" ) type TaskKwargs struct { diff --git a/pkg/jms-sdk-go/model/token.go b/pkg/jms-sdk-go/model/token.go index 760fbf1a0..97bf27335 100644 --- a/pkg/jms-sdk-go/model/token.go +++ b/pkg/jms-sdk-go/model/token.go @@ -47,6 +47,7 @@ func (c *ConnectToken) CreateSession(addr string, LoginFrom: loginFrom, Type: SessionType, ErrReason: LabelField(SessionReplayErrUnsupported), + TokenId: c.Id, } } @@ -77,3 +78,17 @@ type ConnectOptions struct { FilenameConflictResolution string `json:"file_name_conflict_resolution,omitempty"` TerminalThemeName string `json:"terminal_theme_name,omitempty"` } + +// token 授权和过期状态 + +type TokenCheckStatus struct { + Detail string `json:"detail"` + Code string `json:"code"` + Expired bool `json:"expired"` +} + +const ( + CodePermOk = "perm_ok" + CodePermAccountInvalid = "perm_account_invalid" + CodePermExpired = "perm_expired" +) diff --git a/pkg/jms-sdk-go/service/jms_token.go b/pkg/jms-sdk-go/service/jms_token.go index f1d8c53e0..d6ca0000b 100644 --- a/pkg/jms-sdk-go/service/jms_token.go +++ b/pkg/jms-sdk-go/service/jms_token.go @@ -46,3 +46,9 @@ type SuperConnectTokenReq struct { Params map[string]string `json:"-"` } + +func (s *JMService) CheckTokenStatus(tokenId string) (res model.TokenCheckStatus, err error) { + reqURL := fmt.Sprintf(SuperConnectTokenCheckURL, tokenId) + _, err = s.authClient.Get(reqURL, &res) + return +} diff --git a/pkg/jms-sdk-go/service/url.go b/pkg/jms-sdk-go/service/url.go index acbb69d2f..d9af7c86f 100644 --- a/pkg/jms-sdk-go/service/url.go +++ b/pkg/jms-sdk-go/service/url.go @@ -71,6 +71,8 @@ const ( SuperConnectTokenSecretURL = "/api/v1/authentication/super-connection-token/secret/" SuperConnectTokenInfoURL = "/api/v1/authentication/super-connection-token/" + SuperConnectTokenCheckURL = "/api/v1/authentication/super-connection-token/%s/check/" + UserPermsAssetAccountsURL = "/api/v1/perms/users/%s/assets/%s/" AccountSecretURL = "/api/v1/assets/account-secrets/%s/" UserPermsAssetsURL = "/api/v1/perms/users/%s/assets/" diff --git a/pkg/koko/koko.go b/pkg/koko/koko.go index 0a8247e62..38d6cc0c0 100644 --- a/pkg/koko/koko.go +++ b/pkg/koko/koko.go @@ -66,6 +66,8 @@ func runTasks(jmsService *service.JMService) { go uploadRemainFTPFile(jmsService) } go keepHeartbeat(jmsService) + + go RunConnectTokensCheck(jmsService) } func MustJMService() *service.JMService { diff --git a/pkg/koko/task.go b/pkg/koko/task.go index 456f16f2c..bd77332b0 100644 --- a/pkg/koko/task.go +++ b/pkg/koko/task.go @@ -222,13 +222,13 @@ func KeepWsHeartbeat(jmsService *service.JMService) { } func GetStatusData() interface{} { - sessions := session.GetAliveSessions() + ids := session.GetAliveSessionIds() payload := model.HeartbeatData{ - SessionOnlineIds: sessions, + SessionOnlineIds: ids, CpuUsed: common.CpuLoad1Usage(), MemoryUsed: common.MemoryUsagePercent(), DiskUsed: common.DiskUsagePercent(), - SessionOnline: len(sessions), + SessionOnline: len(ids), } return map[string]interface{}{ "type": "status", diff --git a/pkg/koko/token_check.go b/pkg/koko/token_check.go new file mode 100644 index 000000000..4978d0808 --- /dev/null +++ b/pkg/koko/token_check.go @@ -0,0 +1,55 @@ +package koko + +import ( + "time" + + "github.com/jumpserver/koko/pkg/jms-sdk-go/model" + "github.com/jumpserver/koko/pkg/jms-sdk-go/service" + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/session" +) + +// RunConnectTokensCheck every 5 minutes check token status +func RunConnectTokensCheck(jmsService *service.JMService) { + apiClient := jmsService.Copy() + for { + time.Sleep(5 * time.Minute) + sessions := session.GetSessions() + tokens := make(map[string]model.TokenCheckStatus, len(sessions)) + for _, s := range sessions { + ret, ok := tokens[s.TokenId] + if ok { + handleTokenCheck(s, &ret) + continue + } + apiClient.SetCookie("django_language", s.LangCode) + ret, err := apiClient.CheckTokenStatus(s.TokenId) + if err != nil && ret.Code == "" { + logger.Errorf("Check token status failed: %s", err) + continue + } + tokens[s.TokenId] = ret + handleTokenCheck(s, &ret) + } + } +} + +func handleTokenCheck(session *session.Session, tokenStatus *model.TokenCheckStatus) { + var task model.TerminalTask + switch tokenStatus.Code { + case model.CodePermOk: + task = model.TerminalTask{ + Name: model.TaskPermValid, + Args: tokenStatus.Detail, + } + default: + task = model.TerminalTask{ + Name: model.TaskPermExpired, + Args: tokenStatus.Detail, + } + } + if err := session.HandleTask(&task); err != nil { + logger.Errorf("Handle token check task failed: %s", err) + } + +} diff --git a/pkg/proxy/command_check.go b/pkg/proxy/command_check.go index e43ffb8c4..38bd2ccf3 100644 --- a/pkg/proxy/command_check.go +++ b/pkg/proxy/command_check.go @@ -2,6 +2,7 @@ package proxy import ( "context" + "strings" "sync" "github.com/jumpserver/koko/pkg/jms-sdk-go/model" @@ -118,3 +119,9 @@ const ( CtrlC = 3 CtrlD = 4 ) + +func stripNewLine(cmd string) string { + cmd = strings.ReplaceAll(cmd, "\r", "") + cmd = strings.ReplaceAll(cmd, "\n", "") + return cmd +} diff --git a/pkg/proxy/parser.go b/pkg/proxy/parser.go index 112dabeb7..f4e201217 100644 --- a/pkg/proxy/parser.go +++ b/pkg/proxy/parser.go @@ -9,6 +9,7 @@ import ( "time" "github.com/LeeEirc/tclientlib" + "github.com/jumpserver/koko/pkg/config" "github.com/jumpserver/koko/pkg/exchange" "github.com/jumpserver/koko/pkg/i18n" "github.com/jumpserver/koko/pkg/jms-sdk-go/model" @@ -91,6 +92,8 @@ type Parser struct { currentCmdFilterRule CommandRule userInputFilter func([]byte) []byte + + disableInputAsCmd bool } func (p *Parser) setCurrentCmdStatusLevel(level int64) { @@ -119,6 +122,7 @@ func (p *Parser) initial() { p.cmdOutputParser = NewCmdParser(p.id, CommandOutputParserName) p.closed = make(chan struct{}) p.cmdRecordChan = make(chan *ExecutedCommand, 1024) + p.disableInputAsCmd = config.GetConf().DisableInputAsCommand } func (p *Parser) SetUserInputFilter(filter func([]byte) []byte) { @@ -268,6 +272,7 @@ func (p *Parser) parseInputState(b []byte) []byte { case "n": p.confirmStatus.SetStatus(StatusNone) p.srvOutputChan <- []byte("\r\n") + p.command = "" return p.breakInputPacket() default: p.srvOutputChan <- []byte("\r\n" + WarnWaitMsg) @@ -275,7 +280,7 @@ func (p *Parser) parseInputState(b []byte) []byte { return nil } - confirmWaitMsg := lang.T("the reviewers will confirm. continue or not [Y/n]") + confirmWaitMsg := lang.T("The command '%s' requires review. Continue or not [Y/n]?") if p.confirmStatus.InQuery() { switch strings.ToLower(string(b)) { case "y": @@ -322,7 +327,8 @@ func (p *Parser) parseInputState(b []byte) []byte { p.srvOutputChan <- []byte("\r\n") return p.breakInputPacket() default: - p.srvOutputChan <- []byte("\r\n" + confirmWaitMsg) + confirmMsg := fmt.Sprintf(confirmWaitMsg, stripNewLine(p.confirmStatus.Cmd)) + p.srvOutputChan <- []byte("\r\n" + confirmMsg) } return nil } @@ -351,7 +357,8 @@ func (p *Parser) parseInputState(b []byte) []byte { p.confirmStatus.SetCmd(p.command) p.confirmStatus.SetData(string(b)) p.confirmStatus.ResetCtx() - p.srvOutputChan <- []byte("\r\n" + confirmWaitMsg) + confirmMsg := fmt.Sprintf(confirmWaitMsg, stripNewLine(p.confirmStatus.Cmd)) + p.srvOutputChan <- []byte("\r\n" + confirmMsg) return nil case model.ActionWarning: p.setCurrentCmdFilterRule(rule) @@ -437,6 +444,9 @@ func (p *Parser) IsNeedParse() bool { } func (p *Parser) writeInputBuffer(b []byte) { + if p.disableInputAsCmd { + return + } p.inputBuffer.Write(b) } @@ -528,10 +538,8 @@ func (p *Parser) splitCmdStream(b []byte) []byte { p.userOutputChan <- charEnter return nil } - if !p.zmodemParser.IsStartSession() { - p.srvOutputChan <- b + if !p.zmodemParser.IsStartSession() && p.zmodemParser.AbnormalFinish { p.srvOutputChan <- []byte{0x4f, 0x4f} - return nil } return b } else { diff --git a/pkg/proxy/recorder.go b/pkg/proxy/recorder.go index d2f900bc5..7f56a933a 100644 --- a/pkg/proxy/recorder.go +++ b/pkg/proxy/recorder.go @@ -340,7 +340,7 @@ func (r *FTPFileRecorder) CreateFTPFileInfo(logData *model.FTPLog) (info *FTPFil storageTargetName := strings.Join([]string{FtpTargetPrefix, today, logData.ID}, "/") info.absFilePath = absFilePath info.Target = storageTargetName - fd, err := os.OpenFile(info.absFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + fd, err := os.OpenFile(info.absFilePath, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { logger.Errorf("Create FTP file %s error: %s\n", absFilePath, err) return nil, err @@ -397,7 +397,7 @@ func (r *FTPFileRecorder) Record(ftpLog *model.FTPLog, reader io.Reader) (err er return } -func (r *FTPFileRecorder) RecordChunkRead(ftpLog *model.FTPLog, reader io.Reader) (err error) { +func (r *FTPFileRecorder) ChunkedRecord(ftpLog *model.FTPLog, readerAt io.ReaderAt, offset, totalSize int64) (err error) { if r.isNullStorage() { return } @@ -408,11 +408,19 @@ func (r *FTPFileRecorder) RecordChunkRead(ftpLog *model.FTPLog, reader io.Reader if err != nil { return err } + if info.isExceedWrittenSize() { logger.Errorf("FTP file %s is exceeds the max limit and discard it", ftpLog.ID) return nil } - return info.WriteFromReader(reader) + + if err1 := common.ChunkedFileTransfer(info.fd, readerAt, offset, totalSize); err1 != nil { + logger.Errorf("FTP file %s write err: %s", ftpLog.ID, err1) + } + + _ = info.Close() + go r.UploadFile(3, ftpLog.ID) + return } func (r *FTPFileRecorder) isNullStorage() bool { diff --git a/pkg/proxy/server.go b/pkg/proxy/server.go index 7edf5d25f..b0efc09fe 100644 --- a/pkg/proxy/server.go +++ b/pkg/proxy/server.go @@ -94,6 +94,8 @@ func NewServer(conn UserConnection, jmsService *service.JMService, opts ...Conne AccountID: account.ID, OrgID: connOpts.authInfo.OrgId, Type: model.NORMALType, + TokenId: connOpts.authInfo.Id, + LangCode: connOpts.i18nLang, } if !connOpts.authInfo.Actions.EnableConnect() { @@ -939,6 +941,10 @@ func (s *Server) Proxy() { sw.PauseOperation(task.Kwargs.CreatedByUser) case model.TaskUnlockSession: sw.ResumeOperation(task.Kwargs.CreatedByUser) + case model.TaskPermExpired: + sw.PermBecomeExpired(task.Name, task.Args) + case model.TaskPermValid: + sw.PermBecomeValid(task.Name, task.Args) default: return fmt.Errorf("ssh session unknown task %s", task.Name) } diff --git a/pkg/proxy/switch.go b/pkg/proxy/switch.go index a16935ca9..ce20d134d 100644 --- a/pkg/proxy/switch.go +++ b/pkg/proxy/switch.go @@ -36,6 +36,10 @@ type SwitchSession struct { notifyMsgChan chan *exchange.RoomMessage MaxSessionTime time.Time + + invalidPerm atomic.Bool + invalidPermData []byte + invalidPermTime time.Time } func (s *SwitchSession) Terminate(username string) { @@ -71,6 +75,42 @@ func (s *SwitchSession) ResumeOperation(username string) { } } +func (s *SwitchSession) PermBecomeExpired(code, detail string) { + if s.invalidPerm.Load() { + return + } + s.invalidPerm.Store(true) + p, _ := json.Marshal(map[string]string{"code": code, "detail": detail}) + s.invalidPermData = p + s.invalidPermTime = time.Now() + s.notifyMsgChan <- &exchange.RoomMessage{ + Event: exchange.PermExpiredEvent, Body: p} +} + +func (s *SwitchSession) PermBecomeValid(code, detail string) { + if !s.invalidPerm.Load() { + return + } + s.invalidPerm.Store(false) + s.invalidPermTime = s.MaxSessionTime + p, _ := json.Marshal(map[string]string{"code": code, "detail": detail}) + s.invalidPermData = p + s.notifyMsgChan <- &exchange.RoomMessage{ + Event: exchange.PermValidEvent, Body: p} +} + +func (s *SwitchSession) CheckPermissionExpired(now time.Time) bool { + if s.p.CheckPermissionExpired(now) { + return true + } + if s.invalidPerm.Load() { + if now.After(s.invalidPermTime.Add(10 * time.Minute)) { + return true + } + } + return false +} + func (s *SwitchSession) setOperator(username string) { s.currentOperator.Store(username) } @@ -301,7 +341,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo s.recordSessionFinished(model.ReasonErrIdleDisconnect) return } - if s.p.CheckPermissionExpired(now) { + if s.CheckPermissionExpired(now) { msg := lang.T("Permission has expired, disconnect") logger.Infof("Session[%s] permission has expired, disconnect", s.ID) msg = utils.WrapperWarn(msg) diff --git a/pkg/proxy/util.go b/pkg/proxy/util.go index 74b601b1b..4bf624d15 100644 --- a/pkg/proxy/util.go +++ b/pkg/proxy/util.go @@ -234,13 +234,13 @@ func ParseEndpointRegion(s string) string { } endpoint, err := url.Parse(s) if err != nil { - return "" + return s } endpoints := strings.Split(endpoint.Hostname(), ".") if len(endpoints) >= 3 { return endpoints[len(endpoints)-3] } - return "" + return endpoints[0] } func ParseAWSURLRegion(s string) string { diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 08856ed87..cfc0b3e1f 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -13,10 +13,14 @@ func GetSessionById(id string) (s *Session, ok bool) { return } -func GetAliveSessions() []string { +func GetAliveSessionIds() []string { return sessManager.Range() } +func GetSessions() []*Session { + return sessManager.GetSessions() +} + func AddSession(s *Session) { sessManager.Add(s.ID, s) } @@ -68,3 +72,11 @@ func (s *sessionManager) Range() []string { return sids } + +func (s *sessionManager) GetSessions() []*Session { + sessions := make([]*Session, 0, len(s.data)) + for _, sess := range s.data { + sessions = append(sessions, sess) + } + return sessions +} diff --git a/pkg/session/session.go b/pkg/session/session.go index 32fb9be72..7267b0845 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -2,11 +2,16 @@ package session import ( "fmt" + "github.com/jumpserver/koko/pkg/jms-sdk-go/model" ) -func NewSession(s *model.Session, handleTaskFunc func(task *model.TerminalTask) error) *Session { - return &Session{Session: s, handleTaskFunc: handleTaskFunc} +type TaskFunc func(task *model.TerminalTask) error + +func NewSession(s *model.Session, taskFunc TaskFunc) *Session { + return &Session{Session: s, + handleTaskFunc: taskFunc, + } } type Session struct { diff --git a/pkg/srvconn/conn_usql.go b/pkg/srvconn/conn_usql.go index 61c3a63b0..6bafb1281 100644 --- a/pkg/srvconn/conn_usql.go +++ b/pkg/srvconn/conn_usql.go @@ -114,7 +114,7 @@ func (o *sqlOption) USQLCommandArgs() ([]string, error) { } dsn := dsnURL.String() - prompt1 := "--variable=PROMPT1=" + o.AssetName + "%R%#" + prompt1 := "--variable=PROMPT1=" + o.Schema + "%R%#" return []string{dsn, prompt1}, nil } diff --git a/pkg/srvconn/sftp_asset.go b/pkg/srvconn/sftp_asset.go index 7a19ae5c0..c42b8e2e1 100644 --- a/pkg/srvconn/sftp_asset.go +++ b/pkg/srvconn/sftp_asset.go @@ -581,6 +581,11 @@ func (ad *AssetDir) createSftpSession(su *model.PermAccount) (sftpSess *SftpSess case model.TaskKillSession: sftpSession.CloseWithReason(model.ReasonErrAdminTerminate) return nil + case model.TaskPermExpired: + sftpSession.CloseWithReason(model.ReasonErrPermissionExpired) + return nil + case model.TaskPermValid: + return nil } return fmt.Errorf("sftp session not support task: %s", task.Name) } diff --git a/pkg/zmodem/frame_type.go b/pkg/zmodem/frame_type.go index 3de4f43b6..5511d462c 100644 --- a/pkg/zmodem/frame_type.go +++ b/pkg/zmodem/frame_type.go @@ -1,6 +1,6 @@ package zmodem -//FRAME TYPES +// FRAME TYPES const ( ZRQINIT = 0x00 /* request receive init (s->r) */ ZRINIT = 0x01 /* receive init (r->s) */ @@ -90,7 +90,7 @@ const ( CAN = 0x18 ) -//ZDLE SEQUENCES +// ZDLE SEQUENCES const ( ZCRCE = 0x68 /* CRC next, frame ends, header packet follows */ ZCRCG = 0x69 /* CRC next, frame continues nonstop */ diff --git a/pkg/zmodem/zmodem.go b/pkg/zmodem/zmodem.go index 126004061..5ffd4e101 100644 --- a/pkg/zmodem/zmodem.go +++ b/pkg/zmodem/zmodem.go @@ -36,6 +36,8 @@ type ZmodemParser struct { hasDataTransfer bool FireStatusEvent func(event StatusEvent) + + AbnormalFinish bool } // rz sz 解析的入口 @@ -47,6 +49,7 @@ func (z *ZmodemParser) Parse(p []byte) { zSession := z.currentSession zSession.consume(p) if zSession.IsEnd() { + z.AbnormalFinish = zSession.AbnormalFinish z.currentSession = nil if z.FileEventCallback != nil && z.currentZFileInfo != nil { info := z.currentZFileInfo @@ -84,7 +87,6 @@ func (z *ZmodemParser) Parse(p []byte) { z.currentSession = &ZSession{ Type: TypeDownload, endCallback: func() { - z.setStatus(ZParserStatusNone) if z.FireStatusEvent != nil { z.FireStatusEvent(EndEvent) } @@ -101,7 +103,6 @@ func (z *ZmodemParser) Parse(p []byte) { z.currentSession = &ZSession{ Type: TypeUpload, endCallback: func() { - z.setStatus(ZParserStatusNone) if z.FireStatusEvent != nil { z.FireStatusEvent(EndEvent) } diff --git a/pkg/zmodem/zsession.go b/pkg/zmodem/zsession.go index abab81274..5669afd46 100644 --- a/pkg/zmodem/zsession.go +++ b/pkg/zmodem/zsession.go @@ -185,6 +185,8 @@ type ZSession struct { ZFileHeaderCallback func(zInfo *ZFileInfo) zOnHeader func(hd *ZmodemHeader) + + AbnormalFinish bool } // zsession 入口 @@ -199,6 +201,7 @@ func (s *ZSession) consume(p []byte) { return } logger.Infof("Zmodem session %s abnormally finish", s.Type) + s.AbnormalFinish = true return } if s.checkAbort(p) { @@ -347,7 +350,7 @@ func (s *ZSession) onHeader(hd *ZmodemHeader) { s.transferStatus = TransferStatusFinished s.zFileInfo = nil case ZFIN: - s.haveEnd = true + //s.haveEnd = true if s.endCallback != nil { s.endCallback() } diff --git a/ui/.env.development b/ui/.env.development index 4f38f10d3..444822040 100644 --- a/ui/.env.development +++ b/ui/.env.development @@ -5,13 +5,13 @@ VITE_USER_NODE_ENV=development VITE_PUBLIC_PATH=/koko # Socek 路径 -VITE_KOKO_WS_URL="localhost:5050" +VITE_KOKO_WS_URL="http://localhost:5001" # API 接口路径 -VITE_KOKO_API_URL="localhost:8080" +VITE_KOKO_API_URL="http://localhost:8080" # 静态资路径 -VITE_KOKO_STATIC_URL="localhost:8080" +VITE_KOKO_STATIC_URL="http://localhost:8080" # 打包压缩后是否删除源文件 VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE=false diff --git a/ui/package.json b/ui/package.json index a1ffb00f2..e1af205a5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,6 +14,7 @@ "@vueuse/core": "^10.11.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-search": "^0.15.0", + "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "alova": "^3.0.2", "clipboard-polyfill": "^4.1.0", diff --git a/ui/src/components/Kubernetes/MainContent/index.vue b/ui/src/components/Kubernetes/MainContent/index.vue index 5e09d2808..a64a9113c 100644 --- a/ui/src/components/Kubernetes/MainContent/index.vue +++ b/ui/src/components/Kubernetes/MainContent/index.vue @@ -378,6 +378,7 @@ const handleReconnect = () => { const handleContextMenuSelect = (key: string, _option: DropdownOption) => { switch (key) { case 'reconnect': { + mittBus.emit('remove-event'); handleReconnect(); break; } @@ -447,6 +448,7 @@ const handleClickOutside = () => { const resetShareDialog = () => { paramsStore.setShareId(''); paramsStore.setShareCode(''); + userOptions.value = []; dialog.destroyAll(); }; @@ -661,6 +663,10 @@ const handleClose = (name: string) => { findNodeById(nameRef.value); terminalStore.setTerminalConfig('currentTab', nameRef.value); } + + if (panelLength === 0) { + mittBus.emit('remove-event'); + } }; /** diff --git a/ui/src/global.d.ts b/ui/src/global.d.ts index 836fad827..f366674c9 100644 --- a/ui/src/global.d.ts +++ b/ui/src/global.d.ts @@ -19,7 +19,7 @@ declare module 'nora-zmodemjs/src/zmodem_browser' { export class Browser { static send_files( session: ZmodemSession, - files: File, + files: File[], opts?: { on_offer_response?: (obj: any, xfer: ZmodemTransfer) => void; on_file_complete?: (obj: any) => void; diff --git a/ui/src/hooks/helper/index.ts b/ui/src/hooks/helper/index.ts index 6175e0bc2..3662f5854 100644 --- a/ui/src/hooks/helper/index.ts +++ b/ui/src/hooks/helper/index.ts @@ -34,36 +34,35 @@ const { message } = createDiscreteApi(['message']); * @param k8s_id */ export const handleContextMenu = async ( - e: MouseEvent, - config: ILunaConfig, - socket: WebSocket, - terminalId: string, - termSelectionText: string, - k8s_id: string | undefined + e: MouseEvent, + config: ILunaConfig, + socket: WebSocket, + terminalId: string, + termSelectionText: string, + k8s_id: string | undefined ) => { - e.preventDefault(); - if (e.ctrlKey || config.quickPaste !== '1') return; - - let text: string = ''; - - try { - text = await readText(); - } catch (e) { - if (termSelectionText !== '') text = termSelectionText; - } - - if (k8s_id) { - socket.send( - JSON.stringify({ - id: terminalId, - k8s_id, - type: 'TERMINAL_K8S_DATA', - data: text - }) - ); - } else { - socket.send(formatMessage(terminalId, 'TERMINAL_DATA', text)); - } + if (e.ctrlKey || config.quickPaste !== '1') return; + + let text: string = ''; + + try { + text = await readText(); + } catch (e) { + if (termSelectionText !== '') text = termSelectionText; + } + e.preventDefault(); + if (k8s_id) { + socket.send( + JSON.stringify({ + id: terminalId, + k8s_id, + type: 'TERMINAL_K8S_DATA', + data: text + }) + ); + } else { + socket.send(formatMessage(terminalId, 'TERMINAL_DATA', text)); + } }; /** @@ -76,37 +75,39 @@ export const handleContextMenu = async ( * @param socket */ export const handleTerminalResize = ( - cols: number, - rows: number, - type: string, - terminalId: string, - socket: WebSocket + cols: number, + rows: number, + type: string, + terminalId: string, + socket: WebSocket ) => { - let data; + let data; - info('Send Term Resize'); + info('Send Term Resize'); - const treeStore = useTreeStore(); - const { currentNode } = storeToRefs(treeStore); + const treeStore = useTreeStore(); + const { currentNode } = storeToRefs(treeStore); - const eventType = type === 'k8s' ? 'TERMINAL_K8S_RESIZE' : 'TERMINAL_RESIZE'; - const resizeData = JSON.stringify({ cols, rows }); - - if (type === 'k8s' && currentNode.value.children) { - const currentItem = currentNode.value.children[0]; - - data = { - k8s_id: currentItem.k8s_id, - namespace: currentItem.namespace, - pod: currentItem.pod, - container: currentItem.container, - type: eventType, - id: terminalId, - resizeData - }; - } + const eventType = type === 'k8s' ? 'TERMINAL_K8S_RESIZE' : 'TERMINAL_RESIZE'; + const resizeData = JSON.stringify({ cols, rows }); + + data = resizeData; + + if (type === 'k8s' && currentNode.value.children) { + const currentItem = currentNode.value.children[0]; + + data = { + k8s_id: currentItem.k8s_id, + namespace: currentItem.namespace, + pod: currentItem.pod, + container: currentItem.container, + type: eventType, + id: terminalId, + resizeData + }; + } - socket.send(formatMessage(terminalId, eventType, data)); + socket.send(formatMessage(terminalId, eventType, data)); }; /** @@ -118,37 +119,37 @@ export const handleTerminalResize = ( * @param origin */ export const handleCustomKey = ( - e: KeyboardEvent, - terminal: Terminal, - lunaId: string, - origin: string + e: KeyboardEvent, + terminal: Terminal, + lunaId: string, + origin: string ): boolean => { - if (e.altKey && e.shiftKey && (e.key === 'ArrowRight' || e.key === 'ArrowLeft')) { - switch (e.key) { - case 'ArrowRight': - if (lunaId && origin) { - sendEventToLuna('KEYEVENT', 'alt+shift+right', lunaId, origin); - } else { - mittBus.emit('alt-shift-right'); - } - - break; - case 'ArrowLeft': - if (lunaId && origin) { - sendEventToLuna('KEYEVENT', 'alt+shift+left', lunaId, origin); - } else { - mittBus.emit('alt-shift-left'); - } - break; + if (e.altKey && e.shiftKey && (e.key === 'ArrowRight' || e.key === 'ArrowLeft')) { + switch (e.key) { + case 'ArrowRight': + if (lunaId && origin) { + sendEventToLuna('KEYEVENT', 'alt+shift+right', lunaId, origin); + } else { + mittBus.emit('alt-shift-right'); } - return false; - } - if (e.ctrlKey && e.key === 'c' && terminal.hasSelection()) { - return false; + break; + case 'ArrowLeft': + if (lunaId && origin) { + sendEventToLuna('KEYEVENT', 'alt+shift+left', lunaId, origin); + } else { + mittBus.emit('alt-shift-left'); + } + break; } + return false; + } + + if (e.ctrlKey && e.key === 'c' && terminal.hasSelection()) { + return false; + } - return !(e.ctrlKey && e.key === 'v'); + return !(e.ctrlKey && e.key === 'v'); }; /** @@ -158,18 +159,18 @@ export const handleCustomKey = ( * @param termSelectionText */ export const handleTerminalSelection = async (terminal: Terminal, termSelectionText: Ref) => { - termSelectionText.value = terminal.getSelection().trim(); - - if (termSelectionText.value !== '') { - clipboard - .writeText(termSelectionText.value) - .then(() => {}) - .catch(e => { - message.error(`Copy Error for ${e}`); - }); - } else { - // message.warning('Please select the text before copying'); - } + termSelectionText.value = terminal.getSelection().trim(); + + if (termSelectionText.value !== '') { + clipboard + .writeText(termSelectionText.value) + .then(() => {}) + .catch(e => { + message.error(`Copy Error for ${e}`); + }); + } else { + // message.warning('Please select the text before copying'); + } }; /** @@ -183,58 +184,58 @@ export const handleTerminalSelection = async (terminal: Terminal, termSelectionT */ // todo export const handleTerminalOnData = ( - data: string, - type: string, - terminalId: string, - config: ILunaConfig, - socket: WebSocket + data: string, + type: string, + terminalId: string, + config: ILunaConfig, + socket: WebSocket ) => { - const terminalStore = useTerminalStore(); - const { enableZmodem, zmodemStatus } = storeToRefs(terminalStore); + const terminalStore = useTerminalStore(); + const { enableZmodem, zmodemStatus } = storeToRefs(terminalStore); - // 如果未开启 Zmodem 且当前在 Zmodem 状态,不允许输入 - if (!enableZmodem.value && zmodemStatus.value) { - return message.warning('未开启 Zmodem 且当前在 Zmodem 状态,不允许输入'); - } + // 如果未开启 Zmodem 且当前在 Zmodem 状态,不允许输入 + if (!enableZmodem.value && zmodemStatus.value) { + return message.warning('未开启 Zmodem 且当前在 Zmodem 状态,不允许输入'); + } - data = preprocessInput(data, config); - const eventType = type === 'k8s' ? 'TERMINAL_K8S_DATA' : 'TERMINAL_DATA'; - - // 如果类型是 k8s,处理 k8s 的逻辑 - if (type === 'k8s') { - const treeStore = useTreeStore(); - const { currentNode } = storeToRefs(treeStore); - const node = currentNode.value; - - // 获取默认的消息体 - const messageData = { - data: data, - id: terminalId, - type: eventType, - pod: node.pod || '', - k8s_id: node.k8s_id, - namespace: node.namespace || '', - container: node.container || '' - }; - - // 如果有子节点但不是父节点,取第一个子节点的信息 - if (node.children && node.children.length > 0) { - const currentItem = node.children[0]; - Object.assign(messageData, { - pod: currentItem.pod, - k8s_id: currentItem.k8s_id, - namespace: currentItem.namespace, - container: currentItem.container - }); - } + data = preprocessInput(data, config); + const eventType = type === 'k8s' ? 'TERMINAL_K8S_DATA' : 'TERMINAL_DATA'; - // 发送消息 - return socket.send(JSON.stringify(messageData)); + // 如果类型是 k8s,处理 k8s 的逻辑 + if (type === 'k8s') { + const treeStore = useTreeStore(); + const { currentNode } = storeToRefs(treeStore); + const node = currentNode.value; + + // 获取默认的消息体 + const messageData = { + data: data, + id: terminalId, + type: eventType, + pod: node.pod || '', + k8s_id: node.k8s_id, + namespace: node.namespace || '', + container: node.container || '' + }; + + // 如果有子节点但不是父节点,取第一个子节点的信息 + if (node.children && node.children.length > 0) { + const currentItem = node.children[0]; + Object.assign(messageData, { + pod: currentItem.pod, + k8s_id: currentItem.k8s_id, + namespace: currentItem.namespace, + container: currentItem.container + }); } - // 处理非 k8s 的情况 - sendEventToLuna('KEYBOARDEVENT', ''); - socket.send(formatMessage(terminalId, eventType, data)); + // 发送消息 + return socket.send(JSON.stringify(messageData)); + } + + // 处理非 k8s 的情况 + sendEventToLuna('KEYBOARDEVENT', ''); + socket.send(formatMessage(terminalId, eventType, data)); }; /** @@ -247,96 +248,96 @@ export const handleTerminalOnData = ( * @param terminalId */ export const onWebsocketOpen = ( - socket: WebSocket, - lastSendTime: Date, - terminalId: string, - pingInterval: Ref, - lastReceiveTime: Ref + socket: WebSocket, + lastSendTime: Date, + terminalId: string, + pingInterval: Ref, + lastReceiveTime: Ref ) => { - socket.binaryType = 'arraybuffer'; - sendEventToLuna('CONNECTED', ''); + socket.binaryType = 'arraybuffer'; + sendEventToLuna('CONNECTED', ''); - if (pingInterval.value) clearInterval(pingInterval.value); + if (pingInterval.value) clearInterval(pingInterval.value); - pingInterval.value = setInterval(() => { - if (socket.CLOSED === socket.readyState || socket.CLOSING === socket.readyState) { - return clearInterval(pingInterval.value!); - } + pingInterval.value = setInterval(() => { + if (socket.CLOSED === socket.readyState || socket.CLOSING === socket.readyState) { + return clearInterval(pingInterval.value!); + } - let currentDate: Date = new Date(); + let currentDate: Date = new Date(); - if (lastReceiveTime.value.getTime() - currentDate.getTime() > MaxTimeout) { - message.info('More than 30s do not receive data'); - } + if (lastReceiveTime.value.getTime() - currentDate.getTime() > MaxTimeout) { + message.info('More than 30s do not receive data'); + } - let pingTimeout: number = currentDate.getTime() - lastSendTime.getTime(); + let pingTimeout: number = currentDate.getTime() - lastSendTime.getTime(); - if (pingTimeout < 0) return; + if (pingTimeout < 0) return; - socket.send(formatMessage(terminalId, 'PING', '')); - }, 25 * 1000); + socket.send(formatMessage(terminalId, 'PING', '')); + }, 25 * 1000); }; /** * 生成 Socket url */ export const generateWsURL = () => { - const route = useRoute(); + const route = useRoute(); - const routeName = route.name; - const urlParams = new URLSearchParams(window.location.search.slice(1)); + const routeName = route.name; + const urlParams = new URLSearchParams(window.location.search.slice(1)); - let connectURL; + let connectURL; - switch (routeName) { - case 'Token': { - const params = route.params; - const requireParams = new URLSearchParams(); + switch (routeName) { + case 'Token': { + const params = route.params; + const requireParams = new URLSearchParams(); - requireParams.append('type', 'token'); - requireParams.append('target_id', params.id ? params.id.toString() : ''); + requireParams.append('type', 'token'); + requireParams.append('target_id', params.id ? params.id.toString() : ''); - connectURL = BASE_WS_URL + '/koko/ws/token/?' + requireParams.toString(); - break; - } - case 'TokenParams': { - connectURL = urlParams ? `${BASE_WS_URL}/koko/ws/token/?${urlParams.toString()}` : ''; - break; - } - case 'kubernetes': { - connectURL = `${BASE_WS_URL}/koko/ws/terminal/?token=${route.query.token}&type=k8s`; - break; - } - case 'Share': { - const id = route.params.id as string; - const requireParams = new URLSearchParams(); + connectURL = BASE_WS_URL + '/koko/ws/token/?' + requireParams.toString(); + break; + } + case 'TokenParams': { + connectURL = urlParams ? `${BASE_WS_URL}/koko/ws/token/?${urlParams.toString()}` : ''; + break; + } + case 'kubernetes': { + connectURL = `${BASE_WS_URL}/koko/ws/terminal/?token=${route.query.token}&type=k8s`; + break; + } + case 'Share': { + const id = route.params.id as string; + const requireParams = new URLSearchParams(); - requireParams.append('type', 'share'); - requireParams.append('target_id', id); + requireParams.append('type', 'share'); + requireParams.append('target_id', id); - connectURL = BASE_WS_URL + '/koko/ws/terminal/?' + requireParams.toString(); - break; - } - case 'Monitor': { - const id = route.params.id as string; - const requireParams = new URLSearchParams(); + connectURL = BASE_WS_URL + '/koko/ws/terminal/?' + requireParams.toString(); + break; + } + case 'Monitor': { + const id = route.params.id as string; + const requireParams = new URLSearchParams(); - requireParams.append('type', 'monitor'); - requireParams.append('target_id', id); + requireParams.append('type', 'monitor'); + requireParams.append('target_id', id); - connectURL = BASE_WS_URL + '/koko/ws/terminal/?' + requireParams.toString(); - break; - } - default: { - connectURL = urlParams ? `${BASE_WS_URL}/koko/ws/terminal/?${urlParams.toString()}` : ''; - } + connectURL = BASE_WS_URL + '/koko/ws/terminal/?' + requireParams.toString(); + break; } - - if (!connectURL) { - message.error('Unable to generate WebSocket URL, missing parameters.'); + default: { + connectURL = urlParams ? `${BASE_WS_URL}/koko/ws/terminal/?${urlParams.toString()}` : ''; } + } + + if (!connectURL) { + message.error('Unable to generate WebSocket URL, missing parameters.'); + } - return connectURL; + return connectURL; }; /** @@ -347,19 +348,19 @@ export const generateWsURL = () => { * @param type */ export const onWebsocketWrong = (event: Event, type: string, terminal?: Terminal) => { - switch (type) { - case 'error': { - terminal ? terminal.write('Connection Websocket Error') : ''; - break; - } - case 'disconnected': { - terminal ? terminal.write('Connection Websocket Closed') : ''; - break; - } + switch (type) { + case 'error': { + terminal ? terminal.write('Connection Websocket Error') : ''; + break; + } + case 'disconnected': { + terminal ? terminal.write('Connection Websocket Closed') : ''; + break; } + } - fireEvent(new Event('CLOSE', {})); - handleError(event); + fireEvent(new Event('CLOSE', {})); + handleError(event); }; /** @@ -367,13 +368,13 @@ export const onWebsocketWrong = (event: Event, type: string, terminal?: Terminal * @param base64 */ export const base64ToUint8Array = (base64: string): Uint8Array => { - // 转为原始的二进制字符串(binaryString)。 - const binaryString = atob(base64); - const len = binaryString.length; - - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; + // 转为原始的二进制字符串(binaryString)。 + const binaryString = atob(base64); + const len = binaryString.length; + + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; }; diff --git a/ui/src/hooks/useTerminal.ts b/ui/src/hooks/useTerminal.ts index 109aafe98..d8d83cd09 100644 --- a/ui/src/hooks/useTerminal.ts +++ b/ui/src/hooks/useTerminal.ts @@ -2,6 +2,7 @@ import xtermTheme from 'xterm-theme'; import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; +// import { WebglAddon } from '@xterm/addon-webgl'; import { ISearchOptions, SearchAddon } from '@xterm/addon-search'; import { Sentry } from 'nora-zmodemjs/src/zmodem_browser'; import { defaultTheme } from '@/config'; @@ -9,7 +10,7 @@ import { defaultTheme } from '@/config'; // hook import { createDiscreteApi } from 'naive-ui'; import { useSentry } from '@/hooks/useZsentry.ts'; -import { useDebounceFn, useWebSocket } from '@vueuse/core'; +import { useWebSocket } from '@vueuse/core'; // store import { storeToRefs } from 'pinia'; @@ -84,6 +85,7 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr let lastReceiveTime: Ref = ref(new Date()); let messageHandlers = {}; + let handleSocketMessage: any; const dispatch = (data: string) => { if (!data) return; @@ -97,6 +99,7 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr switch (msg.type) { case 'CONNECT': { + fitAddon.fit(); terminalId.value = msg.id; const terminalData = { @@ -129,9 +132,9 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr case 'ZMODEM_START': { terminalStore.setTerminalConfig('zmodemStatus', true); - if (enableZmodem.value) { - option.i18nCallBack && message.info(option.i18nCallBack('WaitFileTransfer')); - } + // if (enableZmodem.value) { + // option.i18nCallBack && message.info(option.i18nCallBack('WaitFileTransfer')); + // } break; } case 'ZMODEM_END': { @@ -140,7 +143,7 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr terminal?.write('\r\n'); - zmodemStatus.value = false; + terminalStore.setTerminalConfig('zmodemStatus', false); } break; } @@ -152,6 +155,7 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr } case 'TERMINAL_ERROR': case 'ERROR': { + terminal?.write(msg.err); break; } case 'MESSAGE_NOTIFY': { @@ -235,7 +239,15 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr if (typeof event.data === 'object') { if (enableZmodem.value) { - sentry.consume(event.data); + try { + sentry.consume(event.data); + } catch (e) { + if (sentry.get_confirmed_session()) { + sentry.get_confirmed_session()?.abort(); + message.error('File transfer error, file transfer interrupted'); + } + console.log(e); + } } else { writeBufferToTerminal(enableZmodem.value, zmodemStatus.value, terminal!, event.data); } @@ -269,8 +281,8 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr break; } case 'TERMINAL_ACTION': { - // k8s 没有 rz 与 sz 的操作 const action = socketData.data; + switch (action) { case 'ZMODEM_START': { option.i18nCallBack && @@ -432,7 +444,13 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr } }); - window.addEventListener('resize', () => useDebounceFn(() => fitAddon.fit(), 500), false); + window.addEventListener( + 'resize', + () => { + fitAddon.fit(); + }, + false + ); if (option.type === 'k8s') { window.addEventListener('keydown', (event: KeyboardEvent) => { @@ -462,6 +480,7 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr if (terminal) { terminal.loadAddon(fitAddon); terminal.loadAddon(searchAddon); + // terminal.loadAddon(new WebglAddon()); terminal.open(el); terminal.focus(); @@ -478,7 +497,8 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr handleTerminalOnData(data, type, terminalId.value, lunaConfig, socket); }); terminal.onResize(({ cols, rows }) => { - useDebounceFn(() => handleTerminalResize(cols, rows, type, terminalId.value, socket), 500); + fitAddon.fit(); + handleTerminalResize(cols, rows, type, terminalId.value, socket); }); } }; @@ -559,14 +579,16 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr } }; - option.transSocket?.addEventListener('message', (e: MessageEvent) => { + handleSocketMessage = (e: MessageEvent) => { // @ts-ignore - const handler = messageHandlers[currentTab.value as string]; + const handler = messageHandlers[currentTab.value]; if (handler) { handler(e); } - }); + }; + + option.transSocket?.addEventListener('message', handleSocketMessage); } else { initSocketEvent(); } @@ -576,6 +598,11 @@ export const useTerminal = async (el: HTMLElement, option: ICallbackOptions): Pr * 初始化事件总线相关事件 */ const initMittBusEvents = () => { + mittBus.on('remove-event', () => { + // @ts-ignore + option.transSocket.removeEventListener('message', handleSocketMessage); + }); + mittBus.on('terminal-search', ({ keyword, type = '' }) => { searchKeyWord(keyword, type); }); diff --git a/ui/src/hooks/useZsentry.ts b/ui/src/hooks/useZsentry.ts index 2ed909b8a..81cf0133b 100644 --- a/ui/src/hooks/useZsentry.ts +++ b/ui/src/hooks/useZsentry.ts @@ -3,7 +3,7 @@ import { h, ref } from 'vue'; import { useLogger } from '@/hooks/useLogger.ts'; import { bytesHuman } from '@/utils'; import { wsIsActivated } from '@/components/CustomTerminal/helper'; -import { createDiscreteApi, UploadFileInfo } from 'naive-ui'; +import { createDiscreteApi, UploadFileInfo, darkTheme } from 'naive-ui'; import { Terminal } from '@xterm/xterm'; import { computed } from 'vue'; @@ -24,7 +24,11 @@ import { Ref } from 'vue'; import { DialogOptions } from 'naive-ui/es/dialog/src/DialogProvider'; // API 初始化 -const { message, dialog } = createDiscreteApi(['message', 'dialog']); +const { message, dialog } = createDiscreteApi(['message', 'dialog'], { + configProviderProps: { + theme: darkTheme + } +}); const { debug, info, error } = useLogger('useSentry'); interface IUseSentry { @@ -50,6 +54,9 @@ export const useSentry = (lastSendTime?: Ref, t?: any): IUseSentry => { } }; + /** + * upload 的回调 + */ const handleUpload = () => { const selectFile: UploadFileInfo = fileList.value[0]; @@ -62,10 +69,8 @@ export const useSentry = (lastSendTime?: Ref, t?: any): IUseSentry => { } if (!zmodeSession.value) return; - - debug(`Zomdem submit file: ${selectFile.file}`); - - ZmodemBrowser.Browser.send_files(zmodeSession.value, selectFile.file as File, { + const files = fileList.value.map(item => item.file as File); + ZmodemBrowser.Browser.send_files(zmodeSession.value, files, { on_offer_response: (_obj: any, xfer: ZmodemTransfer) => { if (xfer) { xfer.on('send_progress', (percent: number) => { @@ -79,10 +84,9 @@ export const useSentry = (lastSendTime?: Ref, t?: any): IUseSentry => { } }) .then(() => { - zmodeSession.value && zmodeSession.value.close(); + zmodeSession.value?.close(); }) .catch((e: Error) => { - // todo)) 现在上传文件会走到这里 console.log(e); }); }; @@ -125,12 +129,16 @@ export const useSentry = (lastSendTime?: Ref, t?: any): IUseSentry => { debug('Cancel Abort'); zmodeSession.value.abort(); } - - debug('删除 Dialog 的文件'); } }; }); + /** + * 展示 progress 的函数 + * + * @param xfer + * @param terminal + */ const updateReceiveProgress = (xfer: ZmodemTransfer, terminal: Terminal) => { let detail = xfer.get_details(); let name = detail.name; @@ -145,9 +153,14 @@ export const useSentry = (lastSendTime?: Ref, t?: any): IUseSentry => { let msg = `${t('Download')} ${name}: ${bytesHuman(total)} ${percent}% `; - terminal.write(msg); + terminal.write('\r' + msg); }; + /** + * 处理 rz 命令 + * @param zsession + * @param terminal + */ const handleSendSession = (zsession: ZmodemSession, terminal: Terminal) => { zmodeSession.value = zsession; @@ -155,12 +168,20 @@ export const useSentry = (lastSendTime?: Ref, t?: any): IUseSentry => { zmodeSession.value = null; fileList.value = []; terminal.write('\r\n'); + + zsession.close(); }); dialog.success(dialogOptions.value); }; + /** + * 处理 sz 命令 + * @param zsession + * @param terminal + */ const handleReceiveSession = (zsession: ZmodemSession, terminal: Terminal) => { + zmodeSession.value = zsession; zsession.on('offer', (xfer: ZmodemTransfer) => { const buffer: Uint8Array[] = []; const detail = xfer.get_details(); @@ -188,7 +209,9 @@ export const useSentry = (lastSendTime?: Ref, t?: any): IUseSentry => { terminal.write('\r\n'); - if (zmodeSession.value) zmodeSession.value.abort(); + if (zmodeSession.value) { + zmodeSession.value.abort(); + } }) .catch((e: Error) => { message.error(`Error: ${e}`); @@ -217,6 +240,7 @@ export const useSentry = (lastSendTime?: Ref, t?: any): IUseSentry => { if (!wsIsActivated(ws)) { return debug('WebSocket Closed'); } + try { lastSendTime && (lastSendTime.value = new Date()); ws.send(new Uint8Array(octets)); @@ -232,6 +256,7 @@ export const useSentry = (lastSendTime?: Ref, t?: any): IUseSentry => { terminal.write('\r\n'); + // @ts-ignore if (zsession.type === 'send') { handleSendSession(zsession, terminal); } else { diff --git a/ui/src/index.css b/ui/src/index.css index 6d295578d..b5c61c956 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -1,7 +1,3 @@ @tailwind base; @tailwind components; @tailwind utilities; - -*{ - font-family: 'Open Sans', 'Monaco', 'Consolas', 'monospace'; -} diff --git a/ui/src/style/fonts/OpenSans-Bold.ttf b/ui/src/style/fonts/OpenSans-Bold.ttf new file mode 100644 index 000000000..b7fadfa4a Binary files /dev/null and b/ui/src/style/fonts/OpenSans-Bold.ttf differ diff --git a/ui/src/style/fonts/OpenSans-BoldItalic.ttf b/ui/src/style/fonts/OpenSans-BoldItalic.ttf new file mode 100644 index 000000000..136a4b4c0 Binary files /dev/null and b/ui/src/style/fonts/OpenSans-BoldItalic.ttf differ diff --git a/ui/src/style/fonts/OpenSans-Italic.ttf b/ui/src/style/fonts/OpenSans-Italic.ttf new file mode 100644 index 000000000..e99cb92d4 Binary files /dev/null and b/ui/src/style/fonts/OpenSans-Italic.ttf differ diff --git a/ui/src/style/fonts/OpenSans-Light.ttf b/ui/src/style/fonts/OpenSans-Light.ttf new file mode 100644 index 000000000..a0ba20432 Binary files /dev/null and b/ui/src/style/fonts/OpenSans-Light.ttf differ diff --git a/ui/src/style/fonts/OpenSans-Regular.ttf b/ui/src/style/fonts/OpenSans-Regular.ttf new file mode 100644 index 000000000..8529c432c Binary files /dev/null and b/ui/src/style/fonts/OpenSans-Regular.ttf differ diff --git a/ui/src/style/reset.scss b/ui/src/style/reset.scss index e63b01258..9d6365081 100644 --- a/ui/src/style/reset.scss +++ b/ui/src/style/reset.scss @@ -1,3 +1,24 @@ +@font-face { + font-family: 'Open Sans'; + src: url('./fonts/OpenSans-Regular.ttf'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Open Sans'; + src: url('./fonts/OpenSans-Bold.ttf'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Open Sans'; + src: url('./fonts/OpenSans-Light.ttf'); + font-weight: 300; + font-style: normal; +} + html, body, #app { @@ -5,7 +26,7 @@ body, height: 100%; padding: 0; margin: 0; - font-family: opensans, sans-serif; + font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; } #app { diff --git a/ui/src/utils/mittBus.ts b/ui/src/utils/mittBus.ts index a217bfac2..604b3900b 100644 --- a/ui/src/utils/mittBus.ts +++ b/ui/src/utils/mittBus.ts @@ -3,6 +3,7 @@ import { customTreeOption } from '@/hooks/interface'; import { shareUser } from '@/views/interface'; type Event = { + 'remove-event': void; 'alt-shift-right': void; 'alt-shift-left': void; 'open-setting': void; diff --git a/ui/src/views/Connection/index.vue b/ui/src/views/Connection/index.vue index 123efb7d8..30d735c19 100644 --- a/ui/src/views/Connection/index.vue +++ b/ui/src/views/Connection/index.vue @@ -22,7 +22,7 @@ import { Terminal } from '@xterm/xterm'; import { storeToRefs } from 'pinia'; import { NMessageProvider } from 'naive-ui'; -import { computed, h, markRaw, nextTick, reactive, Ref, ref } from 'vue'; +import { computed, h, markRaw, nextTick, onUnmounted, reactive, Ref, ref } from 'vue'; import xtermTheme from 'xterm-theme'; import mittBus from '@/utils/mittBus.ts'; @@ -65,9 +65,13 @@ const terminalType = ref('common'); const enableShare = ref(false); const userOptions = ref([]); const terminalRef: Ref = ref(); - +const warningIntervalId = ref(0); const onlineUsersMap = reactive<{ [key: string]: any }>({}); +onUnmounted(() => { + clearInterval(warningIntervalId.value); +}); + const settings = computed((): ISettingProp[] => { return [ { @@ -367,6 +371,20 @@ const onSocketData = (msgType: string, msg: any, terminal: Terminal) => { message.info(`${data.user} ${t('ResumeSession')}`); break; } + case 'TERMINAL_PERM_VALID': { + clearInterval(warningIntervalId.value); + message.info(`${t('PermissionValid')}`); + break; + } + case 'TERMINAL_PERM_EXPIRED': { + const data = JSON.parse(msg.data); + const warningMsg = `${t('PermissionExpired')}: ${data.detail}`; + message.warning(warningMsg); + warningIntervalId.value = setInterval(() => { + message.warning(warningMsg); + }, 1000 * 60); + break; + } case 'CLOSE': { enableShare.value = false; diff --git a/ui/src/views/ShareTerminal/index.vue b/ui/src/views/ShareTerminal/index.vue index 0eda7bf31..7c9614303 100644 --- a/ui/src/views/ShareTerminal/index.vue +++ b/ui/src/views/ShareTerminal/index.vue @@ -2,14 +2,15 @@ import { useI18n } from 'vue-i18n'; -import { h, reactive, ref } from 'vue'; +import { h, onUnmounted, reactive, ref } from 'vue'; import { NInput, NButton, @@ -55,9 +56,14 @@ const verified = ref(false); const terminalId = ref(''); const verifyValue = ref(''); const waterMarkContent = ref(''); +const warningIntervalId = ref(0); const onlineUsersMap = reactive<{ [key: string]: any }>({}); +onUnmounted(() => { + clearInterval(warningIntervalId.value); +}); + const handleVerify = () => { if (verifyValue.value === '') return message.warning(t('InputVerifyCode')); @@ -145,6 +151,20 @@ const onSocketData = (msgType: string, msg: any, _terminal: Terminal) => { break; } + case 'TERMINAL_PERM_VALID': { + clearInterval(warningIntervalId.value); + message.info(`${t('PermissionValid')}`); + break; + } + case 'TERMINAL_PERM_EXPIRED': { + const data = JSON.parse(msg.data); + const warningMsg = `${t('PermissionExpired')}: ${data.detail}`; + message.warning(warningMsg); + warningIntervalId.value = setInterval(() => { + message.warning(warningMsg); + }, 1000 * 26); + break; + } default: { break; } diff --git a/ui/yarn.lock b/ui/yarn.lock index 5718128e6..1617ef812 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -616,6 +616,11 @@ resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0.tgz#5c772d5f14c26546c4bfbeb0c3d4b3333057411f" integrity sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg== +"@xterm/addon-webgl@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0.tgz#9e927cee10af971595fb2a72fd4c3bc2819f0096" + integrity sha512-xCnfMBTI+/HKPdRnSOHaJDRqEpq2Ugy8LEj9GiY4J3zJObo3joylIFaMvzBwbYRg8zLtkO0KQaStCeSfoaI2/w== + "@xterm/xterm@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0.tgz#275fb8f6e14afa6e8a0c05d4ebc94523ff775396" diff --git a/utils/message.sh b/utils/message.sh index 44fbcf807..679388ed5 100755 --- a/utils/message.sh +++ b/utils/message.sh @@ -3,7 +3,7 @@ BASE_DIR=$(cd $(dirname $0);pwd) PROJECT_DIR=$(dirname ${BASE_DIR}) -LANG="zh_CN en_US ja_JP" +LANG="zh_CN zh_Hant en_US ja_JP" DOMAIN=koko BIN=${PROJECT_DIR}/cmd/i18ntool/geni18n.go INPUT=pkg