diff --git a/frontend/package.json b/frontend/package.json
index 441e13ca..d89046c1 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,9 +1,9 @@
{
"name": "nginx-ui-frontend",
- "version": "1.3.2",
+ "version": "1.4.0",
"private": true,
"scripts": {
- "serve": "vue-cli-service serve",
+ "serve": "vue-cli-service serve --port 8021",
"build": "vue-cli-service build --dest dist --modern",
"lint": "vue-cli-service lint"
},
@@ -26,6 +26,7 @@
"core-js": "^3.9.0",
"less": "^3.11.1",
"less-loader": "^5.0.0",
+ "lodash": "^4.17.21",
"lowlight": "^1.20.0",
"moment": "^2.24.0",
"node-sass": "^6.0.1",
@@ -46,7 +47,10 @@
"vue-template-compiler": "^2.6.11",
"vue2-ace-editor": "^0.0.15",
"vuex": "^3.6.2",
- "vuex-persist": "^3.1.3"
+ "vuex-persist": "^3.1.3",
+ "xterm": "^4.19.0",
+ "xterm-addon-attach": "^0.6.0",
+ "xterm-addon-fit": "^0.5.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.15",
diff --git a/frontend/src/locale/en/LC_MESSAGES/app.po b/frontend/src/locale/en/LC_MESSAGES/app.po
index 7b95a4e5..31b0674f 100644
--- a/frontend/src/locale/en/LC_MESSAGES/app.po
+++ b/frontend/src/locale/en/LC_MESSAGES/app.po
@@ -10,11 +10,11 @@ msgstr ""
"Generated-By: easygettext\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: src/router/index.js:99
+#: src/router/index.js:107
msgid "404 Not Found"
msgstr ""
-#: src/router/index.js:77
+#: src/router/index.js:85
msgid "About"
msgstr ""
@@ -119,7 +119,7 @@ msgstr ""
msgid "Destroy"
msgstr ""
-#: src/router/index.js:125
+#: src/router/index.js:133
msgid "Detected version update, this page will refresh."
msgstr ""
@@ -244,7 +244,7 @@ msgstr ""
msgid "Index (index)"
msgstr ""
-#: src/router/index.js:87 src/views/other/Install.vue:51
+#: src/router/index.js:95 src/views/other/Install.vue:51
msgid "Install"
msgstr ""
@@ -269,7 +269,7 @@ msgstr ""
msgid "Load Averages:"
msgstr ""
-#: src/router/index.js:93 src/views/other/Login.vue:25
+#: src/router/index.js:101 src/views/other/Login.vue:25
msgid "Login"
msgstr ""
@@ -339,7 +339,7 @@ msgstr ""
msgid "No, I'm rethink"
msgstr ""
-#: src/router/index.js:105
+#: src/router/index.js:113
msgid "Not Found"
msgstr ""
@@ -353,7 +353,7 @@ msgid ""
"you need to get the certificate."
msgstr ""
-#: src/router/index.js:129
+#: src/router/index.js:137
msgid "OK"
msgstr ""
@@ -460,10 +460,14 @@ msgstr ""
msgid "Swap"
msgstr ""
-#: src/router/index.js:124
+#: src/router/index.js:132
msgid "System message"
msgstr ""
+#: src/router/index.js:77
+msgid "Terminal"
+msgstr ""
+
#: src/views/domain/columns.js:50
msgid ""
"The certificate for the domain will be checked every hour, and will be "
diff --git a/frontend/src/locale/zh_CN/LC_MESSAGES/app.mo b/frontend/src/locale/zh_CN/LC_MESSAGES/app.mo
index 2f64c0b1..74cab53b 100644
Binary files a/frontend/src/locale/zh_CN/LC_MESSAGES/app.mo and b/frontend/src/locale/zh_CN/LC_MESSAGES/app.mo differ
diff --git a/frontend/src/locale/zh_CN/LC_MESSAGES/app.po b/frontend/src/locale/zh_CN/LC_MESSAGES/app.po
index 3116d578..a4a5eea5 100644
--- a/frontend/src/locale/zh_CN/LC_MESSAGES/app.po
+++ b/frontend/src/locale/zh_CN/LC_MESSAGES/app.po
@@ -12,11 +12,11 @@ msgstr ""
"Generated-By: easygettext\n"
"X-Generator: Poedit 3.0.1\n"
-#: src/router/index.js:99
+#: src/router/index.js:107
msgid "404 Not Found"
msgstr "404 未找到页面"
-#: src/router/index.js:77
+#: src/router/index.js:85
msgid "About"
msgstr "关于"
@@ -121,7 +121,7 @@ msgstr "数据库 (可选,默认: database)"
msgid "Destroy"
msgstr "删除"
-#: src/router/index.js:125
+#: src/router/index.js:133
msgid "Detected version update, this page will refresh."
msgstr "检测到版本更新,页面将会刷新。"
@@ -246,7 +246,7 @@ msgstr "HTTPS 监听端口"
msgid "Index (index)"
msgstr "网站首页 (index)"
-#: src/router/index.js:87 src/views/other/Install.vue:51
+#: src/router/index.js:95 src/views/other/Install.vue:51
msgid "Install"
msgstr "安装"
@@ -271,7 +271,7 @@ msgstr "开源许可"
msgid "Load Averages:"
msgstr "系统负载:"
-#: src/router/index.js:93 src/views/other/Login.vue:25
+#: src/router/index.js:101 src/views/other/Login.vue:25
msgid "Login"
msgstr "登录"
@@ -343,7 +343,7 @@ msgstr "下一步"
msgid "No, I'm rethink"
msgstr "再想想"
-#: src/router/index.js:105
+#: src/router/index.js:113
msgid "Not Found"
msgstr "找不到页面"
@@ -357,7 +357,7 @@ msgid ""
"you need to get the certificate."
msgstr "注意:当前配置中的 server_name 必须为需要申请证书的域名。"
-#: src/router/index.js:129
+#: src/router/index.js:137
msgid "OK"
msgstr "确定"
@@ -464,10 +464,14 @@ msgstr "主体名称: %{name}"
msgid "Swap"
msgstr ""
-#: src/router/index.js:124
+#: src/router/index.js:132
msgid "System message"
msgstr "系统消息"
+#: src/router/index.js:77
+msgid "Terminal"
+msgstr "终端"
+
#: src/views/domain/columns.js:50
msgid ""
"The certificate for the domain will be checked every hour, and will be "
@@ -488,8 +492,8 @@ msgid ""
"fields in your configuration file. The configuration filename cannot be "
"changed after it has been created."
msgstr ""
-"只有在您的配置文件中有相应字段时,下列的配置才能生效。配置文件名称创建后不可"
-"修改。"
+"只有在您的配置文件中有相应字段时,下列的配置才能生效。配置文件名称创建后不"
+"可修改。"
#: src/views/domain/DomainAdd.vue:15 src/views/domain/DomainAdd.vue:4
#: src/views/domain/DomainEdit.vue:24 src/views/domain/DomainEdit.vue:5
diff --git a/frontend/src/locale/zh_TW/LC_MESSAGES/app.mo b/frontend/src/locale/zh_TW/LC_MESSAGES/app.mo
index a3db8203..969a80b3 100644
Binary files a/frontend/src/locale/zh_TW/LC_MESSAGES/app.mo and b/frontend/src/locale/zh_TW/LC_MESSAGES/app.mo differ
diff --git a/frontend/src/locale/zh_TW/LC_MESSAGES/app.po b/frontend/src/locale/zh_TW/LC_MESSAGES/app.po
index 5bb179a3..c935f252 100644
--- a/frontend/src/locale/zh_TW/LC_MESSAGES/app.po
+++ b/frontend/src/locale/zh_TW/LC_MESSAGES/app.po
@@ -13,11 +13,11 @@ msgstr ""
"Generated-By: easygettext\n"
"X-Generator: Poedit 3.0.1\n"
-#: src/router/index.js:99
+#: src/router/index.js:107
msgid "404 Not Found"
msgstr "404 未找到頁面"
-#: src/router/index.js:77
+#: src/router/index.js:85
msgid "About"
msgstr "關於"
@@ -122,7 +122,7 @@ msgstr "資料庫 (可選,預設: database)"
msgid "Destroy"
msgstr "删除"
-#: src/router/index.js:125
+#: src/router/index.js:133
msgid "Detected version update, this page will refresh."
msgstr "檢測到版本更新,頁面將會重新整理。"
@@ -247,7 +247,7 @@ msgstr "HTTPS 監聽埠"
msgid "Index (index)"
msgstr "網站首頁 (index)"
-#: src/router/index.js:87 src/views/other/Install.vue:51
+#: src/router/index.js:95 src/views/other/Install.vue:51
msgid "Install"
msgstr "安裝"
@@ -272,7 +272,7 @@ msgstr "開源許可"
msgid "Load Averages:"
msgstr "系統負載:"
-#: src/router/index.js:93 src/views/other/Login.vue:25
+#: src/router/index.js:101 src/views/other/Login.vue:25
msgid "Login"
msgstr "登入"
@@ -344,7 +344,7 @@ msgstr "下一步"
msgid "No, I'm rethink"
msgstr "再想想"
-#: src/router/index.js:105
+#: src/router/index.js:113
msgid "Not Found"
msgstr "找不到頁面"
@@ -358,7 +358,7 @@ msgid ""
"you need to get the certificate."
msgstr "注意:當前配置中的 server_name 必須為需要申請證書的域名。"
-#: src/router/index.js:129
+#: src/router/index.js:137
msgid "OK"
msgstr "確定"
@@ -465,10 +465,14 @@ msgstr "主體名稱: %{name}"
msgid "Swap"
msgstr "交換空間"
-#: src/router/index.js:124
+#: src/router/index.js:132
msgid "System message"
msgstr "系統訊息"
+#: src/router/index.js:77
+msgid "Terminal"
+msgstr "终端"
+
#: src/views/domain/columns.js:50
msgid ""
"The certificate for the domain will be checked every hour, and will be "
@@ -489,8 +493,8 @@ msgid ""
"fields in your configuration file. The configuration filename cannot be "
"changed after it has been created."
msgstr ""
-"只有在您的配置檔案中有相應欄位時,下列的配置才能生效。配置檔名稱建立後不可修"
-"改。"
+"只有在您的配置檔案中有相應欄位時,下列的配置才能生效。配置檔名稱建立後不可"
+"修改。"
#: src/views/domain/DomainAdd.vue:15 src/views/domain/DomainAdd.vue:4
#: src/views/domain/DomainEdit.vue:24 src/views/domain/DomainEdit.vue:5
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index 81fe9313..b03b11ff 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -72,6 +72,14 @@ export const routes = [
hiddenInSidebar: true
},
},
+ {
+ path: 'terminal',
+ name: $gettext('Terminal'),
+ component: () => import('@/views/pty/Terminal'),
+ meta: {
+ icon: 'code'
+ }
+ },
{
path: 'about',
name: $gettext('About'),
diff --git a/frontend/src/translations.json b/frontend/src/translations.json
index d3ff1465..89953f52 100644
--- a/frontend/src/translations.json
+++ b/frontend/src/translations.json
@@ -1 +1 @@
-{"en":{},"zh_CN":{"404 Not Found":"404 未找到页面","About":"关于","Action":"操作","Add Site":"添加站点","Are you sure you want to destroy?":"你确定要删除?","Are you sure you want to restore?":"你确定要反删除?","Auto-renewal disabled for %{name}":"成功关闭 %{name} 自动续签","Auto-renewal enabled for %{name}":"成功启用 %{name} 自动续签","Back":"返回","Base information":"基本信息","Build with":"构建基于","Cancel":"取消","Certificate Auto-renewal":"证书自动续签","Certificate has expired":"此证书已过期","Certificate is valid":"此证书有效","Certificate Path (ssl_certificate)":"TLS 证书路径 (ssl_certificate)","Certificate Status":"证书状态","Configuration Name":"配置名称","Configurations":"配置","Configure SSL":"配置 SSL","CPU Status":"CPU 状态","Create Another":"再创建一个","Created at":"创建时间","Dashboard":"仪表盘","Database (Optional, default: database)":"数据库 (可选,默认: database)","Destroy":"删除","Detected version update, this page will refresh.":"检测到版本更新,页面将会刷新。","Development Mode":"开发模式","Disable auto-renewal failed for %{name}":"关闭 %{name} 自动续签失败","Disabled":"禁用","Disabled successfully":"禁用成功","Disk IO":"磁盘 IO","Do you want to change the template to support the TLS?":"你想要改变模板以支持 TLS 吗?","Domain Config Created Successfully":"域名配置文件创建成功","Edit":"编辑","Edit %{n}":"编辑 %{n}","Edit Configuration":"编辑配置","Edit Configuration File":"编辑配置文件","Edit Site":"编辑站点","Email (*)":"邮箱 (*)","Enable auto-renewal failed for %{name}":"启用 %{name} 自动续签失败","Enable failed":"启用失败","Enable TLS":"启用 TLS","Enabled":"启用","Enabled successfully":"启用成功","Expiration Date: %{date}":"过期时间: %{date}","Failed to disable %{msg}":"禁用失败 %{msg}","Failed to enable %{msg}":"启用失败 %{msg}","File Not Found":"未找到文件","Finished":"完成","Getting Certificate from Let's Encrypt":"从 Let's Encrypt 获取证书","Getting the certificate, please wait...":"正在获取证书,请稍等...","Home":"首页","HTTP Listen Port":"HTTP 监听端口","HTTPS Listen Port":"HTTPS 监听端口","Index (index)":"网站首页 (index)","Install":"安装","Intermediate Certification Authorities: %{issuer}":"中级证书颁发机构: %{issuer}","Invalid E-mail!":"无效的邮箱!","Leave blank for no change":"留空表示不修改","License":{"Project":"开源许可"},"Load Averages:":"系统负载:","Login":"登录","Login successful":"登录成功","Logout successful":"登出成功","Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.":"在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 HTTPChallengePort (默认: 9180)","Manage Configs":"配置管理","Manage Sites":"网站管理","Manage Users":"用户管理","Memory":"内存","Memory and Storage":"内存与存储","Modify Config":"修改配置文件","Name":"名称","Network":"网络","Network Statistics":"流量统计","Network Total Receive":"下载流量","Network Total Send":"上传流量","Next":"下一步","No, I'm rethink":"再想想","Not Found":"找不到页面","Not Valid Before: %{date}":"此前无效: %{date}","Note: The server_name in the current configuration must be the domain name you need to get the certificate.":"注意:当前配置中的 server_name 必须为需要申请证书的域名。","OK":"确定","Password":"密码","Password (*)":"密码 (*)","Please input your E-mail!":"请输入您的邮箱!","Please input your password!":"请输入您的密码!","Please input your username!":"请输入您的用户名!","Private Key Path (ssl_certificate_key)":"私钥路径 (ssl_certificate_key)","Project Team":"项目团队","Reads":"读","Receive":"下载","Restore":"反删除","Root Directory (root)":"网站根目录 (root)","Save":"保存","Save error %{msg}":"保存错误 %{msg}","Saved successfully":"保存成功","Send":"上传","Server error":"服务器错误","Server Info":"服务器信息","Server Names (server_name)":"网站域名 (server_name)","Sites List":"站点列表","Skip":"跳过","Status":"状态","Storage":"存储","Subject Name: %{name}":"主体名称: %{name}","System message":"系统消息","The certificate for the domain will be checked every hour, and will be renewed if it has been more than 1 month since it was last issued.
If you do not have a certificate before, please click \"Getting Certificate from Let's Encrypt\" first.":"系统将会每小时检测一次该域名证书,若距离上次签发已超过1个月,则将自动续签。
如果您之前没有证书,请先点击 \"从 Let's Encrypt 获取证书\"。","The filename cannot contain the following characters: %{c}":"文件名不能包含以下字符: %{c}","The following values will only take effect if you have the corresponding fields in your configuration file. The configuration filename cannot be changed after it has been created.":"只有在您的配置文件中有相应字段时,下列的配置才能生效。配置文件名称创建后不可修改。","This feature is not available in demo.":"该功能在 Demo 中不可用。","This operation will lose the custom configuration.":"该操作将会丢失自定义配置。","Updated at":"修改时间","Uptime:":"运行时间:","Username":"用户名","Username (*)":"用户名 (*)","Writes":"写","Yes, I'm sure":"是的"},"zh_TW":{"404 Not Found":"404 未找到頁面","About":"關於","Action":"操作","Add Site":"新增站點","Are you sure you want to destroy?":"你确定要删除?","Are you sure you want to restore?":"你确定要反删除?","Auto-renewal disabled for %{name}":"成功關閉 %{name} 自動續簽","Auto-renewal enabled for %{name}":"成功啟用 %{name} 自動續簽","Back":"返回","Base information":"基本訊息","Build with":"構建基於","Cancel":"取消","Certificate Auto-renewal":"證書自動續簽","Certificate has expired":"此證書已過期","Certificate is valid":"此證書有效","Certificate Path (ssl_certificate)":"TLS 證書路徑 (ssl_certificate)","Certificate Status":"證書狀態","Configuration Name":"配置名稱","Configurations":"配置","Configure SSL":"配置 SSL","CPU Status":"中央處理器狀態","CPU:":"中央處理器:","Create Another":"再創建一個","Created at":"建立時間","Dashboard":"儀表盤","Database (Optional, default: database)":"資料庫 (可選,預設: database)","Destroy":"删除","Detected version update, this page will refresh.":"檢測到版本更新,頁面將會重新整理。","Development Mode":"開發模式","Disable auto-renewal failed for %{name}":"關閉 %{name} 自動續簽失敗","Disabled":"禁用","Disabled successfully":"禁用成功","Disk IO":"磁碟 IO","Do you want to change the template to support the TLS?":"你想要改變模板以支援 TLS 嗎?","Domain Config Created Successfully":"域名配置文件創建成功","Edit":"编辑","Edit %{n}":"編輯 %{n}","Edit Configuration":"編輯配置","Edit Configuration File":"編輯配置檔案","Edit Site":"編輯站點","Email (*)":"郵箱 (*)","Enable auto-renewal failed for %{name}":"啟用 %{name} 自動續簽失敗","Enable failed":"啟用失敗","Enable TLS":"啟用 TLS","Enabled":"啟用","Enabled successfully":"啟用成功","Expiration Date: %{date}":"過期時間: %{date}","Failed to disable %{msg}":"禁用失敗 %{msg}","Failed to enable %{msg}":"啟用失敗 %{msg}","File Not Found":"未找到檔案","Finished":"完成","Getting Certificate from Let's Encrypt":"從 Let's Encrypt 獲取證書","Getting the certificate, please wait...":"正在獲取證書,請稍等...","Home":"首頁","HTTP Listen Port":"HTTP 監聽埠","HTTPS Listen Port":"HTTPS 監聽埠","Index (index)":"網站首頁 (index)","Install":"安裝","Intermediate Certification Authorities: %{issuer}":"中級證書頒發機構: %{issuer}","Invalid E-mail!":"無效的郵箱!","Leave blank for no change":"留空表示不修改","License":{"Project":"開源許可"},"Load Averages:":"系統負載:","Login":"登入","Login successful":"登入成功","Logout successful":"登出成功","Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.":"在獲取證書前,請確保配置檔案中已將 .well-known 目錄反向代理到 HTTPChallengePort (預設: 9180)","Manage Configs":"配置管理","Manage Sites":"網站管理","Manage Users":"使用者管理","Memory":"記憶體","Memory and Storage":"記憶體和存儲","Modify Config":"修改配置","Name":"名稱","Network":"網路","Network Statistics":"網路統計","Network Total Receive":"下載流量","Network Total Send":"上傳流量","Next":"下一步","No, I'm rethink":"再想想","Not Found":"找不到頁面","Not Valid Before: %{date}":"此前無效: %{date}","Note: The server_name in the current configuration must be the domain name you need to get the certificate.":"注意:當前配置中的 server_name 必須為需要申請證書的域名。","OK":"確定","OS:":"作業系統:","Password":"密碼","Password (*)":"密碼 (*)","Please input your E-mail!":"請輸入您的郵箱!","Please input your password!":"請輸入您的密碼!","Please input your username!":"請輸入您的使用者名稱!","Private Key Path (ssl_certificate_key)":"私鑰路徑 (ssl_certificate_key)","Project Team":"專案團隊","Reads":"讀","Receive":"下載","Restore":"反删除","Root Directory (root)":"網站根目錄 (root)","Save":"儲存","Save error %{msg}":"儲存錯誤 %{msg}","Saved successfully":"儲存成功","Send":"上傳","Server error":"伺服器錯誤","Server Info":"伺服器資訊","Server Names (server_name)":"網站域名 (server_name)","Sites List":"站點列表","Skip":"跳過","Status":"狀態","Storage":"儲存","Subject Name: %{name}":"主體名稱: %{name}","Swap":"交換空間","System message":"系統訊息","The certificate for the domain will be checked every hour, and will be renewed if it has been more than 1 month since it was last issued.
If you do not have a certificate before, please click \"Getting Certificate from Let's Encrypt\" first.":"系統將會每小時檢測一次該域名證書,若距離上次簽發已超過1個月,則將自動續簽。
如果您之前沒有證書,請先點選「從 Let's Encrypt 獲取證書」。","The filename cannot contain the following characters: %{c}":"檔名不能包含以下字元: %{c}","The following values will only take effect if you have the corresponding fields in your configuration file. The configuration filename cannot be changed after it has been created.":"只有在您的配置檔案中有相應欄位時,下列的配置才能生效。配置檔名稱建立後不可修改。","This feature is not available in demo.":"此功能在演示中不可用。","This operation will lose the custom configuration.":"該操作將會丟失自定義配置。","Updated at":"修改時間","Uptime:":"執行時間:","Username":"使用者名稱","Username (*)":"使用者名稱 (*)","Writes":"寫","Yes, I'm sure":"是的"}}
\ No newline at end of file
+{"en":{},"zh_CN":{"404 Not Found":"404 未找到页面","About":"关于","Action":"操作","Add Site":"添加站点","Are you sure you want to destroy?":"你确定要删除?","Are you sure you want to restore?":"你确定要反删除?","Auto-renewal disabled for %{name}":"成功关闭 %{name} 自动续签","Auto-renewal enabled for %{name}":"成功启用 %{name} 自动续签","Back":"返回","Base information":"基本信息","Build with":"构建基于","Cancel":"取消","Certificate Auto-renewal":"证书自动续签","Certificate has expired":"此证书已过期","Certificate is valid":"此证书有效","Certificate Path (ssl_certificate)":"TLS 证书路径 (ssl_certificate)","Certificate Status":"证书状态","Configuration Name":"配置名称","Configurations":"配置","Configure SSL":"配置 SSL","CPU Status":"CPU 状态","Create Another":"再创建一个","Created at":"创建时间","Dashboard":"仪表盘","Database (Optional, default: database)":"数据库 (可选,默认: database)","Destroy":"删除","Detected version update, this page will refresh.":"检测到版本更新,页面将会刷新。","Development Mode":"开发模式","Disable auto-renewal failed for %{name}":"关闭 %{name} 自动续签失败","Disabled":"禁用","Disabled successfully":"禁用成功","Disk IO":"磁盘 IO","Do you want to change the template to support the TLS?":"你想要改变模板以支持 TLS 吗?","Domain Config Created Successfully":"域名配置文件创建成功","Edit":"编辑","Edit %{n}":"编辑 %{n}","Edit Configuration":"编辑配置","Edit Configuration File":"编辑配置文件","Edit Site":"编辑站点","Email (*)":"邮箱 (*)","Enable auto-renewal failed for %{name}":"启用 %{name} 自动续签失败","Enable failed":"启用失败","Enable TLS":"启用 TLS","Enabled":"启用","Enabled successfully":"启用成功","Expiration Date: %{date}":"过期时间: %{date}","Failed to disable %{msg}":"禁用失败 %{msg}","Failed to enable %{msg}":"启用失败 %{msg}","File Not Found":"未找到文件","Finished":"完成","Getting Certificate from Let's Encrypt":"从 Let's Encrypt 获取证书","Getting the certificate, please wait...":"正在获取证书,请稍等...","Home":"首页","HTTP Listen Port":"HTTP 监听端口","HTTPS Listen Port":"HTTPS 监听端口","Index (index)":"网站首页 (index)","Install":"安装","Intermediate Certification Authorities: %{issuer}":"中级证书颁发机构: %{issuer}","Invalid E-mail!":"无效的邮箱!","Leave blank for no change":"留空表示不修改","License":{"Project":"开源许可"},"Load Averages:":"系统负载:","Login":"登录","Login successful":"登录成功","Logout successful":"登出成功","Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.":"在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 HTTPChallengePort (默认: 9180)","Manage Configs":"配置管理","Manage Sites":"网站管理","Manage Users":"用户管理","Memory":"内存","Memory and Storage":"内存与存储","Modify Config":"修改配置文件","Name":"名称","Network":"网络","Network Statistics":"流量统计","Network Total Receive":"下载流量","Network Total Send":"上传流量","Next":"下一步","No, I'm rethink":"再想想","Not Found":"找不到页面","Not Valid Before: %{date}":"此前无效: %{date}","Note: The server_name in the current configuration must be the domain name you need to get the certificate.":"注意:当前配置中的 server_name 必须为需要申请证书的域名。","OK":"确定","Password":"密码","Password (*)":"密码 (*)","Please input your E-mail!":"请输入您的邮箱!","Please input your password!":"请输入您的密码!","Please input your username!":"请输入您的用户名!","Private Key Path (ssl_certificate_key)":"私钥路径 (ssl_certificate_key)","Project Team":"项目团队","Reads":"读","Receive":"下载","Restore":"反删除","Root Directory (root)":"网站根目录 (root)","Save":"保存","Save error %{msg}":"保存错误 %{msg}","Saved successfully":"保存成功","Send":"上传","Server error":"服务器错误","Server Info":"服务器信息","Server Names (server_name)":"网站域名 (server_name)","Sites List":"站点列表","Skip":"跳过","Status":"状态","Storage":"存储","Subject Name: %{name}":"主体名称: %{name}","System message":"系统消息","Terminal":"终端","The certificate for the domain will be checked every hour, and will be renewed if it has been more than 1 month since it was last issued.
If you do not have a certificate before, please click \"Getting Certificate from Let's Encrypt\" first.":"系统将会每小时检测一次该域名证书,若距离上次签发已超过1个月,则将自动续签。
如果您之前没有证书,请先点击 \"从 Let's Encrypt 获取证书\"。","The filename cannot contain the following characters: %{c}":"文件名不能包含以下字符: %{c}","The following values will only take effect if you have the corresponding fields in your configuration file. The configuration filename cannot be changed after it has been created.":"只有在您的配置文件中有相应字段时,下列的配置才能生效。配置文件名称创建后不可修改。","This feature is not available in demo.":"该功能在 Demo 中不可用。","This operation will lose the custom configuration.":"该操作将会丢失自定义配置。","Updated at":"修改时间","Uptime:":"运行时间:","Username":"用户名","Username (*)":"用户名 (*)","Writes":"写","Yes, I'm sure":"是的"},"zh_TW":{"404 Not Found":"404 未找到頁面","About":"關於","Action":"操作","Add Site":"新增站點","Are you sure you want to destroy?":"你确定要删除?","Are you sure you want to restore?":"你确定要反删除?","Auto-renewal disabled for %{name}":"成功關閉 %{name} 自動續簽","Auto-renewal enabled for %{name}":"成功啟用 %{name} 自動續簽","Back":"返回","Base information":"基本訊息","Build with":"構建基於","Cancel":"取消","Certificate Auto-renewal":"證書自動續簽","Certificate has expired":"此證書已過期","Certificate is valid":"此證書有效","Certificate Path (ssl_certificate)":"TLS 證書路徑 (ssl_certificate)","Certificate Status":"證書狀態","Configuration Name":"配置名稱","Configurations":"配置","Configure SSL":"配置 SSL","CPU Status":"中央處理器狀態","CPU:":"中央處理器:","Create Another":"再創建一個","Created at":"建立時間","Dashboard":"儀表盤","Database (Optional, default: database)":"資料庫 (可選,預設: database)","Destroy":"删除","Detected version update, this page will refresh.":"檢測到版本更新,頁面將會重新整理。","Development Mode":"開發模式","Disable auto-renewal failed for %{name}":"關閉 %{name} 自動續簽失敗","Disabled":"禁用","Disabled successfully":"禁用成功","Disk IO":"磁碟 IO","Do you want to change the template to support the TLS?":"你想要改變模板以支援 TLS 嗎?","Domain Config Created Successfully":"域名配置文件創建成功","Edit":"编辑","Edit %{n}":"編輯 %{n}","Edit Configuration":"編輯配置","Edit Configuration File":"編輯配置檔案","Edit Site":"編輯站點","Email (*)":"郵箱 (*)","Enable auto-renewal failed for %{name}":"啟用 %{name} 自動續簽失敗","Enable failed":"啟用失敗","Enable TLS":"啟用 TLS","Enabled":"啟用","Enabled successfully":"啟用成功","Expiration Date: %{date}":"過期時間: %{date}","Failed to disable %{msg}":"禁用失敗 %{msg}","Failed to enable %{msg}":"啟用失敗 %{msg}","File Not Found":"未找到檔案","Finished":"完成","Getting Certificate from Let's Encrypt":"從 Let's Encrypt 獲取證書","Getting the certificate, please wait...":"正在獲取證書,請稍等...","Home":"首頁","HTTP Listen Port":"HTTP 監聽埠","HTTPS Listen Port":"HTTPS 監聽埠","Index (index)":"網站首頁 (index)","Install":"安裝","Intermediate Certification Authorities: %{issuer}":"中級證書頒發機構: %{issuer}","Invalid E-mail!":"無效的郵箱!","Leave blank for no change":"留空表示不修改","License":{"Project":"開源許可"},"Load Averages:":"系統負載:","Login":"登入","Login successful":"登入成功","Logout successful":"登出成功","Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.":"在獲取證書前,請確保配置檔案中已將 .well-known 目錄反向代理到 HTTPChallengePort (預設: 9180)","Manage Configs":"配置管理","Manage Sites":"網站管理","Manage Users":"使用者管理","Memory":"記憶體","Memory and Storage":"記憶體和存儲","Modify Config":"修改配置","Name":"名稱","Network":"網路","Network Statistics":"網路統計","Network Total Receive":"下載流量","Network Total Send":"上傳流量","Next":"下一步","No, I'm rethink":"再想想","Not Found":"找不到頁面","Not Valid Before: %{date}":"此前無效: %{date}","Note: The server_name in the current configuration must be the domain name you need to get the certificate.":"注意:當前配置中的 server_name 必須為需要申請證書的域名。","OK":"確定","OS:":"作業系統:","Password":"密碼","Password (*)":"密碼 (*)","Please input your E-mail!":"請輸入您的郵箱!","Please input your password!":"請輸入您的密碼!","Please input your username!":"請輸入您的使用者名稱!","Private Key Path (ssl_certificate_key)":"私鑰路徑 (ssl_certificate_key)","Project Team":"專案團隊","Reads":"讀","Receive":"下載","Restore":"反删除","Root Directory (root)":"網站根目錄 (root)","Save":"儲存","Save error %{msg}":"儲存錯誤 %{msg}","Saved successfully":"儲存成功","Send":"上傳","Server error":"伺服器錯誤","Server Info":"伺服器資訊","Server Names (server_name)":"網站域名 (server_name)","Sites List":"站點列表","Skip":"跳過","Status":"狀態","Storage":"儲存","Subject Name: %{name}":"主體名稱: %{name}","Swap":"交換空間","System message":"系統訊息","Terminal":"终端","The certificate for the domain will be checked every hour, and will be renewed if it has been more than 1 month since it was last issued.
If you do not have a certificate before, please click \"Getting Certificate from Let's Encrypt\" first.":"系統將會每小時檢測一次該域名證書,若距離上次簽發已超過1個月,則將自動續簽。
如果您之前沒有證書,請先點選「從 Let's Encrypt 獲取證書」。","The filename cannot contain the following characters: %{c}":"檔名不能包含以下字元: %{c}","The following values will only take effect if you have the corresponding fields in your configuration file. The configuration filename cannot be changed after it has been created.":"只有在您的配置檔案中有相應欄位時,下列的配置才能生效。配置檔名稱建立後不可修改。","This feature is not available in demo.":"此功能在演示中不可用。","This operation will lose the custom configuration.":"該操作將會丟失自定義配置。","Updated at":"修改時間","Uptime:":"執行時間:","Username":"使用者名稱","Username (*)":"使用者名稱 (*)","Writes":"寫","Yes, I'm sure":"是的"}}
\ No newline at end of file
diff --git a/frontend/src/views/pty/Terminal.vue b/frontend/src/views/pty/Terminal.vue
new file mode 100644
index 00000000..5f428f2b
--- /dev/null
+++ b/frontend/src/views/pty/Terminal.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index a45ea8d1..64a138f2 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -6963,7 +6963,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.npm.taobao.org/lodash.uniq/download/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-lodash@4.17.21, lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.5, lodash@~4.17.10:
+lodash@4.17.21, lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.5, lodash@~4.17.10:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -11196,6 +11196,21 @@ xtend@^4.0.0, xtend@~4.0.1:
resolved "https://registry.npm.taobao.org/xtend/download/xtend-4.0.2.tgz?cache=0&sync_timestamp=1589682817913&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fxtend%2Fdownload%2Fxtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q=
+xterm-addon-attach@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz#220c23addd62ab88c9914e2d4c06f7407e44680e"
+ integrity sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg==
+
+xterm-addon-fit@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
+ integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
+
+xterm@^4.19.0:
+ version "4.19.0"
+ resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0.tgz#c0f9d09cd61de1d658f43ca75f992197add9ef6d"
+ integrity sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==
+
y18n@^4.0.0:
version "4.0.1"
resolved "https://registry.npm.taobao.org/y18n/download/y18n-4.0.1.tgz?cache=0&sync_timestamp=1609798661541&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fy18n%2Fdownload%2Fy18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
diff --git a/go.mod b/go.mod
index 08e8d7cd..ad5354f1 100644
--- a/go.mod
+++ b/go.mod
@@ -27,6 +27,7 @@ require (
require (
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
+ github.com/creack/pty v1.1.18 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/golang/protobuf v1.3.4 // indirect
diff --git a/go.sum b/go.sum
index 6510cf34..5b4ad7be 100644
--- a/go.sum
+++ b/go.sum
@@ -80,6 +80,8 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
diff --git a/server/api/pty.go b/server/api/pty.go
new file mode 100644
index 00000000..76fd07db
--- /dev/null
+++ b/server/api/pty.go
@@ -0,0 +1,46 @@
+package api
+
+import (
+ "github.com/0xJacky/Nginx-UI/server/tool/pty"
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+ "log"
+ "net/http"
+)
+
+func Pty(c *gin.Context) {
+ var upGrader = websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool {
+ return true
+ },
+ }
+ // upgrade http to websocket
+ ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
+ if err != nil {
+ log.Println("pty ws upgrade error", err)
+ return
+ }
+
+ defer ws.Close()
+
+ p, err := pty.NewPipeLine(ws)
+
+ if err != nil {
+ log.Println("pty.NewPipLine error", err)
+ return
+ }
+
+ defer p.Pty.Close()
+
+ errorChan := make(chan error, 1)
+ go p.ReadPtyAndWriteWs(errorChan)
+ go p.ReadWsAndWritePty(errorChan)
+
+ err = <-errorChan
+
+ if err != nil {
+ log.Println(err)
+ }
+
+ return
+}
diff --git a/server/router/routers.go b/server/router/routers.go
index 567a7303..cd870c5d 100644
--- a/server/router/routers.go
+++ b/server/router/routers.go
@@ -1,89 +1,92 @@
package router
import (
- "bufio"
- "github.com/0xJacky/Nginx-UI/server/api"
- "github.com/0xJacky/Nginx-UI/server/settings"
- "github.com/gin-contrib/static"
- "github.com/gin-gonic/gin"
- "net/http"
- "strings"
+ "bufio"
+ "github.com/0xJacky/Nginx-UI/server/api"
+ "github.com/0xJacky/Nginx-UI/server/settings"
+ "github.com/gin-contrib/static"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "strings"
)
func InitRouter() *gin.Engine {
- r := gin.New()
- r.Use(gin.Logger())
+ r := gin.New()
+ r.Use(gin.Logger())
- r.Use(recovery())
+ r.Use(recovery())
- r.Use(cacheJs())
+ r.Use(cacheJs())
- r.Use(static.Serve("/", mustFS("")))
+ r.Use(static.Serve("/", mustFS("")))
- r.NoRoute(func(c *gin.Context) {
- accept := c.Request.Header.Get("Accept")
- if strings.Contains(accept, "text/html") {
- file, _ := mustFS("").Open("index.html")
- defer file.Close()
- stat, _ := file.Stat()
- c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
- bufio.NewReader(file), nil)
- return
- }
- })
+ r.NoRoute(func(c *gin.Context) {
+ accept := c.Request.Header.Get("Accept")
+ if strings.Contains(accept, "text/html") {
+ file, _ := mustFS("").Open("index.html")
+ defer file.Close()
+ stat, _ := file.Stat()
+ c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
+ bufio.NewReader(file), nil)
+ return
+ }
+ })
- g := r.Group("/api")
- {
+ g := r.Group("/api")
+ {
- g.GET("settings", func(c *gin.Context) {
- c.JSON(http.StatusOK, gin.H{
- "demo": settings.ServerSettings.Demo,
- })
- })
+ g.GET("settings", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "demo": settings.ServerSettings.Demo,
+ })
+ })
- g.GET("install", api.InstallLockCheck)
- g.POST("install", api.InstallNginxUI)
+ g.GET("install", api.InstallLockCheck)
+ g.POST("install", api.InstallNginxUI)
- g.POST("/login", api.Login)
- g.DELETE("/logout", api.Logout)
+ g.POST("/login", api.Login)
+ g.DELETE("/logout", api.Logout)
- g := g.Group("/", authRequired())
- {
- g.GET("/analytic", api.Analytic)
- g.GET("/analytic/init", api.GetAnalyticInit)
+ g := g.Group("/", authRequired())
+ {
+ g.GET("/analytic", api.Analytic)
+ g.GET("/analytic/init", api.GetAnalyticInit)
- g.GET("/users", api.GetUsers)
- g.GET("/user/:id", api.GetUser)
- g.POST("/user", api.AddUser)
- g.POST("/user/:id", api.EditUser)
- g.DELETE("/user/:id", api.DeleteUser)
+ g.GET("/users", api.GetUsers)
+ g.GET("/user/:id", api.GetUser)
+ g.POST("/user", api.AddUser)
+ g.POST("/user/:id", api.EditUser)
+ g.DELETE("/user/:id", api.DeleteUser)
- g.GET("domains", api.GetDomains)
- g.GET("domain/:name", api.GetDomain)
- g.POST("domain/:name", api.EditDomain)
- g.POST("domain/:name/enable", api.EnableDomain)
- g.POST("domain/:name/disable", api.DisableDomain)
- g.DELETE("domain/:name", api.DeleteDomain)
+ g.GET("domains", api.GetDomains)
+ g.GET("domain/:name", api.GetDomain)
+ g.POST("domain/:name", api.EditDomain)
+ g.POST("domain/:name/enable", api.EnableDomain)
+ g.POST("domain/:name/disable", api.DisableDomain)
+ g.DELETE("domain/:name", api.DeleteDomain)
- g.GET("configs", api.GetConfigs)
- g.GET("config/:name", api.GetConfig)
- g.POST("config", api.AddConfig)
- g.POST("config/:name", api.EditConfig)
+ g.GET("configs", api.GetConfigs)
+ g.GET("config/:name", api.GetConfig)
+ g.POST("config", api.AddConfig)
+ g.POST("config/:name", api.EditConfig)
- g.GET("backups", api.GetFileBackupList)
- g.GET("backup/:id", api.GetFileBackup)
+ g.GET("backups", api.GetFileBackupList)
+ g.GET("backup/:id", api.GetFileBackup)
- g.GET("template/:name", api.GetTemplate)
+ g.GET("template/:name", api.GetTemplate)
- g.GET("cert/issue/:domain", api.IssueCert)
- g.GET("cert/:domain/info", api.CertInfo)
+ g.GET("cert/issue/:domain", api.IssueCert)
+ g.GET("cert/:domain/info", api.CertInfo)
- // 添加域名到自动续期列表
- g.POST("cert/:domain", api.AddDomainToAutoCert)
- // 从自动续期列表中删除域名
- g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
- }
- }
+ // 添加域名到自动续期列表
+ g.POST("cert/:domain", api.AddDomainToAutoCert)
+ // 从自动续期列表中删除域名
+ g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
- return r
+ // pty
+ g.GET("pty", api.Pty)
+ }
+ }
+
+ return r
}
diff --git a/server/tool/pty/pipeline.go b/server/tool/pty/pipeline.go
new file mode 100644
index 00000000..fe8ac913
--- /dev/null
+++ b/server/tool/pty/pipeline.go
@@ -0,0 +1,142 @@
+package pty
+
+import (
+ "encoding/json"
+ "github.com/creack/pty"
+ "github.com/gorilla/websocket"
+ "github.com/pkg/errors"
+ "os"
+ "os/exec"
+ "time"
+ "unicode/utf8"
+)
+
+type Pipeline struct {
+ Pty *os.File
+ ws *websocket.Conn
+}
+
+type Message struct {
+ Type MsgType
+ Data json.RawMessage
+}
+
+const bufferSize = 2048
+
+func NewPipeLine(conn *websocket.Conn) (p *Pipeline, err error) {
+ c := exec.Command("login")
+
+ ptmx, err := pty.StartWithSize(c, &pty.Winsize{Cols: 90, Rows: 60})
+ if err != nil {
+ return nil, errors.Wrap(err, "start pty error")
+ }
+
+ p = &Pipeline{
+ Pty: ptmx,
+ ws: conn,
+ }
+
+ return
+}
+
+func (p *Pipeline) ReadWsAndWritePty(errorChan chan error) {
+ for {
+ msgType, payload, err := p.ws.ReadMessage()
+ if err != nil {
+ if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived,
+ websocket.CloseNormalClosure) {
+ errorChan <- errors.Wrap(err, "Error ReadWsAndWritePty unexpected close")
+ }
+ return
+ }
+ if msgType != websocket.TextMessage {
+ errorChan <- errors.Errorf("Error ReadWsAndWritePty Invalid msgType: %v", msgType)
+ return
+ }
+
+ var msg Message
+ err = json.Unmarshal(payload, &msg)
+ if err != nil {
+ errorChan <- errors.Wrap(err, "Error ReadWsAndWritePty json.Unmarshal")
+ return
+ }
+
+ switch msg.Type {
+ case TypeData:
+ var data string
+ err = json.Unmarshal(msg.Data, &data)
+ if err != nil {
+ errorChan <- errors.Wrap(err, "Error ReadWsAndWritePty json.Unmarshal msg.Data")
+ return
+ }
+
+ _, err = p.Pty.Write([]byte(data))
+
+ if err != nil {
+ errorChan <- errors.Wrap(err, "Error ReadWsAndWritePty write pty")
+ return
+ }
+ case TypeResize:
+ var win struct {
+ Cols uint16
+ Rows uint16
+ }
+
+ err = json.Unmarshal(msg.Data, &win)
+ if err != nil {
+ errorChan <- errors.Wrap(err, "Error ReadSktAndWritePty Invalid resize message")
+ return
+ }
+ err = pty.Setsize(p.Pty, &pty.Winsize{Rows: win.Rows, Cols: win.Cols})
+ if err != nil {
+ errorChan <- errors.Wrap(err, "Error ReadSktAndWritePty set pty size")
+ return
+ }
+ case TypePing:
+ err = p.ws.WriteControl(websocket.PongMessage, []byte{}, time.Now().Add(time.Second))
+ if err != nil {
+ errorChan <- errors.Wrap(err, "Error ReadSktAndWritePty write pong")
+ return
+ }
+ default:
+ errorChan <- errors.Errorf("Error ReadWsAndWritePty unknown msg.Type %v", msg.Type)
+ return
+ }
+ }
+}
+
+func (p *Pipeline) ReadPtyAndWriteWs(errorChan chan error) {
+ buf := make([]byte, bufferSize)
+ for {
+ n, err := p.Pty.Read(buf)
+ if err != nil {
+ errorChan <- errors.Wrap(err, "Error ReadPtyAndWriteWs read pty")
+ return
+ }
+ processedOutput := validString(string(buf[:n]))
+ err = p.ws.WriteMessage(websocket.TextMessage, []byte(processedOutput))
+ if err != nil {
+ if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
+ errorChan <- errors.Wrap(err, "Error ReadPtyAndWriteWs websocket write")
+ }
+ return
+ }
+ }
+}
+
+func validString(s string) string {
+ if !utf8.ValidString(s) {
+ v := make([]rune, 0, len(s))
+ for i, r := range s {
+ if r == utf8.RuneError {
+ _, size := utf8.DecodeRuneInString(s[i:])
+ if size == 1 {
+ continue
+ }
+ }
+ v = append(v, r)
+ }
+ s = string(v)
+ }
+ return s
+}
diff --git a/server/tool/pty/type.go b/server/tool/pty/type.go
new file mode 100644
index 00000000..42260df8
--- /dev/null
+++ b/server/tool/pty/type.go
@@ -0,0 +1,10 @@
+package pty
+
+type MsgType int
+
+const (
+ MsgTypeInit MsgType = iota
+ TypeData
+ TypeResize
+ TypePing
+)