From 101c374b9cb4d0801c3168ef1fa5de1521a7e91e Mon Sep 17 00:00:00 2001 From: 0xJacky Date: Wed, 6 Jul 2022 19:46:27 +0800 Subject: [PATCH] add terminal #17 --- frontend/package.json | 10 +- frontend/src/locale/en/LC_MESSAGES/app.po | 20 +-- frontend/src/locale/zh_CN/LC_MESSAGES/app.mo | Bin 7622 -> 8025 bytes frontend/src/locale/zh_CN/LC_MESSAGES/app.po | 24 ++-- frontend/src/locale/zh_TW/LC_MESSAGES/app.mo | Bin 7783 -> 8170 bytes frontend/src/locale/zh_TW/LC_MESSAGES/app.po | 24 ++-- frontend/src/router/index.js | 8 ++ frontend/src/translations.json | 2 +- frontend/src/views/pty/Terminal.vue | 98 +++++++++++++ frontend/yarn.lock | 17 ++- go.mod | 1 + go.sum | 2 + server/api/pty.go | 46 ++++++ server/router/routers.go | 133 ++++++++--------- server/tool/pty/pipeline.go | 142 +++++++++++++++++++ server/tool/pty/type.go | 10 ++ 16 files changed, 439 insertions(+), 98 deletions(-) create mode 100644 frontend/src/views/pty/Terminal.vue create mode 100644 server/api/pty.go create mode 100644 server/tool/pty/pipeline.go create mode 100644 server/tool/pty/type.go 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 2f64c0b1afa5dc99e662faeeb64a1e7c00b81a43..74cab53bde17ed390a105f631b399d6a61bf6a83 100644 GIT binary patch delta 2885 zcmZ|Q4NR3)9LMo<0immrfQl)oH$@Oo^95A!<>E`mi)dPsWLdu467>RJK&5hzjh1f4 zT;4KcZqd|gt6AGq>&0oUHs-r9Hlh>aZ8pVIr-c$1$jOZzGd7$E~MOTlh6< zB`%^Kd=)j2KT!jSp;KKSh+5%MT7Okcr$P-EBW+C?Y6(}P1{lE2=wJ+XqgLWq)Ifem zwTol4X*k+ih+3%%RKGh>?VC{(ZAoVRHL_MJAhZN-HN&~ zjB4LveFHV1_w4mgkU5w()Z20n)xOu(dpXhCigcvjF%!wC;{sGiMYcg%w1HWTItv?d zJcdy158@fyEYyAZs0q!nmf+KrD{T2JI%@E}t@s6X<1N%kJ-kbL zKr*U6!Tsp4Ux%MluEH#w#7TQf*HK&iC93`g>b^8*OmxpCphiB!mWxm` zTZ$UM3iP0E(oCiiD+#4cq9Bqp=3(4q^CF*EOz5}5M=-xg(zEU(9Q_Oy5_*_YF0q|h zN|Y0`i8X}wO-Tn-Z_E9Jwrn+_Eqjb$QjzV_OqDhej}w})4(7dteifC934Y3*Na3Gf zn<-@53SMEef|yB6w{;~rfoQPherEJ%@DZYl2oj43rH6=dM6C*>c**>#H zt!&bnC?fI*?fVnNLShOrkx;rjidkTtgW9?}qMFDdbST#nN=pd8=C9xH$;5hMBcU{x z2odXuGNM1#kQv)wzm`d<_ z68ZnvKh%Rn2BEZ#m_$54JQ@yqvloN{zQ&pcUtNREY+qYl@2juzZ3xuWhiV%AX8!-R z2+^V@6!5z*dTTucBCW%oLD$@$2d#=toaigb3i?8U`i)i9o89??mw5(6nunW{K1~RO zf>qUPx431=)iH^cfx2j84Z;0zNS-I>#}lnxuN~_+)^hpvXFB$@{`ATbzxh|)zGhqP zb`LG}#77I^ywua~AE~!uydBRqcQ(0v-0LGUJ>HJ~=Hb+|Q*p=lbiL(XNbB|_bRK-C ItL0hq7yj)(`v3p{ delta 2501 zcmXxle@szUFFu%)y5A^WL=n-GVy1?o8`)60ZZf+WpT{(e;|=&KrsEzg!TmT9Phu+eV>S+8 z2L9%bqOMP+F-CVbhMh??OvW5siUqg^^Y8_4-+^PPA4Dbej@M5jKRd^UzU?AT!OK{V z(J^5nl~_i-7L|BAj%R#(i5=bSZmh-xDxveJ4u?^R{e?;_BQtEDmXRAu7RE zRKMM*8`*~3u_fGNSit!989TbO0aRvJQ62q>%D9N$G?leD4p*b5xCQxHI~xu3GS0?V zFbhxMEqE5y{~&4!hf)2FU|b!ha&!;oqfUIA4P)DJ_aoGreTkZhZ;@;57gQpDpb{AM z;}J$5PdyKH{xsBiHK-X~gi5SFi}~Nk&UzZ6_zh|XzDFf6=qLV)h1AnXTb7_^s0KM? z>rm&vh`Ph=sDyT)64{GNpci$+M^TA=n9cm_zy%suUi%Jj#Ve=||3$5BI#=nuT+~d& zPybwd`mrno&=EH|l}|-hKo%#izWz z57qGxsE!8w_>dp}8@0x1`DUe0$W)@`}hoA7q(8&K!Hh8pk?>aFr?pN+*)C>=yuD^zHWftX;Ux6{y0IhB(YHGWYoNT*$ z5cL`csAuK^YQSsmb>wfvGC4{8Wh1L+1zw+tdYG#UnSWha=LZ^4nKdGPSclhlqRx90 z)nTuD%neXCavF8s*Z3(8U>P3fpKS~Fp_X!SaoFBc%>3(uy)Rmc%q7abtp+vtJc90$C0FaOqHn-G!~$X+p}Sd2+)K

