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 # 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 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] read full history from [docs](https://damp11113.xyz/PyserSSHDocs/history.html)
> 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.
# Install # Install
Install from pypi Install from pypi
```bash ```bash
pip install PyserSSH 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 ```bash
pip install git+https://github.com/damp11113/PyserSSH.git 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 # Quick Example
This Server use port **2222** for default port
```py ```py
import os
from PyserSSH import Server, Send, AccountManager from PyserSSH import Server, Send, AccountManager
useraccount = AccountManager() useraccount = AccountManager(anyuser=True)
useraccount.add_account("admin", "") # create user without password
ssh = Server(useraccount) ssh = Server(useraccount)
@ssh.on_user("command") @ssh.on_user("command")
@ -35,32 +42,16 @@ def command(client, command: str):
if command == "hello": if command == "hello":
Send(client, "world!") 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 This example you can connect with `ssh admin@localhost -p 2222` and press enter on login
If you input `hello` the response is `world` If you input `hello` the response is `world`
# Demo # 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 https://github.com/damp11113/PyserSSH/assets/64675096/49bef3e2-3b15-4b64-b88e-3ca84a955de7
```
$ 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)**
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. 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 " 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 from setuptools import setup, find_packages
with open('README.md', 'r', encoding='utf-8') as f:
long_description = f.read()
setup( setup(
name='PyserSSH', name='PyserSSH',
version='4.4', version='5.0',
license='MIT', license='MIT',
author='damp11113', author='DPSoftware Foundation',
author_email='damp51252@gmail.com', author_email='contact@damp11113.xyz',
packages=find_packages('src'), packages=find_packages('src'),
package_dir={'': 'src'}, package_dir={'': 'src'},
url='https://github.com/damp11113/PyserSSH', url='https://github.com/damp11113/PyserSSH',
description="python scriptable ssh server library. based on Paramiko", 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', 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=[ 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 PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License MIT License
@ -37,40 +37,75 @@ right - \x1b[C
https://en.wikipedia.org/wiki/ANSI_escape_code https://en.wikipedia.org/wiki/ANSI_escape_code
""" """
import os import os
import ctypes
import logging import logging
from .interactive import * from .interactive import *
from .server import Server from .server import Server
from .account import AccountManager from .account import AccountManager
from .system.info import system_banner from .system.info import system_banner
if os.name == 'nt':
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
try: try:
os.environ["pyserssh_systemmessage"] os.environ["pyserssh_systemmessage"]
except: except:
os.environ["pyserssh_systemmessage"] = "YES" os.environ["pyserssh_systemmessage"] = "YES"
try:
os.environ["pyserssh_enable_damp11113"]
except:
os.environ["pyserssh_enable_damp11113"] = "YES"
try: try:
os.environ["pyserssh_log"] os.environ["pyserssh_log"]
except: except:
os.environ["pyserssh_log"] = "NO" os.environ["pyserssh_log"] = "NO"
if os.environ["pyserssh_log"] == "NO": if os.environ["pyserssh_log"] == "NO":
logging.basicConfig(level=logging.CRITICAL)
logger = logging.getLogger("PyserSSH") logger = logging.getLogger("PyserSSH")
logger.disabled = True #logger.disabled = False
if os.environ["pyserssh_systemmessage"] == "YES": if os.environ["pyserssh_systemmessage"] == "YES":
print(system_banner) print(system_banner)
if __name__ == "__main__": # Server Managers
stadem = input("Do you want to run demo? (y/n): ")
if stadem.upper() in ["Y", "YES"]: class ServerManager:
from .demo import demo1 def __init__(self):
else: self.servers = {}
exit()
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 PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
import os
import pickle import pickle
import time import time
import atexit import atexit
import threading import threading
import hashlib
class AccountManager: 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.accounts = {}
self.anyuser = anyuser self.allow_guest = allow_guest
self.historylimit = historylimit self.historylimit = historylimit
self.autosavedelay = autosavedelay self.autosavedelay = autosavedelay
self.__autosavework = False self.__autosavework = False
self.__autosaveworknexttime = 0 self.__autosaveworknexttime = 0
if self.anyuser:
print("history system can't work if 'anyuser' is enable")
if autoload: if autoload:
self.load(autoloadfile) self.load(autoloadfile)
if autosave: if autosave:
self.__autosavethread = threading.Thread(target=self.__autosave) self.__autosavethread = threading.Thread(target=self.__autosave, daemon=True)
self.__autosavethread.start() self.__autosavethread.start()
atexit.register(self.__saveexit) atexit.register(self.__saveexit)
@ -67,37 +65,89 @@ class AccountManager:
self.save("autosave_session.ses") self.save("autosave_session.ses")
self.__autosavethread.join() self.__autosavethread.join()
def validate_credentials(self, username, password): def validate_credentials(self, username, password=None, public_key=None):
if username in self.accounts and self.accounts[username]["password"] == password or self.anyuser: if self.allow_guest and not self.has_user(username):
return True 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 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): def get_permissions(self, username):
if username in self.accounts: if self.has_user(username):
return self.accounts[username]["permissions"] return self.accounts[username]["permissions"]
return [] return []
def set_prompt(self, username, prompt=">"): def set_prompt(self, username, prompt=">"):
if username in self.accounts: if self.has_user(username):
self.accounts[username]["prompt"] = prompt self.accounts[username]["prompt"] = prompt
def get_prompt(self, username): 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 self.accounts[username]["prompt"]
return ">" # Default prompt if not set for the user return ">" # Default prompt if not set for the user
def add_account(self, username, password, permissions={}): def add_account(self, username, password=None, public_key=None, permissions:list=None):
self.accounts[username] = {"password": password, "permissions": permissions} 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): def change_password(self, username, new_password):
if username in self.accounts: if self.has_user(username):
self.accounts[username]["password"] = new_password self.accounts[username]["password"] = new_password
def set_permissions(self, username, new_permissions): def set_permissions(self, username, new_permissions):
if username in self.accounts: if self.has_user(username):
self.accounts[username]["permissions"] = new_permissions self.accounts[username]["permissions"] = new_permissions
def save(self, filename="session.ssh"): def save(self, filename="session.ses"):
with open(filename, 'wb') as file: with open(filename, 'wb') as file:
pickle.dump(self.accounts, file) pickle.dump(self.accounts, file)
@ -111,103 +161,115 @@ class AccountManager:
print(f"An error occurred: {e}. No accounts loaded.") print(f"An error occurred: {e}. No accounts loaded.")
def set_user_sftp_allow(self, username, allow=True): 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 self.accounts[username]["sftp_allow"] = allow
def get_user_sftp_allow(self, username): def get_user_sftp_allow(self, username):
if username in self.accounts and "sftp_allow" in self.accounts[username]: if self.has_user(username) and "sftp_allow" in self.accounts[username]:
if self.anyuser:
return True
return self.accounts[username]["sftp_allow"] return self.accounts[username]["sftp_allow"]
return True return False
def set_user_sftp_readonly(self, username, readonly=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 self.accounts[username]["sftp_readonly"] = readonly
def get_user_sftp_readonly(self, username): 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 self.accounts[username]["sftp_readonly"]
return False return False
def set_user_sftp_path(self, username, path="/"): def set_user_sftp_root_path(self, username, path="/"):
if username in self.accounts: if self.has_user(username):
if path == "/": if path == "/":
self.accounts[username]["sftp_path"] = "" self.accounts[username]["sftp_root_path"] = os.getcwd()
else: else:
self.accounts[username]["sftp_path"] = path self.accounts[username]["sftp_root_path"] = path
def get_user_sftp_path(self, username): def get_user_sftp_root_path(self, username):
if username in self.accounts and "sftp_path" in self.accounts[username]: if self.has_user(username) and "sftp_root_path" in self.accounts[username]:
return self.accounts[username]["sftp_path"] return self.accounts[username]["sftp_root_path"]
return "" 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): def set_banner(self, username, banner):
if username in self.accounts: if self.has_user(username):
self.accounts[username]["banner"] = banner self.accounts[username]["banner"] = banner
def get_banner(self, username): 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 self.accounts[username]["banner"]
return None return None
def get_user_timeout(self, username): 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 self.accounts[username]["timeout"]
return None return None
def set_user_timeout(self, username, timeout=None): def set_user_timeout(self, username, timeout=None):
if username in self.accounts: if self.has_user(username):
self.accounts[username]["timeout"] = timeout self.accounts[username]["timeout"] = timeout
def get_user_last_login(self, username): 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 self.accounts[username]["lastlogin"]
return None return None
def set_user_last_login(self, username, ip, timelogin=time.time()): 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"] = { self.accounts[username]["lastlogin"] = {
"ip": ip, "ip": ip,
"time": timelogin "time": timelogin
} }
def add_history(self, username, command): def add_history(self, username, command):
if not self.anyuser: if self.has_user(username):
if username in self.accounts: if "history" not in self.accounts[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 not self.anyuser:
if username in self.accounts:
self.accounts[username]["history"] = [] # Initialize history list if it doesn't exist 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): def get_history(self, username, index, getall=False):
if not self.anyuser: if self.has_user(username) and "history" in self.accounts[username]:
if username in self.accounts and "history" in self.accounts[username]: history = self.accounts[username]["history"]
history = self.accounts[username]["history"] history.reverse()
history.reverse() if getall:
if getall: return history
return history else:
if index < len(history):
return history[index]
else: else:
if index < len(history): return None # Index out of range
return history[index] return None # User or history not found
else:
return None # Index out of range
return None # User or history not found
def get_lastcommand(self, username): def get_lastcommand(self, username):
if not self.anyuser: if self.has_user(username) and "lastcommand" in self.accounts[username]:
if username in self.accounts and "lastcommand" in self.accounts[username]: command = self.accounts[username]["lastcommand"]
command = self.accounts[username]["lastcommand"] return command
return command return None # User or history not found
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 PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License MIT License
@ -30,16 +30,19 @@ import shlex
from ..interactive import Send from ..interactive import Send
def are_permissions_met(permission_list, permission_require):
return set(permission_require).issubset(set(permission_list))
class XHandler: class XHandler:
def __init__(self, enablehelp=True, showusageonworng=True): def __init__(self, enablehelp=True, showusageonworng=True):
self.handlers = {} self.handlers = {}
self.categories = {} self.categories = {}
self.enablehelp = enablehelp self.enablehelp = enablehelp
self.showusageonworng = showusageonworng self.showusageonworng = showusageonworng
self.serverself = None
self.commandnotfound = 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): def decorator(func):
nonlocal name, category nonlocal name, category
if name is None: if name is None:
@ -48,21 +51,32 @@ class XHandler:
command_description = func.__doc__ # Read the docstring command_description = func.__doc__ # Read the docstring
parameters = inspect.signature(func).parameters parameters = inspect.signature(func).parameters
command_args = [] command_args = []
has_args = False
has_kwargs = False
for param in list(parameters.values())[1:]: # Exclude first parameter (client) 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: if param.annotation == bool:
command_args.append(f"-{param.name}") command_args.append(f"--{param.name}")
else: else:
command_args.append((f"{param.name}", param.default)) command_args.append((f"{param.name}", param.default))
else: else:
command_args.append(param.name) command_args.append(param.name)
if category is None: if category is None:
category = 'No Category' category = 'No Category'
if category not in self.categories: if category not in self.categories:
self.categories[category] = {} self.categories[category] = {}
self.categories[category][command_name] = { self.categories[category][command_name] = {
'description': command_description.strip() if command_description else "", '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 self.handlers[command_name] = func
if aliases: if aliases:
@ -76,6 +90,7 @@ class XHandler:
tokens = shlex.split(command_string) tokens = shlex.split(command_string)
command_name = tokens[0] command_name = tokens[0]
args = tokens[1:] args = tokens[1:]
if command_name == "help" and self.enablehelp: if command_name == "help" and self.enablehelp:
if args: if args:
Send(client, self.get_help_command_info(args[0])) Send(client, self.get_help_command_info(args[0]))
@ -85,58 +100,68 @@ class XHandler:
else: else:
if command_name in self.handlers: if command_name in self.handlers:
command_func = self.handlers[command_name] 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 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 = {} final_args = {}
for i in range(0, len(args), 2): final_kwargs = {}
if args[i].startswith("--"): i = 0
arg_name = args[i].lstrip('--')
while i < len(args):
arg = args[i]
if arg.startswith('-'):
arg_name = arg.lstrip('-')
if arg_name not in command_args: if arg_name not in command_args:
if self.showusageonworng: if self.showusageonworng:
Send(client, self.get_help_command_info(command_name)) 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 return
try: if command_args[arg_name].annotation == bool:
args[i + 1] final_args[arg_name] = True
except: i += 1
pass
else: else:
if self.showusageonworng: if i + 1 < len(args):
Send(client, self.get_help_command_info(command_name)) final_args[arg_name] = args[i + 1]
i += 2
else: else:
Send(client, f"value '{args[i + 1]}' not available for '{arg_name}' flag for command '{command_name}'.") if self.showusageonworng:
return Send(client, self.get_help_command_info(command_name))
final_args[arg_name] = True Send(client, f"Missing value for flag '{arg_name}' for command '{command_name}'.")
return
else: else:
arg_name = args[i].lstrip('-') if command_info['has_args']:
if arg_name not in command_args: final_args.setdefault('args', []).append(arg)
if self.showusageonworng: elif command_info['has_kwargs']:
Send(client, self.get_help_command_info(command_name)) 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: else:
Send(client, f"Invalid argument '{arg_name}' for command '{command_name}'.") if self.showusageonworng:
return Send(client, self.get_help_command_info(command_name))
arg_value = args[i + 1] Send(client, f"Unexpected argument '{arg}' for command '{command_name}'.")
final_args[arg_name] = arg_value return
# Match parsed arguments to function parameters i += 1
final_args_list = []
# Check for required positional arguments
for param in list(command_args.values())[1:]: # Skip client argument for param in list(command_args.values())[1:]: # Skip client argument
if param.name in final_args: if param.name not in final_args and param.default == inspect.Parameter.empty:
final_args_list.append(final_args[param.name])
elif param.default != inspect.Parameter.empty:
final_args_list.append(param.default)
else:
if self.showusageonworng: if self.showusageonworng:
Send(client, self.get_help_command_info(command_name)) 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 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) return command_func(client, *final_args_list)
else: else:
if self.commandnotfound: if self.commandnotfound:
@ -165,7 +190,10 @@ class XHandler:
'name': command_name, 'name': command_name,
'description': found_command['description'].strip() if found_command['description'] else "", 'description': found_command['description'].strip() if found_command['description'] else "",
'args': found_command['args'], '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): def get_help_command_info(self, command):
@ -185,6 +213,10 @@ class XHandler:
help_message += f" [-{arg[0]} {arg[1]}]" help_message += f" [-{arg[0]} {arg[1]}]"
else: else:
help_message += f" <{arg}>" help_message += f" <{arg}>"
if command_info['has_args']:
help_message += " [<args>...]"
if command_info['has_kwargs']:
help_message += " [--<key>=<value>...]"
return help_message return help_message
def get_help_message(self): def get_help_message(self):
@ -202,4 +234,5 @@ class XHandler:
all_commands = {} all_commands = {}
for category, commands in self.categories.items(): for category, commands in self.categories.items():
all_commands[category] = commands all_commands[category] = commands
return all_commands return all_commands

View File

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

View File

@ -1,8 +1,8 @@
""" """
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License MIT License
@ -31,7 +31,7 @@ from ..interactive import Clear, Send, wait_inputkey
from ..system.sysfunc import text_centered_screen from ..system.sysfunc import text_centered_screen
class TextDialog: class TextDialog:
def __init__(self, client, title="", content=""): def __init__(self, client, content="", title=""):
self.client = client self.client = client
self.windowsize = client["windowsize"] self.windowsize = client["windowsize"]

View File

@ -1,8 +1,8 @@
""" """
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
import time
from ..interactive import Send
def clickable_url(url, link_text=""): def clickable_url(url, link_text=""):
return f"\033]8;;{url}\033\\{link_text}\033]8;;\033\\" return f"\033]8;;{url}\033\\{link_text}\033]8;;\033\\"
class BasicTextFormatter: def Send_karaoke_effect(client, text, delay=0.1, ln=True):
RESET = "\033[0m" printed_text = ""
TEXT_COLORS = { for i, char in enumerate(text):
"black": "\033[30m", # Print already printed text normally
"red": "\033[31m", Send(client, printed_text + char, ln=False)
"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 # Calculate not yet printed text to dim
def format_text(text, color=None, color_level=None, background=None, attributes=None, target_text=''): not_printed_text = text[i + 1:]
formatted_text = "" dimmed_text = ''.join([f"\033[2m{char}\033[0m" for char in not_printed_text])
start_index = text.find(target_text)
end_index = start_index + len(target_text) if start_index != -1 else len(text)
if color in BasicTextFormatter.TEXT_COLORS: # Print dimmed text
if color_level in BasicTextFormatter.TEXT_COLOR_LEVELS: Send(client, dimmed_text, ln=False)
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]
if background in BasicTextFormatter.BACKGROUND_COLORS: # Wait before printing the next character
formatted_text += BasicTextFormatter.BACKGROUND_COLORS[background] time.sleep(delay)
if attributes in BasicTextFormatter.TEXT_ATTRIBUTES: # Clear the line for the next iteration
formatted_text += BasicTextFormatter.TEXT_ATTRIBUTES[attributes] Send(client, '\r' ,ln=False)
if target_text == "": # Prepare the updated printed_text for the next iteration
formatted_text += text + BasicTextFormatter.RESET printed_text += char
else:
formatted_text += text[:start_index] + text[start_index:end_index] + BasicTextFormatter.RESET + text[end_index:] 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 PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License MIT License
@ -25,14 +25,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
""" from ..interactive import Send
note
ansi cursor arrow def ShowCursor(client, show=True):
up - \x1b[A if show:
down - \x1b[B Send(client, "\033[?25h", ln=False)
left - \x1b[D else:
right - \x1b[C 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 PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
# this file is from damp11113-library # this file is from DPSoftware Foundation-library
from itertools import cycle from itertools import cycle
import math import math
@ -37,10 +37,97 @@ from ..system.sysfunc import replace_enter_with_crlf
def Print(channel, string, start="", end="\n"): def Print(channel, string, start="", end="\n"):
channel.send(replace_enter_with_crlf(start + string + end)) channel.send(replace_enter_with_crlf(start + string + end))
try: def get_size_unit2(number, unitp, persec=True, unitsize=1024, decimal=True, space=" "):
from damp11113.utils import get_size_unit2, center_string, TextFormatter, insert_string for unit in ['', 'K', 'M', 'G', 'T', 'P']:
except: if number < unitsize:
raise ModuleNotFoundError("This extension is require damp11113-library") 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 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]'] steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]']
steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]'] 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 PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License MIT License
@ -29,13 +29,42 @@ import socket
from .system.sysfunc import replace_enter_with_crlf from .system.sysfunc import replace_enter_with_crlf
def Send(client, string, ln=True): def Send(client, string, ln=True, directchannel=False):
channel = client["channel"] if directchannel:
channel = client
else:
channel = client["channel"]
if ln: if ln:
channel.send(replace_enter_with_crlf(str(string) + "\n")) channel.send(replace_enter_with_crlf(str(string) + "\n"))
else: else:
channel.send(replace_enter_with_crlf(str(string))) 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): def Clear(client, oldclear=False, keep=False):
sx, sy = client["windowsize"]["width"], client["windowsize"]["height"] 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: else:
return output return output
def wait_inputkey(client, prompt="", raw=False, timeout=0): def wait_inputkey(client, prompt="", raw=True, timeout=0):
channel = client["channel"] channel = client["channel"]
if prompt != "": if prompt != "":
@ -150,7 +179,8 @@ def wait_inputkey(client, prompt="", raw=False, timeout=0):
except socket.timeout: except socket.timeout:
channel.setblocking(False) channel.setblocking(False)
channel.settimeout(None) channel.settimeout(None)
channel.send("\r\n") if prompt != "":
channel.send("\r\n")
return None return None
except Exception: except Exception:
channel.setblocking(False) channel.setblocking(False)
@ -158,6 +188,48 @@ def wait_inputkey(client, prompt="", raw=False, timeout=0):
channel.send("\r\n") channel.send("\r\n")
raise 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): def wait_choose(client, choose, prompt="", timeout=0):
channel = client["channel"] channel = client["channel"]
@ -176,18 +248,18 @@ def wait_choose(client, choose, prompt="", timeout=0):
exported = " ".join(tempchooselist) exported = " ".join(tempchooselist)
if prompt.strip() == "": if prompt.strip() == "":
Send(channel, f'\r{exported}', ln=False) Send(client, f'\r{exported}', ln=False)
else: 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 if keyinput == b'\r': # Enter key
Send(channel, "\033[K") Send(client, "\033[K")
return chooseindex return chooseindex
elif keyinput == b'\x03': # ' ctrl+c' key for cancel elif keyinput == b'\x03': # ' ctrl+c' key for cancel
Send(channel, "\033[K") Send(client, "\033[K")
return None return 0
elif keyinput == b'\x1b[D': # Up arrow key elif keyinput == b'\x1b[D': # Up arrow key
chooseindex -= 1 chooseindex -= 1
if chooseindex < 0: if chooseindex < 0:

View File

@ -1,8 +1,8 @@
""" """
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License MIT License
@ -25,18 +25,21 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
import os
import time import time
import paramiko import paramiko
import threading import threading
from functools import wraps from functools import wraps
import logging import logging
import socket
import random
import traceback
from .system.SFTP import SSHSFTPServer from .system.SFTP import SSHSFTPServer
from .system.sysfunc import replace_enter_with_crlf
from .system.interface import Sinterface from .system.interface import Sinterface
from .interactive import *
from .system.inputsystem import expect 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) # paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22)
@ -45,16 +48,18 @@ sftpclient = ["WinSCP", "Xplore"]
logger = logging.getLogger("PyserSSH") logger = logging.getLogger("PyserSSH")
class Server: 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.sysmess = system_message
self.client_handlers = {} # Dictionary to store event handlers for each client
self.accounts = accounts self.accounts = accounts
self.disable_scroll_with_arrow = disable_scroll_with_arrow self.disable_scroll_with_arrow = disable_scroll_with_arrow
self.sftproot = sftproot
self.sftpena = sftp self.sftpena = sftp
self.enasyscom = system_commands self.enasyscom = system_commands
self.compressena = compression self.compressena = compression
@ -64,10 +69,19 @@ class Server:
self.XHandler = XHandler self.XHandler = XHandler
self.title = title self.title = title
self.inspeed = inspeed 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.__processmode = None
self.__serverisrunning = False self.__serverisrunning = False
self.__server_stopped = threading.Event() # Event to signal server stop self.__daemon = False
if self.enasyscom: if self.enasyscom:
print("\033[33m!!Warning!! System commands is enable! \033[0m") print("\033[33m!!Warning!! System commands is enable! \033[0m")
@ -75,40 +89,41 @@ class Server:
def on_user(self, event_name): def on_user(self, event_name):
def decorator(func): def decorator(func):
@wraps(func) @wraps(func)
def wrapper(channel, *args, **kwargs): def wrapper(client, *args, **kwargs):
# Ignore the third argument # Ignore the third argument
filtered_args = args[:2] + args[3:] filtered_args = args[:2] + args[3:]
return func(channel, *filtered_args, **kwargs) return func(client, *filtered_args, **kwargs)
self._event_handlers[event_name] = wrapper self._event_handlers[event_name] = wrapper
return wrapper return wrapper
return decorator return decorator
def handle_client_disconnection(self, peername, current_user): def handle_client_disconnection(self, handler, chandlers):
if peername in self.client_handlers: if not chandlers["channel"].get_transport().is_active():
del self.client_handlers[peername] if handler:
logger.info(f"User {current_user} disconnected") handler(chandlers)
del self.client_handlers[chandlers["peername"]]
def _handle_event(self, event_name, *args, **kwargs): def _handle_event(self, event_name, *args, **kwargs):
handler = self._event_handlers.get(event_name) handler = self._event_handlers.get(event_name)
if handler: if event_name == "error" and isinstance(args[0], Clientype):
handler(*args, **kwargs) args[0].last_error = traceback.format_exc()
if event_name == "disconnected":
self.handle_client_disconnection(*args, **kwargs) 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) bh_session.add_server_key(self.private_key)
if self.sftpena: bh_session.use_compression(self.compressena)
SSHSFTPServer.ROOT = self.sftproot
SSHSFTPServer.ACCOUNT = self.accounts
SSHSFTPServer.CLIENTHANDELES = self.client_handlers
bh_session.set_subsystem_handler('sftp', paramiko.SFTPServer, SSHSFTPServer)
if self.compressena:
bh_session.use_compression(True)
else:
bh_session.use_compression(False)
bh_session.default_window_size = 2147483647 bh_session.default_window_size = 2147483647
bh_session.packetizer.REKEY_BYTES = pow(2, 40) bh_session.packetizer.REKEY_BYTES = pow(2, 40)
@ -117,102 +132,154 @@ class Server:
bh_session.default_max_packet_size = self.inspeed bh_session.default_max_packet_size = self.inspeed
server = Sinterface(self) server = Sinterface(self)
bh_session.start_server(server=server) try:
bh_session.start_server(server=server)
except:
return
logger.info(bh_session.remote_version) logger.info(bh_session.remote_version)
channel = bh_session.accept() 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: if channel is None:
logger.warning("no channel") logger.warning("no channel")
bh_session.close()
return
try: try:
logger.info("user authenticated") logger.info("user authenticated")
peername = channel.getpeername() peername = bh_session.getpeername()
if peername not in self.client_handlers: if peername not in self.client_handlers:
# Create a new event handler for this client if it doesn't exist # Create a new event handler for this client if it doesn't exist
self.client_handlers[peername] = { self.client_handlers[peername] = Clientype(channel, bh_session, 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
}
client_handler = self.client_handlers[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["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_activity_time"] = time.time()
client_handler["last_login_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]) 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 any(bh_session.remote_version.split("-")[2].startswith(prefix) for prefix in sftpclient):
if not channel.out_window_size == bh_session.default_window_size: if int(channel.out_window_size) != int(bh_session.default_window_size):
while self.client_handlers[channel.getpeername()]["windowsize"] == {}: logger.info("user is ssh")
pass #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"]) userbanner = self.accounts.get_banner(self.client_handlers[channel.getpeername()]["current_user"])
if self.sysmess or userbanner != None: if self.accounts.get_user_enable_inputsystem_echo(self.client_handlers[channel.getpeername()]["current_user"]) and self.inputsysecho:
channel.send(f"\033]0;{self.title}\007".encode()) echo = True
channel.sendall(replace_enter_with_crlf(userbanner)) else:
channel.sendall(replace_enter_with_crlf("\n")) 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: try:
self._handle_event("connect", self.client_handlers[channel.getpeername()]) self._handle_event("connect", self.client_handlers[channel.getpeername()])
except Exception as e: except Exception as e:
self._handle_event("error", self.client_handlers[channel.getpeername()], e) self._handle_event("error", self.client_handlers[channel.getpeername()], e)
client_handler["connecttype"] = "ssh" if self.enainputsystem and self.accounts.get_user_enable_inputsystem(self.client_handlers[channel.getpeername()]["current_user"]):
if self.enainputsystem:
try: try:
if self.accounts.get_user_timeout(self.client_handlers[channel.getpeername()]["current_user"]) != None: if self.accounts.get_user_timeout(self.client_handlers[channel.getpeername()]["current_user"]) != None:
channel.setblocking(False) channel.setblocking(False)
channel.settimeout(self.accounts.get_user_timeout(self.client_handlers[channel.getpeername()]["current_user"])) 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: while True:
expect(self, self.client_handlers[channel.getpeername()]) expect(self, self.client_handlers[channel.getpeername()], echo)
except KeyboardInterrupt: except KeyboardInterrupt:
self._handle_event("disconnected", self.client_handlers[peername]["current_user"]) self._handle_event("disconnected", self.client_handlers[peername])
channel.close() channel.close()
bh_session.close() bh_session.close()
except Exception as e: except Exception as e:
self._handle_event("syserror", client_handler, e) self._handle_event("error", client_handler, e)
logger.error(e) logger.error(e)
finally: finally:
self._handle_event("disconnected", self.client_handlers[peername]["current_user"]) self._handle_event("disconnected", self.client_handlers[peername])
channel.close() channel.close()
else: else:
if self.sftpena: if self.sftpena:
logger.info("user is sftp")
if self.accounts.get_user_sftp_allow(self.client_handlers[channel.getpeername()]["current_user"]): if self.accounts.get_user_sftp_allow(self.client_handlers[channel.getpeername()]["current_user"]):
client_handler["connecttype"] = "sftp" client_handler["connecttype"] = "sftp"
self._handle_event("connectsftp", self.client_handlers[channel.getpeername()]) 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: else:
self._handle_event("disconnected", self.client_handlers[peername]["current_user"]) self._handle_event("disconnected", self.client_handlers[peername])
channel.close() channel.close()
else: else:
self._handle_event("disconnected", self.client_handlers[peername]["current_user"]) self._handle_event("disconnected", self.client_handlers[peername])
channel.close() channel.close()
except: except:
pass bh_session.close()
def stop_server(self): def stop_server(self):
logger.info("Stopping the server...") logger.info("Stopping the server...")
try: try:
for client_handler in self.client_handlers.values(): for client_handler in self.client_handlers.values():
channel = client_handler.get("channel") channel = client_handler.channel
if channel: if channel:
channel.close() channel.close()
self.__serverisrunning = True self.__serverisrunning = False
self.server.close() self.server.close()
logger.info("Server stopped.") logger.info("Server stopped.")
except Exception as e: except Exception as e:
logger.error(f"Error occurred while stopping the server: {e}") logger.error(f"Error occurred while stopping the server: {e}")
@ -223,20 +290,29 @@ class Server:
while self.__serverisrunning: while self.__serverisrunning:
client, addr = self.server.accept() client, addr = self.server.accept()
if self.__processmode == "thread": 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() client_thread.start()
else: else:
self.handle_client(client, addr) self.handle_client(client, addr)
time.sleep(1)
except KeyboardInterrupt:
self.stop_server()
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
def run(self, private_key_path, host="0.0.0.0", port=2222, mode="thread", maxuser=0, daemon=False): def run(self, private_key_path=None, host="0.0.0.0", port=2222, mode="thread", maxuser=0, daemon=False):
"""mode: single, thread""" """mode: single, thread
protocol: ssh, telnet
"""
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
self.server.bind((host, port)) 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: if maxuser == 0:
self.server.listen() self.server.listen()
else: else:
@ -244,81 +320,8 @@ class Server:
self.__processmode = mode.lower() self.__processmode = mode.lower()
self.__serverisrunning = True self.__serverisrunning = True
self.__daemon = daemon
client_thread = threading.Thread(target=self._start_listening_thread) client_thread = threading.Thread(target=self._start_listening_thread, daemon=self.__daemon)
client_thread.daemon = daemon
client_thread.start() 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 PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License MIT License
@ -40,17 +40,20 @@ class SSHSFTPHandle(paramiko.SFTPHandle):
# use the stored filename # use the stored filename
try: try:
paramiko.SFTPServer.set_file_attr(self.filename, attr) paramiko.SFTPServer.set_file_attr(self.filename, attr)
return paramiko.SFTP_OK return paramiko.sftp.SFTP_OK
except OSError as e: except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno) return paramiko.SFTPServer.convert_errno(e.errno)
class SSHSFTPServer(paramiko.SFTPServerInterface): class SSHSFTPServer(paramiko.SFTPServerInterface):
ROOT = None def __init__(self, server: paramiko.ServerInterface, *args, **kwargs):
ACCOUNT = None super().__init__(server)
CLIENTHANDELES = None self.channel = args[0]
self.account = args[1]
self.clientH = args[2]
def _realpath(self, path): 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): def list_folder(self, path):
path = self._realpath(path) path = self._realpath(path)
@ -80,6 +83,12 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
return paramiko.SFTPServer.convert_errno(e.errno) return paramiko.SFTPServer.convert_errno(e.errno)
def open(self, path, flags, attr): 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) path = self._realpath(path)
try: try:
binary_flag = getattr(os, 'O_BINARY', 0) binary_flag = getattr(os, 'O_BINARY', 0)
@ -120,23 +129,32 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
return fobj return fobj
def remove(self, path): 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) path = self._realpath(path)
try: try:
os.remove(path) os.remove(path)
except OSError as e: except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno) return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK return paramiko.sftp.SFTP_OK
def rename(self, oldpath, newpath): 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) oldpath = self._realpath(oldpath)
newpath = self._realpath(newpath) newpath = self._realpath(newpath)
try: try:
os.rename(oldpath, newpath) os.rename(oldpath, newpath)
except OSError as e: except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno) return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK return paramiko.sftp.SFTP_OK
def mkdir(self, path, attr): 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) path = self._realpath(path)
try: try:
os.mkdir(path) os.mkdir(path)
@ -144,45 +162,58 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
paramiko.SFTPServer.set_file_attr(path, attr) paramiko.SFTPServer.set_file_attr(path, attr)
except OSError as e: except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno) return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK return paramiko.sftp.SFTP_OK
def rmdir(self, path): 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) path = self._realpath(path)
try: try:
os.rmdir(path) os.rmdir(path)
except OSError as e: except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno) return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK return paramiko.sftp.SFTP_OK
def chattr(self, path, attr): 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) path = self._realpath(path)
try: try:
paramiko.SFTPServer.set_file_attr(path, attr) paramiko.SFTPServer.set_file_attr(path, attr)
except OSError as e: except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno) return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK return paramiko.sftp.SFTP_OK
def symlink(self, target_path, path): 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) path = self._realpath(path)
if (len(target_path) > 0) and (target_path[0] == '/'): if (len(target_path) > 0) and (target_path[0] == '/'):
# absolute symlink # 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] == '//': if target_path[:2] == '//':
# bug in os.path.join # bug in os.path.join
target_path = target_path[1:] target_path = target_path[1:]
else: else:
# compute relative to path # compute relative to path
abspath = os.path.join(os.path.dirname(path), target_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 # this symlink isn't going to work anyway -- just break it immediately
target_path = '<error>' target_path = '<error>'
try: try:
os.symlink(target_path, path) os.symlink(target_path, path)
except OSError as e: except OSError as e:
return paramiko.SFTPServer.convert_errno(e.errno) return paramiko.SFTPServer.convert_errno(e.errno)
return paramiko.SFTP_OK return paramiko.sftp.SFTP_OK
def readlink(self, path): def readlink(self, path):
root = self.account.get_user_sftp_root_path(self.clientH[self.channel.getpeername()]["current_user"])
path = self._realpath(path) path = self._realpath(path)
try: try:
symlink = os.readlink(path) symlink = os.readlink(path)
@ -190,8 +221,8 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
return paramiko.SFTPServer.convert_errno(e.errno) return paramiko.SFTPServer.convert_errno(e.errno)
if os.path.isabs(symlink): if os.path.isabs(symlink):
if symlink[:len(self.ROOT)] == self.ROOT: if symlink[:len(root)] == root:
symlink = symlink[len(self.ROOT):] symlink = symlink[len(root):]
if (len(symlink) == 0) or (symlink[0] != '/'): if (len(symlink) == 0) or (symlink[0] != '/'):
symlink = '/' + symlink symlink = '/' + symlink
else: else:

View File

@ -1,8 +1,8 @@
""" """
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License 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 PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
import re
__version__ = "4.4" __version__ = "5.0"
system_banner = ( 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[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 PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License MIT License
@ -27,8 +27,6 @@ SOFTWARE.
import socket import socket
import time import time
import logging import logging
import shlex
import traceback
from .sysfunc import replace_enter_with_crlf from .sysfunc import replace_enter_with_crlf
from .syscom import systemcommand from .syscom import systemcommand
@ -53,7 +51,7 @@ def expect(self, client, echo=True):
chan.close() chan.close()
raise EOFError() 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() self.client_handlers[chan.getpeername()]["last_activity_time"] = time.time()
@ -146,7 +144,7 @@ def expect(self, client, echo=True):
else: else:
history_index_position = -1 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 echo:
if outindexall != cursor_position: if outindexall != cursor_position:
chan.sendall(b" ") 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: if self.history and command.strip() != "" and self.accounts.get_lastcommand(client["current_user"]) != command:
self.accounts.add_history(client["current_user"], command) self.accounts.add_history(client["current_user"], command)
client["last_command"] = command
if command.strip() != "": if command.strip() != "":
if self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]) != None: 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: except Exception as e:
self._handle_event("error", client, e) self._handle_event("error", client, e)
if echo:
try: try:
chan.send(replace_enter_with_crlf(client["prompt"] + " ").encode('utf-8')) chan.send(replace_enter_with_crlf(client["prompt"] + " "))
except: except:
logger.error("Send error") logger.error("Send error")
chan.setblocking(False) chan.setblocking(False)
chan.settimeout(None) 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: if self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]) != None:
chan.setblocking(False) chan.setblocking(False)
chan.settimeout(self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"])) chan.settimeout(self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]))
except socket.error:
pass
except Exception as e: except Exception as e:
logger.error(str(e)) logger.error(str(e))
finally: finally:
try: try:
if not byte: if not byte:
logger.info(f"{peername} is disconnected") 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: except:
logger.info(f"{peername} is disconnected by timeout") logger.info(f"{peername} is disconnected")
self._handle_event("timeout", self.client_handlers[peername]["current_user"]) 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 PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
import time
import paramiko 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): class Sinterface(paramiko.ServerInterface):
def __init__(self, serverself): def __init__(self, serverself):
self.current_user = None
self.serverself = serverself self.serverself = serverself
def check_channel_request(self, kind, chanid): def check_channel_request(self, kind, channel_id):
if kind == 'session': if kind == 'session':
return paramiko.OPEN_SUCCEEDED return paramiko.OPEN_SUCCEEDED
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED 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): def check_auth_password(self, username, password):
data = { data = {
"username": username, "username": username,
"password": password, "password": password,
"auth_type": "password"
} }
if self.serverself.accounts.validate_credentials(username, password) and not self.serverself.usexternalauth: 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 return paramiko.AUTH_SUCCESSFUL
else: else:
if self.serverself._handle_event("auth", data): if self.serverself._handle_event("auth", data):
self.current_user = username # Store the current user upon successful authentication
return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_SUCCESSFUL
else: else:
return paramiko.AUTH_FAILED 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): def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes):
data = { data = {
"term": term, "term": term,
@ -69,7 +226,9 @@ class Sinterface(paramiko.ServerInterface):
"pixelheight": pixelheight, "pixelheight": pixelheight,
} }
try: try:
time.sleep(0.01) # fix waiting windowsize
self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data2 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) self.serverself._handle_event("connectpty", self.serverself.client_handlers[channel.getpeername()], data)
except: except:
pass pass
@ -102,4 +261,4 @@ class Sinterface(paramiko.ServerInterface):
"pixelheight": pixelheight "pixelheight": pixelheight
} }
self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data
self.serverself._handle_event("resized", self.serverself.client_handlers[channel.getpeername()], data) self.serverself._handle_event("resized", self.serverself.client_handlers[channel.getpeername()], data)

View File

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

View File

@ -1,8 +1,8 @@
""" """
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
Copyright (C) 2023-2024 damp11113 (MIT) Copyright (C) 2023-2024 DPSoftware Foundation (MIT)
Visit https://github.com/damp11113/PyserSSH Visit https://github.com/DPSoftware-Foundation/PyserSSH
MIT License MIT License
@ -26,10 +26,20 @@ SOFTWARE.
""" """
def replace_enter_with_crlf(input_string): 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') 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=" "): def text_centered_screen(text, screen_width, screen_height, spacecharacter=" "):
screen = [] screen = []