1. Web Terminal 이란?
보통 Putty 혹은 MovaXTerm으로 SSH 연결을 할 것이다.
특정 상황에서는 웹에서 SSH 연결이 된 것 처럼 보여줘야 할 때,
웹상에서 터미널을 사용하게 해주는 것을 의미한다.
라이브러리들이 잘 되어있어서 간단하다.
추가적으로 리눅스 환경과 똑같이 드래그시 복사가 되고, 우클릭시 붙여넣기가 되게 추가하였다.
이번 포스팅에서는 웹터미널이 어떻게 작동하는지 정도만 확인하는 용도여서 동시에 여러사람이 붙거나 이런 경우에는 제대로 동작하지 않을것이다.
다음 포스팅을 작성하게되면 그때 실제로 서버에 적용한 코드를 포스팅하도록 하겠다.
[개발 환경]
Front : React, TypeScript
Backend : NodeJS, Express, TypeScript
+ SSH 로 연결할 서버 하나
2. FrontEnd
먼저 프로젝트를 생성한다.
npx create-react-app [프로젝트명] --template typescript
필요한 모듈 설치
npm i xterm
npm i xterm-addon-fit
npm i socket.io-client
xterm은 화면에 나타는 터미널을 담당하고,
xterm-addon-fit은 터미널이 화면에 맞게 보이게 도와주는 에드온이다.
sokcet.io는 백앤드 서버와 소켓 통신을 담당한다.
프론트와 백앤드간의 통신을 위한 Socket 을 먼저 구현한다.
SocketConfig.tsx
import { io, Socket } from "socket.io-client";
import { Terminal } from "xterm";
class SocketConfig{
socket : Socket;
constructor(){
this.socket = io("http://localhost:3080");
}
init(term : Terminal){
this.socket.on("output", (data : string) => {
term.write(data);
})
}
execute(command : string){
this.socket.emit("input", command);
}
}
export default SocketConfig;
SocketConfig를 파라미터로 받을때, 중복되게 선언이 일어나서 SocketConfigDTO라고 객체 클래스를 만들었다.
import { Terminal } from "xterm";
export type SocketConfigDTO = {
init: (term: Terminal) => void;
execute: (command: string) => void;
};
이제 터미널을 구현한다.
MyTerminal.tsx
import { ITerminalOptions, Terminal } from "xterm";
import { FitAddon } from 'xterm-addon-fit';
import { SocketConfigDTO } from "../constants/SocketConfigDTO";
export default class MyTerminal {
term: Terminal;
_options: ITerminalOptions = {
cursorBlink: true,
scrollSensitivity: 2,
allowProposedApi: true,
};
_fitAddon: FitAddon;
_input: number;
_socket: SocketConfigDTO;
constructor(socket: any) {
this.term = new Terminal(this._options);
this._fitAddon = new FitAddon();
this._input = 0;
this._socket = socket;
this._socket.init(this.term);
this._initWebTerminal();
window.addEventListener("mouseup", (e) => {
if(e.button == 0) this.dragCopy();
});
}
open(element: HTMLElement) {
this.term.open(element);
}
fit() {
this._fitAddon.fit();
}
paste() {
navigator.clipboard.readText().then(text => {
this._socket.execute(text);
});
}
dragCopy(){
const text = this.term.getSelection();
navigator.clipboard.writeText(text);
}
_initWebTerminal() {
this.term.onKey((e) => this._onKeyHandler(e));
this.term.onData((e) => this._onDataHandler(e));
this.term.loadAddon(this._fitAddon);
}
_onKeyHandler(e: { key: string; domEvent: KeyboardEvent }) {
const printable: boolean = !e.domEvent.altKey && !e.domEvent.ctrlKey && !e.domEvent.metaKey;
if (e.domEvent.key === 'Enter') {
this._enter();
} else if (e.domEvent.key === 'Backspace') {
this._backSpace();
} else if (e.domEvent.key === 'ArrowRight') {
this._moveRight();
} else if (e.domEvent.key === 'ArrowLeft') {
this._moveLeft();
} else if (printable) {
this._input++;
}
}
_onDataHandler(e: string) {
this._socket.execute(e);
}
_enter() {
this._input = 0;
}
_backSpace() {
if (this.term.buffer.active.cursorX >= 10) {
this.term.write('\x1B[0J');
this._input--;
}
}
_moveRight() {
const isEnd: boolean = this.term.buffer.active.cursorX - 10 <= this._input;
if (!isEnd) {
this.term.write('\x1B[C');
}
}
_moveLeft() {
const isStart: boolean = this.term.buffer.active.cursorX >= 10;
if (!isStart) {
this.term.write('\x1B[D');
}
}
}
MyTerminal을 사용한다.
WebTerminal.tsx
import { useEffect } from "react";
import "xterm/css/xterm.css";
import { SocketConfigDTO } from "../constants/SocketConfigDTO";
import MyTerminal from "./MyTerminal";
const WebTerminal = (
{socket}: {socket : SocketConfigDTO}
) => {
let terminal : MyTerminal;
useEffect(() => {
terminal = new MyTerminal(socket);
terminal.open(document.getElementById("terminal") as HTMLElement);
terminal.fit();
}, []);
const clickRight = (e : any) => {
e.preventDefault();
terminal.paste();
}
return <div id="terminal" style={{height : "100vh", textAlign : "left"}} onContextMenu={clickRight}/>;
};
export default WebTerminal;
이렇게 WebTerminal 개발이 완료되었다.
이제 WebTermnal과 SocketConfig를 사용한다.
App.tsx
import './App.css';
import SocketConfig from './socket/SocketConfig';
import WebTerminal from './webTerminal/WebTerminal';
function App() {
const socketConfig = new SocketConfig();
return (
<div className="App">
<WebTerminal socket = {socketConfig}/>
</div>
);
}
export default App;
3. BackEnd
마찬가지로 프로젝트를 먼저 생성하는데, 백엔드는 폴더를 만들어서 폴더 안에서 명령어로 생성해주게 된다.
npm init
npm i express typescript @types/express
npm i -D nodemon ts-node
tsc --init
기본 세팅이 끝났으면 app.ts를 만들어준다.
import { createServer } from "http";
import Socket from "./src/Socket";
import express from "express"
const app = express();
const port = 3080;
const server = createServer(app)
server.listen(port, () => {
console.log(`App running on port ${port}...`);
const socket2 = new Socket();
socket2.attach(server);
});
app.get('/', (req, res) => {
res.send('Hello World!');
});
Socket을 구현한다.
import http from 'http';
import { Server } from "socket.io";
import { Client } from "ssh2";
class Socket {
attach(server: http.Server) {
if (!server) {
throw new Error('Server Not Found');
}
const io = new Server(server, {
cors: {
origin: '*',
methods: ['GET', 'POST'],
},
});
io.on('connection', (socket) => {
socket.on('disconnect', () => {
console.log('Socket Disconnected: ', socket.id);
});
const ssh = new Client();
ssh
.on("ready", () => {
console.log("ssh ready");
ssh.shell((err, stream) => {
if(err) console.log("err : ", err);
socket.on("input", (data) => {
stream.write(data);
});
stream
.on("data", (d : any) => {
socket.emit('output', d.toString("binary"));
})
.on("close", () => {
ssh.end();
})
;
});
})
.connect({
host : "192.168.0.7",
port: 22,
username: "root",
password: "1234"
})
.on("error", (e) => {
console.log("error : ", e.message);
})
;
});
}
}
export default Socket;
'NodeJS' 카테고리의 다른 글
WebRDP (0) | 2024.12.18 |
---|---|
Web Terminal Ssh (0) | 2024.11.14 |
WebRTC - SFU, Mediasoup (6) | 2024.08.27 |
4. Screen Sharing - Advancement (0) | 2024.08.14 |
3.Screen Sharing - Mouse Event (0) | 2024.08.12 |