Compare commits

...

17 Commits
4.3 ... main

Author SHA1 Message Date
b9e4bb5887 fix "can't disconnection" 2025-01-04 13:47:02 +07:00
3a751972c1 fix loop disconnect 2025-01-04 13:39:07 +07:00
a3f1b50303 fix setup.py not found utils folder 2025-01-04 13:22:18 +07:00
64bb2b0616 fix ctypes not defined
This update fix ctypes is not defined in linux
2025-01-04 13:12:55 +07:00
78a6459d26 update 5.1
New ServerManager for manage multiple server with multiple protocol. ProWrapper for translate protocol from many protocol to one protocol (function).
2024-12-06 23:33:15 +07:00
1b15383ca7 Merge branch 'main' of https://github.com/DPSoftware-Foundation/PyserSSH 2024-09-04 22:37:16 +07:00
f5cb7ead49 Update README.md 2024-09-04 22:36:50 +07:00
7eda693f78
Update README.md 2024-09-04 11:10:52 +07:00
019836cb00 fix setup.py 2
forgot , too💀
2024-09-03 20:31:55 +07:00
0d6c58b71a fix setup.py
forgot , 💀
2024-09-03 20:30:21 +07:00
31276827c4 Merge branch 'main' of https://github.com/DPSoftware-Foundation/PyserSSH 2024-09-03 20:28:31 +07:00
b6455ce6a3 Update 5.0
New main features
- ServerManager
- RemoDesk protocol support
- New client system
- Supported auth with none, password and public key
- Support remote monitor for mobaxterm user (beta)
- SFTP can set specific folder for user
- Support exec_request

New function
- Send_karaoke_effect
- ShowCursor
- SendBell
- NewSend for advance sending like print()
- Flag_TH for about
- wait_inputmouse for mouse input

Fixing
- (only python) fixing can't print color
without damp11113 library or print color library only in windows on python console
- Fixing sometime can't connect SFTP

and more
2024-09-03 20:27:12 +07:00
f578fc57d5
Update README.md 2024-07-29 16:52:14 +07:00
c4b5314fda
Update README.md 2024-07-20 20:51:41 +07:00
98361c33f2
Update README.md 2024-07-20 20:47:22 +07:00
058daf9f04
hot fix for server
fix freezing when no banner
2024-05-05 17:12:26 +07:00
f33be3bca1 4.4
no demo folder but it on library
python -m PyserSSH
2024-04-19 15:31:11 +07:00
33 changed files with 3813 additions and 746 deletions

View File

