基于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>

效果图

posted @ 2024-06-28 19:59  蓝幻ﹺ  阅读(45)  评论(0编辑  收藏  举报