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 # 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. 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. 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 # Quick Example
This Server use port **2222** for default port This Server use port **2222** for default port
```py ```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 = Server(useraccount)
@ssh.on_user("command") @ssh.on_user("command")
def command(client, command: str): def command(client, command: str):
if command == "hello": if command == "hello":
Send(client, "world!") client.send("world!")
ssh.run("your private key file") ssh.run("your private key file")
``` ```

View File

@ -1,4 +1,8 @@
import os import os
os.environ["damp11113_load_all_module"] = "NO"
from damp11113.utils import TextFormatter
from damp11113.file import sort_files, allfiles
import socket import socket
import time import time
import cv2 import cv2
@ -6,13 +10,14 @@ import traceback
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import numpy as np import numpy as np
import logging
#import logging # Configure logging
#logging.basicConfig(level=logging.DEBUG) 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 import Server, AccountManager
from PyserSSH.interactive import Send, wait_input, wait_inputkey, wait_choose, Clear, wait_inputmouse 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.processbar import indeterminateStatus, LoadingProgress
from PyserSSH.extensions.dialog import MenuDialog, TextDialog, TextInputDialog from PyserSSH.extensions.dialog import MenuDialog, TextDialog, TextInputDialog
from PyserSSH.extensions.moredisplay import clickable_url, Send_karaoke_effect 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.remodesk import RemoDesk
from PyserSSH.extensions.XHandler import XHandler from PyserSSH.extensions.XHandler import XHandler
from PyserSSH.system.clientype import Client 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 = AccountManager(allow_guest=True, autoload=True, autosave=True)
useraccount.add_account("admin", "") # create user without password
useraccount.add_account("test", "test") # create user without password if not os.path.isfile("autosave_session.ses"):
useraccount.add_account("demo") useraccount.add_account("admin", "", sudo=True) # create user without password
useraccount.add_account("remote", "12345", permissions=["remote_desktop"]) useraccount.add_account("test", "test") # create user without password
useraccount.set_user_enable_inputsystem_echo("remote", False) useraccount.add_account("demo")
useraccount.set_user_sftp_allow("admin", True) 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() XH = XHandler()
ssh = Server(useraccount, ssh = Server(useraccount,
@ -42,64 +50,7 @@ remotedesktopserver = RemoDesk()
servername = "PyserSSH" servername = "PyserSSH"
loading = ["PyserSSH", "Extensions"] loading = ["PyserSSH", "openRemoDesk", "XHandler", "RemoteStatus"]
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
@ssh.on_user("pre-shell") @ssh.on_user("pre-shell")
def guestauth(client): def guestauth(client):
@ -185,7 +136,7 @@ def connect(client):
wm = f"""{Flag_TH()}{''*50} wm = f"""{Flag_TH()}{''*50}
Hello {client['current_user']}, 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")} Visit: {clickable_url("https://damp11113.xyz", "DPCloudev")}
{''*50}""" {''*50}"""
@ -258,9 +209,9 @@ def xh_typing(client: Client, messages, speed = 1):
Send(client, "") Send(client, "")
@XH.command(name="renimtest") @XH.command(name="renimtest")
def xh_renimtest(client: Client, path: str): def xh_renimtest(client: Client):
Clear(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) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
width, height = client['windowsize']["width"] - 5, client['windowsize']["height"] - 5 width, height = client['windowsize']["width"] - 5, client['windowsize']["height"] - 5
@ -458,12 +409,14 @@ def xh_status(client: Client):
#@ssh.on_user("command") #@ssh.on_user("command")
#def command(client: Client, command: str): #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 # 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 # 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( setup(
name='PyserSSH', name='PyserSSH',
version='5.0', version='5.1',
license='MIT', license='MIT',
author='DPSoftware Foundation', author='DPSoftware Foundation',
author_email='contact@damp11113.xyz', 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 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -37,15 +37,15 @@ right - \x1b[C
https://en.wikipedia.org/wiki/ANSI_escape_code https://en.wikipedia.org/wiki/ANSI_escape_code
""" """
import os import os
import ctypes
import logging import logging
from .interactive import * from .interactive import *
from .server import Server from .server import Server
from .account import AccountManager from .account import AccountManager
from .system.info import system_banner from .system.info import system_banner, version
if os.name == 'nt': if os.name == 'nt':
import ctypes
kernel32 = ctypes.windll.kernel32 kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
@ -67,45 +67,10 @@ if os.environ["pyserssh_log"] == "NO":
if os.environ["pyserssh_systemmessage"] == "YES": if os.environ["pyserssh_systemmessage"] == "YES":
print(system_banner) print(system_banner)
# Server Managers __author__ = "damp11113"
__url__ = "https://github.com/DPSoftware-Foundation/PyserSSH"
class ServerManager: __copyright__ = "2023-present"
def __init__(self): __license__ = "MIT"
self.servers = {} __version__ = version
__department__ = "DPSoftware"
def add_server(self, name, server): __organization__ = "DOPFoundation"
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()

View File

@ -1,6 +1,6 @@
""" """
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH 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 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
import logging
import os import os
import pickle import pickle
import time import time
@ -31,38 +32,51 @@ import atexit
import threading import threading
import hashlib import hashlib
logger = logging.getLogger("PyserSSH.Account")
class AccountManager: 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.accounts = {}
self.allow_guest = allow_guest self.allow_guest = allow_guest
self.historylimit = historylimit self.historylimit = historylimit
self.autosavedelay = autosavedelay self.autosavedelay = autosavedelay
self.__autosavework = False self.__autosavework = False
self.autosave = autosave
self.__autosaveworknexttime = 0 self.__autosaveworknexttime = 0
self.__autofile = autofile
if autoload: 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 = threading.Thread(target=self.__autosave, daemon=True)
self.__autosavethread.start() self.__autosavethread.start()
atexit.register(self.__saveexit) atexit.register(self.__saveexit)
def __autosave(self): def __autosave(self):
self.save("autosave_session.ses") self.save(self.__autofile)
self.__autosaveworknexttime = time.time() + self.autosavedelay self.__autosaveworknexttime = time.time() + self.autosavedelay
self.__autosavework = True self.__autosavework = True
while self.__autosavework: while self.__autosavework:
if int(self.__autosaveworknexttime) == int(time.time()): if int(self.__autosaveworknexttime) == int(time.time()):
self.save("autosave_session.ses") self.save(self.__autofile)
self.__autosaveworknexttime = time.time() + self.autosavedelay self.__autosaveworknexttime = time.time() + self.autosavedelay
time.sleep(1) # fix cpu load 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): def __saveexit(self):
self.__autosavework = False self.__autosavework = False
self.save("autosave_session.ses") self.save(self.__autofile)
self.__autosavethread.join() self.__autosavethread.join()
def validate_credentials(self, username, password=None, public_key=None): def validate_credentials(self, username, password=None, public_key=None):
@ -90,6 +104,9 @@ class AccountManager:
def has_user(self, username): def has_user(self, username):
return username in self.accounts return username in self.accounts
def list_users(self):
return list(self.accounts.keys())
def get_allowed_auths(self, username): def get_allowed_auths(self, username):
if self.has_user(username) and "allowed_auth" in self.accounts[username]: if self.has_user(username) and "allowed_auth" in self.accounts[username]:
return self.accounts[username]["allowed_auth"] return self.accounts[username]["allowed_auth"]
@ -100,6 +117,7 @@ class AccountManager:
return self.accounts[username]["permissions"] return self.accounts[username]["permissions"]
return [] return []
@__auto_save
def set_prompt(self, username, prompt=">"): def set_prompt(self, username, prompt=">"):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["prompt"] = prompt self.accounts[username]["prompt"] = prompt
@ -109,7 +127,8 @@ class AccountManager:
return self.accounts[username]["prompt"] return self.accounts[username]["prompt"]
return ">" # Default prompt if not set for the user 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): if not self.has_user(username):
allowedlist = [] allowedlist = []
accountkey = {} accountkey = {}
@ -132,34 +151,63 @@ class AccountManager:
accountkey["allowed_auth"] = ",".join(allowedlist) accountkey["allowed_auth"] = ",".join(allowedlist)
self.accounts[username] = accountkey self.accounts[username] = accountkey
if sudo:
if self.has_sudo_user():
raise Exception(f"sudo user is exist")
self.accounts[username]["sudo"] = sudo
else: else:
raise Exception(f"{username} is exist") 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): def remove_account(self, username):
if self.has_user(username): if self.has_user(username):
del self.accounts[username] del self.accounts[username]
@__auto_save
def change_password(self, username, new_password): def change_password(self, username, new_password):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["password"] = new_password self.accounts[username]["password"] = new_password
@__auto_save
def set_permissions(self, username, new_permissions): def set_permissions(self, username, new_permissions):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["permissions"] = new_permissions self.accounts[username]["permissions"] = new_permissions
def save(self, filename="session.ses"): def save(self, filename="session.ses", keep_log=True):
with open(filename, 'wb') as file: if keep_log:
pickle.dump(self.accounts, file) 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): def load(self, filename):
logger.info(f"loading session from {filename}")
try: try:
with open(filename, 'rb') as file: with open(filename, 'rb') as file:
self.accounts = pickle.load(file) self.accounts = pickle.load(file)
logger.info(f"loaded session")
except FileNotFoundError: except FileNotFoundError:
print("File not found. No accounts loaded.") logger.error("can't load session: file not found.")
except Exception as e: 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): def set_user_sftp_allow(self, username, allow=True):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["sftp_allow"] = allow self.accounts[username]["sftp_allow"] = allow
@ -169,6 +217,7 @@ class AccountManager:
return self.accounts[username]["sftp_allow"] return self.accounts[username]["sftp_allow"]
return False return False
@__auto_save
def set_user_sftp_readonly(self, username, readonly=False): def set_user_sftp_readonly(self, username, readonly=False):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["sftp_readonly"] = readonly self.accounts[username]["sftp_readonly"] = readonly
@ -178,6 +227,7 @@ class AccountManager:
return self.accounts[username]["sftp_readonly"] return self.accounts[username]["sftp_readonly"]
return False return False
@__auto_save
def set_user_sftp_root_path(self, username, path="/"): def set_user_sftp_root_path(self, username, path="/"):
if self.has_user(username): if self.has_user(username):
if path == "/": if path == "/":
@ -190,6 +240,7 @@ class AccountManager:
return self.accounts[username]["sftp_root_path"] return self.accounts[username]["sftp_root_path"]
return os.getcwd() return os.getcwd()
@__auto_save
def set_user_enable_inputsystem(self, username, enable=True): def set_user_enable_inputsystem(self, username, enable=True):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["inputsystem"] = enable self.accounts[username]["inputsystem"] = enable
@ -199,6 +250,7 @@ class AccountManager:
return self.accounts[username]["inputsystem"] return self.accounts[username]["inputsystem"]
return True return True
@__auto_save
def set_user_enable_inputsystem_echo(self, username, echo=True): def set_user_enable_inputsystem_echo(self, username, echo=True):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["inputsystem_echo"] = echo self.accounts[username]["inputsystem_echo"] = echo
@ -208,6 +260,7 @@ class AccountManager:
return self.accounts[username]["inputsystem_echo"] return self.accounts[username]["inputsystem_echo"]
return True return True
@__auto_save
def set_banner(self, username, banner): def set_banner(self, username, banner):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["banner"] = banner self.accounts[username]["banner"] = banner
@ -222,6 +275,7 @@ class AccountManager:
return self.accounts[username]["timeout"] return self.accounts[username]["timeout"]
return None return None
@__auto_save
def set_user_timeout(self, username, timeout=None): def set_user_timeout(self, username, timeout=None):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["timeout"] = timeout self.accounts[username]["timeout"] = timeout
@ -231,6 +285,7 @@ class AccountManager:
return self.accounts[username]["lastlogin"] return self.accounts[username]["lastlogin"]
return None return None
@__auto_save
def set_user_last_login(self, username, ip, timelogin=time.time()): def set_user_last_login(self, username, ip, timelogin=time.time()):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["lastlogin"] = { self.accounts[username]["lastlogin"] = {
@ -238,6 +293,7 @@ class AccountManager:
"time": timelogin "time": timelogin
} }
@__auto_save
def add_history(self, username, command): def add_history(self, username, command):
if self.has_user(username): if self.has_user(username):
if "history" not in self.accounts[username]: if "history" not in self.accounts[username]:
@ -251,6 +307,7 @@ class AccountManager:
if len(self.accounts[username]["history"]) > history_limit: if len(self.accounts[username]["history"]) > history_limit:
self.accounts[username]["history"] = self.accounts[username]["history"][-history_limit:] self.accounts[username]["history"] = self.accounts[username]["history"][-history_limit:]
@__auto_save
def clear_history(self, username): def clear_history(self, username):
if self.has_user(username): if self.has_user(username):
self.accounts[username]["history"] = [] # Initialize history list if it doesn't exist 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 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -35,6 +35,13 @@ def are_permissions_met(permission_list, permission_require):
class XHandler: class XHandler:
def __init__(self, enablehelp=True, showusageonworng=True): 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.handlers = {}
self.categories = {} self.categories = {}
self.enablehelp = enablehelp self.enablehelp = enablehelp
@ -43,6 +50,18 @@ class XHandler:
self.commandnotfound = None self.commandnotfound = None
def command(self, category=None, name=None, aliases=None, permissions: list = 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): def decorator(func):
nonlocal name, category nonlocal name, category
if name is None: if name is None:
@ -87,6 +106,16 @@ class XHandler:
return decorator return decorator
def call(self, client, command_string): 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) tokens = shlex.split(command_string)
command_name = tokens[0] command_name = tokens[0]
args = tokens[1:] args = tokens[1:]
@ -102,7 +131,7 @@ class XHandler:
command_func = self.handlers[command_name] command_func = self.handlers[command_name]
command_info = self.get_command_info(command_name) command_info = self.get_command_info(command_name)
if command_info and command_info.get('permissions'): 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}'.") Send(client, f"Permission denied. You do not have permission to execute '{command_name}'.")
return return
@ -172,6 +201,15 @@ class XHandler:
return return
def get_command_info(self, command_name): 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 found_command = None
for category, commands in self.categories.items(): for category, commands in self.categories.items():
if command_name in commands: if command_name in commands:
@ -197,6 +235,15 @@ class XHandler:
} }
def get_help_command_info(self, command): 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) command_info = self.get_command_info(command)
aliases = command_info.get('aliases', []) aliases = command_info.get('aliases', [])
help_message = f"{command_info['name']}" help_message = f"{command_info['name']}"
@ -220,6 +267,12 @@ class XHandler:
return help_message return help_message
def get_help_message(self): 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 = "" help_message = ""
for category, commands in self.categories.items(): for category, commands in self.categories.items():
help_message += f"{category}:\n" help_message += f"{category}:\n"
@ -231,6 +284,13 @@ class XHandler:
return help_message return help_message
def get_all_commands(self): 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 = {} all_commands = {}
for category, commands in self.categories.items(): for category, commands in self.categories.items():
all_commands[category] = commands 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 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 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 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -30,7 +30,21 @@ import re
from ..interactive import Clear, Send, wait_inputkey from ..interactive import Clear, Send, wait_inputkey
from ..system.sysfunc import text_centered_screen from ..system.sysfunc import text_centered_screen
class TextDialog: 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=""): def __init__(self, client, content="", title=""):
self.client = client self.client = client
@ -39,11 +53,15 @@ class TextDialog:
self.content = content self.content = content
def render(self): def render(self):
"""
Renders the dialog by displaying the title, content, and waiting for the user's input.
"""
Clear(self.client) Clear(self.client)
Send(self.client, self.title) Send(self.client, self.title)
Send(self.client, "-" * self.windowsize["width"]) 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) Send(self.client, generatedwindow)
@ -52,13 +70,32 @@ class TextDialog:
self.waituserenter() self.waituserenter()
def waituserenter(self): def waituserenter(self):
"""
Waits for the user to press the 'enter' key to close the dialog.
"""
while True: while True:
if wait_inputkey(self.client, raw=True) == b'\r': if wait_inputkey(self.client, raw=True) == b'\r':
Clear(self.client) Clear(self.client)
break break
pass pass
class MenuDialog: 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=""): def __init__(self, client, choose: list, title="", desc=""):
self.client = client self.client = client
@ -67,9 +104,12 @@ class MenuDialog:
self.desc = desc self.desc = desc
self.contentallindex = len(choose) - 1 self.contentallindex = len(choose) - 1
self.selectedindex = 0 self.selectedindex = 0
self.selectstatus = 0 # 0 none 1 selected 2 cancel self.selectstatus = 0 # 0 none, 1 selected, 2 canceled
def render(self): def render(self):
"""
Renders the menu dialog, displaying the options and allowing the user to navigate and select an option.
"""
tempcontentlist = self.choose.copy() tempcontentlist = self.choose.copy()
Clear(self.client) Clear(self.client)
@ -90,7 +130,8 @@ class MenuDialog:
f"{exported}" 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) Send(self.client, generatedwindow)
@ -99,6 +140,9 @@ class MenuDialog:
self._waituserinput() self._waituserinput()
def _waituserinput(self): def _waituserinput(self):
"""
Waits for user input and updates the selection based on key presses.
"""
keyinput = wait_inputkey(self.client, raw=True) keyinput = wait_inputkey(self.client, raw=True)
if keyinput == b'\r': # Enter key if keyinput == b'\r': # Enter key
@ -124,12 +168,31 @@ class MenuDialog:
self.render() self.render()
def output(self): def output(self):
"""
Returns the selected option index or `None` if the action was canceled.
"""
if self.selectstatus == 2: if self.selectstatus == 2:
return None return None
elif self.selectstatus == 1: elif self.selectstatus == 1:
return self.selectedindex return self.selectedindex
class TextInputDialog: 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): def __init__(self, client, title="", inputtitle="Input Here", password=False):
self.client = client self.client = client
@ -137,27 +200,31 @@ class TextInputDialog:
self.inputtitle = inputtitle self.inputtitle = inputtitle
self.ispassword = password 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.buffer = bytearray()
self.cursor_position = 0 self.cursor_position = 0
def render(self): def render(self):
"""
Renders the text input dialog and waits for user input.
"""
Clear(self.client) Clear(self.client)
Send(self.client, self.title) Send(self.client, self.title)
Send(self.client, "-" * self.client["windowsize"]["width"]) Send(self.client, "-" * self.client["windowsize"]["width"])
if self.ispassword: if self.ispassword:
texts = ( texts = (
f"{self.inputtitle}\n\n" f"{self.inputtitle}\n\n"
"> " + ("*" * len(self.buffer.decode('utf-8'))) "> " + ("*" * len(self.buffer.decode('utf-8')))
) )
else: else:
texts = ( texts = (
f"{self.inputtitle}\n\n" f"{self.inputtitle}\n\n"
"> " + self.buffer.decode('utf-8') "> " + 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) Send(self.client, generatedwindow)
@ -166,6 +233,9 @@ class TextInputDialog:
self._waituserinput() self._waituserinput()
def _waituserinput(self): def _waituserinput(self):
"""
Waits for the user to input text or special commands (backspace, cancel, enter).
"""
keyinput = wait_inputkey(self.client, raw=True) keyinput = wait_inputkey(self.client, raw=True)
if keyinput == b'\r': # Enter key 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.buffer = self.buffer[:self.cursor_position - 1] + self.buffer[self.cursor_position:]
self.cursor_position -= 1 self.cursor_position -= 1
elif bool(re.compile(b'\x1b\[[0-9;]*[mGK]').search(keyinput)): elif bool(re.compile(b'\x1b\[[0-9;]*[mGK]').search(keyinput)):
pass pass # Ignore ANSI escape codes
else: # Regular character else: # Regular character
self.buffer = self.buffer[:self.cursor_position] + keyinput + self.buffer[self.cursor_position:] self.buffer = self.buffer[:self.cursor_position] + keyinput + self.buffer[self.cursor_position:]
self.cursor_position += 1 self.cursor_position += 1
@ -197,6 +267,9 @@ class TextInputDialog:
self.render() self.render()
def output(self): def output(self):
"""
Returns the input text if the input was selected, or `None` if canceled.
"""
if self.inputstatus == 2: if self.inputstatus == 2:
return None return None
elif self.inputstatus == 1: 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 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -29,30 +29,52 @@ import time
from ..interactive import Send from ..interactive import Send
def clickable_url(url, link_text=""): 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\\" return f"\033]8;;{url}\033\\{link_text}\033]8;;\033\\"
def Send_karaoke_effect(client, text, delay=0.1, ln=True): 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 = "" printed_text = ""
for i, char in enumerate(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) 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:] not_printed_text = text[i + 1:]
dimmed_text = ''.join([f"\033[2m{char}\033[0m" for char in not_printed_text]) 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) Send(client, dimmed_text, ln=False)
# Wait before printing the next character # Wait before printing the next character
time.sleep(delay) time.sleep(delay)
# Clear the line for the next iteration # Clear the line to update the text in the next iteration
Send(client, '\r' ,ln=False) 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 printed_text += char
if ln: 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 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -28,10 +28,23 @@ SOFTWARE.
from ..interactive import Send from ..interactive import Send
def ShowCursor(client, show=True): 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: if show:
Send(client, "\033[?25h", ln=False) Send(client, "\033[?25h", ln=False) # Show cursor
else: else:
Send(client, "\033[?25l", ln=False) Send(client, "\033[?25l", ln=False) # Hide cursor
def SendBell(client): 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 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 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
# this file is from DPSoftware Foundation-library # this file is from damp11113-library
from itertools import cycle from itertools import cycle
import math import math
@ -129,17 +129,27 @@ class TextFormatter:
def insert_string(base, inserted, position=0): def insert_string(base, inserted, position=0):
return base[:position] + inserted + base[position + len(inserted):] return base[:position] + inserted + base[position + len(inserted):]
steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]'] class Steps:
steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]'] steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]']
steps3 = ['[ ]', '[- ]', '[-- ]', '[ --]', '[ -]', '[ ]', '[ -]', '[ --]', '[-- ]', '[- ]'] steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]']
steps4 = ['[ ]', '[- ]', '[ - ]', '[ -]', '[ ]', '[ -]', '[ - ]', '[- ]', '[ ]'] steps3 = ['[ ]', '[- ]', '[-- ]', '[ --]', '[ -]', '[ ]', '[ -]', '[ --]', '[-- ]', '[- ]']
steps5 = ['[ ]', '[ -]', '[ --]', '[---]', '[-- ]', '[- ]'] steps4 = ['[ ]', '[- ]', '[ - ]', '[ -]', '[ ]', '[ -]', '[ - ]', '[- ]', '[ ]']
steps6 = ['[ ]', '[ -]', '[ - ]', '[- ]'] 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: class indeterminateStatus:
def __init__(self, client, desc="Loading...", end="[ OK ]", timeout=0.1, fail='[FAILED]', steps=None): def __init__(self, client, desc="Loading...", end="[ OK ]", timeout=0.1, fail='[FAILED]', steps=None):
self.client = client self.client = client
self.desc = desc self.desc = desc
self.end = end self.end = end
self.timeout = timeout self.timeout = timeout
@ -147,13 +157,14 @@ class indeterminateStatus:
self._thread = Thread(target=self._animate, daemon=True) self._thread = Thread(target=self._animate, daemon=True)
if steps is None: if steps is None:
self.steps = steps1 self.steps = Steps.steps1
else: else:
self.steps = steps self.steps = steps
self.done = False self.done = False
self.fail = False self.fail = False
def start(self): def start(self):
"""Start progress bar"""
self._thread.start() self._thread.start()
return self return self
@ -168,12 +179,14 @@ class indeterminateStatus:
self.start() self.start()
def stop(self): def stop(self):
"""stop progress"""
self.done = True self.done = True
cols = self.client["windowsize"]["width"] cols = self.client["windowsize"]["width"]
Print(self.client['channel'], "\r" + " " * cols, end="") Print(self.client['channel'], "\r" + " " * cols, end="")
Print(self.client['channel'], f"\r{self.end}") Print(self.client['channel'], f"\r{self.end}")
def stopfail(self): def stopfail(self):
"""stop progress with error or fail"""
self.done = True self.done = True
self.fail = True self.fail = True
cols = self.client["windowsize"]["width"] cols = self.client["windowsize"]["width"]
@ -234,7 +247,7 @@ class LoadingProgress:
self._thread = Thread(target=self._animate, daemon=True) self._thread = Thread(target=self._animate, daemon=True)
if steps is None: if steps is None:
self.steps = steps1 self.steps = Steps.steps1
else: else:
self.steps = steps self.steps = steps
@ -251,14 +264,17 @@ class LoadingProgress:
self.currentprint = "" self.currentprint = ""
def start(self): def start(self):
"""Start progress bar"""
self._thread.start() self._thread.start()
self.startime = time.perf_counter() self.startime = time.perf_counter()
return self return self
def update(self, i): def update(self, i=1):
"""update progress"""
self.current += i self.current += i
def updatebuffer(self, i): def updatebuffer(self, i=1):
"""update buffer progress"""
self.currentbuffer += i self.currentbuffer += i
def _animate(self): def _animate(self):
@ -374,12 +390,14 @@ class LoadingProgress:
self.start() self.start()
def stop(self): def stop(self):
"""stop progress"""
self.done = True self.done = True
cols = self.client["windowsize"]["width"] cols = self.client["windowsize"]["width"]
Print(self.client["channel"], "\r" + " " * cols, end="") Print(self.client["channel"], "\r" + " " * cols, end="")
Print(self.client["channel"], f"\r{self.end}") Print(self.client["channel"], f"\r{self.end}")
def stopfail(self): def stopfail(self):
"""stop progress with error or fail"""
self.done = True self.done = True
self.fail = True self.fail = True
cols = self.client["windowsize"]["width"] 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 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -40,7 +40,7 @@ import logging
from ..system.clientype import Client from ..system.clientype import Client
logger = logging.getLogger("RemoDeskSSH") logger = logging.getLogger("PyserSSH.Ext.RemoDeskSSH")
class Protocol: class Protocol:
def __init__(self, server): 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 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -28,9 +28,17 @@ import logging
from ..interactive import Send from ..interactive import Send
logger = logging.getLogger("PyserSSH") logger = logging.getLogger("PyserSSH.Ext.ServerUtils")
def kickbyusername(server, username, reason=None): 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()): for peername, client_handler in list(server.client_handlers.items()):
if client_handler["current_user"] == username: if client_handler["current_user"] == username:
channel = client_handler.get("channel") 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}.") logger.info(f"User '{username}' has been kicked by reason {reason}.")
def kickbypeername(server, peername, reason=None): 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) client_handler = server.client_handlers.get(peername)
if client_handler: if client_handler:
channel = client_handler.get("channel") 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}.") logger.info(f"peername '{peername}' has been kicked by reason {reason}.")
def kickall(server, reason=None): 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(): for peername, client_handler in server.client_handlers.items():
channel = client_handler.get("channel") channel = client_handler.get("channel")
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"]) 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}.") logger.info(f"All users have been kicked by reason {reason}.")
def broadcast(server, message): 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(): for client_handler in server.client_handlers.values():
channel = client_handler.get("channel") channel = client_handler.get("channel")
if channel: if channel:
@ -88,6 +118,14 @@ def broadcast(server, message):
logger.error(f"Error occurred while broadcasting message: {e}") logger.error(f"Error occurred while broadcasting message: {e}")
def sendto(server, username, message): 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(): for client_handler in server.client_handlers.values():
if client_handler.get("current_user") == username: if client_handler.get("current_user") == username:
channel = client_handler.get("channel") 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 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -82,8 +82,11 @@ def Clear(client, oldclear=False, keep=False):
def Title(client, title): def Title(client, title):
Send(client, f"\033]0;{title}\007", ln=False) 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):
channel = client["channel"] if directchannel:
channel = client
else:
channel = client["channel"]
channel.send(replace_enter_with_crlf(prompt)) 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') channel.sendall(b'\r\n')
raise raise
else: else:
channel.setblocking(False)
channel.settimeout(None)
output = buffer.decode('utf-8') output = buffer.decode('utf-8')
# Return default value if specified and no input given # 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)): if bool(re.compile(b'\x1b\[[0-9;]*[mGK]').search(byte)):
pass pass
channel.setblocking(False)
channel.settimeout(None)
if prompt != "":
channel.send("\r\n")
return byte.decode('utf-8') # only regular character return byte.decode('utf-8') # only regular character
else: else:
channel.setblocking(False)
channel.settimeout(None)
if prompt != "":
channel.send("\r\n")
return byte return byte
except socket.timeout: except socket.timeout:
@ -185,7 +198,8 @@ def wait_inputkey(client, prompt="", raw=True, timeout=0):
except Exception: except Exception:
channel.setblocking(False) channel.setblocking(False)
channel.settimeout(None) channel.settimeout(None)
channel.send("\r\n") if prompt != "":
channel.send("\r\n")
raise raise
def wait_inputmouse(client, timeout=0): def wait_inputmouse(client, timeout=0):
@ -204,6 +218,8 @@ def wait_inputmouse(client, timeout=0):
if byte.startswith(b'\x1b[M'): if byte.startswith(b'\x1b[M'):
# Parse mouse event # Parse mouse event
if len(byte) < 6 or not byte.startswith(b'\x1b[M'): if len(byte) < 6 or not byte.startswith(b'\x1b[M'):
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[?1000l", ln=False) Send(client, "\033[?1000l", ln=False)
return None, None, None return None, None, None
@ -212,9 +228,13 @@ def wait_inputmouse(client, timeout=0):
x = byte[4] - 32 x = byte[4] - 32
y = byte[5] - 32 y = byte[5] - 32
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[?1000l", ln=False) Send(client, "\033[?1000l", ln=False)
return button, x, y return button, x, y
else: else:
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[?1000l", ln=False) Send(client, "\033[?1000l", ln=False)
return byte, None, None return byte, None, None
@ -255,9 +275,13 @@ def wait_choose(client, choose, prompt="", timeout=0):
keyinput = wait_inputkey(client, raw=True) keyinput = wait_inputkey(client, raw=True)
if keyinput == b'\r': # Enter key if keyinput == b'\r': # Enter key
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[K") Send(client, "\033[K")
return chooseindex return chooseindex
elif keyinput == b'\x03': # ' ctrl+c' key for cancel elif keyinput == b'\x03': # ' ctrl+c' key for cancel
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[K") Send(client, "\033[K")
return 0 return 0
elif keyinput == b'\x1b[D': # Up arrow key 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 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 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.sysfunc import replace_enter_with_crlf
from .system.interface import Sinterface from .system.interface import Sinterface
from .system.inputsystem import expect 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.clientype import Client as Clientype
from .system.ProWrapper import SSHTransport, TelnetTransport, ITransport
# paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22) # paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22)
sftpclient = ["WinSCP", "Xplore"] sftpclient = ["WinSCP", "Xplore"]
logger = logging.getLogger("PyserSSH") logger = logging.getLogger("PyserSSH.Server")
class 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 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) disable_scroll_with_arrow set to False to enable seek text with arrow (Beta)
@ -80,13 +81,18 @@ class Server:
self._event_handlers = {} self._event_handlers = {}
self.client_handlers = {} # Dictionary to store event handlers for each client self.client_handlers = {} # Dictionary to store event handlers for each client
self.__processmode = None self.__processmode = None
self.__serverisrunning = False self.isrunning = False
self.__daemon = False self.__daemon = False
self.private_key = ""
self.__custom_server_args = ()
self.__custom_server = None
self.__protocol = "ssh"
if self.enasyscom: if self.enasyscom:
print("\033[33m!!Warning!! System commands is enable! \033[0m") print("\033[33m!!Warning!! System commands is enable! \033[0m")
def on_user(self, event_name): def on_user(self, event_name):
"""Handle event"""
def decorator(func): def decorator(func):
@wraps(func) @wraps(func)
def wrapper(client, *args, **kwargs): def wrapper(client, *args, **kwargs):
@ -98,7 +104,7 @@ class Server:
return decorator return decorator
def handle_client_disconnection(self, handler, chandlers): def handle_client_disconnection(self, handler, chandlers):
if not chandlers["channel"].get_transport().is_active(): if not chandlers["transport"].is_active():
if handler: if handler:
handler(chandlers) handler(chandlers)
del self.client_handlers[chandlers["peername"]] del self.client_handlers[chandlers["peername"]]
@ -113,32 +119,29 @@ class Server:
elif handler: elif handler:
return handler(*args, **kwargs) return handler(*args, **kwargs)
def handle_client(self, socketchannel, addr): def _handle_client(self, socketchannel, addr):
self._handle_event("pressh", socketchannel) self._handle_event("preserver", 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
logger.info("Starting session...")
server = Sinterface(self) 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: try:
bh_session.start_server(server=server) bh_session.start_server()
except: except:
return return
logger.info(bh_session.remote_version)
channel = bh_session.accept() channel = bh_session.accept()
if self.sftpena: if self.sftpena:
@ -175,8 +178,7 @@ class Server:
logger.info("saved user data to client handlers") logger.info("saved user data to client handlers")
#if not any(bh_session.remote_version.split("-")[2].startswith(prefix) for prefix in sftpclient): #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): if not (int(channel.get_out_window_size()) == int(bh_session.get_default_window_size()) and bh_session.get_connection_type() == "SSH"):
logger.info("user is ssh")
#timeout for waiting 10 sec #timeout for waiting 10 sec
for i in range(100): for i in range(100):
if self.client_handlers[channel.getpeername()]["windowsize"]: if self.client_handlers[channel.getpeername()]["windowsize"]:
@ -260,7 +262,6 @@ class Server:
time.sleep(0.1) time.sleep(0.1)
self._handle_event("disconnected", self.client_handlers[peername]) self._handle_event("disconnected", self.client_handlers[peername])
else: else:
self._handle_event("disconnected", self.client_handlers[peername]) self._handle_event("disconnected", self.client_handlers[peername])
channel.close() channel.close()
@ -271,13 +272,14 @@ class Server:
bh_session.close() bh_session.close()
def stop_server(self): def stop_server(self):
"""Stop server"""
logger.info("Stopping the server...") logger.info("Stopping the server...")
try: try:
for client_handler in self.client_handlers.values(): for client_handler in self.client_handlers.values():
channel = client_handler.channel channel = client_handler.channel
if channel: if channel:
channel.close() channel.close()
self.__serverisrunning = False self.isrunning = False
self.server.close() self.server.close()
logger.info("Server stopped.") logger.info("Server stopped.")
@ -286,42 +288,64 @@ class Server:
def _start_listening_thread(self): def _start_listening_thread(self):
try: try:
logger.info("Start Listening for connections...") self.isrunning = True
while self.__serverisrunning: try:
client, addr = self.server.accept() logger.info("Listening for connections...")
if self.__processmode == "thread": while self.isrunning:
client_thread = threading.Thread(target=self.handle_client, args=(client, addr), daemon=True) client, addr = self.server.accept()
client_thread.start() if self.__processmode == "thread":
else: logger.info(f"Starting client thread for connection {addr}")
self.handle_client(client, addr) client_thread = threading.Thread(target=self._handle_client, args=(client, addr), daemon=True)
time.sleep(1) client_thread.start()
except KeyboardInterrupt: else:
self.stop_server() 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: except Exception as e:
logger.error(e) logger.error(e)
def run(self, private_key_path=None, host="0.0.0.0", port=2222, mode="thread", maxuser=0, daemon=False): 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 """mode: single, thread,
protocol: ssh, telnet 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) if protocol.lower() == "ssh":
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) if private_key_path != None:
self.server.bind((host, port)) logger.info("Loading private key")
self.private_key = paramiko.RSAKey(filename=private_key_path)
else:
raise ValueError("No private key")
if private_key_path != None: self.__processmode = waiting_mode.lower()
self.private_key = paramiko.RSAKey(filename=private_key_path)
else:
raise ValueError("No private key")
if maxuser == 0:
self.server.listen()
else:
self.server.listen(maxuser)
self.__processmode = mode.lower()
self.__serverisrunning = True
self.__daemon = daemon self.__daemon = daemon
client_thread = threading.Thread(target=self._start_listening_thread, daemon=self.__daemon) if custom_server:
client_thread.start() 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)
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 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 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 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 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 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 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. SOFTWARE.
""" """
import time import time
from paramiko.transport import Transport
from paramiko.channel import Channel from ..interactive import Send
from .ProWrapper import IChannel, ITransport
class Client: class Client:
def __init__(self, channel, transport, peername): 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.current_user = None
self.transport: Transport = transport self.transport: ITransport = transport
self.channel: Channel = channel self.channel: IChannel = channel
self.subchannel = {} self.subchannel = {}
self.connecttype = None self.connecttype = None
self.last_activity_time = None self.last_activity_time = None
@ -42,7 +51,7 @@ class Client:
self.prompt = None self.prompt = None
self.inputbuffer = None self.inputbuffer = None
self.peername = peername 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.session_id = None
self.terminal_type = None self.terminal_type = None
self.env_variables = {} self.env_variables = {}
@ -51,54 +60,165 @@ class Client:
self.isexeccommandrunning = False self.isexeccommandrunning = False
def get_id(self): def get_id(self):
"""
Retrieves the client's session ID.
Returns:
str or None: The session ID of the client.
"""
return self.session_id return self.session_id
def get_name(self): def get_name(self):
"""
Retrieves the current username of the client.
Returns:
str: The current username of the client.
"""
return self.current_user return self.current_user
def get_peername(self): 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): def get_prompt(self):
"""
Retrieves the prompt string for the client.
Returns:
str: The prompt string for the client.
"""
return self.prompt return self.prompt
def get_channel(self): def get_channel(self):
"""
Retrieves the communication channel for the client.
Returns:
IChannel: The channel instance for the client.
"""
return self.channel return self.channel
def get_prompt_buffer(self): 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) return str(self.inputbuffer)
def get_terminal_size(self): 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"] return self.windowsize["width"], self.windowsize["height"]
def get_connection_type(self): def get_connection_type(self):
"""
Retrieves the connection type for the client.
Returns:
str: The connection type (e.g., TCP, UDP).
"""
return self.connecttype return self.connecttype
def get_auth_with(self): 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 return self.auth_method
def get_session_duration(self): 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 return time.time() - self.last_login_time
def get_environment(self, variable): 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): 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 return self.last_error
def get_last_command(self): def get_last_command(self):
"""
Retrieves the last command executed by the client.
Returns:
str: The last command executed.
"""
return self.last_command return self.last_command
def set_name(self, name): 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 self.current_user = name
def set_prompt(self, prompt): 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 self.prompt = prompt
def set_environment(self, variable, value): 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 self.env_variables[variable] = value
def open_new_subchannel(self, timeout=None): 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: try:
channel = self.transport.accept(timeout) channel = self.transport.accept(timeout)
id = channel.get_id() id = channel.get_id()
@ -109,35 +229,65 @@ class Client:
return id, channel return id, channel
def get_subchannel(self, id): 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): 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.current_user = user
self.transport.auth_handler.username = user self.transport.set_username(user)
def close_subchannel(self, id): 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() self.subchannel[id].close()
def close(self): def close(self):
"""
Closes the main communication channel for the client.
"""
self.channel.close() 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 # for backward compatibility only
def __getitem__(self, key): def __getitem__(self, key):
return getattr(self, key) return getattr(self, key)
def __setitem__(self, key, value): def __setitem__(self, key, value):
setattr(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 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 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
import re
__version__ = "5.0" version = "5.1"
system_banner = ( 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[33m!!Warning!! This is Testing Version of PyserSSH \033[0m\n"
#"\033[35mUse Putty and WinSCP (SFTP) for best experience \033[0m" #"\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[34m ===== == ===== ==== === == ===== ===== ======== \033[0m\n",
f"\033[37m == == === === == == === === == == \033[0m\n", f"\033[37m == == === === == == === === == == \033[0m\n",
f"\033[31m == == ====== ======= == == ====== ====== == == \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" "\n"
] ]

View File

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

View File

@ -1,6 +1,6 @@
""" """
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -29,7 +29,7 @@ import paramiko
import ast import ast
from .syscom import systemcommand from .syscom import systemcommand
from .remotestatus import startremotestatus from .RemoteStatus import startremotestatus
def parse_exec_request(command_string): def parse_exec_request(command_string):
try: try:

View File

@ -1,6 +1,6 @@
""" """
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -35,12 +35,12 @@ from datetime import datetime
import platform import platform
from ..interactive import Send from ..interactive import Send
from .info import __version__ from .info import version
if platform.system() == "Windows": if platform.system() == "Windows":
import ctypes import ctypes
logger = logging.getLogger("PyserSSH") logger = logging.getLogger("PyserSSH.RemoteStatus")
class LASTINPUTINFO(ctypes.Structure): class LASTINPUTINFO(ctypes.Structure):
_fields_ = [ _fields_ = [
@ -221,7 +221,7 @@ Inactive: 0 kB"""
Send(channel, "", directchannel=True) Send(channel, "", directchannel=True)
Send(channel, "==> /proc/version <==", 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, "", directchannel=True)
Send(channel, "==> /proc/uptime <==", 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 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 Visit https://github.com/DPSoftware-Foundation/PyserSSH
@ -26,9 +26,39 @@ SOFTWARE.
""" """
import shlex 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": if command == "whoami":
Send(client, client["current_user"]) Send(client, client["current_user"])
return True return True
@ -37,6 +67,13 @@ def systemcommand(client, command):
title = args[1] title = args[1]
Title(client, title) Title(client, title)
return True 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": elif command == "exit":
client["channel"].close() client["channel"].close()
return True return True

View File

@ -1,6 +1,6 @@
""" """
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH 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 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 # Replace '\n' with '\r\n' in the string
modified_string = decoded_string.replace('\n', '\r\n') modified_string = decoded_string.replace('\n', '\r\n')
# Encode the modified string back to bytes # Encode the modified string back to bytes
return modified_string.encode() return modified_string.encode()
else: else:
raise TypeError("Input must be a string or bytes") 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}")