From 2e2f1ce2e45f9df6ff5e17f209b171aa88d052c2 Mon Sep 17 00:00:00 2001 From: mjzhang95 Date: Sun, 6 Apr 2025 19:06:11 +0800 Subject: [PATCH 1/6] Create docker-compose.yml --- docker-compose.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bf9be12 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +version: "3.6" +services: + db: + image: mariadb:10.8.2 + container_name: spug-db + restart: always + command: --port 3306 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + volumes: + - /data/spug/mysql:/var/lib/mysql + environment: + - MYSQL_DATABASE=spug + - MYSQL_USER=spug + - MYSQL_PASSWORD=spug.cc + - MYSQL_ROOT_PASSWORD=spug.cc + spug: + build: + context: . + dockerfile: Dockerfile + container_name: spug + privileged: true + restart: always + volumes: + - /data/spug/service:/data/spug + - /data/spug/repos:/data/repos + ports: + - "80:80" + - "23:23" # telnet端口 + environment: + - MYSQL_DATABASE=spug + - MYSQL_USER=spug + - MYSQL_PASSWORD=spug.cc + - MYSQL_HOST=db + - MYSQL_PORT=3306 + depends_on: + - db From 64e15935a25cf7d5ab91ccc642849cd9ca7635a0 Mon Sep 17 00:00:00 2001 From: mjzhang95 Date: Sun, 6 Apr 2025 19:08:13 +0800 Subject: [PATCH 2/6] Create telnet.py --- spug_api/libs/telnet.py | 72 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 spug_api/libs/telnet.py diff --git a/spug_api/libs/telnet.py b/spug_api/libs/telnet.py new file mode 100644 index 0000000..dc8dd6d --- /dev/null +++ b/spug_api/libs/telnet.py @@ -0,0 +1,72 @@ +# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug +# Copyright: (c) +# Released under the AGPL-3.0 License. +import telnetlib +import time + +class AuthenticationException(Exception): + pass + +class Telnet: + def __init__(self, hostname, port, username, password, timeout=10): + self.hostname = hostname + self.port = port + self.username = username + self.password = password + self.timeout = timeout + self._tn = None + + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def connect(self): + try: + self._tn = telnetlib.Telnet(self.hostname, self.port, self.timeout) + + # 等待登录提示 + index, match, text = self._tn.expect([b"login:", b"username:", b"Username:"], self.timeout) + if index != -1: + self._tn.write(self.username.encode('ascii') + b'\n') + + # 等待密码提示 + index, match, text = self._tn.expect([b"Password:", b"password:"], self.timeout) + if index != -1: + self._tn.write(self.password.encode('ascii') + b'\n') + + # 验证登录结果 + index, match, text = self._tn.expect([b"#", b"$", b">"], self.timeout) + if index == -1: + raise AuthenticationException("Authentication failed") + else: + raise AuthenticationException("Password prompt not found") + else: + raise AuthenticationException("Login prompt not found") + except: + if self._tn: + self.close() + raise + + def exec_command(self, command): + """执行命令并返回结果""" + if not self._tn: + raise RuntimeError("Not connected") + + try: + self._tn.write(command.encode('ascii') + b'\n') + time.sleep(0.5) # 等待命令执行 + + # 读取命令输出直到提示符 + response = self._tn.read_until(b"#", self.timeout) + return 0, response.decode('ascii') + except: + return 1, None + + def close(self): + """关闭连接""" + if self._tn: + self._tn.close() + self._tn = None From 47bcaaa0b50c5ce0501a6e8d2fbde6b83fee1bfc Mon Sep 17 00:00:00 2001 From: mjzhang95 Date: Sun, 6 Apr 2025 19:09:14 +0800 Subject: [PATCH 3/6] Update models.py --- spug_api/apps/host/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spug_api/apps/host/models.py b/spug_api/apps/host/models.py index 3726994..65cc1c7 100644 --- a/spug_api/apps/host/models.py +++ b/spug_api/apps/host/models.py @@ -19,6 +19,7 @@ class Host(models.Model, ModelMixin): is_verified = models.BooleanField(default=False) created_at = models.CharField(max_length=20, default=human_datetime) created_by = models.ForeignKey(User, models.PROTECT, related_name='+') + connect_type = models.CharField(max_length=20, default='ssh') @property def private_key(self): @@ -27,6 +28,12 @@ class Host(models.Model, ModelMixin): def get_ssh(self, pkey=None, default_env=None): pkey = pkey or self.private_key return SSH(self.hostname, self.port, self.username, pkey, default_env=default_env) + + def get_telnet(self, password=None): + """获取telnet连接实例""" + from libs.telnet import Telnet + port = self.port or 23 # telnet默认端口23 + return Telnet(self.hostname, port, self.username, password) def to_view(self): tmp = self.to_dict() From 39fd601b8151d7d9342f207766e7c90401539f64 Mon Sep 17 00:00:00 2001 From: mjzhang95 Date: Sun, 6 Apr 2025 19:09:48 +0800 Subject: [PATCH 4/6] Update views.py --- spug_api/apps/host/views.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/spug_api/apps/host/views.py b/spug_api/apps/host/views.py index db5036e..3257a6d 100644 --- a/spug_api/apps/host/views.py +++ b/spug_api/apps/host/views.py @@ -13,7 +13,8 @@ from apps.exec.models import ExecTemplate from apps.app.models import Deploy from apps.schedule.models import Task from apps.monitor.models import Detection -from libs.ssh import SSH, AuthenticationException +from libs.ssh import SSH +from libs.telnet import Telnet, AuthenticationException from paramiko.ssh_exception import BadAuthenticationType from openpyxl import load_workbook from threading import Thread @@ -193,7 +194,23 @@ def batch_valid(request): def _do_host_verify(form): - password = form.pop('password') + password = form.pop('password', None) + connect_type = form.pop('connect_type', 'ssh') + + if connect_type == 'telnet': + if not password: + return False + try: + with Telnet(form.hostname, form.port or 23, form.username, password) as tn: + return True + except AuthenticationException: + raise Exception('Telnet认证失败,请检查用户名和密码是否正确') + except socket.timeout: + raise Exception('连接主机超时,请检查网络') + except Exception as e: + raise Exception(f'Telnet连接失败: {str(e)}') + + # SSH验证逻辑 if form.pkey: try: with SSH(form.hostname, form.port, form.username, form.pkey) as ssh: From 2c9ee7a77b0128fbea9cc6a3ebec73a126cd7919 Mon Sep 17 00:00:00 2001 From: mjzhang95 Date: Sun, 6 Apr 2025 19:11:10 +0800 Subject: [PATCH 5/6] Update Form.js --- spug_web/src/pages/host/Form.js | 79 ++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/spug_web/src/pages/host/Form.js b/spug_web/src/pages/host/Form.js index 3a8a9a3..32971f6 100644 --- a/spug_web/src/pages/host/Form.js +++ b/spug_web/src/pages/host/Form.js @@ -6,51 +6,75 @@ import React, { useState, useEffect } from 'react'; import { observer } from 'mobx-react'; import { ExclamationCircleOutlined, UploadOutlined } from '@ant-design/icons'; -import { Modal, Form, Input, TreeSelect, Button, Upload, Alert, message } from 'antd'; +import { Modal, Form, Input, TreeSelect, Button, Upload, Alert, message, Select } from 'antd'; import { http, X_TOKEN } from 'libs'; import store from './store'; import styles from './index.module.less'; +const { Option } = Select; + export default observer(function () { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const [uploading, setUploading] = useState(false); const [fileList, setFileList] = useState([]); + const [connectType, setConnectType] = useState('ssh'); + const [showPasswordField, setShowPasswordField] = useState(false); + + function handleConnectTypeChange(value) { + setConnectType(value); + setShowPasswordField(value === 'telnet'); + if (value === 'telnet') { + setFileList([]); + } + } useEffect(() => { if (store.record.pkey) { setFileList([{uid: '0', name: '独立密钥', data: store.record.pkey}]) } + if (store.record.connect_type) { + setConnectType(store.record.connect_type); + setShowPasswordField(store.record.connect_type === 'telnet'); + } }, []) function handleSubmit() { setLoading(true); const formData = form.getFieldsValue(); formData['id'] = store.record.id; + formData['connect_type'] = connectType; const file = fileList[0]; if (file && file.data) formData['pkey'] = file.data; + + if (connectType === 'telnet' && !formData.password && !showPasswordField) { + setShowPasswordField(true); + setLoading(false); + return; + } + http.post('/api/host/', formData) .then(res => { if (res === 'auth fail') { - setLoading(false) + setLoading(false); if (formData.pkey) { - message.error('独立密钥认证失败') + message.error(connectType === 'ssh' ? '独立密钥认证失败' : 'Telnet认证失败'); } else { const onChange = v => formData.password = v; Modal.confirm({ icon: , - title: '首次验证请输入密码', + title: connectType === 'ssh' ? '首次验证请输入密码' : 'Telnet认证', content: , onOk: () => handleConfirm(formData), - }) + }); } } else { message.success('验证成功'); store.formVisible = false; store.fetchRecords(); - store.fetchExtend(res.id) + store.fetchExtend(res.id); } - }, () => setLoading(false)) + }, () => setLoading(false)); } function handleConfirm(formData) { @@ -117,27 +141,42 @@ export default observer(function () { - - + + - + + + + - - + + + + + {connectType === 'ssh' && ( + + + {fileList.length === 0 ? : null} + - - - - {fileList.length === 0 ? : null} - - + )} + {connectType === 'telnet' && ( + + + + )} - + From cafa8c728d4120de5ff2e30071c1ed2c3348087e Mon Sep 17 00:00:00 2001 From: mjzhang95 Date: Sun, 6 Apr 2025 19:12:13 +0800 Subject: [PATCH 6/6] Update requirements.txt --- spug_api/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spug_api/requirements.txt b/spug_api/requirements.txt index 768f20b..9743d32 100644 --- a/spug_api/requirements.txt +++ b/spug_api/requirements.txt @@ -9,4 +9,5 @@ requests==2.32.0 GitPython==3.1.41 python-ldap==3.4.0 openpyxl==3.0.3 -user_agents==2.2.0 \ No newline at end of file +user_agents==2.2.0 +telnetlib3==1.0.4