实现可运行的xterm.js方案
This commit is contained in:
parent
86ef89ff21
commit
29fb520e48
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"next-terminal/pkg/utils"
|
"next-terminal/pkg/utils"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,22 +27,29 @@ type Recorder struct {
|
|||||||
timestamp int
|
timestamp int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecorder(filename string) (recorder *Recorder, err error) {
|
func NewRecorder(dir string) (recorder *Recorder, filename string, err error) {
|
||||||
recorder = &Recorder{}
|
recorder = &Recorder{}
|
||||||
|
|
||||||
if utils.FileExists(filename) {
|
if utils.FileExists(dir) {
|
||||||
if err := os.RemoveAll(filename); err != nil {
|
if err := os.RemoveAll(dir); err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = os.MkdirAll(dir, 0777); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = path.Join(dir, "recording.cast")
|
||||||
|
|
||||||
var file *os.File
|
var file *os.File
|
||||||
file, err = os.Create(filename)
|
file, err = os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
recorder.file = file
|
recorder.file = file
|
||||||
return recorder, nil
|
return recorder, filename, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (recorder *Recorder) Close() {
|
func (recorder *Recorder) Close() {
|
||||||
|
@ -38,7 +38,13 @@ func SessionPagingEndpoint(c echo.Context) error {
|
|||||||
|
|
||||||
for i := 0; i < len(items); i++ {
|
for i := 0; i < len(items); i++ {
|
||||||
if status == model.Disconnected && len(items[i].Recording) > 0 {
|
if status == model.Disconnected && len(items[i].Recording) > 0 {
|
||||||
recording := items[i].Recording + "/recording"
|
|
||||||
|
var recording string
|
||||||
|
if items[i].Protocol == "rdp" || items[i].Protocol == "vnc" {
|
||||||
|
recording = items[i].Recording + "/recording"
|
||||||
|
} else {
|
||||||
|
recording = items[i].Recording
|
||||||
|
}
|
||||||
|
|
||||||
if utils.FileExists(recording) {
|
if utils.FileExists(recording) {
|
||||||
logrus.Debugf("检测到录屏文件[%v]存在", recording)
|
logrus.Debugf("检测到录屏文件[%v]存在", recording)
|
||||||
@ -107,12 +113,14 @@ func CloseSessionById(sessionId string, code int, reason string) {
|
|||||||
observable, _ := global.Store.Get(sessionId)
|
observable, _ := global.Store.Get(sessionId)
|
||||||
if observable != nil {
|
if observable != nil {
|
||||||
logrus.Debugf("会话%v创建者退出", observable.Subject.Tunnel.UUID)
|
logrus.Debugf("会话%v创建者退出", observable.Subject.Tunnel.UUID)
|
||||||
_ = observable.Subject.Tunnel.Close()
|
observable.Subject.Close()
|
||||||
|
|
||||||
for i := 0; i < len(observable.Observers); i++ {
|
for i := 0; i < len(observable.Observers); i++ {
|
||||||
_ = observable.Observers[i].Tunnel.Close()
|
observable.Observers[i].Close()
|
||||||
CloseWebSocket(observable.Observers[i].WebSocket, code, reason)
|
CloseWebSocket(observable.Observers[i].WebSocket, code, reason)
|
||||||
logrus.Debugf("强制踢出会话%v的观察者", observable.Observers[i].Tunnel.UUID)
|
logrus.Debugf("强制踢出会话%v的观察者", observable.Observers[i].Tunnel.UUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
CloseWebSocket(observable.Subject.WebSocket, code, reason)
|
CloseWebSocket(observable.Subject.WebSocket, code, reason)
|
||||||
}
|
}
|
||||||
global.Store.Del(sessionId)
|
global.Store.Del(sessionId)
|
||||||
@ -529,8 +537,15 @@ func SessionRecordingEndpoint(c echo.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
recording := path.Join(session.Recording, "recording")
|
|
||||||
logrus.Debugf("读取录屏文件:%s", recording)
|
var recording string
|
||||||
|
if session.Protocol == "rdp" || session.Protocol == "vnc" {
|
||||||
|
recording = session.Recording + "/recording"
|
||||||
|
} else {
|
||||||
|
recording = session.Recording
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(recording))
|
||||||
return c.File(recording)
|
return c.File(recording)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,8 +10,10 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"next-terminal/pkg/guacd"
|
||||||
"next-terminal/pkg/model"
|
"next-terminal/pkg/model"
|
||||||
"next-terminal/pkg/utils"
|
"next-terminal/pkg/utils"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -65,7 +67,7 @@ type WindowSize struct {
|
|||||||
Rows int `json:"rows"`
|
Rows int `json:"rows"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func SSHEndpoint(c echo.Context) error {
|
func SSHEndpoint(c echo.Context) (err error) {
|
||||||
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
|
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("升级为WebSocket协议失败:%v", err.Error())
|
logrus.Errorf("升级为WebSocket协议失败:%v", err.Error())
|
||||||
@ -149,30 +151,44 @@ func SSHEndpoint(c echo.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var recorder *Recorder
|
||||||
|
var recording string
|
||||||
|
property, _ := model.FindPropertyByName(guacd.RecordingPath)
|
||||||
|
if property.Value != "" {
|
||||||
|
dir := path.Join(property.Value, sessionId)
|
||||||
|
recorder, recording, err = NewRecorder(dir)
|
||||||
|
if err != nil {
|
||||||
|
msg := Message{
|
||||||
|
Type: Closed,
|
||||||
|
Content: "创建录屏文件失败 :( " + err.Error(),
|
||||||
|
}
|
||||||
|
return WriteMessage(ws, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
header := &Header{
|
||||||
|
Title: "test",
|
||||||
|
Version: 2,
|
||||||
|
Height: height,
|
||||||
|
Width: width,
|
||||||
|
Env: Env{Shell: "/bin/bash", Term: "xterm-256color"},
|
||||||
|
Timestamp: int(time.Now().Unix()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := recorder.WriteHeader(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := model.UpdateSessionById(&model.Session{Recording: recording}, sessionId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
msg := Message{
|
msg := Message{
|
||||||
Type: Connected,
|
Type: Connected,
|
||||||
Content: "Connect to server successfully.\r\n",
|
Content: "Connect to server successfully.\r\n",
|
||||||
}
|
}
|
||||||
_ = WriteMessage(ws, msg)
|
_ = WriteMessage(ws, msg)
|
||||||
|
|
||||||
recorder, err := NewRecorder("./" + sessionId + ".cast")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
header := &Header{
|
|
||||||
Title: "test",
|
|
||||||
Version: 2,
|
|
||||||
Height: height,
|
|
||||||
Width: width,
|
|
||||||
Env: Env{Shell: "/bin/bash", Term: "xterm-256color"},
|
|
||||||
Timestamp: int(time.Now().Unix()),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recorder.WriteHeader(header); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var mut sync.Mutex
|
var mut sync.Mutex
|
||||||
var active = true
|
var active = true
|
||||||
|
|
||||||
@ -181,7 +197,10 @@ func SSHEndpoint(c echo.Context) error {
|
|||||||
mut.Lock()
|
mut.Lock()
|
||||||
if !active {
|
if !active {
|
||||||
logrus.Debugf("会话: %v -> %v 关闭", sshClient.LocalAddr().String(), sshClient.RemoteAddr().String())
|
logrus.Debugf("会话: %v -> %v 关闭", sshClient.LocalAddr().String(), sshClient.RemoteAddr().String())
|
||||||
recorder.Close()
|
if recorder != nil {
|
||||||
|
recorder.Close()
|
||||||
|
}
|
||||||
|
CloseSessionById(sessionId, Normal, "正常退出")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
mut.Unlock()
|
mut.Unlock()
|
||||||
@ -192,8 +211,10 @@ func SSHEndpoint(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
s := string(p)
|
s := string(p)
|
||||||
// 录屏
|
if recorder != nil {
|
||||||
_ = recorder.WriteData(s)
|
// 录屏
|
||||||
|
_ = recorder.WriteData(s)
|
||||||
|
}
|
||||||
msg := Message{
|
msg := Message{
|
||||||
Type: Data,
|
Type: Data,
|
||||||
Content: s,
|
Content: s,
|
||||||
|
@ -145,6 +145,7 @@ func TunEndpoint(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tun := global.Tun{
|
tun := global.Tun{
|
||||||
|
Protocol: configuration.Protocol,
|
||||||
Tunnel: tunnel,
|
Tunnel: tunnel,
|
||||||
WebSocket: ws,
|
WebSocket: ws,
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,28 @@ package global
|
|||||||
import (
|
import (
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
"next-terminal/pkg/guacd"
|
"next-terminal/pkg/guacd"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tun struct {
|
type Tun struct {
|
||||||
|
Protocol string
|
||||||
Tunnel *guacd.Tunnel
|
Tunnel *guacd.Tunnel
|
||||||
|
SshClient *ssh.Client
|
||||||
SftpClient *sftp.Client
|
SftpClient *sftp.Client
|
||||||
WebSocket *websocket.Conn
|
WebSocket *websocket.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Tun) Close() {
|
||||||
|
if r.Protocol == "rdp" || r.Protocol == "vnc" {
|
||||||
|
_ = r.Tunnel.Close()
|
||||||
|
} else {
|
||||||
|
_ = r.SshClient.Close()
|
||||||
|
_ = r.SftpClient.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Observable struct {
|
type Observable struct {
|
||||||
Subject *Tun
|
Subject *Tun
|
||||||
Observers []Tun
|
Observers []Tun
|
||||||
|
38
web/public/asciinema.html
Normal file
38
web/public/asciinema.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="asciinema-player.css"/>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<asciinema-player id='asciinema-player' src=""></asciinema-player>
|
||||||
|
<script src="asciinema-player.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const server = 'http://localhost:8088';
|
||||||
|
|
||||||
|
function getQueryVariable(variable) {
|
||||||
|
const query = window.location.search.substring(1);
|
||||||
|
const vars = query.split("&");
|
||||||
|
for (let i = 0; i < vars.length; i++) {
|
||||||
|
const pair = vars[i].split("=");
|
||||||
|
if (pair[0] === variable) {
|
||||||
|
return pair[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sessionId = getQueryVariable('sessionId');
|
||||||
|
document.getElementById('asciinema-player').setAttribute('src', `${server}/sessions/${sessionId}/recording`);
|
||||||
|
</script>
|
||||||
|
</html>
|
@ -8,6 +8,7 @@ import {getToken, isEmpty} from "../../utils/utils";
|
|||||||
import {FitAddon} from 'xterm-addon-fit';
|
import {FitAddon} from 'xterm-addon-fit';
|
||||||
import "./Access.css"
|
import "./Access.css"
|
||||||
import request from "../../common/request";
|
import request from "../../common/request";
|
||||||
|
import {message} from "antd";
|
||||||
|
|
||||||
class AccessSSH extends Component {
|
class AccessSSH extends Component {
|
||||||
|
|
||||||
@ -111,8 +112,8 @@ class AccessSSH extends Component {
|
|||||||
switch (msg['type']) {
|
switch (msg['type']) {
|
||||||
case 'connected':
|
case 'connected':
|
||||||
term.clear();
|
term.clear();
|
||||||
console.log(msg['content'])
|
|
||||||
this.onWindowResize();
|
this.onWindowResize();
|
||||||
|
this.updateSessionStatus(sessionId);
|
||||||
break;
|
break;
|
||||||
case 'data':
|
case 'data':
|
||||||
term.write(msg['content']);
|
term.write(msg['content']);
|
||||||
@ -152,6 +153,13 @@ class AccessSSH extends Component {
|
|||||||
return result['data']['id'];
|
return result['data']['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSessionStatus = async (sessionId) => {
|
||||||
|
let result = await request.post(`/sessions/${sessionId}/connect`);
|
||||||
|
if (result['code'] !== 1) {
|
||||||
|
message.error(result['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
terminalSize() {
|
terminalSize() {
|
||||||
return {
|
return {
|
||||||
cols: Math.floor(this.state.width / 7.5),
|
cols: Math.floor(this.state.width / 7.5),
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Col,
|
Col,
|
||||||
@ -64,6 +63,7 @@ class OfflineSession extends Component {
|
|||||||
delBtnLoading: false,
|
delBtnLoading: false,
|
||||||
users: [],
|
users: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
|
selectedRow: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -123,10 +123,10 @@ class OfflineSession extends Component {
|
|||||||
this.loadTableData(queryParams)
|
this.loadTableData(queryParams)
|
||||||
};
|
};
|
||||||
|
|
||||||
showPlayback = (sessionId) => {
|
showPlayback = (row) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
playbackVisible: true,
|
playbackVisible: true,
|
||||||
playbackSessionId: sessionId
|
selectedRow: row
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -289,7 +289,7 @@ class OfflineSession extends Component {
|
|||||||
<div>
|
<div>
|
||||||
<Button type="link" size='small'
|
<Button type="link" size='small'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => this.showPlayback(record.id)}>回放</Button>
|
onClick={() => this.showPlayback(record)}>回放</Button>
|
||||||
<Button type="link" size='small' onClick={() => {
|
<Button type="link" size='small' onClick={() => {
|
||||||
confirm({
|
confirm({
|
||||||
title: '您确定要删除此会话吗?',
|
title: '您确定要删除此会话吗?',
|
||||||
@ -488,7 +488,30 @@ class OfflineSession extends Component {
|
|||||||
destroyOnClose
|
destroyOnClose
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
>
|
>
|
||||||
<Playback sessionId={this.state.playbackSessionId}/>
|
{
|
||||||
|
this.state.selectedRow['protocol'] === 'rdp' || this.state.selectedRow['protocol'] === 'vnc' ?
|
||||||
|
<Playback sessionId={this.state.selectedRow['id']}/>
|
||||||
|
:
|
||||||
|
<iframe
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
// height: this.state.iFrameHeight,
|
||||||
|
overflow: 'visible'
|
||||||
|
}}
|
||||||
|
onLoad={() => {
|
||||||
|
// const obj = ReactDOM.findDOMNode(this);
|
||||||
|
// this.setState({
|
||||||
|
// "iFrameHeight": obj.contentWindow.document.body.scrollHeight + 'px'
|
||||||
|
// });
|
||||||
|
}}
|
||||||
|
ref="iframe"
|
||||||
|
src={'./asciinema.html?sessionId=' + this.state.selectedRow['id']}
|
||||||
|
width="100%"
|
||||||
|
height={window.innerHeight * 0.8}
|
||||||
|
frameBorder="0"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
</Modal> : undefined
|
</Modal> : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Title</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="asciinema-player.css"/>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<asciinema-player src="./903dfd65-838c-453f-9866-eadd5725321b.cast"></asciinema-player>
|
|
||||||
<script src="asciinema-player.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
Loading…
Reference in New Issue
Block a user