@ -1,50 +1,60 @@
# What is PyserSSH
PyserSSH is a library for remote control your code with ssh client. The aim is to provide a scriptable SSH server which can be made to behave like any SSH-enabled device.
This library will be **Pyserminal** (Python Server Terminal) as it supports multiple protocols such as ssh telnet rlogin and mores...
PyserSSH is a free and open-source Python library designed to facilitate the creation of customizable SSH terminal servers. Initially developed for research purposes to address the lack of suitable SSH server libraries in Python, PyserSSH provides a flexible and user-friendly solution for implementing SSH servers, making it easier for developers to handle user interactions and command processing.
The project was started by a solo developer to create a more accessible and flexible tool for managing SSH connections and commands. It offers a simplified API compared to other libraries, such as Paramiko, SSHim, and Twisted, which are either outdated or complex for new users.
This project is part from [damp11113-library](https://github.com/damp11113/damp11113-library)
This Server use port **2222** for default port
## Some smail PyserSSH history
PyserSSH version [1.0](https://github.com/DPSoftware-Foundation/PyserSSH/releases/download/Legacy/PyserSSH10.py) (real filename is "test277.py") was created in 2023/9/3 for experimental purposes only. Because I couldn't find the best ssh server library for python and I started this project only for research. But I have time to develop this research into a real library for use. In software or server.
Read full history from [docs](https://damp11113.xyz/PyserSSHDocs/history.html)
# Install
Install from pypi
```bash
pip install PyserSSH
```
Install from github
Install with [openRemoDesk](https://github.com/DPSoftware-Foundation/openRemoDesk) protocol
```bash
pip install PyserSSH[RemoDesk]
```
Install from Github
```bash
pip install git+https://github.com/damp11113/PyserSSH.git
```
Install from DPCloudev Git
```bash
pip install git+https://git.damp11113.xyz/DPSoftware-Foundation/PyserSSH.git
```
# Quick Example
This Server use port **2222** for default port
```py
import os
from PyserSSH import Server, Send, AccountManager
useraccount = AccountManager()
useraccount.add_account("admin", "") # create user without password
from PyserSSH import Server, AccountManager
useraccount = AccountManager(allow_guest=True)
ssh = Server(useraccount)
@ssh.on_user("command")
def command(client, command: str):
if command == "hello":
Send(client, "world!")
client.send("world!")
ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
ssh.run("your private key file")
```
This example you can connect with `ssh admin@localhost -p 2222` and press enter on login
If you input `hello` the response is `world`
# Demo
https://github.com/damp11113/PyserSSH/assets/64675096/49bef3e2-3b15-4b64-b88e-3ca84a955de7
> [!WARNING]
> For use in product please **generate new private key**! If you still use this demo private key maybe your product getting **hacked**! up to 90%. Please don't use this demo private key for real product.
See [server.py](https://github.com/damp11113/PyserSSH/blob/main/demo/server.py)
https://github.com/damp11113/PyserSSH/assets/64675096/49bef3e2-3b15-4b64-b88e-3ca84a955de7
I intend to leaked private key because that key i generated new. I recommend to generate new key if you want to use on your host because that key is for demo only.
why i talk about this? because when i push private key into this repo in next 5 min++ i getting new email from GitGuardian. in that email say "
GitGuardian has detected the following RSA Private Key exposed within your GitHub account" i dont knows what is GitGuardian and i not install this app into my account.

View File

@ -0,0 +1,157 @@
0
00:00:00,000 --> 00:00:09,200
(Intro)
1
00:00:09,200 --> 00:00:13,100
I saw you dancing in a crowded room
2
00:00:13,200 --> 00:00:17,200
You look so happy when
I'm not with you
3
00:00:17,300 --> 00:00:21,300
But then you saw me, caught
you by surprise
4
00:00:21,400 --> 00:00:26,200
A single teardrop falling
from your eye
5
00:00:26,300 --> 00:00:31,400
I don't know why I run away
6
00:00:34,400 --> 00:00:40,900
I'll make you cry when I run away
7
00:00:41,700 --> 00:00:45,950
You could've asked me why
I broke your heart
8
00:00:46,000 --> 00:00:49,800
You could've told me
that you fell apart
9
00:00:49,900 --> 00:00:53,700
But you walked past me
like I wasn't there
10
00:00:53,800 --> 00:00:58,450
And just pretended like
you didn't care
11
00:00:58,500 --> 00:01:03,000
I don't know why I run away
12
00:01:06,400 --> 00:01:11,300
I'll make you cry when I run away
13
00:01:14,700 --> 00:01:18,600
Take me back 'cause I wanna stay
14
00:01:18,700 --> 00:01:21,600
Save your tears for another
15
00:01:21,700 --> 00:01:28,900
Save your tears for another day
16
00:01:29,800 --> 00:01:34,700
Save your tears for another day
17
00:01:37,000 --> 00:01:42,900
So, I made you think that
I would always stay
18
00:01:43,000 --> 00:01:46,600
I said some things that
I should never say
19
00:01:46,700 --> 00:01:50,600
Yeah, I broke your heart like
someone did to mine
20
00:01:50,700 --> 00:01:55,500
And now you won't love
me for a second time
21
00:01:55,600 --> 00:02:00,400
I don't know why I run away, oh, girl
22
00:02:03,300 --> 00:02:08,800
Said I make you cry when I run away
23
00:02:11,500 --> 00:02:15,600
Girl, take me back 'cause I wanna stay
24
00:02:15,700 --> 00:02:19,200
Save your tears for another
25
00:02:19,300 --> 00:02:23,600
I realize that I'm much too late
26
00:02:23,700 --> 00:02:26,800
And you deserve someone better
27
00:02:26,900 --> 00:02:32,000
Save your tears for another
day (Ooh, yeah)
28
00:02:34,900 --> 00:02:40,900
Save your tears for another day (Yeah)
29
00:02:46,100 --> 00:02:52,300
I don't know why I run away
30
00:02:52,700 --> 00:02:57,400
I'll make you cry when I run away
31
00:02:59,400 --> 00:03:06,650
Save your tears for another
day, ooh, girl (Ah)
32
00:03:06,700 --> 00:03:13,600
I said save your tears
for another day (Ah)
33
00:03:15,700 --> 00:03:23,200
Save your tears for another day (Ah)
34
00:03:23,700 --> 00:03:33,300
Save your tears for another day (Ah)
35
00:03:34,300 --> 00:03:43,300
by DPSoftware Foundation

477
demo/demo1.py Normal file
View File

@ -0,0 +1,477 @@
import os
os.environ["damp11113_load_all_module"] = "NO"
import socket
import time
import cv2
import traceback
import requests
from bs4 import BeautifulSoup
import numpy as np
import logging
# Configure logging
logging.basicConfig(format='[{asctime}] [{levelname}] {name}: {message}', datefmt='%Y-%m-%d %H:%M:%S', style='{', level=logging.DEBUG)
from PyserSSH import Server, AccountManager
from PyserSSH.interactive import Send, wait_input, wait_inputkey, wait_choose, Clear, wait_inputmouse
from PyserSSH.system.info import version, Flag_TH
from PyserSSH.extensions.processbar import indeterminateStatus, LoadingProgress
from PyserSSH.extensions.dialog import MenuDialog, TextDialog, TextInputDialog
from PyserSSH.extensions.moredisplay import clickable_url, Send_karaoke_effect
from PyserSSH.extensions.moreinteractive import ShowCursor
from PyserSSH.extensions.remodesk import RemoDesk
from PyserSSH.extensions.XHandler import XHandler
from PyserSSH.system.clientype import Client
from PyserSSH.system.RemoteStatus import remotestatus
from PyserSSH.utils.ServerManager import ServerManager
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
useraccount = AccountManager(allow_guest=True, autoload=True, autosave=True)
if not os.path.isfile("autosave_session.ses"):
useraccount.add_account("admin", "", sudo=True) # create user without password
useraccount.add_account("test", "test") # create user without password
useraccount.add_account("demo")
useraccount.add_account("remote", "12345", permissions=["remote_desktop"])
useraccount.set_user_enable_inputsystem_echo("remote", False)
useraccount.set_user_sftp_allow("admin", True)
XH = XHandler()
ssh = Server(useraccount,
system_commands=True,
system_message=False,
sftp=True,
enable_preauth_banner=True,
XHandler=XH)
remotedesktopserver = RemoDesk()
servername = "PyserSSH"
loading = ["PyserSSH", "openRemoDesk", "XHandler", "RemoteStatus"]
@ssh.on_user("pre-shell")
def guestauth(client):
if client.get_name() == "remote":
return
if not useraccount.has_user(client.get_name()):
while True:
Clear(client)
Send(client, f"You are currently logged in as a guest. To access, please login or register.\nYour current account: {client.get_name()}\n")
method = wait_choose(client, ["Login", "Register", "Exit"], prompt="Action: ")
Clear(client)
if method == 0: # login
username = wait_input(client, "Username: ", noabort=True)
if not username:
Send(client, "Please Enter username")
wait_inputkey(client, "Press any key to continue...")
continue
password = wait_input(client, "Password: ", password=True, noabort=True)
Send(client, "Please wait...")
if not useraccount.has_user(username):
Send(client, f"Username isn't exist. Please try again")
wait_inputkey(client, "Press any key to continue...")
continue
if not useraccount.validate_credentials(username, password):
Send(client, f"Password incorrect. Please try again")
wait_inputkey(client, "Press any key to continue...")
continue
Clear(client)
client.switch_user(username)
break
elif method == 1: # register
username = wait_input(client, "Please choose a username: ", noabort=True)
if not username:
Send(client, "Please Enter username")
wait_inputkey(client, "Press any key to continue...")
continue
if useraccount.has_user(username):
Send(client, f"Username is exist. Please try again")
wait_inputkey(client, "Press any key to continue...")
continue
password = wait_input(client, "Password: ", password=True, noabort=True)
if not password:
Send(client, "Please Enter password")
wait_inputkey(client, "Press any key to continue...")
continue
confirmpassword = wait_input(client, "Confirm Password: ", password=True, noabort=True)
if not password:
Send(client, "Please Enter confirm password")
wait_inputkey(client, "Press any key to continue...")
continue
if password != confirmpassword:
Send(client, "Password do not matching the confirm password. Please try again.")
wait_inputkey(client, "Press any key to continue...")
continue
Send(client, "Please wait...")
useraccount.add_account(username, password, ["user"])
client.switch_user(username)
Clear(client)
break
else:
client.close()
@ssh.on_user("connect")
def connect(client):
if client.get_name() == "remote":
return
client.set_prompt(client["current_user"] + "@" + servername + ":~$")
wm = f"""{Flag_TH()}{''*50}
Hello {client['current_user']},
This is testing server of PyserSSH v{version}.
Visit: {clickable_url("https://damp11113.xyz", "DPCloudev")}
{''*50}"""
for i in loading:
P = indeterminateStatus(client, f"Starting {i}", f"[ OK ] Started {i}")
P.start()
time.sleep(len(i) / 20)
P.stop()
Di1 = TextDialog(client, "Welcome!\n to PyserSSH test server", "PyserSSH Extension")
Di1.render()
for char in wm:
Send(client, char, ln=False)
#time.sleep(0.005) # Adjust the delay as needed
Send(client, '\n') # Send newline after each line
@ssh.on_user("authbanner")
def banner(tmp):
return "Hello World!\n", "en"
@ssh.on_user("error")
def error(client, error):
if isinstance(error, socket.error):
pass
else:
Send(client, traceback.format_exc())
@XH.command(name="startremotedesktop", category="Remote", permissions=["remote_desktop"])
def remotedesktop(client):
remotedesktopserver.handle_new_client(client)
@XH.command(name="passtest", category="Test Function")
def xh_passtest(client):
user = wait_input(client, "username: ")
password = wait_input(client, "password: ", password=True)
Send(client, f"username: {user} | password: {password}")
@XH.command(name="colortest", category="Test Function")
def xh_colortest(client):
for i in range(0, 255, 5):
Send(client, TextFormatter.format_text_truecolor(" ", background=f"{i};0;0"), ln=False)
Send(client, "")
for i in range(0, 255, 5):
Send(client, TextFormatter.format_text_truecolor(" ", background=f"0;{i};0"), ln=False)
Send(client, "")
for i in range(0, 255, 5):
Send(client, TextFormatter.format_text_truecolor(" ", background=f"0;0;{i}"), ln=False)
Send(client, "")
Send(client, "TrueColors 24-Bit")
@XH.command(name="keytest", category="Test Function")
def xh_keytest(client: Client):
user = wait_inputkey(client, "press any key", raw=True, timeout=1)
Send(client, "")
Send(client, f"key: {user}")
for i in range(10):
user = wait_inputkey(client, "press any key", raw=True, timeout=1)
Send(client, "")
Send(client, f"key: {user}")
@XH.command(name="typing")
def xh_typing(client: Client, messages, speed = 1):
for w in messages:
Send(client, w, ln=False)
time.sleep(float(speed))
Send(client, "")
@XH.command(name="renimtest")
def xh_renimtest(client: Client):
Clear(client)
image = cv2.imread("opensource.png", cv2.IMREAD_COLOR)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
width, height = client['windowsize']["width"] - 5, client['windowsize']["height"] - 5
# resize image
resized = cv2.resize(image, (width, height))
t = ""
# Scan all pixels
for y in range(0, height):
for x in range(0, width):
pixel_color = resized[y, x]
if pixel_color.tolist() != [0, 0, 0]:
t += TextFormatter.format_text_truecolor(" ",
background=f"{pixel_color[0]};{pixel_color[1]};{pixel_color[2]}")
else:
t += " "
Send(client, t, ln=False)
Send(client, "")
t = ""
@XH.command(name="errortest", category="Test Function")
def xh_errortest(client: Client):
raise Exception("hello error")
@XH.command(name="inloadtest", category="Test Function")
def xh_inloadtest(client: Client):
loading = indeterminateStatus(client)
loading.start()
time.sleep(5)
loading.stop()
@XH.command(name="loadtest", category="Test Function")
def xh_loadtest(client: Client):
l = LoadingProgress(client, total=100, color=True)
l.start()
for i in range(101):
l.current = i
l.status = f"loading {i}"
time.sleep(0.05)
l.stop()
@XH.command(name="dialogtest", category="Test Function")
def xh_dialogtest(client: Client):
Di1 = TextDialog(client, "Hello Dialog!", "PyserSSH Extension")
Di1.render()
@XH.command(name="dialogtest2", category="Test Function")
def xh_dialogtest2(client: Client):
Di2 = MenuDialog(client, ["H1", "H2", "H3"], "PyserSSH Extension", "Hello world")
Di2.render()
Send(client, f"selected index: {Di2.output()}")
@XH.command(name="dialogtest3", category="Test Function")
def xh_dialogtest3(client: Client):
Di3 = TextInputDialog(client, "PyserSSH Extension")
Di3.render()
Send(client, f"input: {Di3.output()}")
@XH.command(name="passdialogtest3", category="Test Function")
def xh_passdialogtest3(client: Client):
Di3 = TextInputDialog(client, "PyserSSH Extension", inputtitle="Password Here", password=True)
Di3.render()
Send(client, f"password: {Di3.output()}")
@XH.command(name="choosetest", category="Test Function")
def xh_choosetest(client: Client):
mylist = ["H1", "H2", "H3"]
cindex = wait_choose(client, mylist, "select: ")
Send(client, f"selected: {mylist[cindex]}")
@XH.command(name="vieweb")
def xh_vieweb(client: Client, url: str):
loading = indeterminateStatus(client, desc=f"requesting {url}...")
loading.start()
try:
content = requests.get(url).content
except:
loading.stopfail()
return
loading.stop()
loading = indeterminateStatus(client, desc=f"parsing html {url}...")
loading.start()
try:
soup = BeautifulSoup(content, 'html.parser')
# Extract only the text content
text_content = soup.get_text()
except:
loading.stopfail()
return
loading.stop()
Di1 = TextDialog(client, text_content, url)
Di1.render()
@XH.command(name="shutdown")
def xh_shutdown(client: Client, at: str):
if at == "now":
ssh.stop_server()
@XH.command(name="mouseinput", category="Test Function")
def xh_mouseinput(client: Client):
for i in range(10):
button, x, y = wait_inputmouse(client)
if button == 0:
Send(client, "Left Button")
elif button == 1:
Send(client, "Middle Button")
elif button == 2:
Send(client, "Right Button")
elif button == 3:
Send(client, "Button Up")
Send(client, f"Current POS: X {x} | Y {y} with button {button}")
@XH.command(name="karaoke")
def xh_karaoke(client: Client):
ShowCursor(client, False)
Send_karaoke_effect(client, "Python can print like karaoke!")
ShowCursor(client)
R1 = 1
R2 = 2
K2 = 5
theta_spacing = 0.07
phi_spacing = 0.02
illumination = np.fromiter(".,-~:;=!*#$@", dtype="<U1")
def render_frame(A: float, B: float, screen_size) -> np.ndarray:
K1 = screen_size * K2 * 3 / (8 * (R1 + R2))
"""
Returns a frame of the spinning 3D donut.
Based on the pseudocode from: https://www.a1k0n.net/2011/07/20/donut-math.html
"""
cos_A = np.cos(A)
sin_A = np.sin(A)
cos_B = np.cos(B)
sin_B = np.sin(B)
output = np.full((screen_size, screen_size), " ") # (40, 40)
zbuffer = np.zeros((screen_size, screen_size)) # (40, 40)
cos_phi = np.cos(phi := np.arange(0, 2 * np.pi, phi_spacing)) # (315,)
sin_phi = np.sin(phi) # (315,)
cos_theta = np.cos(theta := np.arange(0, 2 * np.pi, theta_spacing)) # (90,)
sin_theta = np.sin(theta) # (90,)
circle_x = R2 + R1 * cos_theta # (90,)
circle_y = R1 * sin_theta # (90,)
x = (np.outer(cos_B * cos_phi + sin_A * sin_B * sin_phi, circle_x) - circle_y * cos_A * sin_B).T # (90, 315)
y = (np.outer(sin_B * cos_phi - sin_A * cos_B * sin_phi, circle_x) + circle_y * cos_A * cos_B).T # (90, 315)
z = ((K2 + cos_A * np.outer(sin_phi, circle_x)) + circle_y * sin_A).T # (90, 315)
ooz = np.reciprocal(z) # Calculates 1/z
xp = (screen_size / 2 + K1 * ooz * x).astype(int) # (90, 315)
yp = (screen_size / 2 - K1 * ooz * y).astype(int) # (90, 315)
L1 = (((np.outer(cos_phi, cos_theta) * sin_B) - cos_A * np.outer(sin_phi, cos_theta)) - sin_A * sin_theta) # (315, 90)
L2 = cos_B * (cos_A * sin_theta - np.outer(sin_phi, cos_theta * sin_A)) # (315, 90)
L = np.around(((L1 + L2) * 8)).astype(int).T # (90, 315)
mask_L = L >= 0 # (90, 315)
chars = illumination[L] # (90, 315)
for i in range(90):
mask = mask_L[i] & (ooz[i] > zbuffer[xp[i], yp[i]]) # (315,)
zbuffer[xp[i], yp[i]] = np.where(mask, ooz[i], zbuffer[xp[i], yp[i]])
output[xp[i], yp[i]] = np.where(mask, chars[i], output[xp[i], yp[i]])
return output
@XH.command()
def donut(client, screen_size=40):
screen_size = int(screen_size)
A = 1
B = 1
for _ in range(screen_size * screen_size):
A += theta_spacing
B += phi_spacing
Clear(client)
array = render_frame(A, B, screen_size)
for row in array:
Send(client, " ".join(row))
Send(client, "\n")
if wait_inputkey(client, raw=True, timeout=0.01) == "\x03": break
@XH.command(name="status")
def xh_status(client: Client):
remotestatus(ssh, client.channel, True)
#@ssh.on_user("command")
#def command(client: Client, command: str):
#ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
manager = ServerManager()
# Add servers to the manager
manager.add_server("ssh", ssh, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
manager.add_server("telnet", ssh, "", protocol="telnet")
# Start a specific server
manager.start_server("ssh")
manager.start_server("telnet")

29
demo/demo2.py Normal file
View File

@ -0,0 +1,29 @@
import os
import time
from damp11113 import SRTParser
from PyserSSH import Server, Send, AccountManager
from PyserSSH.extensions.XHandler import XHandler
from PyserSSH.extensions.moredisplay import Send_karaoke_effect, ShowCursor
accountmanager = AccountManager()
accountmanager.add_account("admin", "")
XH = XHandler()
server = Server(accountmanager, XHandler=XH)
@XH.command()
def karaoke(client):
ShowCursor(client, False)
subtitle = SRTParser("Save Your Tears lyrics.srt", removeln=True)
for sub in subtitle:
delay = sub["duration"] / len(sub["text"])
Send_karaoke_effect(client, sub["text"], delay)
if sub["next_text_duration"] is not None:
time.sleep(sub["next_text_duration"])
ShowCursor(client)
server.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))

View File

@ -1,27 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAq7UgQtL4Nv2s8rvaJjQryNxpsKpcSeDIsABnvry6Xkd3KhOi
K3c4dkYJjiAb4w4wfPiJ7sFL/PFP3f/slcpNHz18meWZkktia3rBX8uyJQ3soyNw
Vbxm6mOPntAqC4JBoPaYS4HABSYxJYY6yPU1i0UufvWg5pNRgeZIM8kQSyie4q1C
AEFG1T6sabJ5mOWH8Yw/zu3nTQpz2yIZYSVOsvJxBtaCEHCThhmQk2jPb88Ss0XT
a7uzaX/UIRktDz1FN6ooJbFqHsHxOsZJrC6YdZ3lo7DZYJU+jclG4jy95rGe7KpE
0p9cMAYNO0ya6toJM6GwFzJEk+HD0BTxdi7dKQIDAQABAoIBAFY1ciUa1xSE+LhG
KJjVyMXoJAhXAE73VMtI6M2S499B8kpl4R4BlY+MSm/ZHyc4kI+uGVKOKiCs53SG
cboi/+WXcV+zLw+MWbWsxDncg2ynORAPUu840FMN+aW6zeFJXLn8FSqT0lzDeBlm
80zCEEgES/viRw59GIcnn0igwlV5EzO6zhWzwdeMpBO4XFFDaiEY5idIBQf4jCEY
JcfQOrkPpfPgjLyQmFLyeojyaUVLIOOLUGMsSS8Hk7MJlgEdneEIXX7EhPqLDPyc
1f33WsnlTvbHLGWHE7lMG1LbK1ecsNwWWUFfoVQaQzYUQAVSuoqTYYkAYfGnQmV0
nnsIUAECgYEA7xR8cqu/knx9gFeinYwLx+/BbUw1MAX0WSK4GOx0O7qgbDb+scb2
x4aBCZnDE44KRl1/mbLXxb3wq8GN7W4owIHSud/8gZNqJM0uXbiJS7gMW0XnoJuk
D4hr0ADddPn/vGfQyUf0oUOFg2nP+H99GEuyYpHXr39R4Fh4UljXEkECgYEAt9wG
GRUhW6BoE3t2/mUcgkpXrljI7W2SDtHGgGzNb8Mlxcu/KUC4b6qdNwe83t0w3SaP
+34JHXIqnb2cuvigQj8pCoFxaMT6gH7x1zQWI1cORCA+Vfx/NZ2cCyXpebNOytxu
AwtAVo+r/QZlfs4OlG+TVwKBxYz9huCPFaAfQOkCgYA5iViZ0DN+cW9Sn8SG3dlH
+K84OoriT8yKVwyvEti2Nye8Y0/QQO3K/te3E8Yawqg+XuoCd0PuVtPAwggCB+zO
x2+LRBhkprF4wdhSvcJs8pImtSAVSt+kzVQE7vBc4n1lPibFCggZd0J+acyfJS9Z
1X3MswSRO7bcou3yA2dfAQKBgAHz3Dy39Lq8YV6TmRfqivr3Pyci2j9rQnnV0H3c
qfHd6LDJESanAU5uSW0kL+VOBA7VMgJBvGcLp1g1g0yZB1qswQrThRjPvrlOn9Lh
QrrtWcFvdjoDjHZNTjLwHCKmvNd6r9Bodi51KCZvwvQtzAnXhYEPDcHDVY3xJJPe
N3bBAoGADnEc8G2taL/tq7Skcw1G/cZYUp4CZw+ypCLd1xyus7Lnu9Wl6y3U0HEl
pjgzBGlwvTRkvC5ewz46WIWE+hlmOdSih81Cro48baXR2T1OD8jKQ2pXmHK5Z/wy
0V7t7eHd/k8CcXzIWIk6gmpOYhKkIVvQW5g7ssbwsfsk3qD++Fs=
MIIEpQIBAAKCAQEAwNfkia91HNrpyqlHwjYrVKDV5SkDt5P27MxKZDjwOokGBX7E
g5cMXb1wxQeCm+zptg680qIXHfSaaOi1E/DAutaTIQa3GI+gDMphlWMxrEWFuZOB
ylvTuFAxLB8xKcuBjelQX4TYlcgA1WgyeI6LFPNdJPekVHnzkLCZnW+y05PkT6a0
QY1Eoa6DY2TtY8w4NZmnyCy1ZPYV5qLKN/P7aVSU52AD8u25St1WprvxpM4TtZiG
2O9X1Unx+wtco2P8G1M4qcuWqPDdITn4n19DcR7rhuACjUo2poFTlnl9lfEsW11R
5sDfYlgc3n8a4Iw49Ea4GaLkSEMluOfB9eOLUQIDAQABAoIBAAeZmpVTN7uFjyLg
YrEZ6cGXPcbJw9k8zhr3/tM4q+hf/7+WBuWkEtCGR5xO7Ev73XFs3u6IL9QLkKL4
z4YefgypqeO/0YB4zJckdLhqpTRZCxOiEhfpCuI1MDLrycgQD/uJSenHIQgKI/+a
cH7Ffgq7Kp0V22vu4HVVLcCsJxvlIxFd92xCKFl8zRHBdyKikfvZAEidbMu9xdsW
S9DzFCveCGrE8g6HWQyXiCpq2xb4b2C37O+0iZRtYfJQSCrnG99Y/KfWIVbb+3gU
5WbIlYm57TKzMGgKc3LWtGCWxfB/NNP5wOxR+4y78oWDzTibrT5OZDsX2S+mbgNB
wAo/0U8CgYEAxHAOrlz9Ae2kYfyUgx9JTonElIFlDmDVdYcRW8Go7xpeMZ+XG2sR
f/za6t6jiCxI9FSD5gl4nDyOVhx5zRpu2QZvZBHICaWDwEmZC+d3suYtQY/ixR3K
3sdDKK6wzOtta+OBVNPQWAW2rmTr/J1JobguflM0NBm+YZC02gQyL9sCgYEA+1DU
llDGDaU08WQNTLRgW+1RAbzsBTFd+DhvbYM8+mgmlFzHKHJP3jCpwLZmqdBzLl0R
wUZBwpZ5MnkiQV0e9AW4/tnqBw8n9pf+NgNqcssw8MEMXHPbLNwr7OVS/LG8VNOm
LbuLjxq8O8wfbS87eBj2D18c1x4voEIw1AWYn0MCgYEAnPBF2moyPMMmjJ5l7Ggn
ghaxNlA2c4lLoOz7IkqTdAul65FsARzGS3GxWOnsztNKqeGHy1YPxQrgUM3JReLz
YnIwtks6fPJ+Uza5jngr+oLI71NMQl1uAhRChJMkb2M79XE6l5HuJxTRgXzhyN3E
wO5MPuKsl19l6b7ZrkCh8/cCgYEAjIL6+TgcI9D0suo/zV0kawFaw1//jj+1zGyx
UEeKNm848saUy3ZuVUpb/tV8vQFBBPEgVjGT3toG1UOI9Ya9Ia55anQoNt4wd90v
Ur/CKoCU0mb9JEvahVBsdr0ZExPEuqDDTtqHAvHtwHk2MPOxikpaeOmy1EuaUT3w
0vp2BMUCgYEAsLL592l8pclhxk2b0lmgvhPLOmZuQ7QkcnMMyYCeUr9Kt95VN40J
N/LK9LIbf/l9CUN4eO1JqCJkAiMIW2Gvumw3g+TMj+nqcfsufSHJCG1EZNYMUftG
aL7KtccPyFwotMD/P+OaAeJimwuC5247hCep1SSf1A41gbdmutiirM4=
-----END RSA PRIVATE KEY-----

View File

@ -1,197 +0,0 @@
import os
import socket
import time
import shlex
from damp11113 import TextFormatter, sort_files, allfiles
import cv2
import traceback
import requests
from bs4 import BeautifulSoup
from PyserSSH import Server, AccountManager, Send, wait_input, wait_inputkey, wait_choose, Clear, Title
from PyserSSH.system.info import system_banner
from PyserSSH.extensions.processbar import indeterminateStatus, LoadingProgress
from PyserSSH.extensions.dialog import MenuDialog, TextDialog, TextInputDialog
from PyserSSH.extensions.moredisplay import clickable_url
useraccount = AccountManager()
useraccount.add_account("admin", "") # create user without password
useraccount.add_account("test", "test") # create user without password
ssh = Server(useraccount, system_commands=True, system_message=False)
nonamewarning = """Connection Warning:
Unauthorized access or improper use of this system is prohibited.
Please ensure you have proper authorization before proceeding."""
Authorizedmessage = """You have successfully connected to the server.
Enjoy your session and remember to follow security protocols."""
loading = ["PyserSSH", "Extensions"]
@ssh.on_user("connect")
def connect(client):
Title(client, "PyserSSH")
#print(client["windowsize"])
if client['current_user'] == "":
warningmessage = nonamewarning
else:
warningmessage = Authorizedmessage
wm = f"""*********************************************************************************************
Hello {client['current_user']},
{warningmessage}
Visit: {clickable_url("https://damp11113.xyz", "DPCloudev")}
{system_banner}
*********************************************************************************************"""
if client['current_user'] != "test":
for i in loading:
P = indeterminateStatus(client, f"Starting {i}", f"[ OK ] Started {i}")
P.start()
time.sleep(len(i) / 20)
P.stop()
Di1 = TextDialog(client, "PyserSSH Extension", "Welcome!\n to PyserSSH test server")
Di1.render()
for char in wm:
Send(client, char, ln=False)
# time.sleep(0.005) # Adjust the delay as needed
Send(client, '\n') # Send newline after each line
@ssh.on_user("error")
def error(client, error):
if isinstance(error, socket.error):
pass
else:
Send(client, traceback.format_exc())
#@ssh.on_user("onrawtype")
#def onrawtype(client, key):
# print(key)
@ssh.on_user("command")
def command(client, command: str):
if command == "passtest":
user = wait_input(client, "username: ")
password = wait_input(client, "password: ", password=True)
Send(client, f"username: {user} | password: {password}")
elif command == "colortest":
for i in range(0, 255, 5):
Send(client, TextFormatter.format_text_truecolor(" ", background=f"{i};0;0"), ln=False)
Send(client, "")
for i in range(0, 255, 5):
Send(client, TextFormatter.format_text_truecolor(" ", background=f"0;{i};0"), ln=False)
Send(client, "")
for i in range(0, 255, 5):
Send(client, TextFormatter.format_text_truecolor(" ", background=f"0;0;{i}"), ln=False)
Send(client, "")
Send(client, "TrueColors 24-Bit")
elif command == "keytest":
user = wait_inputkey(client, "press any key", raw=True)
Send(client, "")
Send(client, f"key: {user}")
for i in range(10):
user = wait_inputkey(client, "press any key", raw=True)
Send(client, "")
Send(client, f"key: {user}")
elif command.startswith("typing"):
args = shlex.split(command)
messages = args[1]
speed = float(args[2])
for w in messages:
Send(client, w, ln=False)
time.sleep(speed)
Send(client, "")
elif command.startswith("renimtest"):
args = shlex.split(command)
Clear(client)
image = cv2.imread(f"opensource.png", cv2.IMREAD_COLOR)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
width, height = client['windowsize']["width"]-5, client['windowsize']["height"]-5
# resize image
resized = cv2.resize(image, (width, height))
t = ""
# Scan all pixels
for y in range(0, height):
for x in range(0, width):
pixel_color = resized[y, x]
# PyserSSH.Send(channel, f"Pixel color at ({x}, {y}): {pixel_color}")
if pixel_color.tolist() != [0, 0, 0]:
t += TextFormatter.format_text_truecolor(" ", background=f"{pixel_color[0]};{pixel_color[1]};{pixel_color[2]}")
else:
t += " "
Send(client, t, ln=False)
Send(client, "")
t = ""
elif command == "errortest":
raise Exception("hello error")
elif command == "inloadtest":
loading = indeterminateStatus(client)
loading.start()
time.sleep(5)
loading.stop()
elif command == "loadtest":
l = LoadingProgress(client, total=100, color=True)
l.start()
for i in range(101):
l.current = i
l.status = f"loading {i}"
time.sleep(0.05)
l.stop()
elif command == "dialogtest":
Di1 = TextDialog(client, "PyserSSH Extension", "Hello Dialog!")
Di1.render()
elif command == "dialogtest2":
Di2 = MenuDialog(client, ["H1", "H2", "H3"], "PyserSSH Extension", "Hello world")
Di2.render()
Send(client, f"selected index: {Di2.output()}")
elif command == "dialogtest3":
Di3 = TextInputDialog(client, "PyserSSH Extension")
Di3.render()
Send(client, f"input: {Di3.output()}")
elif command == "passdialogtest3":
Di3 = TextInputDialog(client, "PyserSSH Extension", inputtitle="Password Here", password=True)
Di3.render()
Send(client, f"password: {Di3.output()}")
elif command == "choosetest":
cindex = wait_choose(client, ["H1", "H2", "H3"], "select: ")
Send(client, f"selected index: {cindex}")
elif command.startswith("vieweb"):
args = shlex.split(command)
url = args[1]
loading = indeterminateStatus(client, desc=f"requesting {url}...")
loading.start()
try:
content = requests.get(url).content
except:
loading.stopfail()
return
loading.stop()
loading = indeterminateStatus(client, desc=f"parsing html {url}...")
loading.start()
try:
soup = BeautifulSoup(content, 'html.parser')
# Extract only the text content
text_content = soup.get_text()
except:
loading.stopfail()
return
loading.stop()
Di1 = TextDialog(client, url, text_content)
Di1.render()
ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))

View File

@ -1,21 +1,44 @@
from setuptools import setup, find_packages
with open('README.md', 'r', encoding='utf-8') as f:
long_description = f.read()
setup(
name='PyserSSH',
version='4.3',
version='5.1.4',
license='MIT',
author='damp11113',
author_email='damp51252@gmail.com',
author='DPSoftware Foundation',
author_email='contact@damp11113.xyz',
packages=find_packages('src'),
package_dir={'': 'src'},
url='https://github.com/damp11113/PyserSSH',
description="python scriptable ssh server library. based on Paramiko",
long_description=long_description,
long_description=open('README.md', 'r', encoding='utf-8').read(),
long_description_content_type='text/markdown',
keywords="SSH server",
python_requires='>=3.6',
classifiers=[
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.10",
"Topic :: Communications",
"Topic :: Internet",
"Topic :: Internet :: File Transfer Protocol (FTP)",
"Topic :: Software Development",
"Topic :: Terminals"
],
install_requires=[
"paramiko"
]
"paramiko",
"psutil"
],
extras_require={
'RemoDesk': [
"mouse",
"keyboard",
"Brotli",
"pillow",
"numpy",
"opencv-python"
],
}
)

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -42,27 +42,35 @@ import logging
from .interactive import *
from .server import Server
from .account import AccountManager
from .system.info import system_banner
from .system.info import system_banner, version
if os.name == 'nt':
import ctypes
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
try:
os.environ["pyserssh_systemmessage"]
except:
os.environ["pyserssh_systemmessage"] = "YES"
try:
os.environ["pyserssh_enable_damp11113"]
except:
os.environ["pyserssh_enable_damp11113"] = "YES"
try:
os.environ["pyserssh_log"]
except:
os.environ["pyserssh_log"] = "NO"
if os.environ["pyserssh_log"] == "NO":
logging.basicConfig(level=logging.CRITICAL)
logger = logging.getLogger("PyserSSH")
logger.disabled = True
#logger.disabled = False
if os.environ["pyserssh_systemmessage"] == "YES":
print(system_banner)
__author__ = "damp11113"
__url__ = "https://github.com/DPSoftware-Foundation/PyserSSH"
__copyright__ = "2023-present"
__license__ = "MIT"
__version__ = version
__department__ = "DPSoftware"
__organization__ = "DOPFoundation"

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -24,179 +24,309 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import logging
import os
import pickle
import time
import atexit
import threading
import hashlib
logger = logging.getLogger("PyserSSH.Account")
class AccountManager:
def __init__(self, anyuser=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.anyuser = anyuser
self.allow_guest = allow_guest
self.historylimit = historylimit
self.autosavedelay = autosavedelay
self.__autosavework = False
self.autosave = autosave
self.__autosaveworknexttime = 0
if self.anyuser:
print("history system can't work if 'anyuser' is enable")
self.__autofile = autofile
if autoload:
self.load(autoloadfile)
self.load(self.__autofile)
if autosave:
self.__autosavethread = threading.Thread(target=self.__autosave)
if self.autosave:
logger.info("starting autosave")
self.__autosavethread = threading.Thread(target=self.__autosave, daemon=True)
self.__autosavethread.start()
atexit.register(self.__saveexit)
def __autosave(self):
self.save("autosave_session.ses")
self.save(self.__autofile)
self.__autosaveworknexttime = time.time() + self.autosavedelay
self.__autosavework = True
while self.__autosavework:
if int(self.__autosaveworknexttime) == int(time.time()):
self.save("autosave_session.ses")
self.save(self.__autofile)
self.__autosaveworknexttime = time.time() + self.autosavedelay
time.sleep(1) # fix cpu load
def __auto_save(func):
def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
if self.autosave:
self.save(self.__autofile, False)
return result
return wrapper
def __saveexit(self):
self.__autosavework = False
self.save("autosave_session.ses")
self.save(self.__autofile)
self.__autosavethread.join()
def validate_credentials(self, username, password):
if username in self.accounts and self.accounts[username]["password"] == password or self.anyuser:
def validate_credentials(self, username, password=None, public_key=None):
if self.allow_guest and not self.has_user(username):
return True
allowed_auth_list = str(self.accounts[username].get("allowed_auth", "")).split(",")
# Check password authentication
if password is not None and "password" in allowed_auth_list:
stored_password = self.accounts[username].get("password", "")
return stored_password == hashlib.md5(password.encode()).hexdigest()
# Check public key authentication
if public_key is not None and "publickey" in allowed_auth_list:
stored_public_key = self.accounts[username].get("public_key", "")
return stored_public_key == public_key
# Check if 'none' authentication is allowed
if "none" in allowed_auth_list:
return True
return False
def has_user(self, username):
return username in self.accounts
def list_users(self):
return list(self.accounts.keys())
def get_allowed_auths(self, username):
if self.has_user(username) and "allowed_auth" in self.accounts[username]:
return self.accounts[username]["allowed_auth"]
return "none"
def get_permissions(self, username):
if username in self.accounts:
if self.has_user(username):
return self.accounts[username]["permissions"]
return []
@__auto_save
def set_prompt(self, username, prompt=">"):
if username in self.accounts:
if self.has_user(username):
self.accounts[username]["prompt"] = prompt
def get_prompt(self, username):
if username in self.accounts and "prompt" in self.accounts[username]:
if self.has_user(username) and "prompt" in self.accounts[username]:
return self.accounts[username]["prompt"]
return ">" # Default prompt if not set for the user
def add_account(self, username, password, permissions={}):
self.accounts[username] = {"password": password, "permissions": permissions}
@__auto_save
def add_account(self, username, password=None, public_key=None, permissions:list=None, sudo=False):
if not self.has_user(username):
allowedlist = []
accountkey = {}
if permissions is None:
permissions = []
if password != None:
allowedlist.append("password")
accountkey["password"] = hashlib.md5(password.encode()).hexdigest()
if public_key != None:
allowedlist.append("publickey")
accountkey["public_key"] = public_key
if password is None and public_key is None:
allowedlist.append("none")
accountkey["permissions"] = permissions
accountkey["allowed_auth"] = ",".join(allowedlist)
self.accounts[username] = accountkey
if sudo:
if self.has_sudo_user():
raise Exception(f"sudo user is exist")
self.accounts[username]["sudo"] = sudo
else:
raise Exception(f"{username} is exist")
def has_sudo_user(self):
return any(account.get("sudo", False) for account in self.accounts.values())
def is_user_has_sudo(self, username):
if self.has_user(username) and "sudo" in self.accounts[username]:
return self.accounts[username]["sudo"]
return False
@__auto_save
def remove_account(self, username):
if self.has_user(username):
del self.accounts[username]
@__auto_save
def change_password(self, username, new_password):
if username in self.accounts:
if self.has_user(username):
self.accounts[username]["password"] = new_password
@__auto_save
def set_permissions(self, username, new_permissions):
if username in self.accounts:
if self.has_user(username):
self.accounts[username]["permissions"] = new_permissions
def save(self, filename="session.ssh"):
with open(filename, 'wb') as file:
pickle.dump(self.accounts, file)
def save(self, filename="session.ses", keep_log=True):
if keep_log:
logger.info(f"saving session to {filename}")
try:
with open(filename, 'wb') as file:
pickle.dump(self.accounts, file)
if keep_log:
logger.info(f"saved session to {filename}")
except Exception as e:
if keep_log:
logger.error(f"save session failed: {e}")
def load(self, filename):
logger.info(f"loading session from {filename}")
try:
with open(filename, 'rb') as file:
self.accounts = pickle.load(file)
logger.info(f"loaded session")
except FileNotFoundError:
print("File not found. No accounts loaded.")
logger.error("can't load session: file not found.")
except Exception as e:
print(f"An error occurred: {e}. No accounts loaded.")
logger.error(f"can't load session: {e}")
@__auto_save
def set_user_sftp_allow(self, username, allow=True):
if username in self.accounts:
if self.has_user(username):
self.accounts[username]["sftp_allow"] = allow
def get_user_sftp_allow(self, username):
if username in self.accounts and "sftp_allow" in self.accounts[username]:
if self.anyuser:
return True
if self.has_user(username) and "sftp_allow" in self.accounts[username]:
return self.accounts[username]["sftp_allow"]
return True
return False
@__auto_save
def set_user_sftp_readonly(self, username, readonly=False):
if username in self.accounts:
if self.has_user(username):
self.accounts[username]["sftp_readonly"] = readonly
def get_user_sftp_readonly(self, username):
if username in self.accounts and "sftp_readonly" in self.accounts[username]:
if self.has_user(username) and "sftp_readonly" in self.accounts[username]:
return self.accounts[username]["sftp_readonly"]
return False
def set_user_sftp_path(self, username, path="/"):
if username in self.accounts:
@__auto_save
def set_user_sftp_root_path(self, username, path="/"):
if self.has_user(username):
if path == "/":
self.accounts[username]["sftp_path"] = ""
self.accounts[username]["sftp_root_path"] = os.getcwd()
else:
self.accounts[username]["sftp_path"] = path
self.accounts[username]["sftp_root_path"] = path
def get_user_sftp_path(self, username):
if username in self.accounts and "sftp_path" in self.accounts[username]:
return self.accounts[username]["sftp_path"]
return ""
def get_user_sftp_root_path(self, username):
if self.has_user(username) and "sftp_root_path" in self.accounts[username]:
return self.accounts[username]["sftp_root_path"]
return os.getcwd()
@__auto_save
def set_user_enable_inputsystem(self, username, enable=True):
if self.has_user(username):
self.accounts[username]["inputsystem"] = enable
def get_user_enable_inputsystem(self, username):
if self.has_user(username) and "inputsystem" in self.accounts[username]:
return self.accounts[username]["inputsystem"]
return True
@__auto_save
def set_user_enable_inputsystem_echo(self, username, echo=True):
if self.has_user(username):
self.accounts[username]["inputsystem_echo"] = echo
def get_user_enable_inputsystem_echo(self, username):
if self.has_user(username) and "inputsystem_echo" in self.accounts[username]:
return self.accounts[username]["inputsystem_echo"]
return True
@__auto_save
def set_banner(self, username, banner):
if self.has_user(username):
self.accounts[username]["banner"] = banner
def get_banner(self, username):
if self.has_user(username) and "banner" in self.accounts[username]:
return self.accounts[username]["banner"]
return None
def get_user_timeout(self, username):
if username in self.accounts and "timeout" in self.accounts[username]:
if self.has_user(username) and "timeout" in self.accounts[username]:
return self.accounts[username]["timeout"]
return None
@__auto_save
def set_user_timeout(self, username, timeout=None):
if username in self.accounts:
if self.has_user(username):
self.accounts[username]["timeout"] = timeout
def get_user_last_login(self, username):
if username in self.accounts and "lastlogin" in self.accounts[username]:
if self.has_user(username) and "lastlogin" in self.accounts[username]:
return self.accounts[username]["lastlogin"]
return None
@__auto_save
def set_user_last_login(self, username, ip, timelogin=time.time()):
if username in self.accounts:
if self.has_user(username):
self.accounts[username]["lastlogin"] = {
"ip": ip,
"time": timelogin
}
@__auto_save
def add_history(self, username, command):
if not self.anyuser:
if username in self.accounts:
if "history" not in self.accounts[username]:
self.accounts[username]["history"] = [] # Initialize history list if it doesn't exist
history_limit = self.historylimit if self.historylimit is not None else float('inf')
self.accounts[username]["history"].append(command)
self.accounts[username]["lastcommand"] = command
# Trim history to the specified limit
if self.historylimit != None:
if len(self.accounts[username]["history"]) > history_limit:
self.accounts[username]["history"] = self.accounts[username]["history"][-history_limit:]
def clear_history(self, username):
if not self.anyuser:
if username in self.accounts:
if self.has_user(username):
if "history" not in self.accounts[username]:
self.accounts[username]["history"] = [] # Initialize history list if it doesn't exist
history_limit = self.historylimit if self.historylimit is not None else float('inf')
self.accounts[username]["history"].append(command)
self.accounts[username]["lastcommand"] = command
# Trim history to the specified limit
if self.historylimit != None:
if len(self.accounts[username]["history"]) > history_limit:
self.accounts[username]["history"] = self.accounts[username]["history"][-history_limit:]
@__auto_save
def clear_history(self, username):
if self.has_user(username):
self.accounts[username]["history"] = [] # Initialize history list if it doesn't exist
def get_history(self, username, index, getall=False):
if not self.anyuser:
if username in self.accounts and "history" in self.accounts[username]:
history = self.accounts[username]["history"]
history.reverse()
if getall:
return history
if self.has_user(username) and "history" in self.accounts[username]:
history = self.accounts[username]["history"]
history.reverse()
if getall:
return history
else:
if index < len(history):
return history[index]
else:
if index < len(history):
return history[index]
else:
return None # Index out of range
return None # User or history not found
return None # Index out of range
return None # User or history not found
def get_lastcommand(self, username):
if not self.anyuser:
if username in self.accounts and "lastcommand" in self.accounts[username]:
command = self.accounts[username]["lastcommand"]
return command
return None # User or history not found
if self.has_user(username) and "lastcommand" in self.accounts[username]:
command = self.accounts[username]["lastcommand"]
return command
return None # User or history not found

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -30,16 +30,38 @@ import shlex
from ..interactive import Send
def are_permissions_met(permission_list, permission_require):
return set(permission_require).issubset(set(permission_list))
class XHandler:
def __init__(self, enablehelp=True, showusageonworng=True):
"""
Initializes the command handler with optional settings for help messages and usage.
Parameters:
enablehelp (bool): Whether help messages are enabled.
showusageonworng (bool): Whether usage information is shown on wrong usage.
"""
self.handlers = {}
self.categories = {}
self.enablehelp = enablehelp
self.showusageonworng = showusageonworng
self.serverself = None
self.commandnotfound = None
def command(self, category=None, name=None, aliases=None):
def command(self, category=None, name=None, aliases=None, permissions: list = None):
"""
Decorator to register a function as a command with optional category, name, aliases, and permissions.
Parameters:
category (str): The category under which the command falls (default: None).
name (str): The name of the command (default: None).
aliases (list): A list of command aliases (default: None).
permissions (list): A list of permissions required to execute the command (default: None).
Returns:
function: The wrapped function.
"""
def decorator(func):
nonlocal name, category
if name is None:
@ -48,21 +70,32 @@ class XHandler:
command_description = func.__doc__ # Read the docstring
parameters = inspect.signature(func).parameters
command_args = []
has_args = False
has_kwargs = False
for param in list(parameters.values())[1:]: # Exclude first parameter (client)
if param.default != inspect.Parameter.empty: # Check if parameter has default value
if param.kind == inspect.Parameter.VAR_POSITIONAL:
has_args = True
elif param.kind == inspect.Parameter.VAR_KEYWORD:
has_kwargs = True
elif param.default != inspect.Parameter.empty: # Check if parameter has default value
if param.annotation == bool:
command_args.append(f"-{param.name}")
command_args.append(f"--{param.name}")
else:
command_args.append((f"{param.name}", param.default))
else:
command_args.append(param.name)
if category is None:
category = 'No Category'
if category not in self.categories:
self.categories[category] = {}
self.categories[category][command_name] = {
'description': command_description.strip() if command_description else "",
'args': command_args
'args': command_args,
"permissions": permissions,
'has_args': has_args,
'has_kwargs': has_kwargs
}
self.handlers[command_name] = func
if aliases:
@ -73,9 +106,20 @@ class XHandler:
return decorator
def call(self, client, command_string):
"""
Processes a command string, validates arguments, and calls the corresponding function.
Parameters:
client (object): The client sending the command.
command_string (str): The command string to be executed.
Returns:
Any: The result of the command function, or an error message if invalid.
"""
tokens = shlex.split(command_string)
command_name = tokens[0]
args = tokens[1:]
if command_name == "help" and self.enablehelp:
if args:
Send(client, self.get_help_command_info(args[0]))
@ -85,58 +129,68 @@ class XHandler:
else:
if command_name in self.handlers:
command_func = self.handlers[command_name]
command_info = self.get_command_info(command_name)
if command_info and command_info.get('permissions'):
if not are_permissions_met(self.serverself.accounts.get_permissions(client.get_name()), command_info.get('permissions')) or not self.serverself.accounts.is_user_has_sudo(client.get_name()):
Send(client, f"Permission denied. You do not have permission to execute '{command_name}'.")
return
command_args = inspect.signature(command_func).parameters
if len(args) % 2 != 0 and not args[0].startswith("--"):
if self.showusageonworng:
Send(client, self.get_help_command_info(command_name))
else:
Send(client, f"Invalid number of arguments for command '{command_name}'.")
return
# Parse arguments
final_args = {}
for i in range(0, len(args), 2):
if args[i].startswith("--"):
arg_name = args[i].lstrip('--')
final_kwargs = {}
i = 0
while i < len(args):
arg = args[i]
if arg.startswith('-'):
arg_name = arg.lstrip('-')
if arg_name not in command_args:
if self.showusageonworng:
Send(client, self.get_help_command_info(command_name))
else:
Send(client, f"Invalid flag '{arg_name}' for command '{command_name}'.")
Send(client, f"Invalid flag '{arg_name}' for command '{command_name}'.")
return
try:
args[i + 1]
except:
pass
if command_args[arg_name].annotation == bool:
final_args[arg_name] = True
i += 1
else:
if self.showusageonworng:
Send(client, self.get_help_command_info(command_name))
if i + 1 < len(args):
final_args[arg_name] = args[i + 1]
i += 2
else:
Send(client, f"value '{args[i + 1]}' not available for '{arg_name}' flag for command '{command_name}'.")
return
final_args[arg_name] = True
if self.showusageonworng:
Send(client, self.get_help_command_info(command_name))
Send(client, f"Missing value for flag '{arg_name}' for command '{command_name}'.")
return
else:
arg_name = args[i].lstrip('-')
if arg_name not in command_args:
if self.showusageonworng:
Send(client, self.get_help_command_info(command_name))
if command_info['has_args']:
final_args.setdefault('args', []).append(arg)
elif command_info['has_kwargs']:
final_kwargs[arg] = args[i + 1] if i + 1 < len(args) else None
i += 1
else:
if len(final_args) + 1 < len(command_args):
param = list(command_args.values())[len(final_args) + 1]
final_args[param.name] = arg
else:
Send(client, f"Invalid argument '{arg_name}' for command '{command_name}'.")
return
arg_value = args[i + 1]
final_args[arg_name] = arg_value
# Match parsed arguments to function parameters
final_args_list = []
if self.showusageonworng:
Send(client, self.get_help_command_info(command_name))
Send(client, f"Unexpected argument '{arg}' for command '{command_name}'.")
return
i += 1
# Check for required positional arguments
for param in list(command_args.values())[1:]: # Skip client argument
if param.name in final_args:
final_args_list.append(final_args[param.name])
elif param.default != inspect.Parameter.empty:
final_args_list.append(param.default)
else:
if param.name not in final_args and param.default == inspect.Parameter.empty:
if self.showusageonworng:
Send(client, self.get_help_command_info(command_name))
else:
Send(client, f"Missing required argument '{param.name}' for command '{command_name}'")
Send(client, f"Missing required argument '{param.name}' for command '{command_name}'")
return
final_args_list = [final_args.get(param.name, param.default) for param in list(command_args.values())[1:]]
if command_info['has_kwargs']:
final_args_list.append(final_kwargs)
return command_func(client, *final_args_list)
else:
if self.commandnotfound:
@ -147,6 +201,15 @@ class XHandler:
return
def get_command_info(self, command_name):
"""
Retrieves information about a specific command, including its description, arguments, and permissions.
Parameters:
command_name (str): The name of the command.
Returns:
dict: A dictionary containing command details such as name, description, args, and permissions.
"""
found_command = None
for category, commands in self.categories.items():
if command_name in commands:
@ -165,10 +228,22 @@ class XHandler:
'name': command_name,
'description': found_command['description'].strip() if found_command['description'] else "",
'args': found_command['args'],
'category': category
'category': category,
'permissions': found_command['permissions'],
'has_args': found_command['has_args'],
'has_kwargs': found_command['has_kwargs']
}
def get_help_command_info(self, command):
"""
Generates a detailed help message for a specific command.
Parameters:
command (str): The name of the command.
Returns:
str: The formatted help message for the command.
"""
command_info = self.get_command_info(command)
aliases = command_info.get('aliases', [])
help_message = f"{command_info['name']}"
@ -185,9 +260,19 @@ class XHandler:
help_message += f" [-{arg[0]} {arg[1]}]"
else:
help_message += f" <{arg}>"
if command_info['has_args']:
help_message += " [<args>...]"
if command_info['has_kwargs']:
help_message += " [--<key>=<value>...]"
return help_message
def get_help_message(self):
"""
Generates a general help message listing all categories and their associated commands.
Returns:
str: The formatted help message containing all commands and categories.
"""
help_message = ""
for category, commands in self.categories.items():
help_message += f"{category}:\n"
@ -199,7 +284,15 @@ class XHandler:
return help_message
def get_all_commands(self):
"""
Retrieves all registered commands, grouped by category.
Returns:
dict: A dictionary where each key is a category name and the value is a
dictionary of commands within that category.
"""
all_commands = {}
for category, commands in self.categories.items():
all_commands[category] = commands
return all_commands
return all_commands

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License

View File

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

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -24,68 +24,57 @@ 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 time
from ..interactive import Send
def clickable_url(url, link_text=""):
"""
Creates a clickable URL in a terminal client with optional link text.
Args:
url (str): The URL to be linked.
link_text (str, optional): The text to be displayed for the link. Defaults to an empty string, which will display the URL itself.
Returns:
str: A terminal escape sequence that makes the URL clickable with the provided link text.
"""
return f"\033]8;;{url}\033\\{link_text}\033]8;;\033\\"
class BasicTextFormatter:
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"
}
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.
@staticmethod
def format_text(text, color=None, color_level=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)
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.
if color in BasicTextFormatter.TEXT_COLORS:
if color_level in BasicTextFormatter.TEXT_COLOR_LEVELS:
color_code = BasicTextFormatter.TEXT_COLORS[color]
color_format = BasicTextFormatter.TEXT_COLOR_LEVELS[color_level].format(color_code)
formatted_text += color_format
else:
formatted_text += BasicTextFormatter.TEXT_COLORS[color]
This function simulates a typing effect by printing the text character by character,
while dimming the unprinted characters.
"""
printed_text = ""
for i, char in enumerate(text):
# Print the already printed text normally
Send(client, printed_text + char, ln=False)
if background in BasicTextFormatter.BACKGROUND_COLORS:
formatted_text += BasicTextFormatter.BACKGROUND_COLORS[background]
# Calculate the unprinted text and dim it
not_printed_text = text[i + 1:]
dimmed_text = ''.join([f"\033[2m{char}\033[0m" for char in not_printed_text])
if attributes in BasicTextFormatter.TEXT_ATTRIBUTES:
formatted_text += BasicTextFormatter.TEXT_ATTRIBUTES[attributes]
# Print the dimmed text for the remaining characters
Send(client, dimmed_text, ln=False)
if target_text == "":
formatted_text += text + BasicTextFormatter.RESET
else:
formatted_text += text[:start_index] + text[start_index:end_index] + BasicTextFormatter.RESET + text[end_index:]
# Wait before printing the next character
time.sleep(delay)
return formatted_text
# Clear the line to update the text in the next iteration
Send(client, '\r', ln=False)
# Update the printed_text to include the current character
printed_text += char
if ln:
Send(client, "") # Send a newline after the entire text is printed

View File

@ -0,0 +1,50 @@
"""
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.
"""
from ..interactive import Send
def ShowCursor(client, show=True):
"""
Shows or hides the cursor for a specific client.
Args:
client (Client): The client to show/hide the cursor for.
show (bool, optional): A flag to determine whether to show or hide the cursor. Defaults to True (show cursor).
"""
if show:
Send(client, "\033[?25h", ln=False) # Show cursor
else:
Send(client, "\033[?25l", ln=False) # Hide cursor
def SendBell(client):
"""
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,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -37,22 +37,119 @@ from ..system.sysfunc import replace_enter_with_crlf
def Print(channel, string, start="", end="\n"):
channel.send(replace_enter_with_crlf(start + string + end))
try:
from damp11113.utils import get_size_unit2, center_string, TextFormatter, insert_string
except:
raise ModuleNotFoundError("This extension is require damp11113-library")
def get_size_unit2(number, unitp, persec=True, unitsize=1024, decimal=True, space=" "):
for unit in ['', 'K', 'M', 'G', 'T', 'P']:
if number < unitsize:
if decimal:
num = f"{number:.2f}"
else:
num = int(number)
steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]']
steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]']
steps3 = ['[ ]', '[- ]', '[-- ]', '[ --]', '[ -]', '[ ]', '[ -]', '[ --]', '[-- ]', '[- ]']
steps4 = ['[ ]', '[- ]', '[ - ]', '[ -]', '[ ]', '[ -]', '[ - ]', '[- ]', '[ ]']
steps5 = ['[ ]', '[ -]', '[ --]', '[---]', '[-- ]', '[- ]']
steps6 = ['[ ]', '[ -]', '[ - ]', '[- ]']
if persec:
return f"{num}{space}{unit}{unitp}/s"
else:
return f"{num}{space}{unit}{unitp}"
number /= unitsize
def center_string(main_string, replacement_string):
# Find the center index of the main string
center_index = len(main_string) // 2
# Calculate the start and end indices for replacing
start_index = center_index - len(replacement_string) // 2
end_index = start_index + len(replacement_string)
# Replace the substring at the center
new_string = main_string[:start_index] + replacement_string + main_string[end_index:]
return new_string
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(text, color=None, color_level=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 in TextFormatter.TEXT_COLORS:
if color_level in TextFormatter.TEXT_COLOR_LEVELS:
color_code = TextFormatter.TEXT_COLORS[color]
color_format = TextFormatter.TEXT_COLOR_LEVELS[color_level].format(color_code)
formatted_text += color_format
else:
formatted_text += TextFormatter.TEXT_COLORS[color]
if background in TextFormatter.BACKGROUND_COLORS:
formatted_text += TextFormatter.BACKGROUND_COLORS[background]
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
def insert_string(base, inserted, position=0):
return base[:position] + inserted + base[position + len(inserted):]
class Steps:
steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]']
steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]']
steps3 = ['[ ]', '[- ]', '[-- ]', '[ --]', '[ -]', '[ ]', '[ -]', '[ --]', '[-- ]', '[- ]']
steps4 = ['[ ]', '[- ]', '[ - ]', '[ -]', '[ ]', '[ -]', '[ - ]', '[- ]', '[ ]']
steps5 = ['[ ]', '[ -]', '[ --]', '[---]', '[-- ]', '[- ]']
steps6 = ['[ ]', '[ -]', '[ - ]', '[- ]']
expand_contract = ['[ ]', '[= ]', '[== ]', '[=== ]', '[====]', '[ ===]', '[ ==]', '[ =]', '[ ]']
rotating_dots = ['. ', '.. ', '... ', '.... ', '.....', ' ....', ' ...', ' ..', ' .', ' ']
bouncing_ball = ['o ', ' o ', ' o ', ' o ', ' o ', ' o', ' o ', ' o ', ' o ', ' o ', 'o ']
left_right_dots = ['[ ]', '[. ]', '[.. ]', '[... ]', '[....]', '[ ...]', '[ ..]', '[ .]', '[ ]']
expanding_square = ['[ ]', '[■]', '[■■]', '[■■■]', '[■■■■]', '[■■■]', '[■■]', '[■]', '[ ]']
spinner = ['|', '/', '-', '\\', '|', '/', '-', '\\']
zigzag = ['/ ', ' / ', ' / ', ' /', ' / ', ' / ', '/ ', '\\ ', ' \\ ', ' \\ ', ' \\', ' \\ ', ' \\ ', '\\ ']
arrows = ['', '←← ', '←←←', '←← ', '', '', '→→ ', '→→→', '→→ ', '']
snake = ['[> ]', '[=> ]', '[==> ]', '[===> ]', '[====>]', '[ ===>]', '[ ==>]', '[ =>]', '[ >]']
loading_bar = ['[ ]', '[= ]', '[== ]', '[=== ]', '[==== ]', '[===== ]', '[====== ]', '[======= ]', '[======== ]', '[========= ]', '[==========]']
class indeterminateStatus:
def __init__(self, client, desc="Loading...", end="[ OK ]", timeout=0.1, fail='[FAILED]', steps=None):
self.client = client
self.desc = desc
self.end = end
self.timeout = timeout
@ -60,13 +157,14 @@ class indeterminateStatus:
self._thread = Thread(target=self._animate, daemon=True)
if steps is None:
self.steps = steps1
self.steps = Steps.steps1
else:
self.steps = steps
self.done = False
self.fail = False
def start(self):
"""Start progress bar"""
self._thread.start()
return self
@ -81,12 +179,14 @@ class indeterminateStatus:
self.start()
def stop(self):
"""stop progress"""
self.done = True
cols = self.client["windowsize"]["width"]
Print(self.client['channel'], "\r" + " " * cols, end="")
Print(self.client['channel'], f"\r{self.end}")
def stopfail(self):
"""stop progress with error or fail"""
self.done = True
self.fail = True
cols = self.client["windowsize"]["width"]
@ -147,7 +247,7 @@ class LoadingProgress:
self._thread = Thread(target=self._animate, daemon=True)
if steps is None:
self.steps = steps1
self.steps = Steps.steps1
else:
self.steps = steps
@ -164,14 +264,17 @@ class LoadingProgress:
self.currentprint = ""
def start(self):
"""Start progress bar"""
self._thread.start()
self.startime = time.perf_counter()
return self
def update(self, i):
def update(self, i=1):
"""update progress"""
self.current += i
def updatebuffer(self, i):
def updatebuffer(self, i=1):
"""update buffer progress"""
self.currentbuffer += i
def _animate(self):
@ -287,12 +390,14 @@ class LoadingProgress:
self.start()
def stop(self):
"""stop progress"""
self.done = True
cols = self.client["windowsize"]["width"]
Print(self.client["channel"], "\r" + " " * cols, end="")
Print(self.client["channel"], f"\r{self.end}")
def stopfail(self):
"""stop progress with error or fail"""
self.done = True
self.fail = True
cols = self.client["windowsize"]["width"]