NF{MEqGcn)17dS%k{d zA<69nY#%0=x#Sm6jT4AgVv#D8c7hLO@|*s+8Yt^SlG}-Fn+VPD-Gs`MA#rOCcaq=j z4)!(O`jl#&D~YFw$;AIf&&3pC2Js+qC!sPw#O`y&GU5?pp(>Q+#Kz=a_$p`U{eOmr z$Gq`T)K^QTF2ts}^|-N=hgM6p5GvKgLqsf5nlUxl zm$505NY5M&{>faG5;SJlM-uOiI}>cqX^#fK=T%0675R0M#9IaDgRa;}H0UTSjU;-C LJ`dW8ha&$0BDdMh 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 a3db820362c3ed00d83bfc7e6e4f8f74a0eaddcf..969a80b36a9532c864157d6252264bf7a5f58b66 100644 GIT binary patch delta 2941 zcmZ|QTWnNC9LMq5g|<{E&i67)nrxi12X45D>%!2vK5C(FX;UKrirQQ%IC2zrWpoAAE4q|9LJ#sNr^%ZM3q6R4=e4Aj80u|Lko$1sQ~Scm=aLrg=@+OJ?wj(}RcT?gMQ8$=^N^~|V z(IRVKZ*D>*v>lbmZcO6-?n^4V!9~=}uA)}pmiZ7hU@FtpOrPXO31*`P%0nf-7B!K* zn2d)}37$fRaA#2Wxq?}E4I{m%`1u)tDM*_ejoRxer~z|P11`q*a5bu5JF*zjPBMwk z+QNaTl^BM)@px1sGf@f5wet&mv;JD*Rn|~z9d{vPyZ2E`+k{H+7_P&M=*LX5;(2xB zP%APCmBVm6*m<;lkm z)J%@@z>LN3Q8)52mG1LWTP>Fnl z8t_llv*G6nQ@>B@D47-ho}Jy_>ZKF z7om<DN_080IIhBu>rnmnp%VPWJZ^fZiJd|9yJp_B^YZuflA~y`tT(}<%Jk-3T6`dgszV)=4TQSAl4At*A;|{ zKCzu;Jr!-u)7DUh+{10KuPl^8>;BD9wy3C&!qF`0OZs3fKm`XataXyvqh8;NS7C-;vQ zeVaRrmRaRxqLdg;Xe*`@IYfxyO^lY=ruMkAXdAQapcuyytBI9_9^OKtj8G{L@H3j| zPwk*3u077^*0%EH3vxO|JAOEenQYot1*uH*LHx zEd?|J>s@0|O4{$02d zyPfBk%ows#ObheC$K4j3uz+Jxy3k%x8QkPfZyOkoW^|o1@kZ=$+>te#uO|= zKCaemKs|3m>hW9$nG6cL@pc@>RD2g#;218$vsONhw{m|K)v=p)pS&p6P&U#RSBQle z!3egaI&=_=@E9(neK$!aoq{h=gT938aTe8~+~in=b*PRtp*q%O<fku4-gBsde2p6LtfYMx;w@E}g&Nsf)YNW9Rn&;;@gQmh$8ag0LQUl-$j41`QA6jk z9KXdSm`J6Wn2D;tf{T{24n0-4jf^Vnz&EiU`MB#`=%h=a7P$;rGnbE=iDFd48&Mr; zKy_e;J>P|z**?4XQ12Z=Ez!{>%)feel7fEx7~`;rUa=2dDQYHGqdHKDdVecs;~w)x z45l2Z!i}Te{}MIQi>QuWLUm{s)sdSi%)dq+=Asd$qaG9^%kRoC3#(BTwxQOz2ld_n zYDNZ84ZMJA;AMM$1T~Y#@ee$O8}L0E^YJ^6jC%f78taayQ4L&1Rd@|G12<3|2(yFK zU>K2f%cfP8uQAu7-rs~eo{i>!J%8JtdxtGEihSG{7iPzuK~;DGH8Ve;PQx6k z;hT<^qEwECD$c~< zTw@9M5xfmMQA;(9dhb2k5XpW=m|2%lk`d=ZVhW-i)*c8Yh-69UDrZ(SP zhZ<=E>irH>2f9%edZ@KNiF)q?)O!=CbAQ(Er?G(h8BEprPhd-{!7S8qilDw&iyBeA z*^19`-(~j~%o$XJbEp~8j@1k#p}wDks;|^6xAH3Vf(ERh6;(kOs=_{0$A+;P-$7OM z6RM-X+WnuXDgGN7lv}{rl)0#xsY30YPE>m@nXhNF{yOJJDNx17k!5$IcK;D-2_{fq zoU-ztQ5~E`+H}|LK8?R%danRgU&O35Yf%HLN4>XuCG)S$ehNOrXK@wQJn>6lNP(%}D#y(&;|?p1~qA<77Tf80)@hq#M) zhIouvM=0$eI*BbrKcS>u{SdKvq&KO=|0-!HG_pPUH$S>~XV@Q2X$g(oSo*1dCUqdr lkEU0I{1eN2LnDb9=lt(8uEzOO+0~(u6**J>wVdBW{{r*$)KmZf 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 +)