update 5.1

New ServerManager for manage multiple server with multiple protocol. ProWrapper for translate protocol from many protocol to one protocol (function).
This commit is contained in:
dharm pimsen 2024-12-06 23:33:15 +07:00
parent 1b15383ca7
commit 78a6459d26
27 changed files with 1419 additions and 292 deletions

View File

@ -1,5 +1,7 @@
# What is PyserSSH
This library will be **Pyserminal** (Python Server Terminal) as it supports multiple protocols such as ssh telnet rlogin and mores...
PyserSSH is a free and open-source Python library designed to facilitate the creation of customizable SSH terminal servers. Initially developed for research purposes to address the lack of suitable SSH server libraries in Python, PyserSSH provides a flexible and user-friendly solution for implementing SSH servers, making it easier for developers to handle user interactions and command processing.
The project was started by a solo developer to create a more accessible and flexible tool for managing SSH connections and commands. It offers a simplified API compared to other libraries, such as Paramiko, SSHim, and Twisted, which are either outdated or complex for new users.
@ -32,15 +34,15 @@ pip install git+https://git.damp11113.xyz/DPSoftware-Foundation/PyserSSH.git
# Quick Example
This Server use port **2222** for default port
```py
from PyserSSH import Server, Send, AccountManager
from PyserSSH import Server, AccountManager
useraccount = AccountManager(anyuser=True)
useraccount = AccountManager(allow_guest=True)
ssh = Server(useraccount)
@ssh.on_user("command")
def command(client, command: str):
if command == "hello":
Send(client, "world!")
client.send("world!")
ssh.run("your private key file")
```

View File

