基于django+websocket+paramiko实现跳板机(堡垒机)
基于django+websocket+paramiko实现跳板机(堡垒机)
django
routing.py
# bastion/routing.py
from django.urls import path
from .consumers import SSHConsumer
websocket_urlpatterns = [
path('ws/ssh/', SSHConsumer.as_asgi()),
]
consumers.py
# bastion/routing.py
import json
import paramiko
from channels.generic.websocket import WebsocketConsumer
from threading import Thread
from urllib.parse import parse_qs
import time
class SSHConsumer(WebsocketConsumer):
def connect(self):
self.accept()
query_params = parse_qs(self.scope['query_string'].decode('utf-8'))
self.hostname = query_params.get('host', [None])[0]
self.username = query_params.get('username', [None])[0]
self.password = query_params.get('password', [None])[0]
self.ssh_client = paramiko.SSHClient()
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
self.ssh_client.connect(
hostname=self.hostname,
port=22,
username=self.username,
password=self.password
)
except Exception as e:
self.send(text_data=json.dumps({
'message': str(e)
}))
self.close()
return
transport = self.ssh_client.get_transport()
self.ssh_channel = transport.open_session()
self.ssh_channel.get_pty(term='xterm')
self.ssh_channel.invoke_shell()
self.command_history = []
self.last_output = ""
# 启动接收远程命令结果的线程
Thread(target=self.recv_from_host).start()
def disconnect(self, close_code):
self.ssh_client.close()
def receive(self, text_data=None):
text_data = json.loads(text_data)
command = text_data.get('data', '')
self.command_history.append(command.strip())
# 在新线程中发送命令
Thread(target=self.send_command, args=[command + '\n']).start()
def send_command(self, command):
self.ssh_channel.send(command)
time.sleep(0.1) # 延迟,以确保接收到的是命令执行结果
def recv_from_host(self):
try:
buffer = ""
while not self.ssh_channel.exit_status_ready():
if self.ssh_channel.recv_ready():
data = self.ssh_channel.recv(1024).decode('utf-8', 'ignore')
buffer += data
if buffer.endswith(('# ', '$ ', '% ')): # 检查常见的命令提示符
# 如果当前缓冲区包含最新命令,去除命令部分
for command in self.command_history:
if command in buffer:
buffer = buffer.replace(command, '').strip()
self.command_history.remove(command)
if buffer:
message = {'flag': 'success', 'message': buffer.strip()}
self.send(json.dumps(message))
buffer = "" # 清空缓冲区
except Exception as e:
print(f"Error in receiving data: {str(e)}")
asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from bastion.routing import websocket_urlpatterns
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "layui_admin_api.settings")
application = get_asgi_application()
application = ProtocolTypeRouter({
"http": application, # 保留 HTTP 处理
"websocket": AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns
)
),
})
run
uvicorn redis_django.asgi:application --host 0.0.0.0 --port 8000
vue3
WesockerView.vue
<template>
<form @submit.prevent="connectToServer">
<div>
<label for="host">Host:</label>
<input id="host" v-model="host" type="text">
</div>
<div>
<label for="username">Username:</label>
<input id="username" v-model="username" type="text">
</div>
<div>
<label for="password">Password:</label>
<input id="password" v-model="password" type="password">
</div>
<button type="submit">Connect</button>
</form>
<div ref="terminalContainer" class="terminal-container"></div>
</template>
<script setup>
import {ref, onMounted, onBeforeUnmount} from 'vue';
import {Terminal} from 'xterm';
import {FitAddon} from 'xterm-addon-fit';
import 'xterm/css/xterm.css'; // 引入 xterm 的样式
const terminalContainer = ref(null);
const terminal = ref(null);
const fitAddon = ref(null);
const host = ref('100.0.0.129')
const username = ref('root')
const password = ref('123')
const inputBuffer = ref('');
const resizeHandler = () => {
if (fitAddon.value) {
fitAddon.value.fit();
}
};
const connectToServer = () => {
const socket = new WebSocket(`ws://${window.location.hostname}:8000/ws/ssh/?host=${host.value}&username=${username.value}&password=${password.value}`);
socket.onopen = () => {
terminal.value.writeln('开始连接');
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
terminal.value.write(data.message);
};
socket.onerror = (error) => {
console.log('error', error)
terminal.value.writeln(`WebSocket error: ${error.message}`);
};
socket.onclose = () => {
terminal.value.writeln('断开连接');
};
// 处理从终端输入的命令并发送到服务器
terminal.value.onData(data => {
if (data === '\r') { // 检查是否为回车键
socket.send(JSON.stringify({data: inputBuffer.value}));
terminal.value.write('\r\n');
inputBuffer.value = ''
} else if (data === '\x7f') { // 检查是否为退格键 (ASCII码为 \x7f)
if (inputBuffer.value.length > 0) {
inputBuffer.value = inputBuffer.value.slice(0, -1); // 删除最后一个字符
terminal.value.write('\b \b'); // 清除终端上的最后一个字符
}
} else {
inputBuffer.value += data
terminal.value.write(data)
}
});
};
onMounted(() => {
terminal.value = new Terminal();
fitAddon.value = new FitAddon();
terminal.value.loadAddon(fitAddon.value);
terminal.value.open(terminalContainer.value);
fitAddon.value.fit();
window.addEventListener('resize', resizeHandler);
terminal.value.writeln('Welcome to xterm.js in Vue 3!');
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeHandler);
if (terminal.value) {
terminal.value.dispose();
}
});
</script>
<style scoped>
.terminal-container {
width: 100%;
height: 100%;
min-height: 300px;
}
</style>
效果图