mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 02:15:48 +02:00
add terminal #17
This commit is contained in:
parent
a571bccb84
commit
101c374b9c
16 changed files with 439 additions and 98 deletions
|
@ -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",
|
||||
|
|
|
@ -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 "
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
|
|
File diff suppressed because one or more lines are too long
98
frontend/src/views/pty/Terminal.vue
Normal file
98
frontend/src/views/pty/Terminal.vue
Normal file
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<a-card :title="$gettext('Terminal')">
|
||||
<div class="console" id="terminal"></div>
|
||||
</a-card>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket'
|
||||
import 'xterm/css/xterm.css'
|
||||
import {Terminal} from 'xterm'
|
||||
import {FitAddon} from 'xterm-addon-fit'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
export default {
|
||||
name: 'Terminal',
|
||||
data() {
|
||||
return {
|
||||
term: null,
|
||||
ping: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.websocket = new ReconnectingWebSocket(this.getWebSocketRoot() + '/pty?token='
|
||||
+ btoa(this.$store.state.user.token))
|
||||
this.websocket.onmessage = this.wsOnMessage
|
||||
this.websocket.onopen = this.wsOnOpen
|
||||
},
|
||||
mounted() {
|
||||
this.initTerm()
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.fit)
|
||||
clearInterval(this.ping)
|
||||
this.ping = null
|
||||
this.websocket.close()
|
||||
},
|
||||
methods: {
|
||||
fit: _.throttle(function () {
|
||||
this.fitAddon.fit()
|
||||
}, 50),
|
||||
initTerm() {
|
||||
const term = new Terminal({
|
||||
rendererType: 'canvas',
|
||||
convertEol: true,
|
||||
fontSize: 14,
|
||||
cursorStyle: 'block',
|
||||
scrollback: 30,
|
||||
})
|
||||
const fitAddon = new FitAddon()
|
||||
term.loadAddon(fitAddon)
|
||||
this.fitAddon = fitAddon
|
||||
term.open(document.getElementById('terminal'))
|
||||
setTimeout(()=>{
|
||||
fitAddon.fit()
|
||||
}, 60)
|
||||
window.addEventListener('resize', this.fit)
|
||||
term.focus()
|
||||
|
||||
let that = this
|
||||
|
||||
term.onData(function (key) {
|
||||
let order = {
|
||||
Data: key,
|
||||
Type: 1
|
||||
}
|
||||
that.sendMessage(order)
|
||||
})
|
||||
term.onBinary(data => {
|
||||
that.sendMessage({Type: 1, Data: data})
|
||||
})
|
||||
term.onResize(data => {
|
||||
that.sendMessage({Type:2, Data:{Cols:data.cols, Rows: data.rows}})
|
||||
})
|
||||
this.term = term
|
||||
},
|
||||
wsOnMessage(msg) {
|
||||
this.term.write(msg.data)
|
||||
},
|
||||
wsOnOpen() {
|
||||
const that = this
|
||||
this.ping = setInterval(function () {
|
||||
that.sendMessage({Type: 3})
|
||||
}, 10000)
|
||||
},
|
||||
sendMessage(data) {
|
||||
this.websocket.send(JSON.stringify(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.console {
|
||||
min-height: 800px;
|
||||
}
|
||||
</style>
|
|
@ -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"
|
||||
|
|
1
go.mod
1
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
46
server/api/pty.go
Normal file
46
server/api/pty.go
Normal file
|
@ -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
|
||||
}
|
|
@ -82,6 +82,9 @@ func InitRouter() *gin.Engine {
|
|||
g.POST("cert/:domain", api.AddDomainToAutoCert)
|
||||
// 从自动续期列表中删除域名
|
||||
g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
|
||||
|
||||
// pty
|
||||
g.GET("pty", api.Pty)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
142
server/tool/pty/pipeline.go
Normal file
142
server/tool/pty/pipeline.go
Normal file
|
@ -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
|
||||
}
|
10
server/tool/pty/type.go
Normal file
10
server/tool/pty/type.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package pty
|
||||
|
||||
type MsgType int
|
||||
|
||||
const (
|
||||
MsgTypeInit MsgType = iota
|
||||
TypeData
|
||||
TypeResize
|
||||
TypePing
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue