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
This commit is contained in:
dharm pimsen 2024-09-03 20:27:12 +07:00
parent f33be3bca1
commit b6455ce6a3
30 changed files with 2445 additions and 715 deletions

View File

@ -1,33 +1,40 @@
# 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.
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 small 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.
> [!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.
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
useraccount = AccountManager(anyuser=True)
ssh = Server(useraccount)
@ssh.on_user("command")
@ -35,32 +42,16 @@ def command(client, command: str):
if command == "hello":
Send(client, "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.
For run this demo you can use this command
```
$ python -m PyserSSH
```
then
```
Do you want to run demo? (y/n): y
```
But if no [damp11113-library](https://github.com/damp11113/damp11113-library)
```
No 'damp11113-library'
This demo is require 'damp11113-library' for run
```
you need to install [damp11113-library](https://github.com/damp11113/damp11113-library) for run this demo by choose `y` or `yes` in lowercase or uppercase
```
Do you want to install 'damp11113-library'? (y/n): y
```
For exit demo you can use `ctrl+c` or use `shutdown now` in PyserSSH shell **(not in real terminal)**
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 "

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

469
demo/demo1.py Normal file
View File

@ -0,0 +1,469 @@
import os
import socket
import time
import cv2
import traceback
import requests
from bs4 import BeautifulSoup
import numpy as np
#import logging
#logging.basicConfig(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
useraccount = AccountManager(allow_guest=True)
useraccount.add_account("admin", "") # create user without password
useraccount.add_account("test", "test") # create user without password
useraccount.add_account("demo")
useraccount.add_account("remote", "12345", permissions=["remote_desktop"])
useraccount.set_user_enable_inputsystem_echo("remote", False)
useraccount.set_user_sftp_allow("admin", True)
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", "Extensions"]
class TextFormatter:
RESET = "\033[0m"
TEXT_COLORS = {
"black": "\033[30m",
"red": "\033[31m",
"green": "\033[32m",
"yellow": "\033[33m",
"blue": "\033[34m",
"magenta": "\033[35m",
"cyan": "\033[36m",
"white": "\033[37m"
}
TEXT_COLOR_LEVELS = {
"light": "\033[1;{}m", # Light color prefix
"dark": "\033[2;{}m" # Dark color prefix
}
BACKGROUND_COLORS = {
"black": "\033[40m",
"red": "\033[41m",
"green": "\033[42m",
"yellow": "\033[43m",
"blue": "\033[44m",
"magenta": "\033[45m",
"cyan": "\033[46m",
"white": "\033[47m"
}
TEXT_ATTRIBUTES = {
"bold": "\033[1m",
"italic": "\033[3m",
"underline": "\033[4m",
"blink": "\033[5m",
"reverse": "\033[7m",
"strikethrough": "\033[9m"
}
@staticmethod
def format_text_truecolor(text, color=None, background=None, attributes=None, target_text=''):
formatted_text = ""
start_index = text.find(target_text)
end_index = start_index + len(target_text) if start_index != -1 else len(text)
if color:
formatted_text += f"\033[38;2;{color}m"
if background:
formatted_text += f"\033[48;2;{background}m"
if attributes in TextFormatter.TEXT_ATTRIBUTES:
formatted_text += TextFormatter.TEXT_ATTRIBUTES[attributes]
if target_text == "":
formatted_text += text + TextFormatter.RESET
else:
formatted_text += text[:start_index] + text[start_index:end_index] + TextFormatter.RESET + text[end_index:]
return formatted_text
@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, path: str):
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]
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("server1", server1)
# Start a specific server
#manager.start_server("server1", private_key_path="key")

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

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -1,21 +1,43 @@
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.4',
version='5.0',
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"
],
}
)

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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -37,40 +37,75 @@ right - \x1b[C
https://en.wikipedia.org/wiki/ANSI_escape_code
"""
import os
import ctypes
import logging
from .interactive import *
from .server import Server
from .account import AccountManager
from .system.info import system_banner
if os.name == 'nt':
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)
if __name__ == "__main__":
stadem = input("Do you want to run demo? (y/n): ")
if stadem.upper() in ["Y", "YES"]:
from .demo import demo1
else:
exit()
# Server Managers
class ServerManager:
def __init__(self):
self.servers = {}
def add_server(self, name, server):
if name in self.servers:
raise ValueError(f"Server with name '{name}' already exists.")
self.servers[name] = server
def remove_server(self, name):
if name not in self.servers:
raise ValueError(f"No server found with name '{name}'.")
del self.servers[name]
def get_server(self, name):
return self.servers.get(name)
def start_server(self, name, protocol="ssh", *args, **kwargs):
server = self.get_server(name)
if not server:
raise ValueError(f"No server found with name '{name}'.")
print(f"Starting server '{name}'...")
server.run(*args, **kwargs)
def stop_server(self, name):
server = self.get_server(name)
if not server:
raise ValueError(f"No server found with name '{name}'.")
print(f"Stopping server '{name}'...")
server.stop_server()
def start_all_servers(self, *args, **kwargs):
for name, server in self.servers.items():
print(f"Starting server '{name}'...")
server.run(*args, **kwargs)
def stop_all_servers(self):
for name, server in self.servers.items():
print(f"Stopping server '{name}'...")
server.stop_server()

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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -24,30 +24,28 @@ 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 os
import pickle
import time
import atexit
import threading
import hashlib
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, autoloadfile="autosave_session.ses"):
self.accounts = {}
self.anyuser = anyuser
self.allow_guest = allow_guest
self.historylimit = historylimit
self.autosavedelay = autosavedelay
self.__autosavework = False
self.__autosaveworknexttime = 0
if self.anyuser:
print("history system can't work if 'anyuser' is enable")
if autoload:
self.load(autoloadfile)
if autosave:
self.__autosavethread = threading.Thread(target=self.__autosave)
self.__autosavethread = threading.Thread(target=self.__autosave, daemon=True)
self.__autosavethread.start()
atexit.register(self.__saveexit)
@ -67,37 +65,89 @@ class AccountManager:
self.save("autosave_session.ses")
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 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 []
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}
def add_account(self, username, password=None, public_key=None, permissions:list=None):
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
else:
raise Exception(f"{username} is exist")
def remove_account(self, username):
if self.has_user(username):
del self.accounts[username]
def change_password(self, username, new_password):
if username in self.accounts:
if self.has_user(username):
self.accounts[username]["password"] = new_password
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"):
def save(self, filename="session.ses"):
with open(filename, 'wb') as file:
pickle.dump(self.accounts, file)
@ -111,103 +161,115 @@ class AccountManager:
print(f"An error occurred: {e}. No accounts loaded.")
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
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:
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()
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
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
def set_banner(self, username, banner):
if username in self.accounts:
if self.has_user(username):
self.accounts[username]["banner"] = banner
def get_banner(self, username):
if username in self.accounts and "banner" in self.accounts[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
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
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
}
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:]
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,199 +0,0 @@
import os
import socket
import time
import shlex
import cv2
import traceback
import requests
from bs4 import BeautifulSoup
import pyfiglet
from ..server import Server
from ..account import AccountManager
from ..interactive import Send, Clear, wait_input, wait_inputkey, wait_choose
from ..system.info import system_banner, __version__
from ..extensions.processbar import (indeterminateStatus, LoadingProgress)
from ..extensions.dialog import MenuDialog, TextDialog, TextInputDialog
from ..extensions.moredisplay import clickable_url
try:
from damp11113 import TextFormatter
except:
print("No 'damp11113-library'")
print("This demo is require 'damp11113-library' for run")
ins = input("Do you want to install 'damp11113-library'? (y/n): ")
if ins.upper() in ["Y", "YES"]:
import pip
pip.main(["install", "damp11113"])
from damp11113 import TextFormatter
else:
exit()
useraccount = AccountManager()
useraccount.add_account("admin", "") # create user without password
ssh = Server(useraccount, system_commands=True, system_message=False, sftp=False)
loading = ["PyserSSH", "Extensions"]
print("you connect to this demo using 'ssh admin@localhost -p 2222' (no password)")
print("command list: passtest, colortest, typing <speed> <text>, renimtest, errortest, inloadtest, loadtest, dialogtest, dialogtest2, dialogtest3, passdialogtest3, choosetest, vieweb <url>, shutdown now")
print("Do not you this demo private key for real production")
@ssh.on_user("connect")
def connect(client):
wm = f"""{pyfiglet.figlet_format('PyserSSH', font='usaflag', width=client["windowsize"]["width"])}*********************************************************************************************
Hello {client['current_user']},
This is the testing server of PyserSSH v{__version__}.
For use in product please use new private key.
Visit: {clickable_url("https://damp11113.xyz", "DPCloudev")}
{system_banner}
*********************************************************************************************"""
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, 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}")
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 == "renimtest":
Clear(client)
image = cv2.imread(os.path.join(os.path.dirname(os.path.realpath(__file__)), '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 = ""
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()
elif command == "shutdown now":
ssh.stop_server()
ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))

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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -30,16 +30,19 @@ 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):
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):
def decorator(func):
nonlocal name, category
if name is None:
@ -48,21 +51,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:
@ -76,6 +90,7 @@ class XHandler:
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 +100,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')):
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:
@ -165,7 +190,10 @@ 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):
@ -185,6 +213,10 @@ 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):
@ -203,3 +235,4 @@ class XHandler:
for category, commands in self.categories.items():
all_commands[category] = 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-2024 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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -31,7 +31,7 @@ from ..interactive import Clear, Send, wait_inputkey
from ..system.sysfunc import text_centered_screen
class TextDialog:
def __init__(self, client, title="", content=""):
def __init__(self, client, content="", title=""):
self.client = client
self.windowsize = client["windowsize"]

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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -24,68 +24,35 @@ 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=""):
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):
printed_text = ""
for i, char in enumerate(text):
# Print already printed text normally
Send(client, printed_text + char, ln=False)
@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)
# Calculate not yet printed text to dim
not_printed_text = text[i + 1:]
dimmed_text = ''.join([f"\033[2m{char}\033[0m" for char in not_printed_text])
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]
# Print dimmed text
Send(client, dimmed_text, ln=False)
if background in BasicTextFormatter.BACKGROUND_COLORS:
formatted_text += BasicTextFormatter.BACKGROUND_COLORS[background]
# Wait before printing the next character
time.sleep(delay)
if attributes in BasicTextFormatter.TEXT_ATTRIBUTES:
formatted_text += BasicTextFormatter.TEXT_ATTRIBUTES[attributes]
# Clear the line for the next iteration
Send(client, '\r' ,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:]
# Prepare the updated printed_text for the next iteration
printed_text += char
if ln:
Send(client, "") # new line
return formatted_text

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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -25,14 +25,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
"""
note
from ..interactive import Send
ansi cursor arrow
up - \x1b[A
down - \x1b[B
left - \x1b[D
right - \x1b[C
def ShowCursor(client, show=True):
if show:
Send(client, "\033[?25h", ln=False)
else:
Send(client, "\033[?25l", ln=False)
https://en.wikipedia.org/wiki/ANSI_escape_code
"""
def SendBell(client):
Send(client, "\x07", ln=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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -24,7 +24,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
# this file is from damp11113-library
# this file is from DPSoftware Foundation-library
from itertools import cycle
import math
@ -37,10 +37,97 @@ 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)
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):]
steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]']
steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]']

View File

@ -1,30 +0,0 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT)
Visit https://github.com/damp11113/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.
"""
class PTOP:
def __init__(self, client, interval=1):
pass # working

View File

@ -0,0 +1,294 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 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("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,102 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 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")
def kickbyusername(server, username, reason=None):
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):
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):
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):
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):
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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -29,13 +29,42 @@ 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 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"]
@ -123,7 +152,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 != "":
@ -150,7 +179,8 @@ def wait_inputkey(client, prompt="", raw=False, timeout=0):
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)
@ -158,6 +188,48 @@ def wait_inputkey(client, prompt="", raw=False, timeout=0):
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'):
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
Send(client, "\033[?1000l", ln=False)
return button, x, y
else:
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)
channel.send("\r\n")
raise
def wait_choose(client, choose, prompt="", timeout=0):
channel = client["channel"]
@ -176,18 +248,18 @@ 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")
Send(client, "\033[K")
return chooseindex
elif keyinput == b'\x03': # ' ctrl+c' key for cancel
Send(channel, "\033[K")
return 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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -25,18 +25,21 @@ 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 __version__
from .system.info import __version__, system_banner
from .system.clientype import Client as Clientype
# paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22)
@ -45,16 +48,18 @@ sftpclient = ["WinSCP", "Xplore"]
logger = logging.getLogger("PyserSSH")
class Server:
def __init__(self, accounts, system_message=True, disable_scroll_with_arrow=True, sftp=False, 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.accounts = accounts
self.disable_scroll_with_arrow = disable_scroll_with_arrow
self.sftproot = sftproot
self.sftpena = sftp
self.enasyscom = system_commands
self.compressena = compression
@ -64,10 +69,19 @@ 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
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.__serverisrunning = False
self.__server_stopped = threading.Event() # Event to signal server stop
self.__daemon = False
if self.enasyscom:
print("\033[33m!!Warning!! System commands is enable! \033[0m")
@ -75,40 +89,41 @@ class Server:
def on_user(self, event_name):
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["channel"].get_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 == "disconnected":
self.handle_client_disconnection(*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(handler, *args, **kwargs)
elif handler:
return handler(*args, **kwargs)
def handle_client(self, socketchannel, addr):
self._handle_event("pressh", socketchannel)
try:
bh_session = paramiko.Transport(socketchannel)
except OSError:
return
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.use_compression(self.compressena)
bh_session.default_window_size = 2147483647
bh_session.packetizer.REKEY_BYTES = pow(2, 40)
@ -117,102 +132,154 @@ class Server:
bh_session.default_max_packet_size = self.inspeed
server = Sinterface(self)
bh_session.start_server(server=server)
try:
bh_session.start_server(server=server)
except:
return
logger.info(bh_session.remote_version)
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": {},
"prompt": None,
"inputbuffer": None,
"peername": peername
}
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(server.current_user)
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 int(channel.out_window_size) != int(bh_session.default_window_size):
logger.info("user is ssh")
#timeout for waiting 10 sec
for i in range(100):
if self.client_handlers[channel.getpeername()]["windowsize"]:
break
time.sleep(0.1)
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
}
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.sysmess or userbanner != None:
channel.send(f"\033]0;{self.title}\007".encode())
channel.sendall(replace_enter_with_crlf(userbanner))
channel.sendall(replace_enter_with_crlf("\n"))
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.client_handlers[channel.getpeername()]["prompt"] + " ").encode('utf-8'))
if echo:
channel.send(replace_enter_with_crlf(self.client_handlers[channel.getpeername()]["prompt"] + " "))
while True:
expect(self, self.client_handlers[channel.getpeername()])
expect(self, self.client_handlers[channel.getpeername()], echo)
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()
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):
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.__serverisrunning = True
self.__serverisrunning = False
self.server.close()
logger.info("Server stopped.")
except Exception as e:
logger.error(f"Error occurred while stopping the server: {e}")
@ -223,20 +290,29 @@ class Server:
while self.__serverisrunning:
client, addr = self.server.accept()
if self.__processmode == "thread":
client_thread = threading.Thread(target=self.handle_client, args=(client, addr))
client_thread = threading.Thread(target=self.handle_client, args=(client, addr), daemon=True)
client_thread.start()
else:
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, mode="thread", maxuser=0, daemon=False):
"""mode: single, thread"""
def run(self, private_key_path=None, host="0.0.0.0", port=2222, mode="thread", maxuser=0, daemon=False):
"""mode: single, thread
protocol: ssh, telnet
"""
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)
if private_key_path != None:
self.private_key = paramiko.RSAKey(filename=private_key_path)
else:
raise ValueError("No private key")
if maxuser == 0:
self.server.listen()
else:
@ -244,81 +320,8 @@ class Server:
self.__processmode = mode.lower()
self.__serverisrunning = True
self.__daemon = daemon
client_thread = threading.Thread(target=self._start_listening_thread)
client_thread.daemon = daemon
client_thread = threading.Thread(target=self._start_listening_thread, daemon=self.__daemon)
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.")
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(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"])
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:
self.client_handlers.clear()
logger.info("All users have been kicked.")
else:
logger.info(f"All users have been kicked by reason {reason}.")
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.")

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-2024 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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License

View File

@ -0,0 +1,143 @@
"""
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 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 paramiko.transport import Transport
from paramiko.channel import Channel
class Client:
def __init__(self, channel, transport, peername):
self.current_user = None
self.transport: Transport = transport
self.channel: Channel = 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.auth_handler.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):
return self.session_id
def get_name(self):
return self.current_user
def get_peername(self):
return self.current_user
def get_prompt(self):
return self.prompt
def get_channel(self):
return self.channel
def get_prompt_buffer(self):
return str(self.inputbuffer)
def get_terminal_size(self):
return self.windowsize["width"], self.windowsize["height"]
def get_connection_type(self):
return self.connecttype
def get_auth_with(self):
return self.auth_method
def get_session_duration(self):
return time.time() - self.last_login_time
def get_environment(self, variable):
return self.env_variables[variable]
def get_last_error(self):
return self.last_error
def get_last_command(self):
return self.last_command
def set_name(self, name):
self.current_user = name
def set_prompt(self, prompt):
self.prompt = prompt
def set_environment(self, variable, value):
self.env_variables[variable] = value
def open_new_subchannel(self, timeout=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):
return self.subchannel[id]
def switch_user(self, user):
self.current_user = user
self.transport.auth_handler.username = user
def close_subchannel(self, id):
self.subchannel[id].close()
def close(self):
self.channel.close()
# for backward compatibility only
def __getitem__(self, key):
return getattr(self, key)
def __setitem__(self, key, value):
setattr(self, key, value)
def __str__(self):
return f"client id: {self.session_id}"
def __repr__(self):
# Get the dictionary of instance attributes
attrs = vars(self) # or self.__dict__
# Filter out attributes that are None
non_none_attrs = {key: value for key, value in attrs.items() if value is not None}
# Build a string representation
attrs_repr = ', '.join(f"{key}={value!r}" for key, value in non_none_attrs.items())
return f"Client({attrs_repr})"

View File

@ -1,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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -24,11 +24,34 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import re
__version__ = "4.4"
__version__ = "5.0"
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 Foundation 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-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH
Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License
@ -27,8 +27,6 @@ SOFTWARE.
import socket
import time
import logging
import shlex
import traceback
from .sysfunc import replace_enter_with_crlf
from .syscom import systemcommand
@ -53,7 +51,7 @@ def expect(self, client, 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()
@ -146,7 +144,7 @@ def expect(self, client, 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" ")
@ -170,6 +168,7 @@ def expect(self, client, echo=True):
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:
@ -194,11 +193,11 @@ def expect(self, client, echo=True):
except Exception as e:
self._handle_event("error", client, e)
try:
chan.send(replace_enter_with_crlf(client["prompt"] + " ").encode('utf-8'))
except:
logger.error("Send error")
if echo:
try:
chan.send(replace_enter_with_crlf(client["prompt"] + " "))
except:
logger.error("Send error")
chan.setblocking(False)
chan.settimeout(None)
@ -206,14 +205,15 @@ def expect(self, client, 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"])
self._handle_event("disconnected", self.client_handlers[peername])
except:
logger.info(f"{peername} is disconnected by timeout")
self._handle_event("timeout", self.client_handlers[peername]["current_user"])
logger.info(f"{peername} is disconnected")
self._handle_event("disconnected", self.client_handlers[peername])

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-2024 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

View File

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