View File

@ -0,0 +1,294 @@
"""
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 socket
import threading
import brotli
import numpy as np
import cv2
from PIL import ImageGrab
import struct
import queue
import pickle
import mouse
import keyboard
import logging
from ..system.clientype import Client
logger = logging.getLogger("PyserSSH.Ext.RemoDeskSSH")
class Protocol:
def __init__(self, server):
self.listclient = []
self.first = True
self.running = False
self.server = server
self.buffer = queue.Queue(maxsize=10)
def _handle_client(self):
try:
while self.running:
data2send = self.buffer.get()
for iclient in self.listclient:
try:
iclient[2].sendall(data2send)
except Exception as e:
iclient[2].close()
self.listclient.remove(iclient)
if not self.listclient:
self.running = False
self.first = True
logger.info("No clients connected. Server is standby")
break
except socket.error:
pass
except Exception as e:
logger.error(f"Error in handle_client: {e}")
def _handle_client_commands(self, client, id):
try:
while True:
client_socket = client.get_subchannel(id)
try:
# Receive the length of the data
data_length = self._receive_exact(client_socket, 4)
if not data_length:
break
commandmetadata = struct.unpack('!I', data_length)
command_data = self._receive_exact(client_socket, commandmetadata[0])
command = pickle.loads(command_data)
if command:
self.handle_commands(command, client)
except socket.error:
break
except Exception as e:
logger.error(f"Error in handle_client_commands: {e}")
def handle_commands(self, command, client):
pass
def _receive_exact(self, socket, n):
"""Helper function to receive exactly n bytes."""
data = b''
while len(data) < n:
packet = socket.recv(n - len(data))
if not packet:
return None
data += packet
return data
def init(self, client):
pass
def handle_new_client(self, client: Client, directchannel=None):
if directchannel:
id = directchannel.get_id()
channel = directchannel
else:
logger.info("waiting remote channel")
id, channel = client.open_new_subchannel(5)
if id == None or channel == None:
logger.info("client is not connect in 5 sec")
return
self.listclient.append([client, id, channel])
if self.first:
self.running = True
handle_client_thread = threading.Thread(target=self._handle_client, daemon=True)
handle_client_thread.start()
self.init(client)
self.first = False
command_thread = threading.Thread(target=self._handle_client_commands, args=(client, id), daemon=True)
command_thread.start()
class RemoDesk(Protocol):
def __init__(self, server=None, quality=50, compression=50, format="jpeg", resolution: set[int, int] = None, activity_threshold=None, second_compress=True):
"""
Args:
server: ssh server
quality: quality of remote
compression: percent of compression 0-100 %
format: jpeg, webp, avif
resolution: resolution of remote
"""
super().__init__(server)
self.quality = quality
self.compression = compression
self.format = format
self.resolution = resolution
self.threshold = activity_threshold
self.compress2 = second_compress
self.screensize = ()
self.previous_frame = None
def _capture_screen(self):
try:
screenshot = ImageGrab.grab()
self.screensize = screenshot.size
img_np = np.array(screenshot)
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
return img_bgr
except:
return b""
def _detect_activity(self, current_frame):
if self.threshold:
if self.previous_frame is None:
self.previous_frame = current_frame
return False # No previous frame to compare to
# Compute the absolute difference between the current frame and the previous frame
diff = cv2.absdiff(current_frame, self.previous_frame)
# Convert the difference to grayscale
gray_diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
# Apply a threshold to get a binary image
_, thresh = cv2.threshold(gray_diff, self.threshold, 255, cv2.THRESH_BINARY)
# Calculate the number of non-zero pixels in the thresholded image
non_zero_count = np.count_nonzero(thresh)
# Update the previous frame
self.previous_frame = current_frame
# If there are enough non-zero pixels, we consider it as activity
return non_zero_count > 500 # You can adjust the threshold as needed
else:
return True
def _imagenc(self, image):
if self.format == "webp":
retval, buffer = cv2.imencode('.webp', image, [int(cv2.IMWRITE_WEBP_QUALITY), self.quality])
elif self.format == "jpeg":
retval, buffer = cv2.imencode('.jpeg', image, [int(cv2.IMWRITE_JPEG_QUALITY), self.quality])
elif self.format == "avif":
retval, buffer = cv2.imencode('.avif', image, [int(cv2.IMWRITE_AVIF_QUALITY), self.quality])
else:
raise TypeError(f"{self.format} is not supported")
if not retval:
raise ValueError("image encoding failed.")
return np.array(buffer).tobytes()
def _translate_coordinates(self, x, y):
if self.resolution:
translated_x = int(x * (self.screensize[0] / self.resolution[0]))
translated_y = int(y * (self.screensize[1] / self.resolution[1]))
else:
translated_x = int(x * (self.screensize[0] / 1920))
translated_y = int(y * (self.screensize[1] / 1090))
return translated_x, translated_y
def _convert_quality(self, quality):
brotli_quality = int(quality / 100 * 11)
lgwin = int(10 + (quality / 100 * (24 - 10)))
return brotli_quality, lgwin
def _capture(self):
while self.running:
screen_image = self._capture_screen()
if self._detect_activity(screen_image):
if self.resolution:
screen_image = cv2.resize(screen_image, self.resolution, interpolation=cv2.INTER_NEAREST)
else:
self.resolution = self.screensize
data = self._imagenc(screen_image)
if self.compress2:
bquality, lgwin = self._convert_quality(self.compression)
data = brotli.compress(data, quality=bquality, lgwin=lgwin)
data_length = struct.pack('!III', len(data), self.resolution[0], self.resolution[1])
data2send = data_length + data
print(f"Sending data length: {len(data2send)}")
self.buffer.put(data2send)
def handle_commands(self, command, client):
action = command["action"]
data = command["data"]
if action == "move_mouse":
x, y = data["x"], data["y"]
rx, ry = self._translate_coordinates(x, y)
mouse.move(rx, ry)
elif action == "click_mouse":
button = data["button"]
state = data["state"]
if button == 1:
if state == "down":
mouse.press()
else:
mouse.release()
elif button == 2:
if state == "down":
mouse.press(mouse.MIDDLE)
else:
mouse.release(mouse.MIDDLE)
elif button == 3:
if state == "down":
mouse.press(mouse.RIGHT)
else:
mouse.release(mouse.RIGHT)
# elif button == 4:
# mouse.wheel()
# elif button == 5:
# mouse.wheel(-1)
elif action == "keyboard":
key = data["key"]
state = data["state"]
if state == "down":
keyboard.press(key)
else:
keyboard.release(key)
def init(self, client):
capture_thread = threading.Thread(target=self._capture, daemon=True)
capture_thread.start()

View File

@ -0,0 +1,140 @@
"""
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 logging
from ..interactive import Send
logger = logging.getLogger("PyserSSH.Ext.ServerUtils")
def kickbyusername(server, username, reason=None):
"""
Kicks a user from the server by their username.
Args:
server (Server): The server object where clients are connected.
username (str): The username of the client to be kicked.
reason (str, optional): The reason for kicking the user. If None, no reason is provided.
"""
for peername, client_handler in list(server.client_handlers.items()):
if client_handler["current_user"] == username:
channel = client_handler.get("channel")
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"])
if reason is None:
if channel:
channel.close()
logger.info(f"User '{username}' has been kicked.")
else:
if channel:
Send(channel, f"You have been disconnected for {reason}")
channel.close()
logger.info(f"User '{username}' has been kicked by reason {reason}.")
def kickbypeername(server, peername, reason=None):
"""
Kicks a user from the server by their peername.
Args:
server (Server): The server object where clients are connected.
peername (str): The peername of the client to be kicked.
reason (str, optional): The reason for kicking the user. If None, no reason is provided.
"""
client_handler = server.client_handlers.get(peername)
if client_handler:
channel = client_handler.get("channel")
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"])
if reason is None:
if channel:
channel.close()
logger.info(f"peername '{peername}' has been kicked.")
else:
if channel:
Send(channel, f"You have been disconnected for {reason}")
channel.close()
logger.info(f"peername '{peername}' has been kicked by reason {reason}.")
def kickall(server, reason=None):
"""
Kicks all users from the server.
Args:
server (Server): The server object where clients are connected.
reason (str, optional): The reason for kicking all users. If None, no reason is provided.
"""
for peername, client_handler in server.client_handlers.items():
channel = client_handler.get("channel")
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"])
if reason is None:
if channel:
channel.close()
else:
if channel:
Send(channel, f"You have been disconnected for {reason}")
channel.close()
if reason is None:
server.client_handlers.clear()
logger.info("All users have been kicked.")
else:
logger.info(f"All users have been kicked by reason {reason}.")
def broadcast(server, message):
"""
Broadcasts a message to all connected clients.
Args:
server (Server): The server object where clients are connected.
message (str): The message to send to all clients.
"""
for client_handler in server.client_handlers.values():
channel = client_handler.get("channel")
if channel:
try:
# Send the message to the client
Send(channel, message)
except Exception as e:
logger.error(f"Error occurred while broadcasting message: {e}")
def sendto(server, username, message):
"""
Sends a message to a specific user by their username.
Args:
server (Server): The server object where clients are connected.
username (str): The username of the client to send the message to.
message (str): The message to send to the specified user.
"""
for client_handler in server.client_handlers.values():
if client_handler.get("current_user") == username:
channel = client_handler.get("channel")
if channel:
try:
# Send the message to the specific client
Send(channel, message)
except Exception as e:
logger.error(f"Error occurred while sending message to {username}: {e}")
break
else:
logger.warning(f"User '{username}' not found.")

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -29,19 +29,51 @@ import socket
from .system.sysfunc import replace_enter_with_crlf
def Send(client, string, ln=True):
channel = client["channel"]
def Send(client, string, ln=True, directchannel=False):
if directchannel:
channel = client
else:
channel = client["channel"]
if ln:
channel.send(replace_enter_with_crlf(str(string) + "\n"))
else:
channel.send(replace_enter_with_crlf(str(string)))
def Clear(client, oldclear=False):
def NewSend(client, *astring, ln=True, end=b'\n', sep=b' ', directchannel=False):
if directchannel:
channel = client
else:
channel = client["channel"]
if ln:
if not b'\n' in end:
end += b'\n'
else:
# Ensure that `end` does not contain `b'\n'` if `ln` is False
end = end.replace(b'\n', b'')
# Prepare the strings to be sent
if astring:
for i, s in enumerate(astring):
# Convert `s` to bytes if it's a string
if isinstance(s, str):
s = s.encode('utf-8')
# Use a hypothetical `replace_enter_with_crlf` function if needed
channel.send(replace_enter_with_crlf(s))
if i != len(astring) - 1:
channel.send(sep)
channel.send(end)
def Clear(client, oldclear=False, keep=False):
sx, sy = client["windowsize"]["width"], client["windowsize"]["height"]
if oldclear:
for x in range(sy):
Send(client, '\b \b' * sx, ln=False) # Send newline after each line
elif keep:
Send(client, "\033[2J", ln=False)
Send(client, "\033[H", ln=False)
else:
Send(client, "\033[3J", ln=False)
Send(client, "\033[1J", ln=False)
@ -50,8 +82,11 @@ def Clear(client, oldclear=False):
def Title(client, title):
Send(client, f"\033]0;{title}\007", ln=False)
def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=True, password=False, passwordmask=b"*", noabort=False, timeout=0):
channel = client["channel"]
def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=True, password=False, passwordmask=b"*", noabort=False, timeout=0, directchannel=False):
if directchannel:
channel = client
else:
channel = client["channel"]
channel.send(replace_enter_with_crlf(prompt))
@ -112,6 +147,8 @@ def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=T
channel.sendall(b'\r\n')
raise
else:
channel.setblocking(False)
channel.settimeout(None)
output = buffer.decode('utf-8')
# Return default value if specified and no input given
@ -120,7 +157,7 @@ def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=T
else:
return output
def wait_inputkey(client, prompt="", raw=False, timeout=0):
def wait_inputkey(client, prompt="", raw=True, timeout=0):
channel = client["channel"]
if prompt != "":
@ -139,16 +176,74 @@ def wait_inputkey(client, prompt="", raw=False, timeout=0):
if bool(re.compile(b'\x1b\[[0-9;]*[mGK]').search(byte)):
pass
channel.setblocking(False)
channel.settimeout(None)
if prompt != "":
channel.send("\r\n")
return byte.decode('utf-8') # only regular character
else:
channel.setblocking(False)
channel.settimeout(None)
if prompt != "":
channel.send("\r\n")
return byte
except socket.timeout:
channel.setblocking(False)
channel.settimeout(None)
channel.send("\r\n")
if prompt != "":
channel.send("\r\n")
return None
except Exception:
channel.setblocking(False)
channel.settimeout(None)
if prompt != "":
channel.send("\r\n")
raise
def wait_inputmouse(client, timeout=0):
channel = client["channel"]
Send(client, "\033[?1000h", ln=False)
if timeout != 0:
channel.settimeout(timeout)
try:
byte = channel.recv(10)
if not byte or byte == b'\x04':
raise EOFError()
if byte.startswith(b'\x1b[M'):
# Parse mouse event
if len(byte) < 6 or not byte.startswith(b'\x1b[M'):
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[?1000l", ln=False)
return None, None, None
# Extract button, x, y from the sequence
button = byte[3] - 32
x = byte[4] - 32
y = byte[5] - 32
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[?1000l", ln=False)
return button, x, y
else:
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[?1000l", ln=False)
return byte, None, None
except socket.timeout:
channel.setblocking(False)
channel.settimeout(None)
channel.send("\r\n")
Send(client, "\033[?1000l", ln=False)
return None, None, None
except Exception:
channel.setblocking(False)
channel.settimeout(None)
@ -173,18 +268,22 @@ def wait_choose(client, choose, prompt="", timeout=0):
exported = " ".join(tempchooselist)
if prompt.strip() == "":
Send(channel, f'\r{exported}', ln=False)
Send(client, f'\r{exported}', ln=False)
else:
Send(channel, f'\r{prompt}{exported}', ln=False)
Send(client, f'\r{prompt}{exported}', ln=False)
keyinput = wait_inputkey(channel, raw=True)
keyinput = wait_inputkey(client, raw=True)
if keyinput == b'\r': # Enter key
Send(channel, "\033[K")
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[K")
return chooseindex
elif keyinput == b'\x03': # ' ctrl+c' key for cancel
Send(channel, "\033[K")
return None
channel.setblocking(False)
channel.settimeout(None)
Send(client, "\033[K")
return 0
elif keyinput == b'\x1b[D': # Up arrow key
chooseindex -= 1
if chooseindex < 0:

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -25,37 +25,42 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import os
import time
import paramiko
import threading
from functools import wraps
import logging
import socket
import random
import traceback
from .system.SFTP import SSHSFTPServer
from .system.sysfunc import replace_enter_with_crlf
from .system.interface import Sinterface
from .interactive import *
from .system.inputsystem import expect
from .system.info import system_banner, __version__
from .system.info import version, system_banner
from .system.clientype import Client as Clientype
from .system.ProWrapper import SSHTransport, TelnetTransport, ITransport
#paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22)
# paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22)
sftpclient = ["WinSCP", "Xplore"]
logger = logging.getLogger("PyserSSH")
logger = logging.getLogger("PyserSSH.Server")
class Server:
def __init__(self, accounts, system_message=True, disable_scroll_with_arrow=True, sftp=True, sftproot=os.getcwd(), system_commands=True, compression=True, usexternalauth=False, history=True, inputsystem=True, XHandler=None, title=f"PyserSSH v{__version__}", inspeed=32768):
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):
"""
A simple SSH server
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)
sftp set to True to enable SFTP server
system_commands set to False to disable system commmands
compression set to False to disable SSH compression
enable_remote_status set to True to enable mobaxterm remote monitor (Beta)
"""
self._event_handlers = {}
self.sysmess = system_message
self.client_handlers = {} # Dictionary to store event handlers for each client
self.current_users = {} # Dictionary to store current_user for each connected client
self.accounts = accounts
self.disable_scroll_with_arrow = disable_scroll_with_arrow
self.sftproot = sftproot
self.sftpena = sftp
self.enasyscom = system_commands
self.compressena = compression
@ -65,243 +70,289 @@ class Server:
self.XHandler = XHandler
self.title = title
self.inspeed = inspeed
self.enaloginbanner = enable_preauth_banner
self.enasysexec = enable_exec_system_command
self.enaremostatus = enable_remote_status
self.inputsysecho = inputsystem_echo
self.system_banner = system_banner
if self.XHandler != None:
self.XHandler.serverself = self
self._event_handlers = {}
self.client_handlers = {} # Dictionary to store event handlers for each client
self.__processmode = None
self.isrunning = False
self.__daemon = False
self.private_key = ""
self.__custom_server_args = ()
self.__custom_server = None
self.__protocol = "ssh"
if self.enasyscom:
print("\033[33m!!Warning!! System commands is enable! \033[0m")
def on_user(self, event_name):
"""Handle event"""
def decorator(func):
@wraps(func)
def wrapper(channel, *args, **kwargs):
def wrapper(client, *args, **kwargs):
# Ignore the third argument
filtered_args = args[:2] + args[3:]
return func(channel, *filtered_args, **kwargs)
return func(client, *filtered_args, **kwargs)
self._event_handlers[event_name] = wrapper
return wrapper
return decorator
def handle_client_disconnection(self, peername, current_user):
if peername in self.client_handlers:
del self.client_handlers[peername]
logger.info(f"User {current_user} disconnected")
def handle_client_disconnection(self, handler, chandlers):
if not chandlers["transport"].is_active():
if handler:
handler(chandlers)
del self.client_handlers[chandlers["peername"]]
def _handle_event(self, event_name, *args, **kwargs):
handler = self._event_handlers.get(event_name)
if handler:
handler(*args, **kwargs)
if event_name == "error" and isinstance(args[0], Clientype):
args[0].last_error = traceback.format_exc()
if event_name == "disconnected":
self.handle_client_disconnection(*args, **kwargs)
self.handle_client_disconnection(handler, *args, **kwargs)
elif handler:
return handler(*args, **kwargs)
def handle_client(self, client, addr):
bh_session = paramiko.Transport(client)
bh_session.add_server_key(self.private_key)
if self.sftpena:
SSHSFTPServer.ROOT = self.sftproot
SSHSFTPServer.ACCOUNT = self.accounts
SSHSFTPServer.CLIENTHANDELES = self.client_handlers
bh_session.set_subsystem_handler('sftp', paramiko.SFTPServer, SSHSFTPServer)
if self.compressena:
bh_session.use_compression(True)
else:
bh_session.use_compression(False)
bh_session.default_window_size = 2147483647
bh_session.packetizer.REKEY_BYTES = pow(2, 40)
bh_session.packetizer.REKEY_PACKETS = pow(2, 40)
bh_session.default_max_packet_size = self.inspeed
def _handle_client(self, socketchannel, addr):
self._handle_event("preserver", socketchannel)
logger.info("Starting session...")
server = Sinterface(self)
bh_session.start_server(server=server)
logger.info(bh_session.remote_version)
if not self.__custom_server:
if self.__protocol.lower() == "telnet":
bh_session = TelnetTransport(socketchannel, server) # Telnet server
else:
bh_session = SSHTransport(socketchannel, server, self.private_key) # SSH server
else:
bh_session = self.__custom_server(socketchannel, server, *self.__custom_server_args) # custom server
bh_session.enable_compression(self.compressena)
bh_session.max_packet_size(self.inspeed)
try:
bh_session.start_server()
except:
return
channel = bh_session.accept()
if self.sftpena:
bh_session.set_subsystem_handler('sftp', paramiko.SFTPServer, SSHSFTPServer, channel, self.accounts, self.client_handlers)
if not bh_session.is_authenticated():
logger.warning("user not authenticated")
bh_session.close()
return
if channel is None:
logger.warning("no channel")
bh_session.close()
return
try:
logger.info("user authenticated")
peername = channel.getpeername()
peername = bh_session.getpeername()
if peername not in self.client_handlers:
# Create a new event handler for this client if it doesn't exist
self.client_handlers[peername] = {
"event_handlers": {},
"current_user": None,
"channel": channel, # Associate the channel with the client handler,
"last_activity_time": None,
"connecttype": None,
"last_login_time": None,
"windowsize": {},
"x11": {}
}
self.client_handlers[peername] = Clientype(channel, bh_session, peername)
client_handler = self.client_handlers[peername]
client_handler["current_user"] = server.current_user
client_handler["current_user"] = bh_session.get_username()
client_handler["channel"] = channel # Update the channel attribute for the client handler
client_handler["transport"] = bh_session # Update the channel attribute for the client handler
client_handler["last_activity_time"] = time.time()
client_handler["last_login_time"] = time.time()
client_handler["prompt"] = self.accounts.get_prompt(bh_session.get_username())
client_handler["session_id"] = random.randint(10000, 99999) + int(time.time() * 1000)
self.accounts.set_user_last_login(self.client_handlers[channel.getpeername()]["current_user"], peername[0])
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 channel.out_window_size == bh_session.default_window_size:
while self.client_handlers[channel.getpeername()]["windowsize"] == {}:
pass
if not (int(channel.get_out_window_size()) == int(bh_session.get_default_window_size()) and bh_session.get_connection_type() == "SSH"):
#timeout for waiting 10 sec
for i in range(100):
if self.client_handlers[channel.getpeername()]["windowsize"]:
break
time.sleep(0.1)
channel.send(f"\033]0;{self.title}\007".encode())
if self.client_handlers[channel.getpeername()]["windowsize"] == {}:
logger.info("timeout for waiting window size in 10 sec")
self.client_handlers[channel.getpeername()]["windowsize"] = {
"width": 80,
"height": 24,
"pixelwidth": 0,
"pixelheight": 0
}
if self.sysmess:
channel.sendall(replace_enter_with_crlf(self.system_banner))
channel.sendall(replace_enter_with_crlf("\n"))
try:
self._handle_event("pre-shell", self.client_handlers[channel.getpeername()])
except Exception as e:
self._handle_event("error", self.client_handlers[channel.getpeername()], e)
while self.client_handlers[channel.getpeername()]["isexeccommandrunning"]:
time.sleep(0.1)
userbanner = self.accounts.get_banner(self.client_handlers[channel.getpeername()]["current_user"])
if self.accounts.get_user_enable_inputsystem_echo(self.client_handlers[channel.getpeername()]["current_user"]) and self.inputsysecho:
echo = True
else:
echo = False
if echo:
if self.title.strip() != "":
channel.send(f"\033]0;{self.title}\007".encode())
if self.sysmess or userbanner != None:
if userbanner is None and self.sysmess:
channel.sendall(replace_enter_with_crlf(system_banner))
elif userbanner != None and self.sysmess:
channel.sendall(replace_enter_with_crlf(system_banner))
channel.sendall(replace_enter_with_crlf(userbanner))
elif userbanner != None and not self.sysmess:
channel.sendall(replace_enter_with_crlf(userbanner))
channel.sendall(replace_enter_with_crlf("\n"))
client_handler["connecttype"] = "ssh"
try:
self._handle_event("connect", self.client_handlers[channel.getpeername()])
except Exception as e:
self._handle_event("error", self.client_handlers[channel.getpeername()], e)
client_handler["connecttype"] = "ssh"
if self.enainputsystem:
if self.enainputsystem and self.accounts.get_user_enable_inputsystem(self.client_handlers[channel.getpeername()]["current_user"]):
try:
if self.accounts.get_user_timeout(self.client_handlers[channel.getpeername()]["current_user"]) != None:
channel.setblocking(False)
channel.settimeout(self.accounts.get_user_timeout(self.client_handlers[channel.getpeername()]["current_user"]))
channel.send(replace_enter_with_crlf(self.accounts.get_prompt(self.client_handlers[channel.getpeername()]["current_user"]) + " ").encode('utf-8'))
while True:
expect(self, channel, peername)
if echo:
channel.send(replace_enter_with_crlf(self.client_handlers[channel.getpeername()]["prompt"] + " "))
isConnect = True
while isConnect:
isConnect = expect(self, self.client_handlers[channel.getpeername()], echo)
self._handle_event("disconnected", self.client_handlers[peername])
channel.close()
bh_session.close()
except KeyboardInterrupt:
self._handle_event("disconnected", self.client_handlers[peername]["current_user"])
self._handle_event("disconnected", self.client_handlers[peername])
channel.close()
bh_session.close()
except Exception as e:
self._handle_event("syserror", client_handler, e)
self._handle_event("error", client_handler, e)
logger.error(e)
finally:
self._handle_event("disconnected", self.client_handlers[peername]["current_user"])
self._handle_event("disconnected", self.client_handlers[peername])
channel.close()
bh_session.close()
else:
if self.sftpena:
logger.info("user is sftp")
if self.accounts.get_user_sftp_allow(self.client_handlers[channel.getpeername()]["current_user"]):
client_handler["connecttype"] = "sftp"
self._handle_event("connectsftp", self.client_handlers[channel.getpeername()])
while bh_session.is_active():
time.sleep(0.1)
self._handle_event("disconnected", self.client_handlers[peername])
else:
self._handle_event("disconnected", self.client_handlers[peername]["current_user"])
self._handle_event("disconnected", self.client_handlers[peername])
channel.close()
else:
self._handle_event("disconnected", self.client_handlers[peername]["current_user"])
self._handle_event("disconnected", self.client_handlers[peername])
channel.close()
except:
pass
bh_session.close()
def stop_server(self):
"""Stop server"""
logger.info("Stopping the server...")
try:
for client_handler in self.client_handlers.values():
channel = client_handler.get("channel")
channel = client_handler.channel
if channel:
channel.close()
self.isrunning = False
self.server.close()
logger.info("Server stopped.")
except Exception as e:
logger.error(f"Error occurred while stopping the server: {e}")
def _start_listening_thread(self):
try:
self.server.listen(10)
logger.info("Start Listening for connections...")
while True:
client, addr = self.server.accept()
client_thread = threading.Thread(target=self.handle_client, args=(client, addr))
client_thread.start()
self.isrunning = True
try:
logger.info("Listening for connections...")
while self.isrunning:
client, addr = self.server.accept()
if self.__processmode == "thread":
logger.info(f"Starting client thread for connection {addr}")
client_thread = threading.Thread(target=self._handle_client, args=(client, addr), daemon=True)
client_thread.start()
else:
logger.info(f"Starting client for connection {addr}")
self._handle_client(client, addr)
time.sleep(1)
except KeyboardInterrupt:
self.stop_server()
except Exception as e:
logger.error(e)
def run(self, private_key_path, host="0.0.0.0", port=2222):
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))
self.private_key = paramiko.RSAKey(filename=private_key_path)
client_thread = threading.Thread(target=self._start_listening_thread)
client_thread.start()
def kickbyusername(self, username, reason=None):
for peername, client_handler in list(self.client_handlers.items()):
if client_handler["current_user"] == username:
channel = client_handler.get("channel")
self._handle_event("disconnected", channel.getpeername(), self.client_handlers[channel.getpeername()]["current_user"])
if reason is None:
if channel:
channel.close()
logger.info(f"User '{username}' has been kicked.")
else:
if channel:
Send(channel, f"You have been disconnected for {reason}")
channel.close()
logger.info(f"User '{username}' has been kicked by reason {reason}.")
def kickbypeername(self, peername, reason=None):
client_handler = self.client_handlers.get(peername)
if client_handler:
channel = client_handler.get("channel")
self._handle_event("disconnected", channel.getpeername(), self.client_handlers[channel.getpeername()]["current_user"])
if reason is None:
if channel:
channel.close()
logger.info(f"peername '{peername}' has been kicked.")
def run(self, private_key_path=None, host="0.0.0.0", port=2222, waiting_mode="thread", maxuser=0, daemon=False, listen_thread=True, protocol="ssh", custom_server: ITransport = None, custom_server_args: tuple = None, custom_server_require_socket=True):
"""mode: single, thread,
protocol: ssh, telnet (beta), serial, custom
For serial need to set serial port at host (ex. host="com3") and set baudrate at port (ex. port=9600) and change listen_mode to "single".
"""
if protocol.lower() == "ssh":
if private_key_path != None:
logger.info("Loading private key")
self.private_key = paramiko.RSAKey(filename=private_key_path)
else:
if channel:
Send(channel, f"You have been disconnected for {reason}")
channel.close()
logger.info(f"peername '{peername}' has been kicked by reason {reason}.")
raise ValueError("No private key")
def kickall(self, reason=None):
for peername, client_handler in self.client_handlers.items():
channel = client_handler.get("channel")
self._handle_event("disconnected", channel.getpeername(), self.client_handlers[channel.getpeername()]["current_user"])
self.__processmode = waiting_mode.lower()
self.__daemon = daemon
if reason is None:
if channel:
channel.close()
if custom_server:
self.__custom_server = custom_server
self.__custom_server_args = custom_server_args
if ((custom_server and protocol.lower() == "custom") and custom_server_require_socket) or protocol.lower() in ["ssh", "telnet"]:
logger.info("Creating server...")
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
self.server.bind((host, port))
logger.info("Set listen limit")
if maxuser == 0:
self.server.listen()
else:
if channel:
Send(channel, f"You have been disconnected for {reason}")
channel.close()
self.server.listen(maxuser)
if reason is None:
self.client_handlers.clear()
logger.info("All users have been kicked.")
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:
logger.info(f"All users have been kicked by reason {reason}.")
client_thread = threading.Thread(target=self._handle_client, args=(None, None), daemon=True)
client_thread.start()
def broadcast(self, message):
for client_handler in self.client_handlers.values():
channel = client_handler.get("channel")
if channel:
try:
# Send the message to the client
Send(channel, message)
except Exception as e:
logger.error(f"Error occurred while broadcasting message: {e}")
def sendto(self, username, message):
for client_handler in self.client_handlers.values():
if client_handler.get("current_user") == username:
channel = client_handler.get("channel")
if channel:
try:
# Send the message to the specific client
Send(channel, message)
except Exception as e:
logger.error(f"Error occurred while sending message to {username}: {e}")
break
else:
logger.warning(f"User '{username}' not found.")
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,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -40,17 +40,20 @@ class SSHSFTPHandle(paramiko.SFTPHandle):
# use the stored filename
try:
paramiko.SFTPServer.set_file_attr(self.filename, attr)
return paramiko.SFTP_OK
return paramiko.sftp.SFTP_OK
except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno)
class SSHSFTPServer(paramiko.SFTPServerInterface):
ROOT = None
ACCOUNT = None
CLIENTHANDELES = None
def __init__(self, server: paramiko.ServerInterface, *args, **kwargs):
super().__init__(server)
self.channel = args[0]
self.account = args[1]
self.clientH = args[2]
def _realpath(self, path):
return self.ROOT + self.canonicalize(path)
root = self.account.get_user_sftp_root_path(self.clientH[self.channel.getpeername()]["current_user"])
return root + self.canonicalize(path)
def list_folder(self, path):
path = self._realpath(path)
@ -80,6 +83,12 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
return paramiko.SFTPServer.convert_errno(e.errno)
def open(self, path, flags, attr):
# check if write request
is_write = (flags & os.O_WRONLY or flags & os.O_RDWR) + (flags & os.O_CREAT) != 0
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]) and is_write:
return paramiko.sftp.SFTP_PERMISSION_DENIED
path = self._realpath(path)
try:
binary_flag = getattr(os, 'O_BINARY', 0)
@ -120,23 +129,32 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
return fobj
def remove(self, path):
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
return paramiko.sftp.SFTP_PERMISSION_DENIED
path = self._realpath(path)
try:
os.remove(path)
except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK
return paramiko.sftp.SFTP_OK
def rename(self, oldpath, newpath):
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
return paramiko.sftp.SFTP_PERMISSION_DENIED
oldpath = self._realpath(oldpath)
newpath = self._realpath(newpath)
try:
os.rename(oldpath, newpath)
except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK
return paramiko.sftp.SFTP_OK
def mkdir(self, path, attr):
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
return paramiko.sftp.SFTP_PERMISSION_DENIED
path = self._realpath(path)
try:
os.mkdir(path)
@ -144,45 +162,58 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
paramiko.SFTPServer.set_file_attr(path, attr)
except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK
return paramiko.sftp.SFTP_OK
def rmdir(self, path):
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
return paramiko.sftp.SFTP_PERMISSION_DENIED
path = self._realpath(path)
try:
os.rmdir(path)
except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK
return paramiko.sftp.SFTP_OK
def chattr(self, path, attr):
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
return paramiko.sftp.SFTP_PERMISSION_DENIED
path = self._realpath(path)
try:
paramiko.SFTPServer.set_file_attr(path, attr)
except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK
return paramiko.sftp.SFTP_OK
def symlink(self, target_path, path):
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
return paramiko.sftp.SFTP_PERMISSION_DENIED
root = self.account.get_user_sftp_root_path(self.clientH[self.channel.getpeername()]["current_user"])
path = self._realpath(path)
if (len(target_path) > 0) and (target_path[0] == '/'):
# absolute symlink
target_path = os.path.join(self.ROOT, target_path[1:])
target_path = os.path.join(root, target_path[1:])
if target_path[:2] == '//':
# bug in os.path.join
target_path = target_path[1:]
else:
# compute relative to path
abspath = os.path.join(os.path.dirname(path), target_path)
if abspath[:len(self.ROOT)] != self.ROOT:
if abspath[:len(root)] != root:
# this symlink isn't going to work anyway -- just break it immediately
target_path = '<error>'
try:
os.symlink(target_path, path)
except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK
return paramiko.sftp.SFTP_OK
def readlink(self, path):
root = self.account.get_user_sftp_root_path(self.clientH[self.channel.getpeername()]["current_user"])
path = self._realpath(path)
try:
symlink = os.readlink(path)
@ -190,8 +221,8 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
return paramiko.SFTPServer.convert_errno(e.errno)
if os.path.isabs(symlink):
if symlink[:len(self.ROOT)] == self.ROOT:
symlink = symlink[len(self.ROOT):]
if symlink[:len(root)] == root:
symlink = symlink[len(root):]
if (len(symlink) == 0) or (symlink[0] != '/'):
symlink = '/' + symlink
else:

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License