@ -1,4 +1,8 @@
import os
os.environ["damp11113_load_all_module"] = "NO"
from damp11113.utils import TextFormatter
from damp11113.file import sort_files, allfiles
import socket
import time
import cv2
@ -6,13 +10,14 @@ import traceback
import requests
from bs4 import BeautifulSoup
import numpy as np
import logging
#import logging
#logging.basicConfig(level=logging.DEBUG)
# Configure logging
logging.basicConfig(format='[{asctime}] [{levelname}] {name}: {message}', datefmt='%Y-%m-%d %H:%M:%S', style='{', level=logging.DEBUG)
from PyserSSH import Server, AccountManager
from PyserSSH.interactive import Send, wait_input, wait_inputkey, wait_choose, Clear, wait_inputmouse
from PyserSSH.system.info import __version__, Flag_TH
from PyserSSH.system.info import version, Flag_TH
from PyserSSH.extensions.processbar import indeterminateStatus, LoadingProgress
from PyserSSH.extensions.dialog import MenuDialog, TextDialog, TextInputDialog
from PyserSSH.extensions.moredisplay import clickable_url, Send_karaoke_effect
@ -20,15 +25,18 @@ from PyserSSH.extensions.moreinteractive import ShowCursor
from PyserSSH.extensions.remodesk import RemoDesk
from PyserSSH.extensions.XHandler import XHandler
from PyserSSH.system.clientype import Client
from PyserSSH.system.remotestatus import remotestatus
from PyserSSH.system.RemoteStatus import remotestatus
from PyserSSH.utils.ServerManager import ServerManager
useraccount = AccountManager(allow_guest=True)
useraccount.add_account("admin", "") # create user without password
useraccount.add_account("test", "test") # create user without password
useraccount.add_account("demo")
useraccount.add_account("remote", "12345", permissions=["remote_desktop"])
useraccount.set_user_enable_inputsystem_echo("remote", False)
useraccount.set_user_sftp_allow("admin", True)
useraccount = AccountManager(allow_guest=True, autoload=True, autosave=True)
if not os.path.isfile("autosave_session.ses"):
useraccount.add_account("admin", "", sudo=True) # create user without password
useraccount.add_account("test", "test") # create user without password
useraccount.add_account("demo")
useraccount.add_account("remote", "12345", permissions=["remote_desktop"])
useraccount.set_user_enable_inputsystem_echo("remote", False)
useraccount.set_user_sftp_allow("admin", True)
XH = XHandler()
ssh = Server(useraccount,
@ -42,64 +50,7 @@ remotedesktopserver = RemoDesk()
servername = "PyserSSH"
loading = ["PyserSSH", "Extensions"]
class TextFormatter:
RESET = "\033[0m"
TEXT_COLORS = {
"black": "\033[30m",
"red": "\033[31m",
"green": "\033[32m",
"yellow": "\033[33m",
"blue": "\033[34m",
"magenta": "\033[35m",
"cyan": "\033[36m",
"white": "\033[37m"
}
TEXT_COLOR_LEVELS = {
"light": "\033[1;{}m", # Light color prefix
"dark": "\033[2;{}m" # Dark color prefix
}
BACKGROUND_COLORS = {
"black": "\033[40m",
"red": "\033[41m",
"green": "\033[42m",
"yellow": "\033[43m",
"blue": "\033[44m",
"magenta": "\033[45m",
"cyan": "\033[46m",
"white": "\033[47m"
}
TEXT_ATTRIBUTES = {
"bold": "\033[1m",
"italic": "\033[3m",
"underline": "\033[4m",
"blink": "\033[5m",
"reverse": "\033[7m",
"strikethrough": "\033[9m"
}
@staticmethod
def format_text_truecolor(text, color=None, background=None, attributes=None, target_text=''):
formatted_text = ""
start_index = text.find(target_text)
end_index = start_index + len(target_text) if start_index != -1 else len(text)
if color:
formatted_text += f"\033[38;2;{color}m"
if background:
formatted_text += f"\033[48;2;{background}m"
if attributes in TextFormatter.TEXT_ATTRIBUTES:
formatted_text += TextFormatter.TEXT_ATTRIBUTES[attributes]
if target_text == "":
formatted_text += text + TextFormatter.RESET
else:
formatted_text += text[:start_index] + text[start_index:end_index] + TextFormatter.RESET + text[end_index:]
return formatted_text
loading = ["PyserSSH", "openRemoDesk", "XHandler", "RemoteStatus"]
@ssh.on_user("pre-shell")
def guestauth(client):
@ -185,7 +136,7 @@ def connect(client):
wm = f"""{Flag_TH()}{''*50}
Hello {client['current_user']},
This is testing server of PyserSSH v{__version__}.
This is testing server of PyserSSH v{version}.
Visit: {clickable_url("https://damp11113.xyz", "DPCloudev")}
{''*50}"""
@ -258,9 +209,9 @@ def xh_typing(client: Client, messages, speed = 1):
Send(client, "")
@XH.command(name="renimtest")
def xh_renimtest(client: Client, path: str):
def xh_renimtest(client: Client):
Clear(client)
image = cv2.imread(f"opensource.png", cv2.IMREAD_COLOR)
image = cv2.imread("opensource.png", cv2.IMREAD_COLOR)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
width, height = client['windowsize']["width"] - 5, client['windowsize']["height"] - 5
@ -458,12 +409,14 @@ def xh_status(client: Client):
#@ssh.on_user("command")
#def command(client: Client, command: str):
ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
#ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
#manager = ServerManager()
manager = ServerManager()
# Add servers to the manager
#manager.add_server("server1", server1)
manager.add_server("ssh", ssh, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
manager.add_server("telnet", ssh, "", protocol="telnet")
# Start a specific server
#manager.start_server("server1", private_key_path="key")
manager.start_server("ssh")
manager.start_server("telnet")

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name='PyserSSH',
version='5.0',
version='5.1',
license='MIT',
author='DPSoftware Foundation',
author_email='contact@damp11113.xyz',

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -37,15 +37,15 @@ right - \x1b[C
https://en.wikipedia.org/wiki/ANSI_escape_code
"""
import os
import ctypes
import logging
from .interactive import *
from .server import Server
from .account import AccountManager
from .system.info import system_banner
from .system.info import system_banner, version
if os.name == 'nt':
import ctypes
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
@ -67,45 +67,10 @@ if os.environ["pyserssh_log"] == "NO":
if os.environ["pyserssh_systemmessage"] == "YES":
print(system_banner)
# Server Managers
class ServerManager:
def __init__(self):
self.servers = {}
def add_server(self, name, server):
if name in self.servers:
raise ValueError(f"Server with name '{name}' already exists.")
self.servers[name] = server
def remove_server(self, name):
if name not in self.servers:
raise ValueError(f"No server found with name '{name}'.")
del self.servers[name]
def get_server(self, name):
return self.servers.get(name)
def start_server(self, name, protocol="ssh", *args, **kwargs):
server = self.get_server(name)
if not server:
raise ValueError(f"No server found with name '{name}'.")
print(f"Starting server '{name}'...")
server.run(*args, **kwargs)
def stop_server(self, name):
server = self.get_server(name)
if not server:
raise ValueError(f"No server found with name '{name}'.")
print(f"Stopping server '{name}'...")
server.stop_server()
def start_all_servers(self, *args, **kwargs):
for name, server in self.servers.items():
print(f"Starting server '{name}'...")
server.run(*args, **kwargs)
def stop_all_servers(self):
for name, server in self.servers.items():
print(f"Stopping server '{name}'...")
server.stop_server()
__author__ = "damp11113"
__url__ = "https://github.com/DPSoftware-Foundation/PyserSSH"
__copyright__ = "2023-present"
__license__ = "MIT"
__version__ = version
__department__ = "DPSoftware"
__organization__ = "DOPFoundation"

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -24,6 +24,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import logging
import os
import pickle
import time
@ -31,38 +32,51 @@ import atexit
import threading
import hashlib
logger = logging.getLogger("PyserSSH.Account")
class AccountManager:
def __init__(self, allow_guest=False, historylimit=10, autosave=False, autosavedelay=60, autoload=False, autoloadfile="autosave_session.ses"):
def __init__(self, allow_guest=False, historylimit=10, autosave=False, autosavedelay=60, autoload=False, autofile="autosave_session.ses"):
self.accounts = {}
self.allow_guest = allow_guest
self.historylimit = historylimit
self.autosavedelay = autosavedelay
self.__autosavework = False
self.autosave = autosave
self.__autosaveworknexttime = 0
self.__autofile = autofile
if autoload:
self.load(autoloadfile)
self.load(self.__autofile)
if autosave:
if self.autosave:
logger.info("starting autosave")
self.__autosavethread = threading.Thread(target=self.__autosave, daemon=True)
self.__autosavethread.start()
atexit.register(self.__saveexit)
def __autosave(self):
self.save("autosave_session.ses")
self.save(self.__autofile)
self.__autosaveworknexttime = time.time() + self.autosavedelay
self.__autosavework = True
while self.__autosavework:
if int(self.__autosaveworknexttime) == int(time.time()):
self.save("autosave_session.ses")
self.save(self.__autofile)
self.__autosaveworknexttime = time.time() + self.autosavedelay
time.sleep(1) # fix cpu load
def __auto_save(func):
def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
if self.autosave:
self.save(self.__autofile, False)
return result
return wrapper
def __saveexit(self):
self.__autosavework = False
self.save("autosave_session.ses")
self.save(self.__autofile)
self.__autosavethread.join()
def validate_credentials(self, username, password=None, public_key=None):
@ -90,6 +104,9 @@ class AccountManager:
def has_user(self, username):
return username in self.accounts
def list_users(self):
return list(self.accounts.keys())
def get_allowed_auths(self, username):
if self.has_user(username) and "allowed_auth" in self.accounts[username]:
return self.accounts[username]["allowed_auth"]
@ -100,6 +117,7 @@ class AccountManager:
return self.accounts[username]["permissions"]
return []
@__auto_save
def set_prompt(self, username, prompt=">"):
if self.has_user(username):
self.accounts[username]["prompt"] = prompt
@ -109,7 +127,8 @@ class AccountManager:
return self.accounts[username]["prompt"]
return ">" # Default prompt if not set for the user
def add_account(self, username, password=None, public_key=None, permissions:list=None):
@__auto_save
def add_account(self, username, password=None, public_key=None, permissions:list=None, sudo=False):
if not self.has_user(username):
allowedlist = []
accountkey = {}
@ -132,34 +151,63 @@ class AccountManager:
accountkey["allowed_auth"] = ",".join(allowedlist)
self.accounts[username] = accountkey
if sudo:
if self.has_sudo_user():
raise Exception(f"sudo user is exist")
self.accounts[username]["sudo"] = sudo
else:
raise Exception(f"{username} is exist")
def has_sudo_user(self):
return any(account.get("sudo", False) for account in self.accounts.values())
def is_user_has_sudo(self, username):
if self.has_user(username) and "sudo" in self.accounts[username]:
return self.accounts[username]["sudo"]
return False
@__auto_save
def remove_account(self, username):
if self.has_user(username):
del self.accounts[username]
@__auto_save
def change_password(self, username, new_password):
if self.has_user(username):
self.accounts[username]["password"] = new_password
@__auto_save
def set_permissions(self, username, new_permissions):
if self.has_user(username):
self.accounts[username]["permissions"] = new_permissions
def save(self, filename="session.ses"):
def save(self, filename="session.ses", keep_log=True):
if keep_log:
logger.info(f"saving session to {filename}")
try:
with open(filename, 'wb') as file:
pickle.dump(self.accounts, file)
if keep_log:
logger.info(f"saved session to {filename}")
except Exception as e:
if keep_log:
logger.error(f"save session failed: {e}")
def load(self, filename):
logger.info(f"loading session from {filename}")
try:
with open(filename, 'rb') as file:
self.accounts = pickle.load(file)
logger.info(f"loaded session")
except FileNotFoundError:
print("File not found. No accounts loaded.")
logger.error("can't load session: file not found.")
except Exception as e:
print(f"An error occurred: {e}. No accounts loaded.")
logger.error(f"can't load session: {e}")
@__auto_save
def set_user_sftp_allow(self, username, allow=True):
if self.has_user(username):
self.accounts[username]["sftp_allow"] = allow
@ -169,6 +217,7 @@ class AccountManager:
return self.accounts[username]["sftp_allow"]
return False
@__auto_save
def set_user_sftp_readonly(self, username, readonly=False):
if self.has_user(username):
self.accounts[username]["sftp_readonly"] = readonly
@ -178,6 +227,7 @@ class AccountManager:
return self.accounts[username]["sftp_readonly"]
return False
@__auto_save
def set_user_sftp_root_path(self, username, path="/"):
if self.has_user(username):
if path == "/":
@ -190,6 +240,7 @@ class AccountManager:
return self.accounts[username]["sftp_root_path"]
return os.getcwd()
@__auto_save
def set_user_enable_inputsystem(self, username, enable=True):
if self.has_user(username):
self.accounts[username]["inputsystem"] = enable
@ -199,6 +250,7 @@ class AccountManager:
return self.accounts[username]["inputsystem"]
return True
@__auto_save
def set_user_enable_inputsystem_echo(self, username, echo=True):
if self.has_user(username):
self.accounts[username]["inputsystem_echo"] = echo
@ -208,6 +260,7 @@ class AccountManager:
return self.accounts[username]["inputsystem_echo"]
return True
@__auto_save
def set_banner(self, username, banner):
if self.has_user(username):
self.accounts[username]["banner"] = banner
@ -222,6 +275,7 @@ class AccountManager:
return self.accounts[username]["timeout"]
return None
@__auto_save
def set_user_timeout(self, username, timeout=None):
if self.has_user(username):
self.accounts[username]["timeout"] = timeout
@ -231,6 +285,7 @@ class AccountManager:
return self.accounts[username]["lastlogin"]
return None
@__auto_save
def set_user_last_login(self, username, ip, timelogin=time.time()):
if self.has_user(username):
self.accounts[username]["lastlogin"] = {
@ -238,6 +293,7 @@ class AccountManager:
"time": timelogin
}
@__auto_save
def add_history(self, username, command):
if self.has_user(username):
if "history" not in self.accounts[username]:
@ -251,6 +307,7 @@ class AccountManager:
if len(self.accounts[username]["history"]) > history_limit:
self.accounts[username]["history"] = self.accounts[username]["history"][-history_limit:]
@__auto_save
def clear_history(self, username):
if self.has_user(username):
self.accounts[username]["history"] = [] # Initialize history list if it doesn't exist

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -35,6 +35,13 @@ def are_permissions_met(permission_list, permission_require):
class XHandler:
def __init__(self, enablehelp=True, showusageonworng=True):
"""
Initializes the command handler with optional settings for help messages and usage.
Parameters:
enablehelp (bool): Whether help messages are enabled.
showusageonworng (bool): Whether usage information is shown on wrong usage.
"""
self.handlers = {}
self.categories = {}
self.enablehelp = enablehelp
@ -43,6 +50,18 @@ class XHandler:
self.commandnotfound = None
def command(self, category=None, name=None, aliases=None, permissions: list = None):
"""
Decorator to register a function as a command with optional category, name, aliases, and permissions.
Parameters:
category (str): The category under which the command falls (default: None).
name (str): The name of the command (default: None).
aliases (list): A list of command aliases (default: None).
permissions (list): A list of permissions required to execute the command (default: None).
Returns:
function: The wrapped function.
"""
def decorator(func):
nonlocal name, category
if name is None:
@ -87,6 +106,16 @@ class XHandler:
return decorator
def call(self, client, command_string):
"""
Processes a command string, validates arguments, and calls the corresponding function.
Parameters:
client (object): The client sending the command.
command_string (str): The command string to be executed.
Returns:
Any: The result of the command function, or an error message if invalid.
"""
tokens = shlex.split(command_string)
command_name = tokens[0]
args = tokens[1:]
@ -102,7 +131,7 @@ class XHandler:
command_func = self.handlers[command_name]
command_info = self.get_command_info(command_name)
if command_info and command_info.get('permissions'):
if not are_permissions_met(self.serverself.accounts.get_permissions(client.get_name()), command_info.get('permissions')):
if not are_permissions_met(self.serverself.accounts.get_permissions(client.get_name()), command_info.get('permissions')) or not self.serverself.accounts.is_user_has_sudo(client.get_name()):
Send(client, f"Permission denied. You do not have permission to execute '{command_name}'.")
return
@ -172,6 +201,15 @@ class XHandler:
return
def get_command_info(self, command_name):
"""
Retrieves information about a specific command, including its description, arguments, and permissions.
Parameters:
command_name (str): The name of the command.
Returns:
dict: A dictionary containing command details such as name, description, args, and permissions.
"""
found_command = None
for category, commands in self.categories.items():
if command_name in commands:
@ -197,6 +235,15 @@ class XHandler:
}
def get_help_command_info(self, command):
"""
Generates a detailed help message for a specific command.
Parameters:
command (str): The name of the command.
Returns:
str: The formatted help message for the command.
"""
command_info = self.get_command_info(command)
aliases = command_info.get('aliases', [])
help_message = f"{command_info['name']}"
@ -220,6 +267,12 @@ class XHandler:
return help_message
def get_help_message(self):
"""
Generates a general help message listing all categories and their associated commands.
Returns:
str: The formatted help message containing all commands and categories.
"""
help_message = ""
for category, commands in self.categories.items():
help_message += f"{category}:\n"
@ -231,6 +284,13 @@ class XHandler:
return help_message
def get_all_commands(self):
"""
Retrieves all registered commands, grouped by category.
Returns:
dict: A dictionary where each key is a category name and the value is a
dictionary of commands within that category.
"""
all_commands = {}
for category, commands in self.categories.items():
all_commands[category] = commands

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -30,7 +30,21 @@ import re
from ..interactive import Clear, Send, wait_inputkey
from ..system.sysfunc import text_centered_screen
class TextDialog:
"""
A dialog that displays a simple text message with an optional title.
Args:
client (Client): The client to display the dialog to.
content (str, optional): The content to be displayed in the dialog. Defaults to an empty string.
title (str, optional): The title of the dialog. Defaults to an empty string.
Methods:
render(): Renders the dialog, displaying the title and content in the center of the screen.
waituserenter(): Waits for the user to press the 'enter' key to close the dialog.
"""
def __init__(self, client, content="", title=""):
self.client = client
@ -39,11 +53,15 @@ class TextDialog:
self.content = content
def render(self):
"""
Renders the dialog by displaying the title, content, and waiting for the user's input.
"""
Clear(self.client)
Send(self.client, self.title)
Send(self.client, "-" * self.windowsize["width"])
generatedwindow = text_centered_screen(self.content, self.windowsize["width"], self.windowsize["height"]-3, " ")
generatedwindow = text_centered_screen(self.content, self.windowsize["width"], self.windowsize["height"] - 3,
" ")
Send(self.client, generatedwindow)
@ -52,13 +70,32 @@ class TextDialog:
self.waituserenter()
def waituserenter(self):
"""
Waits for the user to press the 'enter' key to close the dialog.
"""
while True:
if wait_inputkey(self.client, raw=True) == b'\r':
Clear(self.client)
break
pass
class MenuDialog:
"""
A menu dialog that allows the user to choose from a list of options, with navigation and selection using arrow keys.
Args:
client (Client): The client to display the menu to.
choose (list): A list of options to be displayed.
title (str, optional): The title of the menu.
desc (str, optional): A description to display above the menu options.
Methods:
render(): Renders the menu dialog and waits for user input.
_waituserinput(): Handles user input for selecting options or canceling.
output(): Returns the selected option index or `None` if canceled.
"""
def __init__(self, client, choose: list, title="", desc=""):
self.client = client
@ -67,9 +104,12 @@ class MenuDialog:
self.desc = desc
self.contentallindex = len(choose) - 1
self.selectedindex = 0
self.selectstatus = 0 # 0 none 1 selected 2 cancel
self.selectstatus = 0 # 0 none, 1 selected, 2 canceled
def render(self):
"""
Renders the menu dialog, displaying the options and allowing the user to navigate and select an option.
"""
tempcontentlist = self.choose.copy()
Clear(self.client)
@ -90,7 +130,8 @@ class MenuDialog:
f"{exported}"
)
generatedwindow = text_centered_screen(contenttoshow, self.client["windowsize"]["width"], self.client["windowsize"]["height"]-3, " ")
generatedwindow = text_centered_screen(contenttoshow, self.client["windowsize"]["width"],
self.client["windowsize"]["height"] - 3, " ")
Send(self.client, generatedwindow)
@ -99,6 +140,9 @@ class MenuDialog:
self._waituserinput()
def _waituserinput(self):
"""
Waits for user input and updates the selection based on key presses.
"""
keyinput = wait_inputkey(self.client, raw=True)
if keyinput == b'\r': # Enter key
@ -124,12 +168,31 @@ class MenuDialog:
self.render()
def output(self):
"""
Returns the selected option index or `None` if the action was canceled.
"""
if self.selectstatus == 2:
return None
elif self.selectstatus == 1:
return self.selectedindex
class TextInputDialog:
"""
A text input dialog that allows the user to input text with optional password masking.
Args:
client (Client): The client to display the dialog to.
title (str, optional): The title of the input dialog.
inputtitle (str, optional): The prompt text for the user input.
password (bool, optional): If `True`, the input will be masked as a password. Defaults to `False`.
Methods:
render(): Renders the input dialog, displaying the prompt and capturing user input.
_waituserinput(): Handles user input, including text input and special keys.
output(): Returns the input text if selected, or `None` if canceled.
"""
def __init__(self, client, title="", inputtitle="Input Here", password=False):
self.client = client
@ -137,11 +200,14 @@ class TextInputDialog:
self.inputtitle = inputtitle
self.ispassword = password
self.inputstatus = 0 # 0 none 1 selected 2 cancel
self.inputstatus = 0 # 0 none, 1 selected, 2 canceled
self.buffer = bytearray()
self.cursor_position = 0
def render(self):
"""
Renders the text input dialog and waits for user input.
"""
Clear(self.client)
Send(self.client, self.title)
Send(self.client, "-" * self.client["windowsize"]["width"])
@ -157,7 +223,8 @@ class TextInputDialog:
"> " + self.buffer.decode('utf-8')
)
generatedwindow = text_centered_screen(texts, self.client["windowsize"]["width"], self.client["windowsize"]["height"]-3, " ")
generatedwindow = text_centered_screen(texts, self.client["windowsize"]["width"],
self.client["windowsize"]["height"] - 3, " ")
Send(self.client, generatedwindow)
@ -166,6 +233,9 @@ class TextInputDialog:
self._waituserinput()
def _waituserinput(self):
"""
Waits for the user to input text or special commands (backspace, cancel, enter).
"""
keyinput = wait_inputkey(self.client, raw=True)
if keyinput == b'\r': # Enter key
@ -182,7 +252,7 @@ class TextInputDialog:
self.buffer = self.buffer[:self.cursor_position - 1] + self.buffer[self.cursor_position:]
self.cursor_position -= 1
elif bool(re.compile(b'\x1b\[[0-9;]*[mGK]').search(keyinput)):
pass
pass # Ignore ANSI escape codes
else: # Regular character
self.buffer = self.buffer[:self.cursor_position] + keyinput + self.buffer[self.cursor_position:]
self.cursor_position += 1
@ -197,6 +267,9 @@ class TextInputDialog:
self.render()
def output(self):
"""
Returns the input text if the input was selected, or `None` if canceled.
"""
if self.inputstatus == 2:
return None
elif self.inputstatus == 1:

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -29,30 +29,52 @@ import time
from ..interactive import Send
def clickable_url(url, link_text=""):
"""
Creates a clickable URL in a terminal client with optional link text.
Args:
url (str): The URL to be linked.
link_text (str, optional): The text to be displayed for the link. Defaults to an empty string, which will display the URL itself.
Returns:
str: A terminal escape sequence that makes the URL clickable with the provided link text.
"""
return f"\033]8;;{url}\033\\{link_text}\033]8;;\033\\"
def Send_karaoke_effect(client, text, delay=0.1, ln=True):
"""
Sends a text with a 'karaoke' effect where the text is printed one character at a time,
with the remaining text dimmed until it is printed.
Args:
client (Client): The client to send the text to.
text (str): The text to be printed with the karaoke effect.
delay (float, optional): The delay in seconds between printing each character. Defaults to 0.1.
ln (bool, optional): Whether to send a newline after the text is finished. Defaults to True.
This function simulates a typing effect by printing the text character by character,
while dimming the unprinted characters.
"""
printed_text = ""
for i, char in enumerate(text):
# Print already printed text normally
# Print the already printed text normally
Send(client, printed_text + char, ln=False)
# Calculate not yet printed text to dim
# Calculate the unprinted text and dim it
not_printed_text = text[i + 1:]
dimmed_text = ''.join([f"\033[2m{char}\033[0m" for char in not_printed_text])
# Print dimmed text
# Print the dimmed text for the remaining characters
Send(client, dimmed_text, ln=False)
# Wait before printing the next character
time.sleep(delay)
# Clear the line for the next iteration
Send(client, '\r' ,ln=False)
# Clear the line to update the text in the next iteration
Send(client, '\r', ln=False)
# Prepare the updated printed_text for the next iteration
# Update the printed_text to include the current character
printed_text += char
if ln:
Send(client, "") # new line
Send(client, "") # Send a newline after the entire text is printed

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -28,10 +28,23 @@ SOFTWARE.
from ..interactive import Send
def ShowCursor(client, show=True):
"""
Shows or hides the cursor for a specific client.
Args:
client (Client): The client to show/hide the cursor for.
show (bool, optional): A flag to determine whether to show or hide the cursor. Defaults to True (show cursor).
"""
if show:
Send(client, "\033[?25h", ln=False)
Send(client, "\033[?25h", ln=False) # Show cursor
else:
Send(client, "\033[?25l", ln=False)
Send(client, "\033[?25l", ln=False) # Hide cursor
def SendBell(client):
Send(client, "\x07", ln=False)
"""
Sends a bell character (alert) to a client.
Args:
client (Client): The client to send the bell character to.
"""
Send(client, "\x07", ln=False) # Bell character (alert)

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -24,7 +24,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
# this file is from DPSoftware Foundation-library
# this file is from damp11113-library
from itertools import cycle
import math
@ -129,17 +129,27 @@ class TextFormatter:
def insert_string(base, inserted, position=0):
return base[:position] + inserted + base[position + len(inserted):]
steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]']
steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]']
steps3 = ['[ ]', '[- ]', '[-- ]', '[ --]', '[ -]', '[ ]', '[ -]', '[ --]', '[-- ]', '[- ]']
steps4 = ['[ ]', '[- ]', '[ - ]', '[ -]', '[ ]', '[ -]', '[ - ]', '[- ]', '[ ]']
steps5 = ['[ ]', '[ -]', '[ --]', '[---]', '[-- ]', '[- ]']
steps6 = ['[ ]', '[ -]', '[ - ]', '[- ]']
class Steps:
steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]']
steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]']
steps3 = ['[ ]', '[- ]', '[-- ]', '[ --]', '[ -]', '[ ]', '[ -]', '[ --]', '[-- ]', '[- ]']
steps4 = ['[ ]', '[- ]', '[ - ]', '[ -]', '[ ]', '[ -]', '[ - ]', '[- ]', '[ ]']
steps5 = ['[ ]', '[ -]', '[ --]', '[---]', '[-- ]', '[- ]']
steps6 = ['[ ]', '[ -]', '[ - ]', '[- ]']
expand_contract = ['[ ]', '[= ]', '[== ]', '[=== ]', '[====]', '[ ===]', '[ ==]', '[ =]', '[ ]']
rotating_dots = ['. ', '.. ', '... ', '.... ', '.....', ' ....', ' ...', ' ..', ' .', ' ']
bouncing_ball = ['o ', ' o ', ' o ', ' o ', ' o ', ' o', ' o ', ' o ', ' o ', ' o ', 'o ']
left_right_dots = ['[ ]', '[. ]', '[.. ]', '[... ]', '[....]', '[ ...]', '[ ..]', '[ .]', '[ ]']
expanding_square = ['[ ]', '[■]', '[■■]', '[■■■]', '[■■■■]', '[■■■]', '[■■]', '[■]', '[ ]']
spinner = ['|', '/', '-', '\\', '|', '/', '-', '\\']
zigzag = ['/ ', ' / ', ' / ', ' /', ' / ', ' / ', '/ ', '\\ ', ' \\ ', ' \\ ', ' \\', ' \\ ', ' \\ ', '\\ ']
arrows = ['', '←← ', '←←←', '←← ', '', '', '→→ ', '→→→', '→→ ', '']
snake = ['[> ]', '[=> ]', '[==> ]', '[===> ]', '[====>]', '[ ===>]', '[ ==>]', '[ =>]', '[ >]']
loading_bar = ['[ ]', '[= ]', '[== ]', '[=== ]', '[==== ]', '[===== ]', '[====== ]', '[======= ]', '[======== ]', '[========= ]', '[==========]']
class indeterminateStatus:
def __init__(self, client, desc="Loading...", end="[ OK ]", timeout=0.1, fail='[FAILED]', steps=None):
self.client = client
self.desc = desc
self.end = end
self.timeout = timeout
@ -147,13 +157,14 @@ class indeterminateStatus:
self._thread = Thread(target=self._animate, daemon=True)
if steps is None:
self.steps = steps1
self.steps = Steps.steps1
else:
self.steps = steps
self.done = False
self.fail = False
def start(self):
"""Start progress bar"""
self._thread.start()
return self
@ -168,12 +179,14 @@ class indeterminateStatus:
self.start()
def stop(self):
"""stop progress"""
self.done = True
cols = self.client["windowsize"]["width"]
Print(self.client['channel'], "\r" + " " * cols, end="")
Print(self.client['channel'], f"\r{self.end}")
def stopfail(self):
"""stop progress with error or fail"""
self.done = True
self.fail = True
cols = self.client["windowsize"]["width"]
@ -234,7 +247,7 @@ class LoadingProgress:
self._thread = Thread(target=self._animate, daemon=True)
if steps is None:
self.steps = steps1
self.steps = Steps.steps1
else:
self.steps = steps
@ -251,14 +264,17 @@ class LoadingProgress:
self.currentprint = ""
def start(self):
"""Start progress bar"""
self._thread.start()
self.startime = time.perf_counter()
return self
def update(self, i):
def update(self, i=1):
"""update progress"""
self.current += i
def updatebuffer(self, i):
def updatebuffer(self, i=1):
"""update buffer progress"""
self.currentbuffer += i
def _animate(self):
@ -374,12 +390,14 @@ class LoadingProgress:
self.start()
def stop(self):
"""stop progress"""
self.done = True
cols = self.client["windowsize"]["width"]
Print(self.client["channel"], "\r" + " " * cols, end="")
Print(self.client["channel"], f"\r{self.end}")
def stopfail(self):
"""stop progress with error or fail"""
self.done = True
self.fail = True
cols = self.client["windowsize"]["width"]

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -40,7 +40,7 @@ import logging
from ..system.clientype import Client
logger = logging.getLogger("RemoDeskSSH")
logger = logging.getLogger("PyserSSH.Ext.RemoDeskSSH")
class Protocol:
def __init__(self, server):

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -28,9 +28,17 @@ import logging
from ..interactive import Send
logger = logging.getLogger("PyserSSH")
logger = logging.getLogger("PyserSSH.Ext.ServerUtils")
def kickbyusername(server, username, reason=None):
"""
Kicks a user from the server by their username.
Args:
server (Server): The server object where clients are connected.
username (str): The username of the client to be kicked.
reason (str, optional): The reason for kicking the user. If None, no reason is provided.
"""
for peername, client_handler in list(server.client_handlers.items()):
if client_handler["current_user"] == username:
channel = client_handler.get("channel")
@ -46,6 +54,14 @@ def kickbyusername(server, username, reason=None):
logger.info(f"User '{username}' has been kicked by reason {reason}.")
def kickbypeername(server, peername, reason=None):
"""
Kicks a user from the server by their peername.
Args:
server (Server): The server object where clients are connected.
peername (str): The peername of the client to be kicked.
reason (str, optional): The reason for kicking the user. If None, no reason is provided.
"""
client_handler = server.client_handlers.get(peername)
if client_handler:
channel = client_handler.get("channel")
@ -61,6 +77,13 @@ def kickbypeername(server, peername, reason=None):
logger.info(f"peername '{peername}' has been kicked by reason {reason}.")
def kickall(server, reason=None):
"""
Kicks all users from the server.
Args:
server (Server): The server object where clients are connected.
reason (str, optional): The reason for kicking all users. If None, no reason is provided.
"""
for peername, client_handler in server.client_handlers.items():
channel = client_handler.get("channel")
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"])
@ -78,6 +101,13 @@ def kickall(server, reason=None):
logger.info(f"All users have been kicked by reason {reason}.")
def broadcast(server, message):
"""
Broadcasts a message to all connected clients.
Args:
server (Server): The server object where clients are connected.
message (str): The message to send to all clients.
"""
for client_handler in server.client_handlers.values():
channel = client_handler.get("channel")
if channel:
@ -88,6 +118,14 @@ def broadcast(server, message):
logger.error(f"Error occurred while broadcasting message: {e}")
def sendto(server, username, message):
"""
Sends a message to a specific user by their username.
Args:
server (Server): The server object where clients are connected.
username (str): The username of the client to send the message to.
message (str): The message to send to the specified user.
"""
for client_handler in server.client_handlers.values():
if client_handler.get("current_user") == username:
channel = client_handler.get("channel")

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -82,7 +82,10 @@ def Clear(client, oldclear=False, keep=False):
def Title(client, title):
Send(client, f"\033]0;{title}\007", ln=False)
def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=True, password=False, passwordmask=b"*", noabort=False, timeout=0):
def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=True, password=False, passwordmask=b"*", noabort=False, timeout=0, directchannel=False):
if directchannel:
channel = client
else:
channel = client["channel"]
channel.send(replace_enter_with_crlf(prompt))
@ -144,6 +147,8 @@ def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=T
channel.sendall(b'\r\n')
raise
else:
channel.setblocking(False)
channel.settimeout(None)
output = buffer.decode('utf-8')
# Return default value if specified and no input given
@ -171,9 +176,17 @@ def wait_inputkey(client, prompt="", raw=True, timeout=0):
if bool(re.compile(b'\x1b\[[0-9;]*[mGK]').search(byte)):
pass
channel.setblocking(False)
channel.settimeout(None)
if prompt != "":
channel.send("\r\n")
return byte.decode('utf-8') # only regular character
else:
channel.setblocking(False)
channel.settimeout(None)
if prompt != "":
channel.send("\r\n")
return byte
except socket.timeout:
@ -185,6 +198,7 @@ def wait_inputkey(client, prompt="", raw=True, timeout=0):
except Exception:
channel.setblocking(False)
channel.settimeout(None)
if prompt != "":
channel.send("\r\n")
raise
@ -204,6 +218,8 @@ def wait_inputmouse(client, timeout=0):
if byte.startswith(b'\x1b[M'):
# Parse mouse event
if len(byte) < 6 or not byte.startswith(b'\x1b[M'):
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[?1000l", ln=False)
return None, None, None
@ -212,9 +228,13 @@ def wait_inputmouse(client, timeout=0):
x = byte[4] - 32
y = byte[5] - 32
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[?1000l", ln=False)
return button, x, y
else:
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[?1000l", ln=False)
return byte, None, None
@ -255,9 +275,13 @@ def wait_choose(client, choose, prompt="", timeout=0):
keyinput = wait_inputkey(client, raw=True)
if keyinput == b'\r': # Enter key
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[K")
return chooseindex
elif keyinput == b'\x03': # ' ctrl+c' key for cancel
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[K")
return 0
elif keyinput == b'\x1b[D': # Up arrow key

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -38,17 +38,18 @@ from .system.SFTP import SSHSFTPServer
from .system.sysfunc import replace_enter_with_crlf
from .system.interface import Sinterface
from .system.inputsystem import expect
from .system.info import __version__, system_banner
from .system.info import version, system_banner
from .system.clientype import Client as Clientype
from .system.ProWrapper import SSHTransport, TelnetTransport, ITransport
# paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22)
sftpclient = ["WinSCP", "Xplore"]
logger = logging.getLogger("PyserSSH")
logger = logging.getLogger("PyserSSH.Server")
class Server:
def __init__(self, accounts, system_message=True, disable_scroll_with_arrow=True, sftp=False, system_commands=True, compression=True, usexternalauth=False, history=True, inputsystem=True, XHandler=None, title=f"PyserSSH v{__version__}", inspeed=32768, enable_preauth_banner=False, enable_exec_system_command=True, enable_remote_status=False, inputsystem_echo=True):
def __init__(self, accounts, system_message=True, disable_scroll_with_arrow=True, sftp=False, system_commands=True, compression=True, usexternalauth=False, history=True, inputsystem=True, XHandler=None, title=f"PyserSSH v{version}", inspeed=32768, enable_preauth_banner=False, enable_exec_system_command=True, enable_remote_status=False, inputsystem_echo=True):
"""
system_message set to False to disable welcome message from system
disable_scroll_with_arrow set to False to enable seek text with arrow (Beta)
@ -80,13 +81,18 @@ class Server:
self._event_handlers = {}
self.client_handlers = {} # Dictionary to store event handlers for each client
self.__processmode = None
self.__serverisrunning = False
self.isrunning = False
self.__daemon = False
self.private_key = ""
self.__custom_server_args = ()
self.__custom_server = None
self.__protocol = "ssh"
if self.enasyscom:
print("\033[33m!!Warning!! System commands is enable! \033[0m")
def on_user(self, event_name):
"""Handle event"""
def decorator(func):
@wraps(func)
def wrapper(client, *args, **kwargs):
@ -98,7 +104,7 @@ class Server:
return decorator
def handle_client_disconnection(self, handler, chandlers):
if not chandlers["channel"].get_transport().is_active():
if not chandlers["transport"].is_active():
if handler:
handler(chandlers)
del self.client_handlers[chandlers["peername"]]
@ -113,32 +119,29 @@ class Server:
elif handler:
return handler(*args, **kwargs)
def handle_client(self, socketchannel, addr):
self._handle_event("pressh", socketchannel)
try:
bh_session = paramiko.Transport(socketchannel)
except OSError:
return
bh_session.add_server_key(self.private_key)
bh_session.use_compression(self.compressena)
bh_session.default_window_size = 2147483647
bh_session.packetizer.REKEY_BYTES = pow(2, 40)
bh_session.packetizer.REKEY_PACKETS = pow(2, 40)
bh_session.default_max_packet_size = self.inspeed
def _handle_client(self, socketchannel, addr):
self._handle_event("preserver", socketchannel)
logger.info("Starting session...")
server = Sinterface(self)
if not self.__custom_server:
if self.__protocol.lower() == "telnet":
bh_session = TelnetTransport(socketchannel, server) # Telnet server
else:
bh_session = SSHTransport(socketchannel, server, self.private_key) # SSH server
else:
bh_session = self.__custom_server(socketchannel, server, *self.__custom_server_args) # custom server
bh_session.enable_compression(self.compressena)
bh_session.max_packet_size(self.inspeed)
try:
bh_session.start_server(server=server)
bh_session.start_server()
except:
return
logger.info(bh_session.remote_version)
channel = bh_session.accept()
if self.sftpena:
@ -175,8 +178,7 @@ class Server:
logger.info("saved user data to client handlers")
#if not any(bh_session.remote_version.split("-")[2].startswith(prefix) for prefix in sftpclient):
if int(channel.out_window_size) != int(bh_session.default_window_size):
logger.info("user is ssh")
if not (int(channel.get_out_window_size()) == int(bh_session.get_default_window_size()) and bh_session.get_connection_type() == "SSH"):
#timeout for waiting 10 sec
for i in range(100):
if self.client_handlers[channel.getpeername()]["windowsize"]:
@ -260,7 +262,6 @@ class Server:
time.sleep(0.1)
self._handle_event("disconnected", self.client_handlers[peername])
else:
self._handle_event("disconnected", self.client_handlers[peername])
channel.close()
@ -271,13 +272,14 @@ class Server:
bh_session.close()
def stop_server(self):
"""Stop server"""
logger.info("Stopping the server...")
try:
for client_handler in self.client_handlers.values():
channel = client_handler.channel
if channel:
channel.close()
self.__serverisrunning = False
self.isrunning = False
self.server.close()
logger.info("Server stopped.")
@ -286,42 +288,64 @@ class Server:
def _start_listening_thread(self):
try:
logger.info("Start Listening for connections...")
while self.__serverisrunning:
self.isrunning = True
try:
logger.info("Listening for connections...")
while self.isrunning:
client, addr = self.server.accept()
if self.__processmode == "thread":
client_thread = threading.Thread(target=self.handle_client, args=(client, addr), daemon=True)
logger.info(f"Starting client thread for connection {addr}")
client_thread = threading.Thread(target=self._handle_client, args=(client, addr), daemon=True)
client_thread.start()
else:
self.handle_client(client, addr)
logger.info(f"Starting client for connection {addr}")
self._handle_client(client, addr)
time.sleep(1)
except KeyboardInterrupt:
self.stop_server()
except Exception as e:
logger.error(e)
def run(self, private_key_path=None, host="0.0.0.0", port=2222, mode="thread", maxuser=0, daemon=False):
"""mode: single, thread
protocol: ssh, telnet
def run(self, private_key_path=None, host="0.0.0.0", port=2222, waiting_mode="thread", maxuser=0, daemon=False, listen_thread=True, protocol="ssh", custom_server: ITransport = None, custom_server_args: tuple = None, custom_server_require_socket=True):
"""mode: single, thread,
protocol: ssh, telnet (beta), serial, custom
For serial need to set serial port at host (ex. host="com3") and set baudrate at port (ex. port=9600) and change listen_mode to "single".
"""
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
self.server.bind((host, port))
if protocol.lower() == "ssh":
if private_key_path != None:
logger.info("Loading private key")
self.private_key = paramiko.RSAKey(filename=private_key_path)
else:
raise ValueError("No private key")
self.__processmode = waiting_mode.lower()
self.__daemon = daemon
if custom_server:
self.__custom_server = custom_server
self.__custom_server_args = custom_server_args
if ((custom_server and protocol.lower() == "custom") and custom_server_require_socket) or protocol.lower() in ["ssh", "telnet"]:
logger.info("Creating server...")
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
self.server.bind((host, port))
logger.info("Set listen limit")
if maxuser == 0:
self.server.listen()
else:
self.server.listen(maxuser)
self.__processmode = mode.lower()
self.__serverisrunning = True
self.__daemon = daemon
if listen_thread:
logger.info("Starting listening in threading")
client_thread = threading.Thread(target=self._start_listening_thread, daemon=self.__daemon)
client_thread.start()
else:
print(f"\033[32mServer is running on {host}:{port}\033[0m")
self._start_listening_thread()
else:
client_thread = threading.Thread(target=self._handle_client, args=(None, None), daemon=True)
client_thread.start()
print(f"\033[32mServer is running on {host}:{port}\033[0m")

View File

@ -0,0 +1,495 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import serial
import socket
import paramiko
from abc import ABC, abstractmethod
from typing import Union
from .interface import Sinterface
from ..interactive import Send, wait_input
class ITransport(ABC):
@abstractmethod
def enable_compression(self, enable: bool) -> None:
"""
Enables or disables data compression for the transport.
Args:
enable (bool): If True, enable compression. If False, disable it.
"""
pass
@abstractmethod
def max_packet_size(self, size: int) -> None:
"""
Sets the maximum packet size for the transport.
Args:
size (int): The maximum packet size in bytes.
"""
pass
@abstractmethod
def start_server(self) -> None:
"""
Starts the server for the transport, allowing it to accept incoming connections.
"""
pass
@abstractmethod
def accept(self, timeout: Union[int, None] = None) -> "IChannel":
"""
Accepts an incoming connection and returns an IChannel instance for communication.
Args:
timeout (Union[int, None]): The time in seconds to wait for a connection.
If None, waits indefinitely.
Returns:
IChannel: An instance of IChannel representing the connection.
"""
pass
@abstractmethod
def set_subsystem_handler(self, name: str, handler: callable, *args: any, **kwargs: any) -> None:
"""
Sets a handler for a specific subsystem in the transport.
Args:
name (str): The name of the subsystem.
handler (callable): The handler function to be called for the subsystem.
*args: Arguments to pass to the handler.
**kwargs: Keyword arguments to pass to the handler.
"""
pass
@abstractmethod
def close(self) -> None:
"""
Closes the transport connection, releasing any resources used.
"""
pass
@abstractmethod
def is_authenticated(self) -> bool:
"""
Checks if the transport is authenticated.
Returns:
bool: True if the transport is authenticated, otherwise False.
"""
pass
@abstractmethod
def getpeername(self) -> tuple[str, int]: # (host, port)
"""
Retrieves the peer's address and port.
Returns:
tuple[str, int]: The host and port of the peer.
"""
pass
@abstractmethod
def get_username(self) -> str:
"""
Retrieves the username associated with the transport.
Returns:
str: The username.
"""
pass
@abstractmethod
def is_active(self) -> bool:
"""
Checks if the transport is active.
Returns:
bool: True if the transport is active, otherwise False.
"""
pass
@abstractmethod
def get_auth_method(self) -> str:
"""
Retrieves the authentication method used for the transport.
Returns:
str: The authentication method (e.g., password, public key).
"""
pass
@abstractmethod
def set_username(self, username: str) -> None:
"""
Sets the username for the transport.
Args:
username (str): The username to be set.
"""
pass
@abstractmethod
def get_default_window_size(self) -> int:
"""
Retrieves the default window size for the transport.
Returns:
int: The default window size.
"""
pass
@abstractmethod
def get_connection_type(self) -> str:
"""
Retrieves the type of connection for the transport.
Returns:
str: The connection type (e.g., TCP, UDP).
"""
pass
class IChannel(ABC):
@abstractmethod
def send(self, s: Union[bytes, bytearray]) -> None:
"""
Sends data over the channel.
Args:
s (Union[bytes, bytearray]): The data to send.
"""
pass
@abstractmethod
def sendall(self, s: Union[bytes, bytearray]) -> None:
"""
Sends all data over the channel, blocking until all data is sent.
Args:
s (Union[bytes, bytearray]): The data to send.
"""
pass
@abstractmethod
def getpeername(self) -> tuple[str, int]:
"""
Retrieves the peer's address and port.
Returns:
tuple[str, int]: The host and port of the peer.
"""
pass
@abstractmethod
def settimeout(self, timeout: Union[float, None]) -> None:
"""
Sets the timeout for blocking operations on the channel.
Args:
timeout (Union[float, None]): The timeout in seconds. If None, the operation will block indefinitely.
"""
pass
@abstractmethod
def setblocking(self, blocking: bool) -> None:
"""
Sets whether the channel operates in blocking mode or non-blocking mode.
Args:
blocking (bool): If True, the channel operates in blocking mode. If False, non-blocking mode.
"""
pass
@abstractmethod
def recv(self, nbytes: int) -> bytes:
"""
Receives data from the channel.
Args:
nbytes (int): The number of bytes to receive.
Returns:
bytes: The received data.
"""
pass
@abstractmethod
def get_id(self) -> int:
"""
Retrieves the unique identifier for the channel.
Returns:
int: The channel's unique identifier.
"""
pass
@abstractmethod
def close(self) -> None:
"""
Closes the channel and releases any resources used.
"""
pass
@abstractmethod
def get_out_window_size(self) -> int:
"""
Retrieves the output window size for the channel.
Returns:
int: The output window size.
"""
pass
#--------------------------------------------------------------------------------------------
class SSHTransport(ITransport):
def __init__(self, socketchannel: socket.socket, interface: Sinterface, key):
self.socket: socket.socket = socketchannel
self.interface: Sinterface = interface
self.key = key
self.bh_session = paramiko.Transport(self.socket)
self.bh_session.add_server_key(self.key)
self.bh_session.default_window_size = 2147483647
def enable_compression(self, enable):
self.bh_session.use_compression(enable)
def max_packet_size(self, size):
self.bh_session.default_max_packet_size = size
def start_server(self):
self.bh_session.start_server(server=self.interface)
def accept(self, timeout=None):
return SSHChannel(self.bh_session.accept(timeout))
def set_subsystem_handler(self, name, handler, *args, **kwargs):
self.bh_session.set_subsystem_handler(name, handler, *args, **kwargs)
def close(self):
self.bh_session.close()
def is_authenticated(self):
return self.bh_session.is_authenticated()
def getpeername(self):
return self.bh_session.getpeername()
def get_username(self):
return self.bh_session.get_username()
def is_active(self):
return self.bh_session.is_active()
def get_auth_method(self):
return self.bh_session.auth_handler.auth_method
def set_username(self, username):
self.bh_session.auth_handler.username = username
def get_default_window_size(self):
return self.bh_session.default_window_size
def get_connection_type(self):
return "SSH"
class SSHChannel(IChannel):
def __init__(self, channel: paramiko.Channel):
self.channel: paramiko.Channel = channel
def send(self, s):
self.channel.send(s)
def sendall(self, s):
self.channel.sendall(s)
def getpeername(self):
return self.channel.getpeername()
def settimeout(self, timeout):
self.channel.settimeout(timeout)
def setblocking(self, blocking):
self.channel.setblocking(blocking)
def recv(self, nbytes):
return self.channel.recv(nbytes)
def get_id(self):
return self.channel.get_id()
def close(self):
self.channel.close()
def get_out_window_size(self):
return self.channel.out_window_size
#--------------------------------------------------------------------------------------------
# Telnet command and option codes
IAC = 255
DO = 253
WILL = 251
TTYPE = 24
ECHO = 1
SGA = 3 # Suppress Go Ahead
def send_telnet_command(sock, command, option):
sock.send(bytes([IAC, command, option]))
class TelnetTransport(ITransport):
def __init__(self, socketchannel: socket.socket, interface: Sinterface):
self.socket: socket.socket = socketchannel
self.interface: Sinterface = interface
self.username = None
self.isactive = True
self.isauth = False
self.auth_method = None
def enable_compression(self, enable):
pass
def max_packet_size(self, size):
pass
def start_server(self):
pass
def set_subsystem_handler(self, name: str, handler: callable, *args: any, **kwargs: any) -> None:
pass
def negotiate_options(self):
# Negotiating TTYPE (Terminal Type), ECHO, and SGA (Suppress Go Ahead)
send_telnet_command(self.socket, DO, TTYPE)
send_telnet_command(self.socket, WILL, ECHO)
send_telnet_command(self.socket, WILL, SGA)
def accept(self, timeout=None):
# Perform Telnet negotiation
self.negotiate_options()
# Simple authentication prompt
username = wait_input(self.socket, "Login as: ", directchannel=True)
try:
allowauth = self.interface.get_allowed_auths(username).split(',')
except:
allowauth = self.interface.get_allowed_auths(username)
if allowauth[0] == "password":
password = wait_input(self.socket, "Password", password=True, directchannel=True)
result = self.interface.check_auth_password(username, password)
if result == 0:
self.isauth = True
self.username = username
self.auth_method = "password"
return TelnetChannel(self.socket)
else:
Send(self.socket, "Access denied", directchannel=True)
self.close()
elif allowauth[0] == "public_key":
Send(self.socket, "Public key isn't supported for telnet", directchannel=True)
self.close()
elif allowauth[0] == "none":
result = self.interface.check_auth_none(username)
if result == 0:
self.username = username
self.isauth = True
self.auth_method = "none"
return TelnetChannel(self.socket)
else:
Send(self.socket, "Access denied", directchannel=True)
self.close()
else:
Send(self.socket, "Access denied", directchannel=True)
def close(self):
self.isactive = False
self.socket.close()
def is_authenticated(self):
return self.isauth
def getpeername(self):
return self.socket.getpeername()
def get_username(self):
return self.username
def is_active(self):
return self.isactive
def get_auth_method(self):
return self.auth_method
def set_username(self, username):
self.username = username
def get_default_window_size(self):
return 0
def get_connection_type(self):
return "Telnet"
class TelnetChannel(IChannel):
def __init__(self, channel: socket.socket):
self.channel: socket.socket = channel
def send(self, s):
self.channel.send(s)
def sendall(self, s):
self.channel.sendall(s)
def getpeername(self):
return self.channel.getpeername()
def settimeout(self, timeout):
self.channel.settimeout(timeout)
def setblocking(self, blocking):
self.channel.setblocking(blocking)
def recv(self, nbytes):
return self.channel.recv(nbytes)
def get_id(self):
return 0
def close(self) -> None:
return self.channel.close()
def get_out_window_size(self) -> int:
return 0

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -25,14 +25,23 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import time
from paramiko.transport import Transport
from paramiko.channel import Channel
from ..interactive import Send
from .ProWrapper import IChannel, ITransport
class Client:
def __init__(self, channel, transport, peername):
"""
Initializes a new client instance.
Args:
channel (IChannel): The communication channel for the client.
transport (ITransport): The transport layer for the client.
peername (tuple): The peer's address and port (host, port).
"""
self.current_user = None
self.transport: Transport = transport
self.channel: Channel = channel
self.transport: ITransport = transport
self.channel: IChannel = channel
self.subchannel = {}
self.connecttype = None
self.last_activity_time = None
@ -42,7 +51,7 @@ class Client:
self.prompt = None
self.inputbuffer = None
self.peername = peername
self.auth_method = self.transport.auth_handler.auth_method
self.auth_method = self.transport.get_auth_method()
self.session_id = None
self.terminal_type = None
self.env_variables = {}
@ -51,54 +60,165 @@ class Client:
self.isexeccommandrunning = False
def get_id(self):
"""
Retrieves the client's session ID.
Returns:
str or None: The session ID of the client.
"""
return self.session_id
def get_name(self):
"""
Retrieves the current username of the client.
Returns:
str: The current username of the client.
"""
return self.current_user
def get_peername(self):
return self.current_user
"""
Retrieves the peer's address (host, port) for the client.
Returns:
tuple: The peer's address (host, port).
"""
return self.peername
def get_prompt(self):
"""
Retrieves the prompt string for the client.
Returns:
str: The prompt string for the client.
"""
return self.prompt
def get_channel(self):
"""
Retrieves the communication channel for the client.
Returns:
IChannel: The channel instance for the client.
"""
return self.channel
def get_prompt_buffer(self):
"""
Retrieves the current input buffer for the client as a string.
Returns:
str: The input buffer as a string.
"""
return str(self.inputbuffer)
def get_terminal_size(self):
"""
Retrieves the terminal size (width, height) for the client.
Returns:
tuple[int, int]: The terminal's width and height.
"""
return self.windowsize["width"], self.windowsize["height"]
def get_connection_type(self):
"""
Retrieves the connection type for the client.
Returns:
str: The connection type (e.g., TCP, UDP).
"""
return self.connecttype
def get_auth_with(self):
"""
Retrieves the authentication method used for the client.
Returns:
str: The authentication method (e.g., password, public key).
"""
return self.auth_method
def get_session_duration(self):
"""
Calculates the duration of the current session for the client.
Returns:
float: The duration of the session in seconds.
"""
return time.time() - self.last_login_time
def get_environment(self, variable):
return self.env_variables[variable]
"""
Retrieves the value of an environment variable for the client.
Args:
variable (str): The name of the environment variable.
Returns:
str: The value of the environment variable.
"""
return self.env_variables.get(variable)
def get_last_error(self):
"""
Retrieves the last error message encountered by the client.
Returns:
str: The last error message, or None if no error occurred.
"""
return self.last_error
def get_last_command(self):
"""
Retrieves the last command executed by the client.
Returns:
str: The last command executed.
"""
return self.last_command
def set_name(self, name):
"""
Sets the current username for the client.
Args:
name (str): The username to set for the client.
"""
self.current_user = name
def set_prompt(self, prompt):
"""
Sets the prompt string for the client.
Args:
prompt (str): The prompt string to set for the client.
"""
self.prompt = prompt
def set_environment(self, variable, value):
"""
Sets the value of an environment variable for the client.
Args:
variable (str): The name of the environment variable.
value (str): The value to set for the environment variable.
"""
self.env_variables[variable] = value
def open_new_subchannel(self, timeout=None):
"""
Opens a new subchannel for communication with the client.
Args:
timeout (Union[int, None]): The timeout duration in seconds.
If None, the operation waits indefinitely.
Returns:
tuple: A tuple containing the subchannel ID and the new subchannel
(IChannel). If an error occurs, returns (None, None).
"""
try:
channel = self.transport.accept(timeout)
id = channel.get_id()
@ -109,35 +229,65 @@ class Client:
return id, channel
def get_subchannel(self, id):
return self.subchannel[id]
"""
Retrieves a subchannel by its ID.
Args:
id (int): The ID of the subchannel to retrieve.
Returns:
IChannel: The subchannel instance.
"""
return self.subchannel.get(id)
def switch_user(self, user):
"""
Switches the current user for the client.
Args:
user (str): The new username to switch to.
"""
self.current_user = user
self.transport.auth_handler.username = user
self.transport.set_username(user)
def close_subchannel(self, id):
"""
Closes a specific subchannel by its ID.
Args:
id (int): The ID of the subchannel to close.
"""
self.subchannel[id].close()
def close(self):
"""
Closes the main communication channel for the client.
"""
self.channel.close()
def send(self, data):
"""
Sends data over the main communication channel.
Args:
data (str): The data to send.
"""
Send(self.channel, data, directchannel=True)
def __str__(self):
return f"client id: {self.session_id}"
def __repr__(self):
attrs = vars(self) # or self.__dict__
non_none_attrs = {key: value for key, value in attrs.items() if value is not None}
attrs_repr = ', '.join(f"{key}={value!r}" for key, value in non_none_attrs.items())
return f"Client({attrs_repr})"
# for backward compatibility only
def __getitem__(self, key):
return getattr(self, key)
def __setitem__(self, key, value):
setattr(self, key, value)
def __str__(self):
return f"client id: {self.session_id}"
def __repr__(self):
# Get the dictionary of instance attributes
attrs = vars(self) # or self.__dict__
# Filter out attributes that are None
non_none_attrs = {key: value for key, value in attrs.items() if value is not None}
# Build a string representation
attrs_repr = ', '.join(f"{key}={value!r}" for key, value in non_none_attrs.items())
return f"Client({attrs_repr})"

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -24,12 +24,11 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import re
__version__ = "5.0"
version = "5.1"
system_banner = (
f"\033[36mPyserSSH V{__version__} \033[0m"
f"\033[36mPyserSSH V{version} \033[0m"
#"\033[33m!!Warning!! This is Testing Version of PyserSSH \033[0m\n"
#"\033[35mUse Putty and WinSCP (SFTP) for best experience \033[0m"
)
@ -43,7 +42,7 @@ def Flag_TH(returnlist=False):
f"\033[34m ===== == ===== ==== === == ===== ===== ======== \033[0m\n",
f"\033[37m == == === === == == === === == == \033[0m\n",
f"\033[31m == == ====== ======= == == ====== ====== == == \033[0m\n",
" Made by \033[33mD\033[38;2;255;126;1mP\033[38;2;43;205;150mSoftware\033[0m Foundation from Thailand\n",
" Made by \033[33mD\033[38;2;255;126;1mP\033[38;2;43;205;150mSoftware\033[0m \033[38;2;204;208;43mFoundation\033[0m from Thailand\n",
"\n"
]

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -31,7 +31,7 @@ import logging
from .sysfunc import replace_enter_with_crlf
from .syscom import systemcommand
logger = logging.getLogger("PyserSSH")
logger = logging.getLogger("PyserSSH.InputSystem")
def expect(self, client, echo=True):
buffer = bytearray()
@ -177,7 +177,7 @@ def expect(self, client, echo=True):
try:
if self.enasyscom:
sct = systemcommand(client, command)
sct = systemcommand(client, command, self)
else:
sct = False

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -29,7 +29,7 @@ import paramiko
import ast
from .syscom import systemcommand
from .remotestatus import startremotestatus
from .RemoteStatus import startremotestatus
def parse_exec_request(command_string):
try:

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -35,12 +35,12 @@ from datetime import datetime
import platform
from ..interactive import Send
from .info import __version__
from .info import version
if platform.system() == "Windows":
import ctypes
logger = logging.getLogger("PyserSSH")
logger = logging.getLogger("PyserSSH.RemoteStatus")
class LASTINPUTINFO(ctypes.Structure):
_fields_ = [
@ -221,7 +221,7 @@ Inactive: 0 kB"""
Send(channel, "", directchannel=True)
Send(channel, "==> /proc/version <==", directchannel=True)
Send(channel, f"PyserSSH v{__version__} run on {platform.platform()} {platform.machine()} {platform.architecture()[0]} with python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} {sys.version_info.releaselevel} {platform.python_build()[0]} {platform.python_build()[1]} {platform.python_compiler()} {platform.python_implementation()} {platform.python_revision()}", directchannel=True)
Send(channel, f"PyserSSH v{version} run on {platform.platform()} {platform.machine()} {platform.architecture()[0]} with python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} {sys.version_info.releaselevel} {platform.python_build()[0]} {platform.python_build()[1]} {platform.python_compiler()} {platform.python_implementation()} {platform.python_revision()}", directchannel=True)
Send(channel, "", directchannel=True)
Send(channel, "==> /proc/uptime <==", directchannel=True)

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -26,9 +26,39 @@ SOFTWARE.
"""
import shlex
from ..interactive import Send, Clear, Title
from ..interactive import Send, Clear, Title, wait_choose
def systemcommand(client, command):
def system_account_command(client, accounts, action):
banner = "accman adduser <username> <password>\naccman deluser <username>\naccman passwd <username> <new password>\naccman list"
try:
if action[0] == "adduser":
accounts.add_account(action[1], action[2])
Send(client, f"Added {action[1]}")
elif action[0] == "deluser":
if accounts.has_user(action[1]):
if not accounts.is_user_has_sudo(action[1]):
if wait_choose(client, ["No", "Yes"], prompt="Sure? ") == 1:
accounts.remove_account(action[1])
Send(client, f"Removed {action[1]}")
else:
Send(client, f"{action[1]} isn't removable.")
else:
Send(client, f"{action[1]} not found")
elif action[0] == "passwd":
if accounts.has_user(action[1]):
accounts.change_password(action[1], action[2])
Send(client, f"Password updated successfully.")
else:
Send(client, f"{action[1]} not found")
elif action[0] == "list":
for user in accounts.list_users():
Send(client, user)
else:
Send(client, banner)
except:
Send(client, banner)
def systemcommand(client, command, serverself):
if command == "whoami":
Send(client, client["current_user"])
return True
@ -37,6 +67,13 @@ def systemcommand(client, command):
title = args[1]
Title(client, title)
return True
elif command.startswith("accman"):
args = shlex.split(command)
if serverself.accounts.is_user_has_sudo(client.current_user):
system_account_command(client, serverself.accounts, args[1:])
else:
Send(client, "accman: Permission denied.")
return True
elif command == "exit":
client["channel"].close()
return True

View File

@ -1,6 +1,6 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Copyright (C) 2023-present DPSoftware Foundation (MIT)
Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -37,6 +37,7 @@ def replace_enter_with_crlf(input_string):
# Replace '\n' with '\r\n' in the string
modified_string = decoded_string.replace('\n', '\r\n')
# Encode the modified string back to bytes
return modified_string.encode()
else:
raise TypeError("Input must be a string or bytes")

View File

@ -0,0 +1,174 @@
import time
import logging
logger = logging.getLogger("PyserSSH.Utils.ServerManager")
class ServerManager:
def __init__(self):
self.servers = {}
def add_server(self, name, server, *args, **kwargs):
"""
Adds a server to the manager with the specified name. Raises an error if a server with the same name already exists.
Args:
name (str): The name of the server.
server (object): The server instance to be added.
*args: Arguments for server initialization.
**kwargs: Keyword arguments for server initialization.
Raises:
ValueError: If a server with the same name already exists.
"""
if name in self.servers:
raise ValueError(f"Server with name '{name}' already exists.")
self.servers[name] = {"server": server, "args": args, "kwargs": kwargs, "status": "stopped"}
logger.info(f"Server '{name}' added.")
def remove_server(self, name):
"""
Removes a server from the manager by name. Raises an error if the server does not exist.
Args:
name (str): The name of the server to be removed.
Raises:
ValueError: If no server with the specified name exists.
"""
if name not in self.servers:
raise ValueError(f"No server found with name '{name}'.")
del self.servers[name]
logger.info(f"Server '{name}' removed.")
def get_server(self, name):
"""
Retrieves a server by its name.
Args:
name (str): The name of the server to retrieve.
Returns:
dict: A dictionary containing the server instance, arguments, keyword arguments, and its status, or None if the server is not found.
"""
return self.servers.get(name, None)
def start_server(self, name):
"""
Starts a server with the specified name if it is not already running. Blocks until the server starts.
Args:
name (str): The name of the server to start.
Raises:
ValueError: If no server with the specified name exists or the server cannot be started.
"""
server_info = self.get_server(name)
if not server_info:
raise ValueError(f"No server found with name '{name}'.")
if server_info["status"] == "running":
logger.info(f"Server '{name}' is already running.")
return
server = server_info["server"]
args, kwargs = server_info["args"], server_info["kwargs"]
logger.info(f"Starting server '{name}' with arguments {args}...")
server_info["status"] = "starting"
server.run(*args, **kwargs)
while not server.isrunning:
logger.debug(f"Waiting for server '{name}' to start...")
time.sleep(0.1)
server_info["status"] = "running"
logger.info(f"Server '{name}' is now running.")
def stop_server(self, name):
"""
Stops a server with the specified name if it is running. Blocks until the server stops.
Args:
name (str): The name of the server to stop.
Raises:
ValueError: If no server with the specified name exists or the server cannot be stopped.
"""
server_info = self.get_server(name)
if not server_info:
raise ValueError(f"No server found with name '{name}'.")
if server_info["status"] == "stopped":
logger.info(f"Server '{name}' is already stopped.")
return
server = server_info["server"]
logger.info(f"Shutting down server '{name}'...")
server_info["status"] = "shutting down"
server.stop_server()
while server.isrunning:
logger.debug(f"Waiting for server '{name}' to shut down...")
time.sleep(0.1)
server_info["status"] = "stopped"
logger.info(f"Server '{name}' has been stopped.")
def start_all_servers(self):
"""
Starts all servers managed by the ServerManager. Blocks until each server starts.
"""
for name, server_info in self.servers.items():
if server_info["status"] == "running":
logger.info(f"Server '{name}' is already running.")
continue
server, args, kwargs = server_info["server"], server_info["args"], server_info["kwargs"]
logger.info(f"Starting server '{name}' with arguments {args}...")
server_info["status"] = "starting"
server.run(*args, **kwargs)
while not server.isrunning:
logger.debug(f"Waiting for server '{name}' to start...")
time.sleep(0.1)
server_info["status"] = "running"
logger.info(f"Server '{name}' is now running.")
def stop_all_servers(self):
"""
Stops all servers managed by the ServerManager. Blocks until each server stops.
"""
for name, server_info in self.servers.items():
if server_info["status"] == "stopped":
logger.info(f"Server '{name}' is already stopped.")
continue
server = server_info["server"]
logger.info(f"Shutting down server '{name}'...")
server_info["status"] = "shutting down"
server.stop_server()
while server.isrunning:
logger.debug(f"Waiting for server '{name}' to shut down...")
time.sleep(0.1)
server_info["status"] = "stopped"
logger.info(f"Server '{name}' has been stopped.")
def get_status(self, name):
"""
Retrieves the status of a server by name.
Args:
name (str): The name of the server to get the status of.
Returns:
str: The current status of the server (e.g., 'running', 'stopped', etc.).
Raises:
ValueError: If no server with the specified name exists.
"""
server_info = self.get_server(name)
if not server_info:
raise ValueError(f"No server found with name '{name}'.")
return server_info["status"]

View File

@ -0,0 +1,22 @@
import paramiko
def generate_ssh_keypair(private_key_path='id_rsa', public_key_path='id_rsa.pub', key_size=2048):
"""
Generates an SSH key pair (private and public) and saves them to specified files.
Args:
- private_key_path (str): Path to save the private key.
- public_key_path (str): Path to save the public key.
- key_size (int): Size of the RSA key (default is 2048).
"""
# Generate RSA key pair
private_key = paramiko.RSAKey.generate(key_size)
# Save the private key to a file
private_key.write_private_key_file(private_key_path)
# Save the public key to a file
with open(public_key_path, 'w') as pub_file:
pub_file.write(f"{private_key.get_name()} {private_key.get_base64()}")
print(f"SSH Key pair generated: {private_key_path} and {public_key_path}")