View File

@ -0,0 +1,293 @@
"""
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 time
from ..interactive import Send
from .ProWrapper import IChannel, ITransport
class Client:
def __init__(self, channel, transport, peername):
"""
Initializes a new client instance.
Args:
channel (IChannel): The communication channel for the client.
transport (ITransport): The transport layer for the client.
peername (tuple): The peer's address and port (host, port).
"""
self.current_user = None
self.transport: ITransport = transport
self.channel: IChannel = channel
self.subchannel = {}
self.connecttype = None
self.last_activity_time = None
self.last_login_time = None
self.windowsize = {}
self.x11 = {}
self.prompt = None
self.inputbuffer = None
self.peername = peername
self.auth_method = self.transport.get_auth_method()
self.session_id = None
self.terminal_type = None
self.env_variables = {}
self.last_error = None
self.last_command = None
self.isexeccommandrunning = False
def get_id(self):
"""
Retrieves the client's session ID.
Returns:
str or None: The session ID of the client.
"""
return self.session_id
def get_name(self):
"""
Retrieves the current username of the client.
Returns:
str: The current username of the client.
"""
return self.current_user
def get_peername(self):
"""
Retrieves the peer's address (host, port) for the client.
Returns:
tuple: The peer's address (host, port).
"""
return self.peername
def get_prompt(self):
"""
Retrieves the prompt string for the client.
Returns:
str: The prompt string for the client.
"""
return self.prompt
def get_channel(self):
"""
Retrieves the communication channel for the client.
Returns:
IChannel: The channel instance for the client.
"""
return self.channel
def get_prompt_buffer(self):
"""
Retrieves the current input buffer for the client as a string.
Returns:
str: The input buffer as a string.
"""
return str(self.inputbuffer)
def get_terminal_size(self):
"""
Retrieves the terminal size (width, height) for the client.
Returns:
tuple[int, int]: The terminal's width and height.
"""
return self.windowsize["width"], self.windowsize["height"]
def get_connection_type(self):
"""
Retrieves the connection type for the client.
Returns:
str: The connection type (e.g., TCP, UDP).
"""
return self.connecttype
def get_auth_with(self):
"""
Retrieves the authentication method used for the client.
Returns:
str: The authentication method (e.g., password, public key).
"""
return self.auth_method
def get_session_duration(self):
"""
Calculates the duration of the current session for the client.
Returns:
float: The duration of the session in seconds.
"""
return time.time() - self.last_login_time
def get_environment(self, variable):
"""
Retrieves the value of an environment variable for the client.
Args:
variable (str): The name of the environment variable.
Returns:
str: The value of the environment variable.
"""
return self.env_variables.get(variable)
def get_last_error(self):
"""
Retrieves the last error message encountered by the client.
Returns:
str: The last error message, or None if no error occurred.
"""
return self.last_error
def get_last_command(self):
"""
Retrieves the last command executed by the client.
Returns:
str: The last command executed.
"""
return self.last_command
def set_name(self, name):
"""
Sets the current username for the client.
Args:
name (str): The username to set for the client.
"""
self.current_user = name
def set_prompt(self, prompt):
"""
Sets the prompt string for the client.
Args:
prompt (str): The prompt string to set for the client.
"""
self.prompt = prompt
def set_environment(self, variable, value):
"""
Sets the value of an environment variable for the client.
Args:
variable (str): The name of the environment variable.
value (str): The value to set for the environment variable.
"""
self.env_variables[variable] = value
def open_new_subchannel(self, timeout=None):
"""
Opens a new subchannel for communication with the client.
Args:
timeout (Union[int, None]): The timeout duration in seconds.
If None, the operation waits indefinitely.
Returns:
tuple: A tuple containing the subchannel ID and the new subchannel
(IChannel). If an error occurs, returns (None, None).
"""
try:
channel = self.transport.accept(timeout)
id = channel.get_id()
except:
return None, None
self.subchannel[id] = channel
return id, channel
def get_subchannel(self, id):
"""
Retrieves a subchannel by its ID.
Args:
id (int): The ID of the subchannel to retrieve.
Returns:
IChannel: The subchannel instance.
"""
return self.subchannel.get(id)
def switch_user(self, user):
"""
Switches the current user for the client.
Args:
user (str): The new username to switch to.
"""
self.current_user = user
self.transport.set_username(user)
def close_subchannel(self, id):
"""
Closes a specific subchannel by its ID.
Args:
id (int): The ID of the subchannel to close.
"""
self.subchannel[id].close()
def close(self):
"""
Closes the main communication channel for the client.
"""
self.channel.close()
def send(self, data):
"""
Sends data over the main communication channel.
Args:
data (str): The data to send.
"""
Send(self.channel, data, directchannel=True)
def __str__(self):
return f"client id: {self.session_id}"
def __repr__(self):
attrs = vars(self) # or self.__dict__
non_none_attrs = {key: value for key, value in attrs.items() if value is not None}
attrs_repr = ', '.join(f"{key}={value!r}" for key, value in non_none_attrs.items())
return f"Client({attrs_repr})"
# for backward compatibility only
def __getitem__(self, key):
return getattr(self, key)
def __setitem__(self, key, value):
setattr(self, key, value)

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -25,10 +25,32 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
__version__ = "4.2"
version = "5.1.4"
system_banner = (
f"\033[36mPyserSSH V{__version__} \033[0m\n"
f"\033[36mPyserSSH V{version} \033[0m"
#"\033[33m!!Warning!! This is Testing Version of PyserSSH \033[0m\n"
"\033[35mUse Putty and WinSCP (SFTP) for best experience \033[0m"
#"\033[35mUse Putty and WinSCP (SFTP) for best experience \033[0m"
)
def Flag_TH(returnlist=False):
Flags = [
"\n",
f"\033[31m ======= == == ====== ======= ====== ====== ====== == == \033[0m\n",
f"\033[37m == == == == == === == == == == == == \033[0m\n",
f"\033[34m ======= ==== ======= ======= ====== ======= ======= ======== \033[0m\n",
f"\033[34m ===== == ===== ==== === == ===== ===== ======== \033[0m\n",
f"\033[37m == == === === == == === === == == \033[0m\n",
f"\033[31m == == ====== ======= == == ====== ====== == == \033[0m\n",
" Made by \033[33mD\033[38;2;255;126;1mP\033[38;2;43;205;150mSoftware\033[0m \033[38;2;204;208;43mFoundation\033[0m from Thailand\n",
"\n"
]
if returnlist:
return Flags
else:
exporttext = ""
for line in Flags:
exporttext += line
return exporttext

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -27,20 +27,20 @@ SOFTWARE.
import socket
import time
import logging
import shlex
import traceback
from .sysfunc import replace_enter_with_crlf
from .syscom import systemcommand
logger = logging.getLogger("PyserSSH")
logger = logging.getLogger("PyserSSH.InputSystem")
def expect(self, chan, peername, echo=True):
def expect(self, client, echo=True):
buffer = bytearray()
cursor_position = 0
outindexall = 0
history_index_position = 0 # Initialize history index position outside the loop
currentuser = self.client_handlers[chan.getpeername()]
chan = client["channel"]
peername = client["peername"]
try:
while True:
try:
@ -51,7 +51,7 @@ def expect(self, chan, peername, echo=True):
chan.close()
raise EOFError()
self._handle_event("onrawtype", self.client_handlers[chan.getpeername()], byte)
self._handle_event("rawtype", self.client_handlers[chan.getpeername()], byte)
self.client_handlers[chan.getpeername()]["last_activity_time"] = time.time()
@ -66,7 +66,11 @@ def expect(self, chan, peername, echo=True):
buffer = buffer[:cursor_position - 1] + buffer[cursor_position:]
cursor_position -= 1
outindexall -= 1
chan.sendall(b"\b \b")
if cursor_position != outindexall:
chan.sendall(b"\b \b")
chan.sendall(buffer[cursor_position:])
else:
chan.sendall(b"\b \b")
else:
chan.sendall(b"\x07")
elif byte == b"\x1b" and chan.recv(1) == b'[':
@ -76,18 +80,22 @@ def expect(self, chan, peername, echo=True):
# Right arrow key, move cursor right if not at the end
if cursor_position < len(buffer):
chan.sendall(b'\x1b[C')
cursor_position += 1
# cursor_position += 1
cursor_position = min(len(buffer), cursor_position + 1)
elif arrow_key == b'D':
# Left arrow key, move cursor left if not at the beginning
if cursor_position > 0:
chan.sendall(b'\x1b[D')
cursor_position -= 1
elif self.history:
# cursor_position -= 1
cursor_position = max(0, cursor_position - 1)
if self.history:
if arrow_key == b'A':
if history_index_position == 0:
command = self.accounts.get_lastcommand(currentuser["current_user"])
command = self.accounts.get_lastcommand(client["current_user"])
else:
command = self.accounts.get_history(currentuser["current_user"], history_index_position)
command = self.accounts.get_history(client["current_user"], history_index_position)
# Clear the buffer
for i in range(cursor_position):
@ -105,9 +113,9 @@ def expect(self, chan, peername, echo=True):
elif arrow_key == b'B':
if history_index_position != -1:
if history_index_position == 0:
command = self.accounts.get_lastcommand(currentuser["current_user"])
command = self.accounts.get_lastcommand(client["current_user"])
else:
command = self.accounts.get_history(currentuser["current_user"], history_index_position)
command = self.accounts.get_history(client["current_user"], history_index_position)
# Clear the buffer
for i in range(cursor_position):
@ -136,11 +144,13 @@ def expect(self, chan, peername, echo=True):
else:
history_index_position = -1
self._handle_event("ontype", self.client_handlers[chan.getpeername()], byte)
self._handle_event("type", self.client_handlers[chan.getpeername()], byte)
if echo:
if outindexall != cursor_position:
chan.sendall(b" ")
chan.sendall(b'\033[s')
chan.sendall(byte + buffer[cursor_position:])
chan.sendall(f"\033[{cursor_position}G".encode())
chan.sendall(b'\033[u')
else:
chan.sendall(byte)
@ -149,13 +159,16 @@ def expect(self, chan, peername, echo=True):
cursor_position += 1
outindexall += 1
client["inputbuffer"] = buffer
if echo:
chan.sendall(b'\r\n')
command = str(buffer.decode('utf-8')).strip()
if self.history and command.strip() != "" and self.accounts.get_lastcommand(currentuser["current_user"]) != command:
self.accounts.add_history(currentuser["current_user"], command)
if self.history and command.strip() != "" and self.accounts.get_lastcommand(client["current_user"]) != command:
self.accounts.add_history(client["current_user"], command)
client["last_command"] = command
if command.strip() != "":
if self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]) != None:
@ -164,27 +177,27 @@ def expect(self, chan, peername, echo=True):
try:
if self.enasyscom:
sct = systemcommand(currentuser, command)
sct = systemcommand(client, command, self)
else:
sct = False
if not sct:
if self.XHandler != None:
self._handle_event("beforexhandler", currentuser, command)
self._handle_event("beforexhandler", client, command)
self.XHandler.call(currentuser, command)
self.XHandler.call(client, command)
self._handle_event("afterxhandler", currentuser, command)
self._handle_event("afterxhandler", client, command)
else:
self._handle_event("command", currentuser, command)
self._handle_event("command", client, command)
except Exception as e:
self._handle_event("error", currentuser, e)
try:
chan.send(replace_enter_with_crlf(self.accounts.get_prompt(currentuser["current_user"]) + " ").encode('utf-8'))
except:
logger.error("Send error")
self._handle_event("error", client, e)
if echo:
try:
chan.send(replace_enter_with_crlf(client["prompt"] + " "))
except:
logger.error("Send error")
chan.setblocking(False)
chan.settimeout(None)
@ -192,14 +205,14 @@ def expect(self, chan, peername, echo=True):
if self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]) != None:
chan.setblocking(False)
chan.settimeout(self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]))
except socket.error:
pass
except Exception as e:
logger.error(str(e))
finally:
try:
if not byte:
logger.info(f"{peername} is disconnected")
self._handle_event("disconnected", self.client_handlers[peername]["current_user"])
return False
return True
except:
logger.info(f"{peername} is disconnected by timeout")
self._handle_event("timeout", self.client_handlers[peername]["current_user"])
return False

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -24,35 +24,192 @@ 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 time
import paramiko
import ast
from .syscom import systemcommand
from .RemoteStatus import startremotestatus
def parse_exec_request(command_string):
try:
# Remove the leading 'b' and convert bytes to string
command_string = command_string.decode('utf-8')
# Split the string into precommand and env parts
try:
parts = command_string.split(', ')
except:
parts = command_string.split(',')
precommand_str = None
env_str = None
user_str = None
for part in parts:
if part.startswith('precommand='):
precommand_str = part.split('=', 1)[1].strip()
elif part.startswith('env='):
env_str = part.split('=', 1)[1].strip()
elif part.startswith('user='):
user_str = part.split('=', 1)[1].strip()
# Parse precommand using ast.literal_eval if present
precommand = ast.literal_eval(precommand_str) if precommand_str else None
# Parse env using ast.literal_eval if present
env = ast.literal_eval(env_str) if env_str else None
user = ast.literal_eval(user_str) if user_str else None
return precommand, env, user
except (ValueError, SyntaxError, TypeError) as e:
# Handle parsing errors here
print(f"Error parsing SSH command string: {e}")
return None, None, None
def parse_exec_request_kwargs(command_string):
try:
# Remove the leading 'b' and convert bytes to string
command_string = command_string.decode('utf-8')
# Split the string into key-value pairs
try:
parts = command_string.split(', ')
except:
parts = command_string.split(',')
kwargs = {}
for part in parts:
if '=' in part:
key, value = part.split('=', 1)
key = key.strip()
try:
value = ast.literal_eval(value.strip())
except (ValueError, SyntaxError):
# If literal_eval fails, treat value as string
value = value.strip()
kwargs[key] = value
return kwargs
except (ValueError, SyntaxError, TypeError) as e:
# Handle parsing errors here
print(f"Error parsing command kwargs: {e}")
return {}
class Sinterface(paramiko.ServerInterface):
def __init__(self, serverself):
self.current_user = None
self.serverself = serverself
def check_channel_request(self, kind, chanid):
def check_channel_request(self, kind, channel_id):
if kind == 'session':
return paramiko.OPEN_SUCCEEDED
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
def get_allowed_auths(self, username):
return self.serverself.accounts.get_allowed_auths(username)
def check_auth_password(self, username, password):
data = {
"username": username,
"password": password,
"auth_type": "password"
}
if self.serverself.accounts.validate_credentials(username, password) and not self.serverself.usexternalauth:
self.current_user = username # Store the current user upon successful authentication
return paramiko.AUTH_SUCCESSFUL
else:
if self.serverself._handle_event("auth", data):
self.current_user = username # Store the current user upon successful authentication
return paramiko.AUTH_SUCCESSFUL
else:
return paramiko.AUTH_FAILED
def check_auth_none(self, username):
data = {
"username": username,
"auth_type": "none"
}
if self.serverself.accounts.validate_credentials(username) and not self.serverself.usexternalauth:
return paramiko.AUTH_SUCCESSFUL
else:
if self.serverself._handle_event("auth", data):
return paramiko.AUTH_SUCCESSFUL
else:
return paramiko.AUTH_FAILED
def check_auth_publickey(self, username, key):
data = {
"username": username,
"public_key": key,
"auth_type": "key"
}
if self.serverself.accounts.validate_credentials(username, public_key=key) and not self.serverself.usexternalauth:
return paramiko.AUTH_SUCCESSFUL
else:
if self.serverself._handle_event("auth", data):
return paramiko.AUTH_SUCCESSFUL
else:
return paramiko.AUTH_FAILED
def get_banner(self):
if self.serverself.enaloginbanner:
try:
banner, lang = self.serverself._handle_event("authbanner", None)
return banner, lang
except:
return "", ""
else:
return "", ""
def check_channel_exec_request(self, channel, execommand):
if b"##Moba##" in execommand and self.serverself.enaremostatus:
startremotestatus(self.serverself, channel)
client = self.serverself.client_handlers[channel.getpeername()]
if self.serverself.enasysexec:
precommand, env, user = parse_exec_request(execommand)
if env != None:
client.env_variables = env
if user != None:
self.serverself._handle_event("exec", client, user)
if precommand != None:
client.isexeccommandrunning = True
try:
if self.serverself.enasyscom:
sct = systemcommand(client, precommand)
else:
sct = False
if not sct:
if self.serverself.XHandler != None:
self.serverself._handle_event("beforexhandler", client, precommand)
self.serverself.XHandler.call(client, precommand)
self.serverself._handle_event("afterxhandler", client, precommand)
else:
self.serverself._handle_event("command", client, precommand)
except Exception as e:
self.serverself._handle_event("error", client, e)
client.isexeccommandrunning = False
else:
kwargs = parse_exec_request_kwargs(execommand)
self.serverself._handle_event("exec", client, **kwargs)
return True
def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes):
data = {
"term": term,
@ -69,7 +226,9 @@ class Sinterface(paramiko.ServerInterface):
"pixelheight": pixelheight,
}
try:
time.sleep(0.01) # fix waiting windowsize
self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data2
self.serverself.client_handlers[channel.getpeername()]["terminal_type"] = term
self.serverself._handle_event("connectpty", self.serverself.client_handlers[channel.getpeername()], data)
except:
pass
@ -102,4 +261,4 @@ class Sinterface(paramiko.ServerInterface):
"pixelheight": pixelheight
}
self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data
self.serverself._handle_event("resized", self.serverself.client_handlers[channel.getpeername()], data)
self.serverself._handle_event("resized", self.serverself.client_handlers[channel.getpeername()], data)

View File

@ -0,0 +1,272 @@
"""
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 logging
import os
import socket
import threading
import time
import sys
import psutil
from datetime import datetime
import platform
from ..interactive import Send
from .info import version
if platform.system() == "Windows":
import ctypes
logger = logging.getLogger("PyserSSH.RemoteStatus")
if platform.system() == "Windows":
class LASTINPUTINFO(ctypes.Structure):
_fields_ = [
('cbSize', ctypes.c_uint),
('dwTime', ctypes.c_uint),
]
def get_idle_time():
if platform.system() == "Windows":
lastInputInfo = LASTINPUTINFO()
lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo)
ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lastInputInfo))
millis = ctypes.windll.kernel32.GetTickCount() - lastInputInfo.dwTime
return millis / 1000.0
elif platform.system() == "Linux":
with open('/proc/stat') as f:
for line in f:
if line.startswith('btime'):
boot_time = float(line.split()[1])
break
with open('/proc/uptime') as f:
uptime_seconds = float(f.readline().split()[0])
idle_time_seconds = uptime_seconds - (time.time() - boot_time)
return idle_time_seconds
else:
return time.time() - psutil.boot_time()
def get_system_uptime():
if platform.system() == "Windows":
kernel32 = ctypes.windll.kernel32
uptime = kernel32.GetTickCount64() / 1000.0
return uptime
elif platform.system() == "Linux":
with open('/proc/uptime') as f:
uptime_seconds = float(f.readline().split()[0])
return uptime_seconds
else:
return 0
def get_folder_size(folder_path):
total_size = 0
for dirpath, _, filenames in os.walk(folder_path):
for f in filenames:
fp = os.path.join(dirpath, f)
total_size += os.path.getsize(fp)
return total_size
def get_folder_usage(folder_path, limit_size):
folder_size = get_folder_size(folder_path)
used_size = folder_size
free_size = limit_size - folder_size
percent_used = (folder_size / limit_size) * 100 if limit_size > 0 else 0
return used_size, free_size, limit_size, percent_used
librarypath = os.path.abspath(__file__).replace("\\", "/").split("/system/remotestatus.py")[0]
def remotestatus(serverself, channel, oneloop=False):
try:
while True:
# Get RAM information
mem = psutil.virtual_memory()
ramoutput = f"""\
==> /proc/meminfo <==
MemTotal: {mem.total // 1024} kB
MemFree: {mem.free // 1024} kB
MemAvailable: {mem.available // 1024} kB
Buffers: 0 kB
Cached: 0 kB
SwapCached: 0 kB
Active: 0 kB
Inactive: 0 kB"""
cpu_data = []
#currentprocess = psutil.Process().cpu_times()
#cpu_data.append(["cpu", int(currentprocess.user), 0, int(currentprocess.system), 0, 0, 0, 0])
#if platform.system() == "Linux":
io_counters = psutil.disk_io_counters(perdisk=False)
io_wait_time = io_counters.read_time + io_counters.write_time
for idx, cpu_time in enumerate(psutil.cpu_times(True), start=-1):
if idx == -1:
cpu_data.append(["cpu", int(cpu_time.user), 0, int(cpu_time.system), int(cpu_time.idle), io_wait_time, int(cpu_time.interrupt), 0, 0, 0, 0])
else:
cpu_data.append(
[f"cpu{idx}", int(cpu_time.user), 0, int(cpu_time.system), int(cpu_time.idle), io_wait_time, int(cpu_time.interrupt), 0, 0, 0, 0])
# Calculate maximum widths for formatting (optional)
max_widths = [max(len(str(row[i])) for row in cpu_data) for i in range(len(cpu_data[0]))]
disk_data = [
["Filesystem", "1K-blocks", "Used", "Available", "Use%", "Mounted on"],
]
for disk in psutil.disk_partitions(True):
usage = psutil.disk_usage(disk.device)
mountpoint = disk.mountpoint
if mountpoint == "C:\\":
mountpoint = "/"
disk_data.append(
[disk.device.replace('\\', '/'), usage.total // 1024, usage.used // 1024, usage.free // 1024, f"{int(usage.percent)}%",
mountpoint])
libused, libfree, libtotal, libpercent = get_folder_usage(librarypath, 1024*1024)
disk_data.append(["/dev/pyserssh", libtotal // 1024, libused // 1024, libfree // 1024, f"{int(libpercent)}%", "/python/pyserssh"])
max_widths3 = [max(len(str(row[i])) for row in disk_data) for i in range(len(disk_data[0]))]
"""
network_data = [
["Inter-|", " Receive", "", "", "", "", "", "", " |", " Transmit" "", "", "", "", "", "", "", ""],
[" face |", "bytes", "packets", "errs", "drop", "fifo", "frame", "compressed", "multicast|", "bytes", "packets",
"errs", "drop", "fifo", "colls", "carrier", "compressed"]
]
for interface, stats in psutil.net_io_counters(pernic=True).items():
network_data.append(
[f"{interface}:", stats.bytes_recv, stats.packets_recv, stats.errin, stats.dropin, 0, 0, 0, 0,
stats.bytes_sent, stats.packets_sent, stats.errout, stats.dropout, 0, 0, 0, 0])
max_widths2 = [max(len(str(row[i])) for row in network_data) for i in range(len(network_data[0]))]
protocol_names = {
(socket.AF_INET, socket.SOCK_STREAM): 'tcp',
(socket.AF_INET, socket.SOCK_DGRAM): 'udp',
(socket.AF_INET6, socket.SOCK_STREAM): 'tcp6',
(socket.AF_INET6, socket.SOCK_DGRAM): 'udp6',
}
netstat_data = [
["Proto", "Recv-Q", "Send-Q", "Local Address", "Foreign Address", "State", "PID/Program name"],
]
for conn in psutil.net_connections("all"):
if conn.status in ['TIME_WAIT', 'CLOSING', "NONE"]:
continue
laddr_ip, laddr_port = conn.laddr if conn.laddr else ('', '')
raddr_ip, raddr_port = conn.raddr if conn.raddr else ('', '')
protocol = protocol_names.get((conn.family, conn.type), 'Unknown')
try:
process = psutil.Process(conn.pid)
processname = f"{conn.pid}/{process.name()}"
except psutil.NoSuchProcess:
processname = conn.pid
netstat_data.append(
[protocol, 0, 0, f"{laddr_ip}:{laddr_port}", f"{raddr_ip}:{raddr_port}", conn.status, processname])
max_widths4 = [max(len(str(row[i])) for row in netstat_data) for i in range(len(netstat_data[0]))]
"""
who_data = []
for idx, client in enumerate(serverself.client_handlers.values()):
last_login_date = datetime.utcfromtimestamp(client.last_login_time).strftime('%Y-%m-%d %H:%M')
who_data.append([client.current_user, f"pty/{idx}", last_login_date, f"({client.peername[0]})"])
max_widths5 = [max(len(str(row[i])) for row in who_data) for i in range(len(who_data[0]))]
Send(channel, ramoutput, directchannel=True)
Send(channel, "", directchannel=True)
# only support for CPU status current python process
Send(channel, "==> /proc/stat <==", directchannel=True)
for row in cpu_data:
Send(channel, " ".join("{:<{width}}".format(item, width=max_widths[i]) for i, item in enumerate(row)), directchannel=True)
Send(channel, "", directchannel=True)
Send(channel, "==> /proc/version <==", directchannel=True)
Send(channel, f"PyserSSH v{version} run on {platform.platform()} {platform.machine()} {platform.architecture()[0]} with python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} {sys.version_info.releaselevel} {platform.python_build()[0]} {platform.python_build()[1]} {platform.python_compiler()} {platform.python_implementation()} {platform.python_revision()}", directchannel=True)
Send(channel, "", directchannel=True)
Send(channel, "==> /proc/uptime <==", directchannel=True)
Send(channel, f"{get_system_uptime()} {get_idle_time()}", directchannel=True)
Send(channel, "", directchannel=True)
Send(channel, "==> /proc/sys/kernel/hostname <==", directchannel=True)
Send(channel, platform.node(), directchannel=True)
# fixing later for network status
#Send(channel, "", directchannel=True)
#Send(channel, "==> /proc/net/dev <==", directchannel=True)
#for row in network_data:
# Send(channel, " ".join("{:<{width}}".format(item, width=max_widths2[i]) for i, item in enumerate(row)), directchannel=True)
Send(channel, "", directchannel=True)
Send(channel, "==> /proc/df <==", directchannel=True)
for row in disk_data:
Send(channel, " ".join("{:<{width}}".format(item, width=max_widths3[i]) for i, item in enumerate(row)), directchannel=True)
# fixing later for network status
#Send(channel, "", directchannel=True)
#Send(channel, "==> /proc/netstat <==", directchannel=True)
#for row in netstat_data:
# Send(channel, " ".join("{:<{width}}".format(item, width=max_widths4[i]) for i, item in enumerate(row)), directchannel=True)
Send(channel, "", directchannel=True)
Send(channel, "==> /proc/who <==", directchannel=True)
for row in who_data:
Send(channel, " ".join("{:<{width}}".format(item, width=max_widths5[i]) for i, item in enumerate(row)), directchannel=True)
Send(channel, "", directchannel=True)
Send(channel, "==> /proc/end <==", directchannel=True)
Send(channel, "##Moba##", directchannel=True)
if oneloop:
break
time.sleep(1)
except socket.error:
pass
except Exception as e:
logger.error(e)
def startremotestatus(serverself, channel):
t = threading.Thread(target=remotestatus, args=(serverself, channel), daemon=True)
t.start()

View File

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

View File

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -26,10 +26,21 @@ SOFTWARE.
"""
def replace_enter_with_crlf(input_string):
if '\n' in input_string:
if isinstance(input_string, str):
# Replace '\n' with '\r\n' in the string
input_string = input_string.replace('\n', '\r\n')
return input_string
# Encode the string to bytes
return input_string.encode()
elif isinstance(input_string, bytes):
# Decode bytes to string
decoded_string = input_string.decode()
# Replace '\n' with '\r\n' in the string
modified_string = decoded_string.replace('\n', '\r\n')
# Encode the modified string back to bytes
return modified_string.encode()
else:
raise TypeError("Input must be a string or bytes")
def text_centered_screen(text, screen_width, screen_height, spacecharacter=" "):
screen = []

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

@ -1,8 +1,8 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
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/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -25,6 +25,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
class PTOP:
def __init__(self, client, interval=1):
pass # working
"""
note
ansi cursor arrow
up - \x1b[A
down - \x1b[B
left - \x1b[D
right - \x1b[C
https://en.wikipedia.org/wiki/ANSI_escape_code
"""

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}")

View File

@ -9,4 +9,5 @@ python setup.py sdist
title uploading to pypi
twine upload -r pypi dist/*
title done!
pause