mirror of
https://github.com/damp11113/PyserSSH.git
synced 2025-04-27 22:48:11 +00:00
Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
b9e4bb5887 | |||
3a751972c1 | |||
a3f1b50303 | |||
64bb2b0616 | |||
78a6459d26 | |||
1b15383ca7 | |||
f5cb7ead49 | |||
7eda693f78 | |||
019836cb00 | |||
0d6c58b71a | |||
31276827c4 | |||
b6455ce6a3 | |||
f578fc57d5 | |||
c4b5314fda | |||
98361c33f2 | |||
058daf9f04 | |||
f33be3bca1 | |||
79353cf668 | |||
c9ffdf5201 | |||
|
527094f2bb | ||
|
24398998b8 | ||
|
6439cb0358 | ||
|
634ade9f46 | ||
|
d8c7f19d81 | ||
|
16dc6b9a6e | ||
|
50e5983931 | ||
|
8fa7a21537 | ||
8742212952 |
34
.github/ISSUE_TEMPLATE/extension-request.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/extension-request.md
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Extension Request
|
||||
about: Request new extension for PyserSSH
|
||||
title: Extension Request
|
||||
labels: new extension
|
||||
assignees: damp11113
|
||||
|
||||
---
|
||||
|
||||
**Extension Name**
|
||||
[Enter Extension Name]
|
||||
|
||||
**Purpose**
|
||||
[Briefly describe the purpose or functionality of the extension.]
|
||||
|
||||
**Expected Features (optional)**
|
||||
[List the features or capabilities that this extension will provide.]
|
||||
|
||||
**Use Case**
|
||||
[Provide a scenario or use case where this extension would be beneficial.]
|
||||
|
||||
**Code/Repository Link**
|
||||
[Provide a link to the code repository or any relevant code snippets.]
|
||||
|
||||
**Rationale**
|
||||
[Why you request this extension]
|
||||
|
||||
**Implementation Suggestions**
|
||||
[If you have any suggestions or ideas on how this extension could be implemented, please share them here.]
|
||||
|
||||
**Have more Information? (optional)**
|
||||
[Include any additional details, documentation, or resources that may be relevant to this extension request.]
|
||||
|
||||
**Note:** This request will undergo review by the maintainers of PyserSSH. Your input and feedback are appreciated.
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
dist/
|
||||
src/PyserSSH.egg-info/
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 dharm pimsen
|
||||
Copyright (c) 2023-2024 dharm pimsen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
62
README.md
62
README.md
@ -1,2 +1,60 @@
|
||||
# PyserSSH
|
||||
Part from [damp11113](https://github.com/damp11113/damp11113-library)
|
||||
# What is PyserSSH
|
||||
|
||||
This library will be **Pyserminal** (Python Server Terminal) as it supports multiple protocols such as ssh telnet rlogin and mores...
|
||||
|
||||
PyserSSH is a free and open-source Python library designed to facilitate the creation of customizable SSH terminal servers. Initially developed for research purposes to address the lack of suitable SSH server libraries in Python, PyserSSH provides a flexible and user-friendly solution for implementing SSH servers, making it easier for developers to handle user interactions and command processing.
|
||||
|
||||
The project was started by a solo developer to create a more accessible and flexible tool for managing SSH connections and commands. It offers a simplified API compared to other libraries, such as Paramiko, SSHim, and Twisted, which are either outdated or complex for new users.
|
||||
|
||||
This project is part from [damp11113-library](https://github.com/damp11113/damp11113-library)
|
||||
|
||||
## Some smail PyserSSH history
|
||||
PyserSSH version [1.0](https://github.com/DPSoftware-Foundation/PyserSSH/releases/download/Legacy/PyserSSH10.py) (real filename is "test277.py") was created in 2023/9/3 for experimental purposes only. Because I couldn't find the best ssh server library for python and I started this project only for research. But I have time to develop this research into a real library for use. In software or server.
|
||||
|
||||
Read full history from [docs](https://damp11113.xyz/PyserSSHDocs/history.html)
|
||||
|
||||
# Install
|
||||
Install from pypi
|
||||
```bash
|
||||
pip install PyserSSH
|
||||
```
|
||||
Install with [openRemoDesk](https://github.com/DPSoftware-Foundation/openRemoDesk) protocol
|
||||
```bash
|
||||
pip install PyserSSH[RemoDesk]
|
||||
```
|
||||
Install from Github
|
||||
```bash
|
||||
pip install git+https://github.com/damp11113/PyserSSH.git
|
||||
```
|
||||
Install from DPCloudev Git
|
||||
```bash
|
||||
pip install git+https://git.damp11113.xyz/DPSoftware-Foundation/PyserSSH.git
|
||||
```
|
||||
|
||||
# Quick Example
|
||||
This Server use port **2222** for default port
|
||||
```py
|
||||
from PyserSSH import Server, AccountManager
|
||||
|
||||
useraccount = AccountManager(allow_guest=True)
|
||||
ssh = Server(useraccount)
|
||||
|
||||
@ssh.on_user("command")
|
||||
def command(client, command: str):
|
||||
if command == "hello":
|
||||
client.send("world!")
|
||||
|
||||
ssh.run("your private key file")
|
||||
```
|
||||
This example you can connect with `ssh admin@localhost -p 2222` and press enter on login
|
||||
If you input `hello` the response is `world`
|
||||
|
||||
# Demo
|
||||
> [!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.
|
||||
|
||||
https://github.com/damp11113/PyserSSH/assets/64675096/49bef3e2-3b15-4b64-b88e-3ca84a955de7
|
||||
|
||||
I intend to leaked private key because that key i generated new. I recommend to generate new key if you want to use on your host because that key is for demo only.
|
||||
why i talk about this? because when i push private key into this repo in next 5 min++ i getting new email from GitGuardian. in that email say "
|
||||
GitGuardian has detected the following RSA Private Key exposed within your GitHub account" i dont knows what is GitGuardian and i not install this app into my account.
|
||||
|
157
demo/Save Your Tears lyrics.srt
Normal file
157
demo/Save Your Tears lyrics.srt
Normal file
@ -0,0 +1,157 @@
|
||||
0
|
||||
00:00:00,000 --> 00:00:09,200
|
||||
(Intro)
|
||||
|
||||
1
|
||||
00:00:09,200 --> 00:00:13,100
|
||||
I saw you dancing in a crowded room
|
||||
|
||||
2
|
||||
00:00:13,200 --> 00:00:17,200
|
||||
You look so happy when
|
||||
I'm not with you
|
||||
|
||||
3
|
||||
00:00:17,300 --> 00:00:21,300
|
||||
But then you saw me, caught
|
||||
you by surprise
|
||||
|
||||
4
|
||||
00:00:21,400 --> 00:00:26,200
|
||||
A single teardrop falling
|
||||
from your eye
|
||||
|
||||
5
|
||||
00:00:26,300 --> 00:00:31,400
|
||||
I don't know why I run away
|
||||
|
||||
6
|
||||
00:00:34,400 --> 00:00:40,900
|
||||
I'll make you cry when I run away
|
||||
|
||||
7
|
||||
00:00:41,700 --> 00:00:45,950
|
||||
You could've asked me why
|
||||
I broke your heart
|
||||
|
||||
8
|
||||
00:00:46,000 --> 00:00:49,800
|
||||
You could've told me
|
||||
that you fell apart
|
||||
|
||||
9
|
||||
00:00:49,900 --> 00:00:53,700
|
||||
But you walked past me
|
||||
like I wasn't there
|
||||
|
||||
10
|
||||
00:00:53,800 --> 00:00:58,450
|
||||
And just pretended like
|
||||
you didn't care
|
||||
|
||||
11
|
||||
00:00:58,500 --> 00:01:03,000
|
||||
I don't know why I run away
|
||||
|
||||
12
|
||||
00:01:06,400 --> 00:01:11,300
|
||||
I'll make you cry when I run away
|
||||
|
||||
13
|
||||
00:01:14,700 --> 00:01:18,600
|
||||
Take me back 'cause I wanna stay
|
||||
|
||||
14
|
||||
00:01:18,700 --> 00:01:21,600
|
||||
Save your tears for another
|
||||
|
||||
15
|
||||
00:01:21,700 --> 00:01:28,900
|
||||
Save your tears for another day
|
||||
|
||||
16
|
||||
00:01:29,800 --> 00:01:34,700
|
||||
Save your tears for another day
|
||||
|
||||
17
|
||||
00:01:37,000 --> 00:01:42,900
|
||||
So, I made you think that
|
||||
I would always stay
|
||||
|
||||
18
|
||||
00:01:43,000 --> 00:01:46,600
|
||||
I said some things that
|
||||
I should never say
|
||||
|
||||
19
|
||||
00:01:46,700 --> 00:01:50,600
|
||||
Yeah, I broke your heart like
|
||||
someone did to mine
|
||||
|
||||
20
|
||||
00:01:50,700 --> 00:01:55,500
|
||||
And now you won't love
|
||||
me for a second time
|
||||
|
||||
21
|
||||
00:01:55,600 --> 00:02:00,400
|
||||
I don't know why I run away, oh, girl
|
||||
|
||||
22
|
||||
00:02:03,300 --> 00:02:08,800
|
||||
Said I make you cry when I run away
|
||||
|
||||
23
|
||||
00:02:11,500 --> 00:02:15,600
|
||||
Girl, take me back 'cause I wanna stay
|
||||
|
||||
24
|
||||
00:02:15,700 --> 00:02:19,200
|
||||
Save your tears for another
|
||||
|
||||
25
|
||||
00:02:19,300 --> 00:02:23,600
|
||||
I realize that I'm much too late
|
||||
|
||||
26
|
||||
00:02:23,700 --> 00:02:26,800
|
||||
And you deserve someone better
|
||||
|
||||
27
|
||||
00:02:26,900 --> 00:02:32,000
|
||||
Save your tears for another
|
||||
day (Ooh, yeah)
|
||||
|
||||
28
|
||||
00:02:34,900 --> 00:02:40,900
|
||||
Save your tears for another day (Yeah)
|
||||
|
||||
29
|
||||
00:02:46,100 --> 00:02:52,300
|
||||
I don't know why I run away
|
||||
|
||||
30
|
||||
00:02:52,700 --> 00:02:57,400
|
||||
I'll make you cry when I run away
|
||||
|
||||
31
|
||||
00:02:59,400 --> 00:03:06,650
|
||||
Save your tears for another
|
||||
day, ooh, girl (Ah)
|
||||
|
||||
32
|
||||
00:03:06,700 --> 00:03:13,600
|
||||
I said save your tears
|
||||
for another day (Ah)
|
||||
|
||||
33
|
||||
00:03:15,700 --> 00:03:23,200
|
||||
Save your tears for another day (Ah)
|
||||
|
||||
34
|
||||
00:03:23,700 --> 00:03:33,300
|
||||
Save your tears for another day (Ah)
|
||||
|
||||
35
|
||||
00:03:34,300 --> 00:03:43,300
|
||||
by DPSoftware Foundation
|
477
demo/demo1.py
Normal file
477
demo/demo1.py
Normal file
@ -0,0 +1,477 @@
|
||||
import os
|
||||
os.environ["damp11113_load_all_module"] = "NO"
|
||||
|
||||
import socket
|
||||
import time
|
||||
import cv2
|
||||
import traceback
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import numpy as np
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(format='[{asctime}] [{levelname}] {name}: {message}', datefmt='%Y-%m-%d %H:%M:%S', style='{', level=logging.DEBUG)
|
||||
|
||||
from PyserSSH import Server, AccountManager
|
||||
from PyserSSH.interactive import Send, wait_input, wait_inputkey, wait_choose, Clear, wait_inputmouse
|
||||
from PyserSSH.system.info import version, Flag_TH
|
||||
from PyserSSH.extensions.processbar import indeterminateStatus, LoadingProgress
|
||||
from PyserSSH.extensions.dialog import MenuDialog, TextDialog, TextInputDialog
|
||||
from PyserSSH.extensions.moredisplay import clickable_url, Send_karaoke_effect
|
||||
from PyserSSH.extensions.moreinteractive import ShowCursor
|
||||
from PyserSSH.extensions.remodesk import RemoDesk
|
||||
from PyserSSH.extensions.XHandler import XHandler
|
||||
from PyserSSH.system.clientype import Client
|
||||
from PyserSSH.system.RemoteStatus import remotestatus
|
||||
from PyserSSH.utils.ServerManager import ServerManager
|
||||
|
||||
class TextFormatter:
|
||||
RESET = "\033[0m"
|
||||
TEXT_COLORS = {
|
||||
"black": "\033[30m",
|
||||
"red": "\033[31m",
|
||||
"green": "\033[32m",
|
||||
"yellow": "\033[33m",
|
||||
"blue": "\033[34m",
|
||||
"magenta": "\033[35m",
|
||||
"cyan": "\033[36m",
|
||||
"white": "\033[37m"
|
||||
}
|
||||
TEXT_COLOR_LEVELS = {
|
||||
"light": "\033[1;{}m", # Light color prefix
|
||||
"dark": "\033[2;{}m" # Dark color prefix
|
||||
}
|
||||
BACKGROUND_COLORS = {
|
||||
"black": "\033[40m",
|
||||
"red": "\033[41m",
|
||||
"green": "\033[42m",
|
||||
"yellow": "\033[43m",
|
||||
"blue": "\033[44m",
|
||||
"magenta": "\033[45m",
|
||||
"cyan": "\033[46m",
|
||||
"white": "\033[47m"
|
||||
}
|
||||
TEXT_ATTRIBUTES = {
|
||||
"bold": "\033[1m",
|
||||
"italic": "\033[3m",
|
||||
"underline": "\033[4m",
|
||||
"blink": "\033[5m",
|
||||
"reverse": "\033[7m",
|
||||
"strikethrough": "\033[9m"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def format_text_truecolor(text, color=None, background=None, attributes=None, target_text=''):
|
||||
formatted_text = ""
|
||||
start_index = text.find(target_text)
|
||||
end_index = start_index + len(target_text) if start_index != -1 else len(text)
|
||||
|
||||
if color:
|
||||
formatted_text += f"\033[38;2;{color}m"
|
||||
|
||||
if background:
|
||||
formatted_text += f"\033[48;2;{background}m"
|
||||
|
||||
if attributes in TextFormatter.TEXT_ATTRIBUTES:
|
||||
formatted_text += TextFormatter.TEXT_ATTRIBUTES[attributes]
|
||||
|
||||
if target_text == "":
|
||||
formatted_text += text + TextFormatter.RESET
|
||||
else:
|
||||
formatted_text += text[:start_index] + text[start_index:end_index] + TextFormatter.RESET + text[end_index:]
|
||||
|
||||
return formatted_text
|
||||
|
||||
useraccount = AccountManager(allow_guest=True, autoload=True, autosave=True)
|
||||
|
||||
if not os.path.isfile("autosave_session.ses"):
|
||||
useraccount.add_account("admin", "", sudo=True) # create user without password
|
||||
useraccount.add_account("test", "test") # create user without password
|
||||
useraccount.add_account("demo")
|
||||
useraccount.add_account("remote", "12345", permissions=["remote_desktop"])
|
||||
useraccount.set_user_enable_inputsystem_echo("remote", False)
|
||||
useraccount.set_user_sftp_allow("admin", True)
|
||||
|
||||
XH = XHandler()
|
||||
ssh = Server(useraccount,
|
||||
system_commands=True,
|
||||
system_message=False,
|
||||
sftp=True,
|
||||
enable_preauth_banner=True,
|
||||
XHandler=XH)
|
||||
|
||||
remotedesktopserver = RemoDesk()
|
||||
|
||||
servername = "PyserSSH"
|
||||
|
||||
loading = ["PyserSSH", "openRemoDesk", "XHandler", "RemoteStatus"]
|
||||
|
||||
@ssh.on_user("pre-shell")
|
||||
def guestauth(client):
|
||||
if client.get_name() == "remote":
|
||||
return
|
||||
|
||||
if not useraccount.has_user(client.get_name()):
|
||||
while True:
|
||||
Clear(client)
|
||||
Send(client, f"You are currently logged in as a guest. To access, please login or register.\nYour current account: {client.get_name()}\n")
|
||||
method = wait_choose(client, ["Login", "Register", "Exit"], prompt="Action: ")
|
||||
Clear(client)
|
||||
if method == 0: # login
|
||||
username = wait_input(client, "Username: ", noabort=True)
|
||||
|
||||
if not username:
|
||||
Send(client, "Please Enter username")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
password = wait_input(client, "Password: ", password=True, noabort=True)
|
||||
|
||||
Send(client, "Please wait...")
|
||||
if not useraccount.has_user(username):
|
||||
Send(client, f"Username isn't exist. Please try again")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
if not useraccount.validate_credentials(username, password):
|
||||
Send(client, f"Password incorrect. Please try again")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
Clear(client)
|
||||
client.switch_user(username)
|
||||
break
|
||||
elif method == 1: # register
|
||||
username = wait_input(client, "Please choose a username: ", noabort=True)
|
||||
if not username:
|
||||
Send(client, "Please Enter username")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
if useraccount.has_user(username):
|
||||
Send(client, f"Username is exist. Please try again")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
password = wait_input(client, "Password: ", password=True, noabort=True)
|
||||
|
||||
if not password:
|
||||
Send(client, "Please Enter password")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
confirmpassword = wait_input(client, "Confirm Password: ", password=True, noabort=True)
|
||||
|
||||
if not password:
|
||||
Send(client, "Please Enter confirm password")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
if password != confirmpassword:
|
||||
Send(client, "Password do not matching the confirm password. Please try again.")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
Send(client, "Please wait...")
|
||||
useraccount.add_account(username, password, ["user"])
|
||||
client.switch_user(username)
|
||||
Clear(client)
|
||||
break
|
||||
else:
|
||||
client.close()
|
||||
|
||||
@ssh.on_user("connect")
|
||||
def connect(client):
|
||||
if client.get_name() == "remote":
|
||||
return
|
||||
|
||||
client.set_prompt(client["current_user"] + "@" + servername + ":~$")
|
||||
|
||||
wm = f"""{Flag_TH()}{'–'*50}
|
||||
Hello {client['current_user']},
|
||||
|
||||
This is testing server of PyserSSH v{version}.
|
||||
|
||||
Visit: {clickable_url("https://damp11113.xyz", "DPCloudev")}
|
||||
{'–'*50}"""
|
||||
|
||||
for i in loading:
|
||||
P = indeterminateStatus(client, f"Starting {i}", f"[ OK ] Started {i}")
|
||||
P.start()
|
||||
|
||||
time.sleep(len(i) / 20)
|
||||
|
||||
P.stop()
|
||||
|
||||
Di1 = TextDialog(client, "Welcome!\n to PyserSSH test server", "PyserSSH Extension")
|
||||
Di1.render()
|
||||
|
||||
for char in wm:
|
||||
Send(client, char, ln=False)
|
||||
#time.sleep(0.005) # Adjust the delay as needed
|
||||
Send(client, '\n') # Send newline after each line
|
||||
|
||||
@ssh.on_user("authbanner")
|
||||
def banner(tmp):
|
||||
return "Hello World!\n", "en"
|
||||
|
||||
@ssh.on_user("error")
|
||||
def error(client, error):
|
||||
if isinstance(error, socket.error):
|
||||
pass
|
||||
else:
|
||||
Send(client, traceback.format_exc())
|
||||
|
||||
@XH.command(name="startremotedesktop", category="Remote", permissions=["remote_desktop"])
|
||||
def remotedesktop(client):
|
||||
remotedesktopserver.handle_new_client(client)
|
||||
|
||||
@XH.command(name="passtest", category="Test Function")
|
||||
def xh_passtest(client):
|
||||
user = wait_input(client, "username: ")
|
||||
password = wait_input(client, "password: ", password=True)
|
||||
Send(client, f"username: {user} | password: {password}")
|
||||
|
||||
@XH.command(name="colortest", category="Test Function")
|
||||
def xh_colortest(client):
|
||||
for i in range(0, 255, 5):
|
||||
Send(client, TextFormatter.format_text_truecolor(" ", background=f"{i};0;0"), ln=False)
|
||||
Send(client, "")
|
||||
for i in range(0, 255, 5):
|
||||
Send(client, TextFormatter.format_text_truecolor(" ", background=f"0;{i};0"), ln=False)
|
||||
Send(client, "")
|
||||
for i in range(0, 255, 5):
|
||||
Send(client, TextFormatter.format_text_truecolor(" ", background=f"0;0;{i}"), ln=False)
|
||||
Send(client, "")
|
||||
Send(client, "TrueColors 24-Bit")
|
||||
|
||||
@XH.command(name="keytest", category="Test Function")
|
||||
def xh_keytest(client: Client):
|
||||
user = wait_inputkey(client, "press any key", raw=True, timeout=1)
|
||||
Send(client, "")
|
||||
Send(client, f"key: {user}")
|
||||
for i in range(10):
|
||||
user = wait_inputkey(client, "press any key", raw=True, timeout=1)
|
||||
Send(client, "")
|
||||
Send(client, f"key: {user}")
|
||||
|
||||
@XH.command(name="typing")
|
||||
def xh_typing(client: Client, messages, speed = 1):
|
||||
for w in messages:
|
||||
Send(client, w, ln=False)
|
||||
time.sleep(float(speed))
|
||||
Send(client, "")
|
||||
|
||||
@XH.command(name="renimtest")
|
||||
def xh_renimtest(client: Client):
|
||||
Clear(client)
|
||||
image = cv2.imread("opensource.png", cv2.IMREAD_COLOR)
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
|
||||
width, height = client['windowsize']["width"] - 5, client['windowsize']["height"] - 5
|
||||
|
||||
# resize image
|
||||
resized = cv2.resize(image, (width, height))
|
||||
t = ""
|
||||
|
||||
# Scan all pixels
|
||||
for y in range(0, height):
|
||||
for x in range(0, width):
|
||||
pixel_color = resized[y, x]
|
||||
if pixel_color.tolist() != [0, 0, 0]:
|
||||
t += TextFormatter.format_text_truecolor(" ",
|
||||
background=f"{pixel_color[0]};{pixel_color[1]};{pixel_color[2]}")
|
||||
else:
|
||||
t += " "
|
||||
|
||||
Send(client, t, ln=False)
|
||||
Send(client, "")
|
||||
t = ""
|
||||
|
||||
@XH.command(name="errortest", category="Test Function")
|
||||
def xh_errortest(client: Client):
|
||||
raise Exception("hello error")
|
||||
|
||||
@XH.command(name="inloadtest", category="Test Function")
|
||||
def xh_inloadtest(client: Client):
|
||||
loading = indeterminateStatus(client)
|
||||
loading.start()
|
||||
time.sleep(5)
|
||||
loading.stop()
|
||||
|
||||
@XH.command(name="loadtest", category="Test Function")
|
||||
def xh_loadtest(client: Client):
|
||||
l = LoadingProgress(client, total=100, color=True)
|
||||
l.start()
|
||||
for i in range(101):
|
||||
l.current = i
|
||||
l.status = f"loading {i}"
|
||||
time.sleep(0.05)
|
||||
l.stop()
|
||||
|
||||
@XH.command(name="dialogtest", category="Test Function")
|
||||
def xh_dialogtest(client: Client):
|
||||
Di1 = TextDialog(client, "Hello Dialog!", "PyserSSH Extension")
|
||||
Di1.render()
|
||||
|
||||
@XH.command(name="dialogtest2", category="Test Function")
|
||||
def xh_dialogtest2(client: Client):
|
||||
Di2 = MenuDialog(client, ["H1", "H2", "H3"], "PyserSSH Extension", "Hello world")
|
||||
Di2.render()
|
||||
Send(client, f"selected index: {Di2.output()}")
|
||||
|
||||
@XH.command(name="dialogtest3", category="Test Function")
|
||||
def xh_dialogtest3(client: Client):
|
||||
Di3 = TextInputDialog(client, "PyserSSH Extension")
|
||||
Di3.render()
|
||||
Send(client, f"input: {Di3.output()}")
|
||||
|
||||
@XH.command(name="passdialogtest3", category="Test Function")
|
||||
def xh_passdialogtest3(client: Client):
|
||||
Di3 = TextInputDialog(client, "PyserSSH Extension", inputtitle="Password Here", password=True)
|
||||
Di3.render()
|
||||
Send(client, f"password: {Di3.output()}")
|
||||
|
||||
@XH.command(name="choosetest", category="Test Function")
|
||||
def xh_choosetest(client: Client):
|
||||
mylist = ["H1", "H2", "H3"]
|
||||
cindex = wait_choose(client, mylist, "select: ")
|
||||
Send(client, f"selected: {mylist[cindex]}")
|
||||
|
||||
@XH.command(name="vieweb")
|
||||
def xh_vieweb(client: Client, url: str):
|
||||
loading = indeterminateStatus(client, desc=f"requesting {url}...")
|
||||
loading.start()
|
||||
try:
|
||||
content = requests.get(url).content
|
||||
except:
|
||||
loading.stopfail()
|
||||
return
|
||||
loading.stop()
|
||||
loading = indeterminateStatus(client, desc=f"parsing html {url}...")
|
||||
loading.start()
|
||||
try:
|
||||
soup = BeautifulSoup(content, 'html.parser')
|
||||
# Extract only the text content
|
||||
text_content = soup.get_text()
|
||||
except:
|
||||
loading.stopfail()
|
||||
return
|
||||
loading.stop()
|
||||
Di1 = TextDialog(client, text_content, url)
|
||||
Di1.render()
|
||||
|
||||
@XH.command(name="shutdown")
|
||||
def xh_shutdown(client: Client, at: str):
|
||||
if at == "now":
|
||||
ssh.stop_server()
|
||||
|
||||
@XH.command(name="mouseinput", category="Test Function")
|
||||
def xh_mouseinput(client: Client):
|
||||
for i in range(10):
|
||||
button, x, y = wait_inputmouse(client)
|
||||
if button == 0:
|
||||
Send(client, "Left Button")
|
||||
elif button == 1:
|
||||
Send(client, "Middle Button")
|
||||
elif button == 2:
|
||||
Send(client, "Right Button")
|
||||
elif button == 3:
|
||||
Send(client, "Button Up")
|
||||
|
||||
Send(client, f"Current POS: X {x} | Y {y} with button {button}")
|
||||
|
||||
@XH.command(name="karaoke")
|
||||
def xh_karaoke(client: Client):
|
||||
ShowCursor(client, False)
|
||||
Send_karaoke_effect(client, "Python can print like karaoke!")
|
||||
ShowCursor(client)
|
||||
|
||||
R1 = 1
|
||||
R2 = 2
|
||||
K2 = 5
|
||||
|
||||
theta_spacing = 0.07
|
||||
phi_spacing = 0.02
|
||||
|
||||
illumination = np.fromiter(".,-~:;=!*#$@", dtype="<U1")
|
||||
|
||||
def render_frame(A: float, B: float, screen_size) -> np.ndarray:
|
||||
K1 = screen_size * K2 * 3 / (8 * (R1 + R2))
|
||||
"""
|
||||
Returns a frame of the spinning 3D donut.
|
||||
Based on the pseudocode from: https://www.a1k0n.net/2011/07/20/donut-math.html
|
||||
"""
|
||||
cos_A = np.cos(A)
|
||||
sin_A = np.sin(A)
|
||||
cos_B = np.cos(B)
|
||||
sin_B = np.sin(B)
|
||||
|
||||
output = np.full((screen_size, screen_size), " ") # (40, 40)
|
||||
zbuffer = np.zeros((screen_size, screen_size)) # (40, 40)
|
||||
|
||||
cos_phi = np.cos(phi := np.arange(0, 2 * np.pi, phi_spacing)) # (315,)
|
||||
sin_phi = np.sin(phi) # (315,)
|
||||
cos_theta = np.cos(theta := np.arange(0, 2 * np.pi, theta_spacing)) # (90,)
|
||||
sin_theta = np.sin(theta) # (90,)
|
||||
circle_x = R2 + R1 * cos_theta # (90,)
|
||||
circle_y = R1 * sin_theta # (90,)
|
||||
|
||||
x = (np.outer(cos_B * cos_phi + sin_A * sin_B * sin_phi, circle_x) - circle_y * cos_A * sin_B).T # (90, 315)
|
||||
y = (np.outer(sin_B * cos_phi - sin_A * cos_B * sin_phi, circle_x) + circle_y * cos_A * cos_B).T # (90, 315)
|
||||
z = ((K2 + cos_A * np.outer(sin_phi, circle_x)) + circle_y * sin_A).T # (90, 315)
|
||||
ooz = np.reciprocal(z) # Calculates 1/z
|
||||
xp = (screen_size / 2 + K1 * ooz * x).astype(int) # (90, 315)
|
||||
yp = (screen_size / 2 - K1 * ooz * y).astype(int) # (90, 315)
|
||||
L1 = (((np.outer(cos_phi, cos_theta) * sin_B) - cos_A * np.outer(sin_phi, cos_theta)) - sin_A * sin_theta) # (315, 90)
|
||||
L2 = cos_B * (cos_A * sin_theta - np.outer(sin_phi, cos_theta * sin_A)) # (315, 90)
|
||||
L = np.around(((L1 + L2) * 8)).astype(int).T # (90, 315)
|
||||
mask_L = L >= 0 # (90, 315)
|
||||
chars = illumination[L] # (90, 315)
|
||||
|
||||
for i in range(90):
|
||||
mask = mask_L[i] & (ooz[i] > zbuffer[xp[i], yp[i]]) # (315,)
|
||||
|
||||
zbuffer[xp[i], yp[i]] = np.where(mask, ooz[i], zbuffer[xp[i], yp[i]])
|
||||
output[xp[i], yp[i]] = np.where(mask, chars[i], output[xp[i], yp[i]])
|
||||
|
||||
return output
|
||||
|
||||
@XH.command()
|
||||
def donut(client, screen_size=40):
|
||||
screen_size = int(screen_size)
|
||||
|
||||
A = 1
|
||||
B = 1
|
||||
|
||||
for _ in range(screen_size * screen_size):
|
||||
A += theta_spacing
|
||||
B += phi_spacing
|
||||
Clear(client)
|
||||
array = render_frame(A, B, screen_size)
|
||||
for row in array:
|
||||
Send(client, " ".join(row))
|
||||
Send(client, "\n")
|
||||
|
||||
if wait_inputkey(client, raw=True, timeout=0.01) == "\x03": break
|
||||
|
||||
|
||||
@XH.command(name="status")
|
||||
def xh_status(client: Client):
|
||||
remotestatus(ssh, client.channel, True)
|
||||
|
||||
#@ssh.on_user("command")
|
||||
#def command(client: Client, command: str):
|
||||
|
||||
#ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
|
||||
|
||||
manager = ServerManager()
|
||||
|
||||
# Add servers to the manager
|
||||
manager.add_server("ssh", ssh, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
|
||||
manager.add_server("telnet", ssh, "", protocol="telnet")
|
||||
|
||||
# Start a specific server
|
||||
manager.start_server("ssh")
|
||||
manager.start_server("telnet")
|
29
demo/demo2.py
Normal file
29
demo/demo2.py
Normal 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'))
|
BIN
demo/opensource.png
Normal file
BIN
demo/opensource.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
27
demo/private_key.pem
Normal file
27
demo/private_key.pem
Normal file
@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEAwNfkia91HNrpyqlHwjYrVKDV5SkDt5P27MxKZDjwOokGBX7E
|
||||
g5cMXb1wxQeCm+zptg680qIXHfSaaOi1E/DAutaTIQa3GI+gDMphlWMxrEWFuZOB
|
||||
ylvTuFAxLB8xKcuBjelQX4TYlcgA1WgyeI6LFPNdJPekVHnzkLCZnW+y05PkT6a0
|
||||
QY1Eoa6DY2TtY8w4NZmnyCy1ZPYV5qLKN/P7aVSU52AD8u25St1WprvxpM4TtZiG
|
||||
2O9X1Unx+wtco2P8G1M4qcuWqPDdITn4n19DcR7rhuACjUo2poFTlnl9lfEsW11R
|
||||
5sDfYlgc3n8a4Iw49Ea4GaLkSEMluOfB9eOLUQIDAQABAoIBAAeZmpVTN7uFjyLg
|
||||
YrEZ6cGXPcbJw9k8zhr3/tM4q+hf/7+WBuWkEtCGR5xO7Ev73XFs3u6IL9QLkKL4
|
||||
z4YefgypqeO/0YB4zJckdLhqpTRZCxOiEhfpCuI1MDLrycgQD/uJSenHIQgKI/+a
|
||||
cH7Ffgq7Kp0V22vu4HVVLcCsJxvlIxFd92xCKFl8zRHBdyKikfvZAEidbMu9xdsW
|
||||
S9DzFCveCGrE8g6HWQyXiCpq2xb4b2C37O+0iZRtYfJQSCrnG99Y/KfWIVbb+3gU
|
||||
5WbIlYm57TKzMGgKc3LWtGCWxfB/NNP5wOxR+4y78oWDzTibrT5OZDsX2S+mbgNB
|
||||
wAo/0U8CgYEAxHAOrlz9Ae2kYfyUgx9JTonElIFlDmDVdYcRW8Go7xpeMZ+XG2sR
|
||||
f/za6t6jiCxI9FSD5gl4nDyOVhx5zRpu2QZvZBHICaWDwEmZC+d3suYtQY/ixR3K
|
||||
3sdDKK6wzOtta+OBVNPQWAW2rmTr/J1JobguflM0NBm+YZC02gQyL9sCgYEA+1DU
|
||||
llDGDaU08WQNTLRgW+1RAbzsBTFd+DhvbYM8+mgmlFzHKHJP3jCpwLZmqdBzLl0R
|
||||
wUZBwpZ5MnkiQV0e9AW4/tnqBw8n9pf+NgNqcssw8MEMXHPbLNwr7OVS/LG8VNOm
|
||||
LbuLjxq8O8wfbS87eBj2D18c1x4voEIw1AWYn0MCgYEAnPBF2moyPMMmjJ5l7Ggn
|
||||
ghaxNlA2c4lLoOz7IkqTdAul65FsARzGS3GxWOnsztNKqeGHy1YPxQrgUM3JReLz
|
||||
YnIwtks6fPJ+Uza5jngr+oLI71NMQl1uAhRChJMkb2M79XE6l5HuJxTRgXzhyN3E
|
||||
wO5MPuKsl19l6b7ZrkCh8/cCgYEAjIL6+TgcI9D0suo/zV0kawFaw1//jj+1zGyx
|
||||
UEeKNm848saUy3ZuVUpb/tV8vQFBBPEgVjGT3toG1UOI9Ya9Ia55anQoNt4wd90v
|
||||
Ur/CKoCU0mb9JEvahVBsdr0ZExPEuqDDTtqHAvHtwHk2MPOxikpaeOmy1EuaUT3w
|
||||
0vp2BMUCgYEAsLL592l8pclhxk2b0lmgvhPLOmZuQ7QkcnMMyYCeUr9Kt95VN40J
|
||||
N/LK9LIbf/l9CUN4eO1JqCJkAiMIW2Gvumw3g+TMj+nqcfsufSHJCG1EZNYMUftG
|
||||
aL7KtccPyFwotMD/P+OaAeJimwuC5247hCep1SSf1A41gbdmutiirM4=
|
||||
-----END RSA PRIVATE KEY-----
|
3
setup.cfg
Normal file
3
setup.cfg
Normal file
@ -0,0 +1,3 @@
|
||||
[metadata]
|
||||
description-file=README.md
|
||||
license_files=LICENSE.rst
|
44
setup.py
Normal file
44
setup.py
Normal file
@ -0,0 +1,44 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='PyserSSH',
|
||||
version='5.1.4',
|
||||
license='MIT',
|
||||
author='DPSoftware Foundation',
|
||||
author_email='contact@damp11113.xyz',
|
||||
packages=find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
url='https://github.com/damp11113/PyserSSH',
|
||||
description="python scriptable ssh server library. based on Paramiko",
|
||||
long_description=open('README.md', 'r', encoding='utf-8').read(),
|
||||
long_description_content_type='text/markdown',
|
||||
keywords="SSH server",
|
||||
python_requires='>=3.6',
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Topic :: Communications",
|
||||
"Topic :: Internet",
|
||||
"Topic :: Internet :: File Transfer Protocol (FTP)",
|
||||
"Topic :: Software Development",
|
||||
"Topic :: Terminals"
|
||||
],
|
||||
install_requires=[
|
||||
"paramiko",
|
||||
"psutil"
|
||||
],
|
||||
extras_require={
|
||||
'RemoDesk': [
|
||||
"mouse",
|
||||
"keyboard",
|
||||
"Brotli",
|
||||
"pillow",
|
||||
"numpy",
|
||||
"opencv-python"
|
||||
],
|
||||
}
|
||||
)
|
76
src/PyserSSH/__init__.py
Normal file
76
src/PyserSSH/__init__.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
"""
|
||||
note
|
||||
|
||||
ansi cursor arrow
|
||||
up - \x1b[A
|
||||
down - \x1b[B
|
||||
left - \x1b[D
|
||||
right - \x1b[C
|
||||
|
||||
https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
|
||||
from .interactive import *
|
||||
from .server import Server
|
||||
from .account import AccountManager
|
||||
from .system.info import system_banner, version
|
||||
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||
|
||||
try:
|
||||
os.environ["pyserssh_systemmessage"]
|
||||
except:
|
||||
os.environ["pyserssh_systemmessage"] = "YES"
|
||||
|
||||
try:
|
||||
os.environ["pyserssh_log"]
|
||||
except:
|
||||
os.environ["pyserssh_log"] = "NO"
|
||||
|
||||
if os.environ["pyserssh_log"] == "NO":
|
||||
logging.basicConfig(level=logging.CRITICAL)
|
||||
logger = logging.getLogger("PyserSSH")
|
||||
#logger.disabled = False
|
||||
|
||||
if os.environ["pyserssh_systemmessage"] == "YES":
|
||||
print(system_banner)
|
||||
|
||||
__author__ = "damp11113"
|
||||
__url__ = "https://github.com/DPSoftware-Foundation/PyserSSH"
|
||||
__copyright__ = "2023-present"
|
||||
__license__ = "MIT"
|
||||
__version__ = version
|
||||
__department__ = "DPSoftware"
|
||||
__organization__ = "DOPFoundation"
|
332
src/PyserSSH/account.py
Normal file
332
src/PyserSSH/account.py
Normal file
@ -0,0 +1,332 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import time
|
||||
import atexit
|
||||
import threading
|
||||
import hashlib
|
||||
|
||||
logger = logging.getLogger("PyserSSH.Account")
|
||||
|
||||
class AccountManager:
|
||||
def __init__(self, allow_guest=False, historylimit=10, autosave=False, autosavedelay=60, autoload=False, autofile="autosave_session.ses"):
|
||||
self.accounts = {}
|
||||
self.allow_guest = allow_guest
|
||||
self.historylimit = historylimit
|
||||
self.autosavedelay = autosavedelay
|
||||
|
||||
self.__autosavework = False
|
||||
self.autosave = autosave
|
||||
self.__autosaveworknexttime = 0
|
||||
self.__autofile = autofile
|
||||
|
||||
if autoload:
|
||||
self.load(self.__autofile)
|
||||
|
||||
if self.autosave:
|
||||
logger.info("starting autosave")
|
||||
self.__autosavethread = threading.Thread(target=self.__autosave, daemon=True)
|
||||
self.__autosavethread.start()
|
||||
atexit.register(self.__saveexit)
|
||||
|
||||
def __autosave(self):
|
||||
self.save(self.__autofile)
|
||||
self.__autosaveworknexttime = time.time() + self.autosavedelay
|
||||
self.__autosavework = True
|
||||
while self.__autosavework:
|
||||
if int(self.__autosaveworknexttime) == int(time.time()):
|
||||
self.save(self.__autofile)
|
||||
self.__autosaveworknexttime = time.time() + self.autosavedelay
|
||||
|
||||
time.sleep(1) # fix cpu load
|
||||
|
||||
def __auto_save(func):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
result = func(self, *args, **kwargs)
|
||||
if self.autosave:
|
||||
self.save(self.__autofile, False)
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
def __saveexit(self):
|
||||
self.__autosavework = False
|
||||
self.save(self.__autofile)
|
||||
self.__autosavethread.join()
|
||||
|
||||
def validate_credentials(self, username, password=None, public_key=None):
|
||||
if self.allow_guest and not self.has_user(username):
|
||||
return True
|
||||
|
||||
allowed_auth_list = str(self.accounts[username].get("allowed_auth", "")).split(",")
|
||||
|
||||
# Check password authentication
|
||||
if password is not None and "password" in allowed_auth_list:
|
||||
stored_password = self.accounts[username].get("password", "")
|
||||
return stored_password == hashlib.md5(password.encode()).hexdigest()
|
||||
|
||||
# Check public key authentication
|
||||
if public_key is not None and "publickey" in allowed_auth_list:
|
||||
stored_public_key = self.accounts[username].get("public_key", "")
|
||||
return stored_public_key == public_key
|
||||
|
||||
# Check if 'none' authentication is allowed
|
||||
if "none" in allowed_auth_list:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_user(self, username):
|
||||
return username in self.accounts
|
||||
|
||||
def list_users(self):
|
||||
return list(self.accounts.keys())
|
||||
|
||||
def get_allowed_auths(self, username):
|
||||
if self.has_user(username) and "allowed_auth" in self.accounts[username]:
|
||||
return self.accounts[username]["allowed_auth"]
|
||||
return "none"
|
||||
|
||||
def get_permissions(self, username):
|
||||
if self.has_user(username):
|
||||
return self.accounts[username]["permissions"]
|
||||
return []
|
||||
|
||||
@__auto_save
|
||||
def set_prompt(self, username, prompt=">"):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["prompt"] = prompt
|
||||
|
||||
def get_prompt(self, username):
|
||||
if self.has_user(username) and "prompt" in self.accounts[username]:
|
||||
return self.accounts[username]["prompt"]
|
||||
return ">" # Default prompt if not set for the user
|
||||
|
||||
@__auto_save
|
||||
def add_account(self, username, password=None, public_key=None, permissions:list=None, sudo=False):
|
||||
if not self.has_user(username):
|
||||
allowedlist = []
|
||||
accountkey = {}
|
||||
|
||||
if permissions is None:
|
||||
permissions = []
|
||||
|
||||
if password != None:
|
||||
allowedlist.append("password")
|
||||
accountkey["password"] = hashlib.md5(password.encode()).hexdigest()
|
||||
|
||||
if public_key != None:
|
||||
allowedlist.append("publickey")
|
||||
accountkey["public_key"] = public_key
|
||||
|
||||
if password is None and public_key is None:
|
||||
allowedlist.append("none")
|
||||
|
||||
accountkey["permissions"] = permissions
|
||||
accountkey["allowed_auth"] = ",".join(allowedlist)
|
||||
|
||||
self.accounts[username] = accountkey
|
||||
|
||||
if sudo:
|
||||
if self.has_sudo_user():
|
||||
raise Exception(f"sudo user is exist")
|
||||
|
||||
self.accounts[username]["sudo"] = sudo
|
||||
else:
|
||||
raise Exception(f"{username} is exist")
|
||||
|
||||
def has_sudo_user(self):
|
||||
return any(account.get("sudo", False) for account in self.accounts.values())
|
||||
|
||||
def is_user_has_sudo(self, username):
|
||||
if self.has_user(username) and "sudo" in self.accounts[username]:
|
||||
return self.accounts[username]["sudo"]
|
||||
return False
|
||||
|
||||
@__auto_save
|
||||
def remove_account(self, username):
|
||||
if self.has_user(username):
|
||||
del self.accounts[username]
|
||||
|
||||
@__auto_save
|
||||
def change_password(self, username, new_password):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["password"] = new_password
|
||||
|
||||
@__auto_save
|
||||
def set_permissions(self, username, new_permissions):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["permissions"] = new_permissions
|
||||
|
||||
def save(self, filename="session.ses", keep_log=True):
|
||||
if keep_log:
|
||||
logger.info(f"saving session to {filename}")
|
||||
try:
|
||||
with open(filename, 'wb') as file:
|
||||
pickle.dump(self.accounts, file)
|
||||
|
||||
if keep_log:
|
||||
logger.info(f"saved session to {filename}")
|
||||
except Exception as e:
|
||||
if keep_log:
|
||||
logger.error(f"save session failed: {e}")
|
||||
|
||||
def load(self, filename):
|
||||
logger.info(f"loading session from {filename}")
|
||||
try:
|
||||
with open(filename, 'rb') as file:
|
||||
self.accounts = pickle.load(file)
|
||||
logger.info(f"loaded session")
|
||||
except FileNotFoundError:
|
||||
logger.error("can't load session: file not found.")
|
||||
except Exception as e:
|
||||
logger.error(f"can't load session: {e}")
|
||||
|
||||
@__auto_save
|
||||
def set_user_sftp_allow(self, username, allow=True):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["sftp_allow"] = allow
|
||||
|
||||
def get_user_sftp_allow(self, username):
|
||||
if self.has_user(username) and "sftp_allow" in self.accounts[username]:
|
||||
return self.accounts[username]["sftp_allow"]
|
||||
return False
|
||||
|
||||
@__auto_save
|
||||
def set_user_sftp_readonly(self, username, readonly=False):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["sftp_readonly"] = readonly
|
||||
|
||||
def get_user_sftp_readonly(self, username):
|
||||
if self.has_user(username) and "sftp_readonly" in self.accounts[username]:
|
||||
return self.accounts[username]["sftp_readonly"]
|
||||
return False
|
||||
|
||||
@__auto_save
|
||||
def set_user_sftp_root_path(self, username, path="/"):
|
||||
if self.has_user(username):
|
||||
if path == "/":
|
||||
self.accounts[username]["sftp_root_path"] = os.getcwd()
|
||||
else:
|
||||
self.accounts[username]["sftp_root_path"] = path
|
||||
|
||||
def get_user_sftp_root_path(self, username):
|
||||
if self.has_user(username) and "sftp_root_path" in self.accounts[username]:
|
||||
return self.accounts[username]["sftp_root_path"]
|
||||
return os.getcwd()
|
||||
|
||||
@__auto_save
|
||||
def set_user_enable_inputsystem(self, username, enable=True):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["inputsystem"] = enable
|
||||
|
||||
def get_user_enable_inputsystem(self, username):
|
||||
if self.has_user(username) and "inputsystem" in self.accounts[username]:
|
||||
return self.accounts[username]["inputsystem"]
|
||||
return True
|
||||
|
||||
@__auto_save
|
||||
def set_user_enable_inputsystem_echo(self, username, echo=True):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["inputsystem_echo"] = echo
|
||||
|
||||
def get_user_enable_inputsystem_echo(self, username):
|
||||
if self.has_user(username) and "inputsystem_echo" in self.accounts[username]:
|
||||
return self.accounts[username]["inputsystem_echo"]
|
||||
return True
|
||||
|
||||
@__auto_save
|
||||
def set_banner(self, username, banner):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["banner"] = banner
|
||||
|
||||
def get_banner(self, username):
|
||||
if self.has_user(username) and "banner" in self.accounts[username]:
|
||||
return self.accounts[username]["banner"]
|
||||
return None
|
||||
|
||||
def get_user_timeout(self, username):
|
||||
if self.has_user(username) and "timeout" in self.accounts[username]:
|
||||
return self.accounts[username]["timeout"]
|
||||
return None
|
||||
|
||||
@__auto_save
|
||||
def set_user_timeout(self, username, timeout=None):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["timeout"] = timeout
|
||||
|
||||
def get_user_last_login(self, username):
|
||||
if self.has_user(username) and "lastlogin" in self.accounts[username]:
|
||||
return self.accounts[username]["lastlogin"]
|
||||
return None
|
||||
|
||||
@__auto_save
|
||||
def set_user_last_login(self, username, ip, timelogin=time.time()):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["lastlogin"] = {
|
||||
"ip": ip,
|
||||
"time": timelogin
|
||||
}
|
||||
|
||||
@__auto_save
|
||||
def add_history(self, username, command):
|
||||
if self.has_user(username):
|
||||
if "history" not in self.accounts[username]:
|
||||
self.accounts[username]["history"] = [] # Initialize history list if it doesn't exist
|
||||
|
||||
history_limit = self.historylimit if self.historylimit is not None else float('inf')
|
||||
self.accounts[username]["history"].append(command)
|
||||
self.accounts[username]["lastcommand"] = command
|
||||
# Trim history to the specified limit
|
||||
if self.historylimit != None:
|
||||
if len(self.accounts[username]["history"]) > history_limit:
|
||||
self.accounts[username]["history"] = self.accounts[username]["history"][-history_limit:]
|
||||
|
||||
@__auto_save
|
||||
def clear_history(self, username):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["history"] = [] # Initialize history list if it doesn't exist
|
||||
|
||||
def get_history(self, username, index, getall=False):
|
||||
if self.has_user(username) and "history" in self.accounts[username]:
|
||||
history = self.accounts[username]["history"]
|
||||
history.reverse()
|
||||
if getall:
|
||||
return history
|
||||
else:
|
||||
if index < len(history):
|
||||
return history[index]
|
||||
else:
|
||||
return None # Index out of range
|
||||
return None # User or history not found
|
||||
|
||||
def get_lastcommand(self, username):
|
||||
if self.has_user(username) and "lastcommand" in self.accounts[username]:
|
||||
command = self.accounts[username]["lastcommand"]
|
||||
return command
|
||||
return None # User or history not found
|
298
src/PyserSSH/extensions/XHandler.py
Normal file
298
src/PyserSSH/extensions/XHandler.py
Normal file
@ -0,0 +1,298 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import shlex
|
||||
|
||||
from ..interactive import Send
|
||||
|
||||
def are_permissions_met(permission_list, permission_require):
|
||||
return set(permission_require).issubset(set(permission_list))
|
||||
|
||||
class XHandler:
|
||||
def __init__(self, enablehelp=True, showusageonworng=True):
|
||||
"""
|
||||
Initializes the command handler with optional settings for help messages and usage.
|
||||
|
||||
Parameters:
|
||||
enablehelp (bool): Whether help messages are enabled.
|
||||
showusageonworng (bool): Whether usage information is shown on wrong usage.
|
||||
"""
|
||||
self.handlers = {}
|
||||
self.categories = {}
|
||||
self.enablehelp = enablehelp
|
||||
self.showusageonworng = showusageonworng
|
||||
self.serverself = None
|
||||
self.commandnotfound = None
|
||||
|
||||
def command(self, category=None, name=None, aliases=None, permissions: list = None):
|
||||
"""
|
||||
Decorator to register a function as a command with optional category, name, aliases, and permissions.
|
||||
|
||||
Parameters:
|
||||
category (str): The category under which the command falls (default: None).
|
||||
name (str): The name of the command (default: None).
|
||||
aliases (list): A list of command aliases (default: None).
|
||||
permissions (list): A list of permissions required to execute the command (default: None).
|
||||
|
||||
Returns:
|
||||
function: The wrapped function.
|
||||
"""
|
||||
def decorator(func):
|
||||
nonlocal name, category
|
||||
if name is None:
|
||||
name = func.__name__
|
||||
command_name = name
|
||||
command_description = func.__doc__ # Read the docstring
|
||||
parameters = inspect.signature(func).parameters
|
||||
command_args = []
|
||||
has_args = False
|
||||
has_kwargs = False
|
||||
|
||||
for param in list(parameters.values())[1:]: # Exclude first parameter (client)
|
||||
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
has_args = True
|
||||
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
||||
has_kwargs = True
|
||||
elif param.default != inspect.Parameter.empty: # Check if parameter has default value
|
||||
if param.annotation == bool:
|
||||
command_args.append(f"--{param.name}")
|
||||
else:
|
||||
command_args.append((f"{param.name}", param.default))
|
||||
else:
|
||||
command_args.append(param.name)
|
||||
|
||||
if category is None:
|
||||
category = 'No Category'
|
||||
if category not in self.categories:
|
||||
self.categories[category] = {}
|
||||
self.categories[category][command_name] = {
|
||||
'description': command_description.strip() if command_description else "",
|
||||
'args': command_args,
|
||||
"permissions": permissions,
|
||||
'has_args': has_args,
|
||||
'has_kwargs': has_kwargs
|
||||
}
|
||||
self.handlers[command_name] = func
|
||||
if aliases:
|
||||
for alias in aliases:
|
||||
self.handlers[alias] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def call(self, client, command_string):
|
||||
"""
|
||||
Processes a command string, validates arguments, and calls the corresponding function.
|
||||
|
||||
Parameters:
|
||||
client (object): The client sending the command.
|
||||
command_string (str): The command string to be executed.
|
||||
|
||||
Returns:
|
||||
Any: The result of the command function, or an error message if invalid.
|
||||
"""
|
||||
tokens = shlex.split(command_string)
|
||||
command_name = tokens[0]
|
||||
args = tokens[1:]
|
||||
|
||||
if command_name == "help" and self.enablehelp:
|
||||
if args:
|
||||
Send(client, self.get_help_command_info(args[0]))
|
||||
else:
|
||||
Send(client, self.get_help_message())
|
||||
Send(client, "Type 'help <command>' for more info on a command.")
|
||||
else:
|
||||
if command_name in self.handlers:
|
||||
command_func = self.handlers[command_name]
|
||||
command_info = self.get_command_info(command_name)
|
||||
if command_info and command_info.get('permissions'):
|
||||
if not are_permissions_met(self.serverself.accounts.get_permissions(client.get_name()), command_info.get('permissions')) or not self.serverself.accounts.is_user_has_sudo(client.get_name()):
|
||||
Send(client, f"Permission denied. You do not have permission to execute '{command_name}'.")
|
||||
return
|
||||
|
||||
command_args = inspect.signature(command_func).parameters
|
||||
final_args = {}
|
||||
final_kwargs = {}
|
||||
i = 0
|
||||
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if arg.startswith('-'):
|
||||
arg_name = arg.lstrip('-')
|
||||
if arg_name not in command_args:
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
Send(client, f"Invalid flag '{arg_name}' for command '{command_name}'.")
|
||||
return
|
||||
if command_args[arg_name].annotation == bool:
|
||||
final_args[arg_name] = True
|
||||
i += 1
|
||||
else:
|
||||
if i + 1 < len(args):
|
||||
final_args[arg_name] = args[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
Send(client, f"Missing value for flag '{arg_name}' for command '{command_name}'.")
|
||||
return
|
||||
else:
|
||||
if command_info['has_args']:
|
||||
final_args.setdefault('args', []).append(arg)
|
||||
elif command_info['has_kwargs']:
|
||||
final_kwargs[arg] = args[i + 1] if i + 1 < len(args) else None
|
||||
i += 1
|
||||
else:
|
||||
if len(final_args) + 1 < len(command_args):
|
||||
param = list(command_args.values())[len(final_args) + 1]
|
||||
final_args[param.name] = arg
|
||||
else:
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
Send(client, f"Unexpected argument '{arg}' for command '{command_name}'.")
|
||||
return
|
||||
i += 1
|
||||
|
||||
# Check for required positional arguments
|
||||
for param in list(command_args.values())[1:]: # Skip client argument
|
||||
if param.name not in final_args and param.default == inspect.Parameter.empty:
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
Send(client, f"Missing required argument '{param.name}' for command '{command_name}'")
|
||||
return
|
||||
|
||||
final_args_list = [final_args.get(param.name, param.default) for param in list(command_args.values())[1:]]
|
||||
|
||||
if command_info['has_kwargs']:
|
||||
final_args_list.append(final_kwargs)
|
||||
|
||||
return command_func(client, *final_args_list)
|
||||
else:
|
||||
if self.commandnotfound:
|
||||
self.commandnotfound(client, command_name)
|
||||
return
|
||||
else:
|
||||
Send(client, f"{command_name} not found")
|
||||
return
|
||||
|
||||
def get_command_info(self, command_name):
|
||||
"""
|
||||
Retrieves information about a specific command, including its description, arguments, and permissions.
|
||||
|
||||
Parameters:
|
||||
command_name (str): The name of the command.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing command details such as name, description, args, and permissions.
|
||||
"""
|
||||
found_command = None
|
||||
for category, commands in self.categories.items():
|
||||
if command_name in commands:
|
||||
found_command = commands[command_name]
|
||||
break
|
||||
else:
|
||||
for cmd, cmd_info in commands.items():
|
||||
if 'aliases' in cmd_info and command_name in cmd_info['aliases']:
|
||||
found_command = cmd_info
|
||||
break
|
||||
if found_command:
|
||||
break
|
||||
|
||||
if found_command:
|
||||
return {
|
||||
'name': command_name,
|
||||
'description': found_command['description'].strip() if found_command['description'] else "",
|
||||
'args': found_command['args'],
|
||||
'category': category,
|
||||
'permissions': found_command['permissions'],
|
||||
'has_args': found_command['has_args'],
|
||||
'has_kwargs': found_command['has_kwargs']
|
||||
}
|
||||
|
||||
def get_help_command_info(self, command):
|
||||
"""
|
||||
Generates a detailed help message for a specific command.
|
||||
|
||||
Parameters:
|
||||
command (str): The name of the command.
|
||||
|
||||
Returns:
|
||||
str: The formatted help message for the command.
|
||||
"""
|
||||
command_info = self.get_command_info(command)
|
||||
aliases = command_info.get('aliases', [])
|
||||
help_message = f"{command_info['name']}"
|
||||
if aliases:
|
||||
help_message += f" ({', '.join(aliases)})"
|
||||
help_message += "\n"
|
||||
help_message += f"{command_info['description']}\n"
|
||||
help_message += f"Usage: {command_info['name']}"
|
||||
for arg in command_info['args']:
|
||||
if isinstance(arg, tuple):
|
||||
if isinstance(arg[1], bool):
|
||||
help_message += f" [--{arg[0]}]"
|
||||
else:
|
||||
help_message += f" [-{arg[0]} {arg[1]}]"
|
||||
else:
|
||||
help_message += f" <{arg}>"
|
||||
if command_info['has_args']:
|
||||
help_message += " [<args>...]"
|
||||
if command_info['has_kwargs']:
|
||||
help_message += " [--<key>=<value>...]"
|
||||
return help_message
|
||||
|
||||
def get_help_message(self):
|
||||
"""
|
||||
Generates a general help message listing all categories and their associated commands.
|
||||
|
||||
Returns:
|
||||
str: The formatted help message containing all commands and categories.
|
||||
"""
|
||||
help_message = ""
|
||||
for category, commands in self.categories.items():
|
||||
help_message += f"{category}:\n"
|
||||
for command_name, command_info in commands.items():
|
||||
help_message += f" {command_name}"
|
||||
if command_info['description']:
|
||||
help_message += f" - {command_info['description']}"
|
||||
help_message += "\n"
|
||||
return help_message
|
||||
|
||||
def get_all_commands(self):
|
||||
"""
|
||||
Retrieves all registered commands, grouped by category.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary where each key is a category name and the value is a
|
||||
dictionary of commands within that category.
|
||||
"""
|
||||
all_commands = {}
|
||||
for category, commands in self.categories.items():
|
||||
all_commands[category] = commands
|
||||
return all_commands
|
||||
|
38
src/PyserSSH/extensions/__init__.py
Normal file
38
src/PyserSSH/extensions/__init__.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
"""
|
||||
note
|
||||
|
||||
ansi cursor arrow
|
||||
up - \x1b[A
|
||||
down - \x1b[B
|
||||
left - \x1b[D
|
||||
right - \x1b[C
|
||||
|
||||
https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
"""
|
276
src/PyserSSH/extensions/dialog.py
Normal file
276
src/PyserSSH/extensions/dialog.py
Normal file
@ -0,0 +1,276 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from ..interactive import Clear, Send, wait_inputkey
|
||||
from ..system.sysfunc import text_centered_screen
|
||||
|
||||
|
||||
class TextDialog:
|
||||
"""
|
||||
A dialog that displays a simple text message with an optional title.
|
||||
|
||||
Args:
|
||||
client (Client): The client to display the dialog to.
|
||||
content (str, optional): The content to be displayed in the dialog. Defaults to an empty string.
|
||||
title (str, optional): The title of the dialog. Defaults to an empty string.
|
||||
|
||||
Methods:
|
||||
render(): Renders the dialog, displaying the title and content in the center of the screen.
|
||||
waituserenter(): Waits for the user to press the 'enter' key to close the dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, client, content="", title=""):
|
||||
self.client = client
|
||||
|
||||
self.windowsize = client["windowsize"]
|
||||
self.title = title
|
||||
self.content = content
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Renders the dialog by displaying the title, content, and waiting for the user's input.
|
||||
"""
|
||||
Clear(self.client)
|
||||
Send(self.client, self.title)
|
||||
Send(self.client, "-" * self.windowsize["width"])
|
||||
|
||||
generatedwindow = text_centered_screen(self.content, self.windowsize["width"], self.windowsize["height"] - 3,
|
||||
" ")
|
||||
|
||||
Send(self.client, generatedwindow)
|
||||
|
||||
Send(self.client, "Press 'enter' to continue", ln=False)
|
||||
|
||||
self.waituserenter()
|
||||
|
||||
def waituserenter(self):
|
||||
"""
|
||||
Waits for the user to press the 'enter' key to close the dialog.
|
||||
"""
|
||||
while True:
|
||||
if wait_inputkey(self.client, raw=True) == b'\r':
|
||||
Clear(self.client)
|
||||
break
|
||||
pass
|
||||
|
||||
|
||||
class MenuDialog:
|
||||
"""
|
||||
A menu dialog that allows the user to choose from a list of options, with navigation and selection using arrow keys.
|
||||
|
||||
Args:
|
||||
client (Client): The client to display the menu to.
|
||||
choose (list): A list of options to be displayed.
|
||||
title (str, optional): The title of the menu.
|
||||
desc (str, optional): A description to display above the menu options.
|
||||
|
||||
Methods:
|
||||
render(): Renders the menu dialog and waits for user input.
|
||||
_waituserinput(): Handles user input for selecting options or canceling.
|
||||
output(): Returns the selected option index or `None` if canceled.
|
||||
"""
|
||||
|
||||
def __init__(self, client, choose: list, title="", desc=""):
|
||||
self.client = client
|
||||
|
||||
self.title = title
|
||||
self.choose = choose
|
||||
self.desc = desc
|
||||
self.contentallindex = len(choose) - 1
|
||||
self.selectedindex = 0
|
||||
self.selectstatus = 0 # 0 none, 1 selected, 2 canceled
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Renders the menu dialog, displaying the options and allowing the user to navigate and select an option.
|
||||
"""
|
||||
tempcontentlist = self.choose.copy()
|
||||
|
||||
Clear(self.client)
|
||||
Send(self.client, self.title)
|
||||
Send(self.client, "-" * self.client["windowsize"]["width"])
|
||||
|
||||
tempcontentlist[self.selectedindex] = "> " + tempcontentlist[self.selectedindex]
|
||||
|
||||
exported = "\n".join(tempcontentlist)
|
||||
|
||||
if not self.desc.strip() == "":
|
||||
contenttoshow = (
|
||||
f"{self.desc}\n\n"
|
||||
f"{exported}"
|
||||
)
|
||||
else:
|
||||
contenttoshow = (
|
||||
f"{exported}"
|
||||
)
|
||||
|
||||
generatedwindow = text_centered_screen(contenttoshow, self.client["windowsize"]["width"],
|
||||
self.client["windowsize"]["height"] - 3, " ")
|
||||
|
||||
Send(self.client, generatedwindow)
|
||||
|
||||
Send(self.client, "Use arrow up/down key to choose and press 'enter' to select or 'c' to cancel", ln=False)
|
||||
|
||||
self._waituserinput()
|
||||
|
||||
def _waituserinput(self):
|
||||
"""
|
||||
Waits for user input and updates the selection based on key presses.
|
||||
"""
|
||||
keyinput = wait_inputkey(self.client, raw=True)
|
||||
|
||||
if keyinput == b'\r': # Enter key
|
||||
Clear(self.client)
|
||||
self.selectstatus = 1
|
||||
elif keyinput == b'c': # 'c' key for cancel
|
||||
Clear(self.client)
|
||||
self.selectstatus = 2
|
||||
elif keyinput == b'\x1b[A': # Up arrow key
|
||||
self.selectedindex -= 1
|
||||
if self.selectedindex < 0:
|
||||
self.selectedindex = 0
|
||||
elif keyinput == b'\x1b[B': # Down arrow key
|
||||
self.selectedindex += 1
|
||||
if self.selectedindex > self.contentallindex:
|
||||
self.selectedindex = self.contentallindex
|
||||
|
||||
if self.selectstatus == 2:
|
||||
self.output()
|
||||
elif self.selectstatus == 1:
|
||||
self.output()
|
||||
else:
|
||||
self.render()
|
||||
|
||||
def output(self):
|
||||
"""
|
||||
Returns the selected option index or `None` if the action was canceled.
|
||||
"""
|
||||
if self.selectstatus == 2:
|
||||
return None
|
||||
elif self.selectstatus == 1:
|
||||
return self.selectedindex
|
||||
|
||||
|
||||
class TextInputDialog:
|
||||
"""
|
||||
A text input dialog that allows the user to input text with optional password masking.
|
||||
|
||||
Args:
|
||||
client (Client): The client to display the dialog to.
|
||||
title (str, optional): The title of the input dialog.
|
||||
inputtitle (str, optional): The prompt text for the user input.
|
||||
password (bool, optional): If `True`, the input will be masked as a password. Defaults to `False`.
|
||||
|
||||
Methods:
|
||||
render(): Renders the input dialog, displaying the prompt and capturing user input.
|
||||
_waituserinput(): Handles user input, including text input and special keys.
|
||||
output(): Returns the input text if selected, or `None` if canceled.
|
||||
"""
|
||||
|
||||
def __init__(self, client, title="", inputtitle="Input Here", password=False):
|
||||
self.client = client
|
||||
|
||||
self.title = title
|
||||
self.inputtitle = inputtitle
|
||||
self.ispassword = password
|
||||
|
||||
self.inputstatus = 0 # 0 none, 1 selected, 2 canceled
|
||||
self.buffer = bytearray()
|
||||
self.cursor_position = 0
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Renders the text input dialog and waits for user input.
|
||||
"""
|
||||
Clear(self.client)
|
||||
Send(self.client, self.title)
|
||||
Send(self.client, "-" * self.client["windowsize"]["width"])
|
||||
|
||||
if self.ispassword:
|
||||
texts = (
|
||||
f"{self.inputtitle}\n\n"
|
||||
"> " + ("*" * len(self.buffer.decode('utf-8')))
|
||||
)
|
||||
else:
|
||||
texts = (
|
||||
f"{self.inputtitle}\n\n"
|
||||
"> " + self.buffer.decode('utf-8')
|
||||
)
|
||||
|
||||
generatedwindow = text_centered_screen(texts, self.client["windowsize"]["width"],
|
||||
self.client["windowsize"]["height"] - 3, " ")
|
||||
|
||||
Send(self.client, generatedwindow)
|
||||
|
||||
Send(self.client, "Press 'enter' to select or 'ctrl+c' to cancel", ln=False)
|
||||
|
||||
self._waituserinput()
|
||||
|
||||
def _waituserinput(self):
|
||||
"""
|
||||
Waits for the user to input text or special commands (backspace, cancel, enter).
|
||||
"""
|
||||
keyinput = wait_inputkey(self.client, raw=True)
|
||||
|
||||
if keyinput == b'\r': # Enter key
|
||||
Clear(self.client)
|
||||
self.inputstatus = 1
|
||||
elif keyinput == b'\x03': # 'ctrl + c' key for cancel
|
||||
Clear(self.client)
|
||||
self.inputstatus = 2
|
||||
|
||||
try:
|
||||
if keyinput == b'\x7f' or keyinput == b'\x08': # Backspace
|
||||
if self.cursor_position > 0:
|
||||
# Move cursor back, erase character, move cursor back again
|
||||
self.buffer = self.buffer[:self.cursor_position - 1] + self.buffer[self.cursor_position:]
|
||||
self.cursor_position -= 1
|
||||
elif bool(re.compile(b'\x1b\[[0-9;]*[mGK]').search(keyinput)):
|
||||
pass # Ignore ANSI escape codes
|
||||
else: # Regular character
|
||||
self.buffer = self.buffer[:self.cursor_position] + keyinput + self.buffer[self.cursor_position:]
|
||||
self.cursor_position += 1
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
if self.inputstatus == 2:
|
||||
self.output()
|
||||
elif self.inputstatus == 1:
|
||||
self.output()
|
||||
else:
|
||||
self.render()
|
||||
|
||||
def output(self):
|
||||
"""
|
||||
Returns the input text if the input was selected, or `None` if canceled.
|
||||
"""
|
||||
if self.inputstatus == 2:
|
||||
return None
|
||||
elif self.inputstatus == 1:
|
||||
return self.buffer.decode('utf-8')
|
80
src/PyserSSH/extensions/moredisplay.py
Normal file
80
src/PyserSSH/extensions/moredisplay.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
import time
|
||||
|
||||
from ..interactive import Send
|
||||
|
||||
def clickable_url(url, link_text=""):
|
||||
"""
|
||||
Creates a clickable URL in a terminal client with optional link text.
|
||||
|
||||
Args:
|
||||
url (str): The URL to be linked.
|
||||
link_text (str, optional): The text to be displayed for the link. Defaults to an empty string, which will display the URL itself.
|
||||
|
||||
Returns:
|
||||
str: A terminal escape sequence that makes the URL clickable with the provided link text.
|
||||
"""
|
||||
return f"\033]8;;{url}\033\\{link_text}\033]8;;\033\\"
|
||||
|
||||
def Send_karaoke_effect(client, text, delay=0.1, ln=True):
|
||||
"""
|
||||
Sends a text with a 'karaoke' effect where the text is printed one character at a time,
|
||||
with the remaining text dimmed until it is printed.
|
||||
|
||||
Args:
|
||||
client (Client): The client to send the text to.
|
||||
text (str): The text to be printed with the karaoke effect.
|
||||
delay (float, optional): The delay in seconds between printing each character. Defaults to 0.1.
|
||||
ln (bool, optional): Whether to send a newline after the text is finished. Defaults to True.
|
||||
|
||||
This function simulates a typing effect by printing the text character by character,
|
||||
while dimming the unprinted characters.
|
||||
"""
|
||||
printed_text = ""
|
||||
for i, char in enumerate(text):
|
||||
# Print the already printed text normally
|
||||
Send(client, printed_text + char, ln=False)
|
||||
|
||||
# Calculate the unprinted text and dim it
|
||||
not_printed_text = text[i + 1:]
|
||||
dimmed_text = ''.join([f"\033[2m{char}\033[0m" for char in not_printed_text])
|
||||
|
||||
# Print the dimmed text for the remaining characters
|
||||
Send(client, dimmed_text, ln=False)
|
||||
|
||||
# Wait before printing the next character
|
||||
time.sleep(delay)
|
||||
|
||||
# Clear the line to update the text in the next iteration
|
||||
Send(client, '\r', ln=False)
|
||||
|
||||
# Update the printed_text to include the current character
|
||||
printed_text += char
|
||||
|
||||
if ln:
|
||||
Send(client, "") # Send a newline after the entire text is printed
|
50
src/PyserSSH/extensions/moreinteractive.py
Normal file
50
src/PyserSSH/extensions/moreinteractive.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
from ..interactive import Send
|
||||
|
||||
def ShowCursor(client, show=True):
|
||||
"""
|
||||
Shows or hides the cursor for a specific client.
|
||||
|
||||
Args:
|
||||
client (Client): The client to show/hide the cursor for.
|
||||
show (bool, optional): A flag to determine whether to show or hide the cursor. Defaults to True (show cursor).
|
||||
"""
|
||||
if show:
|
||||
Send(client, "\033[?25h", ln=False) # Show cursor
|
||||
else:
|
||||
Send(client, "\033[?25l", ln=False) # Hide cursor
|
||||
|
||||
def SendBell(client):
|
||||
"""
|
||||
Sends a bell character (alert) to a client.
|
||||
|
||||
Args:
|
||||
client (Client): The client to send the bell character to.
|
||||
"""
|
||||
Send(client, "\x07", ln=False) # Bell character (alert)
|
409
src/PyserSSH/extensions/processbar.py
Normal file
409
src/PyserSSH/extensions/processbar.py
Normal file
@ -0,0 +1,409 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
# this file is from damp11113-library
|
||||
|
||||
from itertools import cycle
|
||||
import math
|
||||
import time
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
from ..system.sysfunc import replace_enter_with_crlf
|
||||
|
||||
def Print(channel, string, start="", end="\n"):
|
||||
channel.send(replace_enter_with_crlf(start + string + end))
|
||||
|
||||
def get_size_unit2(number, unitp, persec=True, unitsize=1024, decimal=True, space=" "):
|
||||
for unit in ['', 'K', 'M', 'G', 'T', 'P']:
|
||||
if number < unitsize:
|
||||
if decimal:
|
||||
num = f"{number:.2f}"
|
||||
else:
|
||||
num = int(number)
|
||||
|
||||
if persec:
|
||||
return f"{num}{space}{unit}{unitp}/s"
|
||||
else:
|
||||
return f"{num}{space}{unit}{unitp}"
|
||||
number /= unitsize
|
||||
|
||||
def center_string(main_string, replacement_string):
|
||||
# Find the center index of the main string
|
||||
center_index = len(main_string) // 2
|
||||
|
||||
# Calculate the start and end indices for replacing
|
||||
start_index = center_index - len(replacement_string) // 2
|
||||
end_index = start_index + len(replacement_string)
|
||||
|
||||
# Replace the substring at the center
|
||||
new_string = main_string[:start_index] + replacement_string + main_string[end_index:]
|
||||
|
||||
return new_string
|
||||
|
||||
class TextFormatter:
|
||||
RESET = "\033[0m"
|
||||
TEXT_COLORS = {
|
||||
"black": "\033[30m",
|
||||
"red": "\033[31m",
|
||||
"green": "\033[32m",
|
||||
"yellow": "\033[33m",
|
||||
"blue": "\033[34m",
|
||||
"magenta": "\033[35m",
|
||||
"cyan": "\033[36m",
|
||||
"white": "\033[37m"
|
||||
}
|
||||
TEXT_COLOR_LEVELS = {
|
||||
"light": "\033[1;{}m", # Light color prefix
|
||||
"dark": "\033[2;{}m" # Dark color prefix
|
||||
}
|
||||
BACKGROUND_COLORS = {
|
||||
"black": "\033[40m",
|
||||
"red": "\033[41m",
|
||||
"green": "\033[42m",
|
||||
"yellow": "\033[43m",
|
||||
"blue": "\033[44m",
|
||||
"magenta": "\033[45m",
|
||||
"cyan": "\033[46m",
|
||||
"white": "\033[47m"
|
||||
}
|
||||
TEXT_ATTRIBUTES = {
|
||||
"bold": "\033[1m",
|
||||
"italic": "\033[3m",
|
||||
"underline": "\033[4m",
|
||||
"blink": "\033[5m",
|
||||
"reverse": "\033[7m",
|
||||
"strikethrough": "\033[9m"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def format_text(text, color=None, color_level=None, background=None, attributes=None, target_text=''):
|
||||
formatted_text = ""
|
||||
start_index = text.find(target_text)
|
||||
end_index = start_index + len(target_text) if start_index != -1 else len(text)
|
||||
|
||||
if color in TextFormatter.TEXT_COLORS:
|
||||
if color_level in TextFormatter.TEXT_COLOR_LEVELS:
|
||||
color_code = TextFormatter.TEXT_COLORS[color]
|
||||
color_format = TextFormatter.TEXT_COLOR_LEVELS[color_level].format(color_code)
|
||||
formatted_text += color_format
|
||||
else:
|
||||
formatted_text += TextFormatter.TEXT_COLORS[color]
|
||||
|
||||
if background in TextFormatter.BACKGROUND_COLORS:
|
||||
formatted_text += TextFormatter.BACKGROUND_COLORS[background]
|
||||
|
||||
if attributes in TextFormatter.TEXT_ATTRIBUTES:
|
||||
formatted_text += TextFormatter.TEXT_ATTRIBUTES[attributes]
|
||||
|
||||
if target_text == "":
|
||||
formatted_text += text + TextFormatter.RESET
|
||||
else:
|
||||
formatted_text += text[:start_index] + text[start_index:end_index] + TextFormatter.RESET + text[end_index:]
|
||||
|
||||
return formatted_text
|
||||
|
||||
def insert_string(base, inserted, position=0):
|
||||
return base[:position] + inserted + base[position + len(inserted):]
|
||||
|
||||
class Steps:
|
||||
steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]']
|
||||
steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]']
|
||||
steps3 = ['[ ]', '[- ]', '[-- ]', '[ --]', '[ -]', '[ ]', '[ -]', '[ --]', '[-- ]', '[- ]']
|
||||
steps4 = ['[ ]', '[- ]', '[ - ]', '[ -]', '[ ]', '[ -]', '[ - ]', '[- ]', '[ ]']
|
||||
steps5 = ['[ ]', '[ -]', '[ --]', '[---]', '[-- ]', '[- ]']
|
||||
steps6 = ['[ ]', '[ -]', '[ - ]', '[- ]']
|
||||
expand_contract = ['[ ]', '[= ]', '[== ]', '[=== ]', '[====]', '[ ===]', '[ ==]', '[ =]', '[ ]']
|
||||
rotating_dots = ['. ', '.. ', '... ', '.... ', '.....', ' ....', ' ...', ' ..', ' .', ' ']
|
||||
bouncing_ball = ['o ', ' o ', ' o ', ' o ', ' o ', ' o', ' o ', ' o ', ' o ', ' o ', 'o ']
|
||||
left_right_dots = ['[ ]', '[. ]', '[.. ]', '[... ]', '[....]', '[ ...]', '[ ..]', '[ .]', '[ ]']
|
||||
expanding_square = ['[ ]', '[■]', '[■■]', '[■■■]', '[■■■■]', '[■■■]', '[■■]', '[■]', '[ ]']
|
||||
spinner = ['|', '/', '-', '\\', '|', '/', '-', '\\']
|
||||
zigzag = ['/ ', ' / ', ' / ', ' /', ' / ', ' / ', '/ ', '\\ ', ' \\ ', ' \\ ', ' \\', ' \\ ', ' \\ ', '\\ ']
|
||||
arrows = ['← ', '←← ', '←←←', '←← ', '← ', '→ ', '→→ ', '→→→', '→→ ', '→ ']
|
||||
snake = ['[> ]', '[=> ]', '[==> ]', '[===> ]', '[====>]', '[ ===>]', '[ ==>]', '[ =>]', '[ >]']
|
||||
loading_bar = ['[ ]', '[= ]', '[== ]', '[=== ]', '[==== ]', '[===== ]', '[====== ]', '[======= ]', '[======== ]', '[========= ]', '[==========]']
|
||||
|
||||
class indeterminateStatus:
|
||||
def __init__(self, client, desc="Loading...", end="[ OK ]", timeout=0.1, fail='[FAILED]', steps=None):
|
||||
self.client = client
|
||||
self.desc = desc
|
||||
self.end = end
|
||||
self.timeout = timeout
|
||||
self.faill = fail
|
||||
|
||||
self._thread = Thread(target=self._animate, daemon=True)
|
||||
if steps is None:
|
||||
self.steps = Steps.steps1
|
||||
else:
|
||||
self.steps = steps
|
||||
self.done = False
|
||||
self.fail = False
|
||||
|
||||
def start(self):
|
||||
"""Start progress bar"""
|
||||
self._thread.start()
|
||||
return self
|
||||
|
||||
def _animate(self):
|
||||
for c in cycle(self.steps):
|
||||
if self.done:
|
||||
break
|
||||
Print(self.client['channel'], f"\r{c} {self.desc}" , end="")
|
||||
sleep(self.timeout)
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
|
||||
def stop(self):
|
||||
"""stop progress"""
|
||||
self.done = True
|
||||
cols = self.client["windowsize"]["width"]
|
||||
Print(self.client['channel'], "\r" + " " * cols, end="")
|
||||
Print(self.client['channel'], f"\r{self.end}")
|
||||
|
||||
def stopfail(self):
|
||||
"""stop progress with error or fail"""
|
||||
self.done = True
|
||||
self.fail = True
|
||||
cols = self.client["windowsize"]["width"]
|
||||
Print(self.client['channel'], "\r" + " " * cols, end="")
|
||||
Print(self.client['channel'], f"\r{self.faill}")
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
# handle exceptions with those variables ^
|
||||
self.stop()
|
||||
|
||||
class LoadingProgress:
|
||||
def __init__(self, client, total=100, totalbuffer=None, length=50, fill='█', fillbufferbar='█', desc="Loading...", status="", enabuinstatus=True, end="[ OK ]", timeout=0.1, fail='[FAILED]', steps=None, unit="it", barbackground="-", shortnum=False, buffer=False, shortunitsize=1000, currentshortnum=False, show=True, indeterminate=False, barcolor="red", bufferbarcolor="white",barbackgroundcolor="black", color=True):
|
||||
"""
|
||||
Simple loading progress bar python
|
||||
@param client: from ssh client request
|
||||
@param total: change all total
|
||||
@param desc: change description
|
||||
@param status: change progress status
|
||||
@param end: change success progress
|
||||
@param timeout: change speed
|
||||
@param fail: change error stop
|
||||
@param steps: change steps animation
|
||||
@param unit: change unit
|
||||
@param buffer: enable buffer progress (experiment)
|
||||
@param show: show progress bar
|
||||
@param indeterminate: indeterminate mode
|
||||
@param barcolor: change bar color
|
||||
@param bufferbarcolor: change buffer bar color
|
||||
@param barbackgroundcolor: change background color
|
||||
@param color: enable colorful
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
self.desc = desc
|
||||
self.end = end
|
||||
self.timeout = timeout
|
||||
self.faill = fail
|
||||
self.total = total
|
||||
self.length = length
|
||||
self.fill = fill
|
||||
self.enbuinstatus = enabuinstatus
|
||||
self.status = status
|
||||
self.barbackground = barbackground
|
||||
self.unit = unit
|
||||
self.shortnum = shortnum
|
||||
self.shortunitsize = shortunitsize
|
||||
self.currentshortnum = currentshortnum
|
||||
self.printed = show
|
||||
self.indeterminate = indeterminate
|
||||
self.barcolor = barcolor
|
||||
self.barbackgroundcolor = barbackgroundcolor
|
||||
self.enabuffer = buffer
|
||||
self.bufferbarcolor = bufferbarcolor
|
||||
self.fillbufferbar = fillbufferbar
|
||||
self.totalbuffer = totalbuffer
|
||||
self.enacolor = color
|
||||
|
||||
self._thread = Thread(target=self._animate, daemon=True)
|
||||
|
||||
if steps is None:
|
||||
self.steps = Steps.steps1
|
||||
else:
|
||||
self.steps = steps
|
||||
|
||||
if self.totalbuffer is None:
|
||||
self.totalbuffer = self.total
|
||||
|
||||
self.currentpercent = 0
|
||||
self.currentbufferpercent = 0
|
||||
self.current = 0
|
||||
self.currentbuffer = 0
|
||||
self.startime = 0
|
||||
self.done = False
|
||||
self.fail = False
|
||||
self.currentprint = ""
|
||||
|
||||
def start(self):
|
||||
"""Start progress bar"""
|
||||
self._thread.start()
|
||||
self.startime = time.perf_counter()
|
||||
return self
|
||||
|
||||
def update(self, i=1):
|
||||
"""update progress"""
|
||||
self.current += i
|
||||
|
||||
def updatebuffer(self, i=1):
|
||||
"""update buffer progress"""
|
||||
self.currentbuffer += i
|
||||
|
||||
def _animate(self):
|
||||
for c in cycle(self.steps):
|
||||
if self.done:
|
||||
break
|
||||
|
||||
if not self.indeterminate:
|
||||
if self.total != 0 or math.trunc(float(self.currentpercent)) > 100:
|
||||
if self.enabuffer:
|
||||
self.currentpercent = ("{0:.1f}").format(100 * (self.current / float(self.total)))
|
||||
|
||||
filled_length = int(self.length * self.current // self.total)
|
||||
|
||||
if self.enacolor:
|
||||
bar = TextFormatter.format_text(self.fill * filled_length, self.barcolor)
|
||||
else:
|
||||
bar = self.fill * filled_length
|
||||
|
||||
self.currentbufferpercent = ("{0:.1f}").format(
|
||||
100 * (self.currentbuffer / float(self.totalbuffer)))
|
||||
|
||||
if float(self.currentbufferpercent) >= 100.0:
|
||||
self.currentbufferpercent = 100
|
||||
|
||||
filled_length_buffer = int(self.length * self.currentbuffer // self.totalbuffer)
|
||||
|
||||
if filled_length_buffer >= self.length:
|
||||
filled_length_buffer = self.length
|
||||
|
||||
if self.enacolor:
|
||||
bufferbar = TextFormatter.format_text(self.fillbufferbar * filled_length_buffer,
|
||||
self.bufferbarcolor)
|
||||
else:
|
||||
bufferbar = self.fillbufferbar * filled_length_buffer
|
||||
|
||||
bar = insert_string(bufferbar, bar)
|
||||
|
||||
if self.enacolor:
|
||||
bar += TextFormatter.format_text(self.barbackground * (self.length - filled_length_buffer),
|
||||
self.barbackgroundcolor)
|
||||
else:
|
||||
bar += self.barbackground * (self.length - filled_length_buffer)
|
||||
else:
|
||||
self.currentpercent = ("{0:.1f}").format(100 * (self.current / float(self.total)))
|
||||
filled_length = int(self.length * self.current // self.total)
|
||||
if self.enacolor:
|
||||
bar = TextFormatter.format_text(self.fill * filled_length, self.barcolor)
|
||||
|
||||
bar += TextFormatter.format_text(self.barbackground * (self.length - filled_length),
|
||||
self.barbackgroundcolor)
|
||||
else:
|
||||
bar = self.fill * filled_length
|
||||
if self.enacolor:
|
||||
bar = TextFormatter.format_text(bar, self.barcolor)
|
||||
bar += self.barbackground * (self.length - filled_length)
|
||||
|
||||
|
||||
if self.enbuinstatus:
|
||||
elapsed_time = time.perf_counter() - self.startime
|
||||
speed = self.current / elapsed_time if elapsed_time > 0 else 0
|
||||
remaining = self.total - self.current
|
||||
eta_seconds = remaining / speed if speed > 0 else 0
|
||||
elapsed_formatted = time.strftime('%H:%M:%S', time.gmtime(elapsed_time))
|
||||
eta_formatted = time.strftime('%H:%M:%S', time.gmtime(eta_seconds))
|
||||
if self.shortnum:
|
||||
stotal = get_size_unit2(self.total, '', False, self.shortunitsize, False, '')
|
||||
scurrent = get_size_unit2(self.current, '', False, self.shortunitsize, self.currentshortnum, '')
|
||||
else:
|
||||
stotal = self.total
|
||||
scurrent = self.current
|
||||
|
||||
if math.trunc(float(self.currentpercent)) > 100:
|
||||
elapsed_time = time.perf_counter() - self.startime
|
||||
elapsed_formatted = time.strftime('%H:%M:%S', time.gmtime(elapsed_time))
|
||||
|
||||
bar = center_string(self.barbackground * self.length, TextFormatter.format_text("Indeterminate", self.barcolor))
|
||||
|
||||
self.currentprint = f"{c} {self.desc} | --%|{bar}| {scurrent}/{stotal} | {elapsed_formatted} | {get_size_unit2(speed, self.unit, self.shortunitsize)} | {self.status}"
|
||||
|
||||
else:
|
||||
self.currentprint = f"{c} {self.desc} | {math.trunc(float(self.currentpercent))}%|{bar}| {scurrent}/{stotal} | {elapsed_formatted}<{eta_formatted} | {get_size_unit2(speed, self.unit, self.shortunitsize)} | {self.status}"
|
||||
else:
|
||||
if self.shortnum:
|
||||
stotal = get_size_unit2(self.total, '', False, self.shortunitsize, False, '')
|
||||
scurrent = get_size_unit2(self.current, '', False, self.shortunitsize, self.currentshortnum, '')
|
||||
else:
|
||||
stotal = self.total
|
||||
scurrent = self.current
|
||||
|
||||
self.currentprint = f"{c} {self.desc} | {math.trunc(float(self.currentpercent))}%|{bar}| {scurrent}/{stotal} | {self.status}"
|
||||
else:
|
||||
elapsed_time = time.perf_counter() - self.startime
|
||||
elapsed_formatted = time.strftime('%H:%M:%S', time.gmtime(elapsed_time))
|
||||
|
||||
bar = center_string(self.barbackground * self.length, TextFormatter.format_text("Indeterminate", self.barcolor))
|
||||
|
||||
self.currentprint = f"{c} {self.desc} | --%|{bar}| {elapsed_formatted} | {self.status}"
|
||||
else:
|
||||
elapsed_time = time.perf_counter() - self.startime
|
||||
elapsed_formatted = time.strftime('%H:%M:%S', time.gmtime(elapsed_time))
|
||||
|
||||
bar = center_string(self.barbackground * self.length, TextFormatter.format_text("Indeterminate", self.barcolor))
|
||||
|
||||
self.currentprint = f"{c} {self.desc} | --%|{bar}| {elapsed_formatted} | {self.status}"
|
||||
|
||||
if self.printed:
|
||||
Print(self.client["channel"], f"\r{self.currentprint}", end="")
|
||||
|
||||
sleep(self.timeout)
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
|
||||
def stop(self):
|
||||
"""stop progress"""
|
||||
self.done = True
|
||||
cols = self.client["windowsize"]["width"]
|
||||
Print(self.client["channel"], "\r" + " " * cols, end="")
|
||||
Print(self.client["channel"], f"\r{self.end}")
|
||||
|
||||
def stopfail(self):
|
||||
"""stop progress with error or fail"""
|
||||
self.done = True
|
||||
self.fail = True
|
||||
cols = self.client["windowsize"]["width"]
|
||||
Print(self.client["channel"], "\r" + " " * cols, end="")
|
||||
Print(self.client["channel"], f"\r{self.faill}")
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
# handle exceptions with those variables ^
|
||||
self.stop()
|
294
src/PyserSSH/extensions/remodesk.py
Normal file
294
src/PyserSSH/extensions/remodesk.py
Normal file
@ -0,0 +1,294 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import brotli
|
||||
import numpy as np
|
||||
import cv2
|
||||
from PIL import ImageGrab
|
||||
import struct
|
||||
import queue
|
||||
import pickle
|
||||
import mouse
|
||||
import keyboard
|
||||
import logging
|
||||
|
||||
from ..system.clientype import Client
|
||||
|
||||
logger = logging.getLogger("PyserSSH.Ext.RemoDeskSSH")
|
||||
|
||||
class Protocol:
|
||||
def __init__(self, server):
|
||||
self.listclient = []
|
||||
self.first = True
|
||||
self.running = False
|
||||
self.server = server
|
||||
self.buffer = queue.Queue(maxsize=10)
|
||||
|
||||
def _handle_client(self):
|
||||
try:
|
||||
while self.running:
|
||||
data2send = self.buffer.get()
|
||||
|
||||
for iclient in self.listclient:
|
||||
try:
|
||||
iclient[2].sendall(data2send)
|
||||
except Exception as e:
|
||||
iclient[2].close()
|
||||
self.listclient.remove(iclient)
|
||||
|
||||
if not self.listclient:
|
||||
self.running = False
|
||||
self.first = True
|
||||
logger.info("No clients connected. Server is standby")
|
||||
break
|
||||
|
||||
except socket.error:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error in handle_client: {e}")
|
||||
|
||||
def _handle_client_commands(self, client, id):
|
||||
try:
|
||||
while True:
|
||||
client_socket = client.get_subchannel(id)
|
||||
|
||||
try:
|
||||
# Receive the length of the data
|
||||
data_length = self._receive_exact(client_socket, 4)
|
||||
if not data_length:
|
||||
break
|
||||
|
||||
commandmetadata = struct.unpack('!I', data_length)
|
||||
command_data = self._receive_exact(client_socket, commandmetadata[0])
|
||||
command = pickle.loads(command_data)
|
||||
|
||||
if command:
|
||||
self.handle_commands(command, client)
|
||||
|
||||
except socket.error:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in handle_client_commands: {e}")
|
||||
|
||||
def handle_commands(self, command, client):
|
||||
pass
|
||||
|
||||
def _receive_exact(self, socket, n):
|
||||
"""Helper function to receive exactly n bytes."""
|
||||
data = b''
|
||||
while len(data) < n:
|
||||
packet = socket.recv(n - len(data))
|
||||
if not packet:
|
||||
return None
|
||||
data += packet
|
||||
return data
|
||||
|
||||
def init(self, client):
|
||||
pass
|
||||
|
||||
def handle_new_client(self, client: Client, directchannel=None):
|
||||
if directchannel:
|
||||
id = directchannel.get_id()
|
||||
channel = directchannel
|
||||
else:
|
||||
logger.info("waiting remote channel")
|
||||
id, channel = client.open_new_subchannel(5)
|
||||
if id == None or channel == None:
|
||||
logger.info("client is not connect in 5 sec")
|
||||
return
|
||||
|
||||
self.listclient.append([client, id, channel])
|
||||
|
||||
if self.first:
|
||||
self.running = True
|
||||
handle_client_thread = threading.Thread(target=self._handle_client, daemon=True)
|
||||
handle_client_thread.start()
|
||||
|
||||
self.init(client)
|
||||
|
||||
self.first = False
|
||||
|
||||
command_thread = threading.Thread(target=self._handle_client_commands, args=(client, id), daemon=True)
|
||||
command_thread.start()
|
||||
|
||||
class RemoDesk(Protocol):
|
||||
def __init__(self, server=None, quality=50, compression=50, format="jpeg", resolution: set[int, int] = None, activity_threshold=None, second_compress=True):
|
||||
"""
|
||||
Args:
|
||||
server: ssh server
|
||||
quality: quality of remote
|
||||
compression: percent of compression 0-100 %
|
||||
format: jpeg, webp, avif
|
||||
resolution: resolution of remote
|
||||
"""
|
||||
|
||||
super().__init__(server)
|
||||
|
||||
self.quality = quality
|
||||
self.compression = compression
|
||||
self.format = format
|
||||
self.resolution = resolution
|
||||
self.threshold = activity_threshold
|
||||
self.compress2 = second_compress
|
||||
self.screensize = ()
|
||||
self.previous_frame = None
|
||||
|
||||
def _capture_screen(self):
|
||||
try:
|
||||
screenshot = ImageGrab.grab()
|
||||
self.screensize = screenshot.size
|
||||
img_np = np.array(screenshot)
|
||||
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
||||
return img_bgr
|
||||
except:
|
||||
return b""
|
||||
|
||||
def _detect_activity(self, current_frame):
|
||||
if self.threshold:
|
||||
if self.previous_frame is None:
|
||||
self.previous_frame = current_frame
|
||||
return False # No previous frame to compare to
|
||||
|
||||
# Compute the absolute difference between the current frame and the previous frame
|
||||
diff = cv2.absdiff(current_frame, self.previous_frame)
|
||||
|
||||
# Convert the difference to grayscale
|
||||
gray_diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# Apply a threshold to get a binary image
|
||||
_, thresh = cv2.threshold(gray_diff, self.threshold, 255, cv2.THRESH_BINARY)
|
||||
|
||||
# Calculate the number of non-zero pixels in the thresholded image
|
||||
non_zero_count = np.count_nonzero(thresh)
|
||||
|
||||
# Update the previous frame
|
||||
self.previous_frame = current_frame
|
||||
|
||||
# If there are enough non-zero pixels, we consider it as activity
|
||||
return non_zero_count > 500 # You can adjust the threshold as needed
|
||||
else:
|
||||
return True
|
||||
|
||||
def _imagenc(self, image):
|
||||
if self.format == "webp":
|
||||
retval, buffer = cv2.imencode('.webp', image, [int(cv2.IMWRITE_WEBP_QUALITY), self.quality])
|
||||
elif self.format == "jpeg":
|
||||
retval, buffer = cv2.imencode('.jpeg', image, [int(cv2.IMWRITE_JPEG_QUALITY), self.quality])
|
||||
elif self.format == "avif":
|
||||
retval, buffer = cv2.imencode('.avif', image, [int(cv2.IMWRITE_AVIF_QUALITY), self.quality])
|
||||
|
||||
else:
|
||||
raise TypeError(f"{self.format} is not supported")
|
||||
|
||||
if not retval:
|
||||
raise ValueError("image encoding failed.")
|
||||
|
||||
return np.array(buffer).tobytes()
|
||||
|
||||
def _translate_coordinates(self, x, y):
|
||||
if self.resolution:
|
||||
translated_x = int(x * (self.screensize[0] / self.resolution[0]))
|
||||
translated_y = int(y * (self.screensize[1] / self.resolution[1]))
|
||||
else:
|
||||
translated_x = int(x * (self.screensize[0] / 1920))
|
||||
translated_y = int(y * (self.screensize[1] / 1090))
|
||||
return translated_x, translated_y
|
||||
|
||||
def _convert_quality(self, quality):
|
||||
brotli_quality = int(quality / 100 * 11)
|
||||
lgwin = int(10 + (quality / 100 * (24 - 10)))
|
||||
|
||||
return brotli_quality, lgwin
|
||||
|
||||
def _capture(self):
|
||||
while self.running:
|
||||
screen_image = self._capture_screen()
|
||||
|
||||
if self._detect_activity(screen_image):
|
||||
if self.resolution:
|
||||
screen_image = cv2.resize(screen_image, self.resolution, interpolation=cv2.INTER_NEAREST)
|
||||
else:
|
||||
self.resolution = self.screensize
|
||||
|
||||
data = self._imagenc(screen_image)
|
||||
|
||||
if self.compress2:
|
||||
bquality, lgwin = self._convert_quality(self.compression)
|
||||
data = brotli.compress(data, quality=bquality, lgwin=lgwin)
|
||||
|
||||
data_length = struct.pack('!III', len(data), self.resolution[0], self.resolution[1])
|
||||
data2send = data_length + data
|
||||
|
||||
print(f"Sending data length: {len(data2send)}")
|
||||
self.buffer.put(data2send)
|
||||
|
||||
def handle_commands(self, command, client):
|
||||
action = command["action"]
|
||||
data = command["data"]
|
||||
|
||||
if action == "move_mouse":
|
||||
x, y = data["x"], data["y"]
|
||||
rx, ry = self._translate_coordinates(x, y)
|
||||
mouse.move(rx, ry)
|
||||
|
||||
elif action == "click_mouse":
|
||||
button = data["button"]
|
||||
state = data["state"]
|
||||
|
||||
if button == 1:
|
||||
if state == "down":
|
||||
mouse.press()
|
||||
else:
|
||||
mouse.release()
|
||||
elif button == 2:
|
||||
if state == "down":
|
||||
mouse.press(mouse.MIDDLE)
|
||||
else:
|
||||
mouse.release(mouse.MIDDLE)
|
||||
elif button == 3:
|
||||
if state == "down":
|
||||
mouse.press(mouse.RIGHT)
|
||||
else:
|
||||
mouse.release(mouse.RIGHT)
|
||||
# elif button == 4:
|
||||
# mouse.wheel()
|
||||
# elif button == 5:
|
||||
# mouse.wheel(-1)
|
||||
elif action == "keyboard":
|
||||
key = data["key"]
|
||||
state = data["state"]
|
||||
|
||||
if state == "down":
|
||||
keyboard.press(key)
|
||||
else:
|
||||
keyboard.release(key)
|
||||
|
||||
def init(self, client):
|
||||
capture_thread = threading.Thread(target=self._capture, daemon=True)
|
||||
capture_thread.start()
|
140
src/PyserSSH/extensions/serverutils.py
Normal file
140
src/PyserSSH/extensions/serverutils.py
Normal file
@ -0,0 +1,140 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from ..interactive import Send
|
||||
|
||||
logger = logging.getLogger("PyserSSH.Ext.ServerUtils")
|
||||
|
||||
def kickbyusername(server, username, reason=None):
|
||||
"""
|
||||
Kicks a user from the server by their username.
|
||||
|
||||
Args:
|
||||
server (Server): The server object where clients are connected.
|
||||
username (str): The username of the client to be kicked.
|
||||
reason (str, optional): The reason for kicking the user. If None, no reason is provided.
|
||||
"""
|
||||
for peername, client_handler in list(server.client_handlers.items()):
|
||||
if client_handler["current_user"] == username:
|
||||
channel = client_handler.get("channel")
|
||||
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"])
|
||||
if reason is None:
|
||||
if channel:
|
||||
channel.close()
|
||||
logger.info(f"User '{username}' has been kicked.")
|
||||
else:
|
||||
if channel:
|
||||
Send(channel, f"You have been disconnected for {reason}")
|
||||
channel.close()
|
||||
logger.info(f"User '{username}' has been kicked by reason {reason}.")
|
||||
|
||||
def kickbypeername(server, peername, reason=None):
|
||||
"""
|
||||
Kicks a user from the server by their peername.
|
||||
|
||||
Args:
|
||||
server (Server): The server object where clients are connected.
|
||||
peername (str): The peername of the client to be kicked.
|
||||
reason (str, optional): The reason for kicking the user. If None, no reason is provided.
|
||||
"""
|
||||
client_handler = server.client_handlers.get(peername)
|
||||
if client_handler:
|
||||
channel = client_handler.get("channel")
|
||||
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"])
|
||||
if reason is None:
|
||||
if channel:
|
||||
channel.close()
|
||||
logger.info(f"peername '{peername}' has been kicked.")
|
||||
else:
|
||||
if channel:
|
||||
Send(channel, f"You have been disconnected for {reason}")
|
||||
channel.close()
|
||||
logger.info(f"peername '{peername}' has been kicked by reason {reason}.")
|
||||
|
||||
def kickall(server, reason=None):
|
||||
"""
|
||||
Kicks all users from the server.
|
||||
|
||||
Args:
|
||||
server (Server): The server object where clients are connected.
|
||||
reason (str, optional): The reason for kicking all users. If None, no reason is provided.
|
||||
"""
|
||||
for peername, client_handler in server.client_handlers.items():
|
||||
channel = client_handler.get("channel")
|
||||
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"])
|
||||
if reason is None:
|
||||
if channel:
|
||||
channel.close()
|
||||
else:
|
||||
if channel:
|
||||
Send(channel, f"You have been disconnected for {reason}")
|
||||
channel.close()
|
||||
if reason is None:
|
||||
server.client_handlers.clear()
|
||||
logger.info("All users have been kicked.")
|
||||
else:
|
||||
logger.info(f"All users have been kicked by reason {reason}.")
|
||||
|
||||
def broadcast(server, message):
|
||||
"""
|
||||
Broadcasts a message to all connected clients.
|
||||
|
||||
Args:
|
||||
server (Server): The server object where clients are connected.
|
||||
message (str): The message to send to all clients.
|
||||
"""
|
||||
for client_handler in server.client_handlers.values():
|
||||
channel = client_handler.get("channel")
|
||||
if channel:
|
||||
try:
|
||||
# Send the message to the client
|
||||
Send(channel, message)
|
||||
except Exception as e:
|
||||
logger.error(f"Error occurred while broadcasting message: {e}")
|
||||
|
||||
def sendto(server, username, message):
|
||||
"""
|
||||
Sends a message to a specific user by their username.
|
||||
|
||||
Args:
|
||||
server (Server): The server object where clients are connected.
|
||||
username (str): The username of the client to send the message to.
|
||||
message (str): The message to send to the specified user.
|
||||
"""
|
||||
for client_handler in server.client_handlers.values():
|
||||
if client_handler.get("current_user") == username:
|
||||
channel = client_handler.get("channel")
|
||||
if channel:
|
||||
try:
|
||||
# Send the message to the specific client
|
||||
Send(channel, message)
|
||||
except Exception as e:
|
||||
logger.error(f"Error occurred while sending message to {username}: {e}")
|
||||
break
|
||||
else:
|
||||
logger.warning(f"User '{username}' not found.")
|
304
src/PyserSSH/interactive.py
Normal file
304
src/PyserSSH/interactive.py
Normal file
@ -0,0 +1,304 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
import re
|
||||
import socket
|
||||
|
||||
from .system.sysfunc import replace_enter_with_crlf
|
||||
|
||||
def Send(client, string, ln=True, directchannel=False):
|
||||
if directchannel:
|
||||
channel = client
|
||||
else:
|
||||
channel = client["channel"]
|
||||
|
||||
if ln:
|
||||
channel.send(replace_enter_with_crlf(str(string) + "\n"))
|
||||
else:
|
||||
channel.send(replace_enter_with_crlf(str(string)))
|
||||
|
||||
def NewSend(client, *astring, ln=True, end=b'\n', sep=b' ', directchannel=False):
|
||||
if directchannel:
|
||||
channel = client
|
||||
else:
|
||||
channel = client["channel"]
|
||||
|
||||
if ln:
|
||||
if not b'\n' in end:
|
||||
end += b'\n'
|
||||
else:
|
||||
# Ensure that `end` does not contain `b'\n'` if `ln` is False
|
||||
end = end.replace(b'\n', b'')
|
||||
|
||||
# Prepare the strings to be sent
|
||||
if astring:
|
||||
for i, s in enumerate(astring):
|
||||
# Convert `s` to bytes if it's a string
|
||||
if isinstance(s, str):
|
||||
s = s.encode('utf-8')
|
||||
# Use a hypothetical `replace_enter_with_crlf` function if needed
|
||||
channel.send(replace_enter_with_crlf(s))
|
||||
if i != len(astring) - 1:
|
||||
channel.send(sep)
|
||||
channel.send(end)
|
||||
|
||||
def Clear(client, oldclear=False, keep=False):
|
||||
sx, sy = client["windowsize"]["width"], client["windowsize"]["height"]
|
||||
|
||||
if oldclear:
|
||||
for x in range(sy):
|
||||
Send(client, '\b \b' * sx, ln=False) # Send newline after each line
|
||||
elif keep:
|
||||
Send(client, "\033[2J", ln=False)
|
||||
Send(client, "\033[H", ln=False)
|
||||
else:
|
||||
Send(client, "\033[3J", ln=False)
|
||||
Send(client, "\033[1J", ln=False)
|
||||
Send(client, "\033[H", ln=False)
|
||||
|
||||
def Title(client, title):
|
||||
Send(client, f"\033]0;{title}\007", ln=False)
|
||||
|
||||
def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=True, password=False, passwordmask=b"*", noabort=False, timeout=0, directchannel=False):
|
||||
if directchannel:
|
||||
channel = client
|
||||
else:
|
||||
channel = client["channel"]
|
||||
|
||||
channel.send(replace_enter_with_crlf(prompt))
|
||||
|
||||
buffer = bytearray()
|
||||
cursor_position = 0
|
||||
|
||||
if timeout != 0:
|
||||
channel.settimeout(timeout)
|
||||
|
||||
try:
|
||||
while True:
|
||||
byte = channel.recv(1)
|
||||
|
||||
if not byte or byte == b'\x04':
|
||||
raise EOFError()
|
||||
elif byte == b'\x03' and not noabort:
|
||||
break
|
||||
elif byte == b'\t':
|
||||
pass
|
||||
elif byte == b'\x7f' or byte == b'\x08': # Backspace
|
||||
if cursor_position > 0:
|
||||
# Move cursor back, erase character, move cursor back again
|
||||
channel.sendall(b'\b \b')
|
||||
buffer = buffer[:cursor_position - 1] + buffer[cursor_position:]
|
||||
cursor_position -= 1
|
||||
elif byte == b'\x1b' and channel.recv(1) == b'[': # Arrow keys
|
||||
arrow_key = channel.recv(1)
|
||||
if cursor_scroll:
|
||||
if arrow_key == b'C': # Right arrow key
|
||||
if cursor_position < len(buffer):
|
||||
channel.sendall(b'\x1b[C')
|
||||
cursor_position += 1
|
||||
elif arrow_key == b'D': # Left arrow key
|
||||
if cursor_position > 0:
|
||||
channel.sendall(b'\x1b[D')
|
||||
cursor_position -= 1
|
||||
elif byte in (b'\r', b'\n'): # Enter key
|
||||
break
|
||||
else: # Regular character
|
||||
buffer = buffer[:cursor_position] + byte + buffer[cursor_position:]
|
||||
cursor_position += 1
|
||||
if echo or password:
|
||||
if password:
|
||||
channel.sendall(passwordmask)
|
||||
else:
|
||||
channel.sendall(byte)
|
||||
|
||||
channel.sendall(b'\r\n')
|
||||
|
||||
except socket.timeout:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
channel.sendall(b'\r\n')
|
||||
output = ""
|
||||
except Exception:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
channel.sendall(b'\r\n')
|
||||
raise
|
||||
else:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
output = buffer.decode('utf-8')
|
||||
|
||||
# Return default value if specified and no input given
|
||||
if defaultvalue is not None and not output.strip():
|
||||
return defaultvalue
|
||||
else:
|
||||
return output
|
||||
|
||||
def wait_inputkey(client, prompt="", raw=True, timeout=0):
|
||||
channel = client["channel"]
|
||||
|
||||
if prompt != "":
|
||||
channel.send(replace_enter_with_crlf(prompt))
|
||||
|
||||
if timeout != 0:
|
||||
channel.settimeout(timeout)
|
||||
|
||||
try:
|
||||
byte = channel.recv(10)
|
||||
|
||||
if not byte or byte == b'\x04':
|
||||
raise EOFError()
|
||||
|
||||
if not raw:
|
||||
if bool(re.compile(b'\x1b\[[0-9;]*[mGK]').search(byte)):
|
||||
pass
|
||||
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
if prompt != "":
|
||||
channel.send("\r\n")
|
||||
return byte.decode('utf-8') # only regular character
|
||||
|
||||
else:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
if prompt != "":
|
||||
channel.send("\r\n")
|
||||
return byte
|
||||
|
||||
except socket.timeout:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
if prompt != "":
|
||||
channel.send("\r\n")
|
||||
return None
|
||||
except Exception:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
if prompt != "":
|
||||
channel.send("\r\n")
|
||||
raise
|
||||
|
||||
def wait_inputmouse(client, timeout=0):
|
||||
channel = client["channel"]
|
||||
Send(client, "\033[?1000h", ln=False)
|
||||
|
||||
if timeout != 0:
|
||||
channel.settimeout(timeout)
|
||||
|
||||
try:
|
||||
byte = channel.recv(10)
|
||||
|
||||
if not byte or byte == b'\x04':
|
||||
raise EOFError()
|
||||
|
||||
if byte.startswith(b'\x1b[M'):
|
||||
# Parse mouse event
|
||||
if len(byte) < 6 or not byte.startswith(b'\x1b[M'):
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
Send(client, "\033[?1000l", ln=False)
|
||||
return None, None, None
|
||||
|
||||
# Extract button, x, y from the sequence
|
||||
button = byte[3] - 32
|
||||
x = byte[4] - 32
|
||||
y = byte[5] - 32
|
||||
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
Send(client, "\033[?1000l", ln=False)
|
||||
return button, x, y
|
||||
else:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
Send(client, "\033[?1000l", ln=False)
|
||||
return byte, None, None
|
||||
|
||||
except socket.timeout:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
channel.send("\r\n")
|
||||
Send(client, "\033[?1000l", ln=False)
|
||||
return None, None, None
|
||||
except Exception:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
channel.send("\r\n")
|
||||
raise
|
||||
|
||||
def wait_choose(client, choose, prompt="", timeout=0):
|
||||
channel = client["channel"]
|
||||
|
||||
chooseindex = 0
|
||||
chooselen = len(choose) - 1
|
||||
|
||||
if timeout != 0:
|
||||
channel.settimeout(timeout)
|
||||
|
||||
while True:
|
||||
try:
|
||||
tempchooselist = choose.copy()
|
||||
|
||||
tempchooselist[chooseindex] = "[" + tempchooselist[chooseindex] + "]"
|
||||
|
||||
exported = " ".join(tempchooselist)
|
||||
|
||||
if prompt.strip() == "":
|
||||
Send(client, f'\r{exported}', ln=False)
|
||||
else:
|
||||
Send(client, f'\r{prompt}{exported}', ln=False)
|
||||
|
||||
keyinput = wait_inputkey(client, raw=True)
|
||||
|
||||
if keyinput == b'\r': # Enter key
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
Send(client, "\033[K")
|
||||
return chooseindex
|
||||
elif keyinput == b'\x03': # ' ctrl+c' key for cancel
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
Send(client, "\033[K")
|
||||
return 0
|
||||
elif keyinput == b'\x1b[D': # Up arrow key
|
||||
chooseindex -= 1
|
||||
if chooseindex < 0:
|
||||
chooseindex = 0
|
||||
elif keyinput == b'\x1b[C': # Down arrow key
|
||||
chooseindex += 1
|
||||
if chooseindex > chooselen:
|
||||
chooseindex = chooselen
|
||||
except socket.timeout:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
channel.send("\r\n")
|
||||
return chooseindex
|
||||
except Exception:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
channel.send("\r\n")
|
||||
raise
|
358
src/PyserSSH/server.py
Normal file
358
src/PyserSSH/server.py
Normal file
@ -0,0 +1,358 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import time
|
||||
import paramiko
|
||||
import threading
|
||||
from functools import wraps
|
||||
import logging
|
||||
import socket
|
||||
import random
|
||||
import traceback
|
||||
|
||||
from .system.SFTP import SSHSFTPServer
|
||||
from .system.sysfunc import replace_enter_with_crlf
|
||||
from .system.interface import Sinterface
|
||||
from .system.inputsystem import expect
|
||||
from .system.info import version, system_banner
|
||||
from .system.clientype import Client as Clientype
|
||||
from .system.ProWrapper import SSHTransport, TelnetTransport, ITransport
|
||||
|
||||
# paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22)
|
||||
|
||||
sftpclient = ["WinSCP", "Xplore"]
|
||||
|
||||
logger = logging.getLogger("PyserSSH.Server")
|
||||
|
||||
class Server:
|
||||
def __init__(self, accounts, system_message=True, disable_scroll_with_arrow=True, sftp=False, system_commands=True, compression=True, usexternalauth=False, history=True, inputsystem=True, XHandler=None, title=f"PyserSSH v{version}", inspeed=32768, enable_preauth_banner=False, enable_exec_system_command=True, enable_remote_status=False, inputsystem_echo=True):
|
||||
"""
|
||||
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.sysmess = system_message
|
||||
self.accounts = accounts
|
||||
self.disable_scroll_with_arrow = disable_scroll_with_arrow
|
||||
self.sftpena = sftp
|
||||
self.enasyscom = system_commands
|
||||
self.compressena = compression
|
||||
self.usexternalauth = usexternalauth
|
||||
self.history = history
|
||||
self.enainputsystem = inputsystem
|
||||
self.XHandler = XHandler
|
||||
self.title = title
|
||||
self.inspeed = inspeed
|
||||
self.enaloginbanner = enable_preauth_banner
|
||||
self.enasysexec = enable_exec_system_command
|
||||
self.enaremostatus = enable_remote_status
|
||||
self.inputsysecho = inputsystem_echo
|
||||
|
||||
if self.XHandler != None:
|
||||
self.XHandler.serverself = self
|
||||
|
||||
self._event_handlers = {}
|
||||
self.client_handlers = {} # Dictionary to store event handlers for each client
|
||||
self.__processmode = None
|
||||
self.isrunning = False
|
||||
self.__daemon = False
|
||||
self.private_key = ""
|
||||
self.__custom_server_args = ()
|
||||
self.__custom_server = None
|
||||
self.__protocol = "ssh"
|
||||
|
||||
if self.enasyscom:
|
||||
print("\033[33m!!Warning!! System commands is enable! \033[0m")
|
||||
|
||||
def on_user(self, event_name):
|
||||
"""Handle event"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(client, *args, **kwargs):
|
||||
# Ignore the third argument
|
||||
filtered_args = args[:2] + args[3:]
|
||||
return func(client, *filtered_args, **kwargs)
|
||||
self._event_handlers[event_name] = wrapper
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def handle_client_disconnection(self, handler, chandlers):
|
||||
if not chandlers["transport"].is_active():
|
||||
if handler:
|
||||
handler(chandlers)
|
||||
del self.client_handlers[chandlers["peername"]]
|
||||
|
||||
def _handle_event(self, event_name, *args, **kwargs):
|
||||
handler = self._event_handlers.get(event_name)
|
||||
if event_name == "error" and isinstance(args[0], Clientype):
|
||||
args[0].last_error = traceback.format_exc()
|
||||
|
||||
if event_name == "disconnected":
|
||||
self.handle_client_disconnection(handler, *args, **kwargs)
|
||||
elif handler:
|
||||
return handler(*args, **kwargs)
|
||||
|
||||
def _handle_client(self, socketchannel, addr):
|
||||
self._handle_event("preserver", socketchannel)
|
||||
|
||||
logger.info("Starting session...")
|
||||
server = Sinterface(self)
|
||||
|
||||
if not self.__custom_server:
|
||||
if self.__protocol.lower() == "telnet":
|
||||
bh_session = TelnetTransport(socketchannel, server) # Telnet server
|
||||
else:
|
||||
bh_session = SSHTransport(socketchannel, server, self.private_key) # SSH server
|
||||
else:
|
||||
bh_session = self.__custom_server(socketchannel, server, *self.__custom_server_args) # custom server
|
||||
|
||||
bh_session.enable_compression(self.compressena)
|
||||
|
||||
bh_session.max_packet_size(self.inspeed)
|
||||
|
||||
try:
|
||||
bh_session.start_server()
|
||||
except:
|
||||
return
|
||||
|
||||
channel = bh_session.accept()
|
||||
|
||||
if self.sftpena:
|
||||
bh_session.set_subsystem_handler('sftp', paramiko.SFTPServer, SSHSFTPServer, channel, self.accounts, self.client_handlers)
|
||||
|
||||
if not bh_session.is_authenticated():
|
||||
logger.warning("user not authenticated")
|
||||
bh_session.close()
|
||||
return
|
||||
|
||||
if channel is None:
|
||||
logger.warning("no channel")
|
||||
bh_session.close()
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info("user authenticated")
|
||||
peername = bh_session.getpeername()
|
||||
if peername not in self.client_handlers:
|
||||
# Create a new event handler for this client if it doesn't exist
|
||||
self.client_handlers[peername] = Clientype(channel, bh_session, peername)
|
||||
|
||||
client_handler = self.client_handlers[peername]
|
||||
client_handler["current_user"] = bh_session.get_username()
|
||||
client_handler["channel"] = channel # Update the channel attribute for the client handler
|
||||
client_handler["transport"] = bh_session # Update the channel attribute for the client handler
|
||||
client_handler["last_activity_time"] = time.time()
|
||||
client_handler["last_login_time"] = time.time()
|
||||
client_handler["prompt"] = self.accounts.get_prompt(bh_session.get_username())
|
||||
client_handler["session_id"] = random.randint(10000, 99999) + int(time.time() * 1000)
|
||||
|
||||
self.accounts.set_user_last_login(self.client_handlers[channel.getpeername()]["current_user"], peername[0])
|
||||
|
||||
logger.info("saved user data to client handlers")
|
||||
|
||||
#if not any(bh_session.remote_version.split("-")[2].startswith(prefix) for prefix in sftpclient):
|
||||
if not (int(channel.get_out_window_size()) == int(bh_session.get_default_window_size()) and bh_session.get_connection_type() == "SSH"):
|
||||
#timeout for waiting 10 sec
|
||||
for i in range(100):
|
||||
if self.client_handlers[channel.getpeername()]["windowsize"]:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
if self.client_handlers[channel.getpeername()]["windowsize"] == {}:
|
||||
logger.info("timeout for waiting window size in 10 sec")
|
||||
self.client_handlers[channel.getpeername()]["windowsize"] = {
|
||||
"width": 80,
|
||||
"height": 24,
|
||||
"pixelwidth": 0,
|
||||
"pixelheight": 0
|
||||
}
|
||||
|
||||
try:
|
||||
self._handle_event("pre-shell", self.client_handlers[channel.getpeername()])
|
||||
except Exception as e:
|
||||
self._handle_event("error", self.client_handlers[channel.getpeername()], e)
|
||||
|
||||
while self.client_handlers[channel.getpeername()]["isexeccommandrunning"]:
|
||||
time.sleep(0.1)
|
||||
|
||||
userbanner = self.accounts.get_banner(self.client_handlers[channel.getpeername()]["current_user"])
|
||||
|
||||
if self.accounts.get_user_enable_inputsystem_echo(self.client_handlers[channel.getpeername()]["current_user"]) and self.inputsysecho:
|
||||
echo = True
|
||||
else:
|
||||
echo = False
|
||||
|
||||
if echo:
|
||||
if self.title.strip() != "":
|
||||
channel.send(f"\033]0;{self.title}\007".encode())
|
||||
|
||||
if self.sysmess or userbanner != None:
|
||||
if userbanner is None and self.sysmess:
|
||||
channel.sendall(replace_enter_with_crlf(system_banner))
|
||||
elif userbanner != None and self.sysmess:
|
||||
channel.sendall(replace_enter_with_crlf(system_banner))
|
||||
channel.sendall(replace_enter_with_crlf(userbanner))
|
||||
elif userbanner != None and not self.sysmess:
|
||||
channel.sendall(replace_enter_with_crlf(userbanner))
|
||||
|
||||
channel.sendall(replace_enter_with_crlf("\n"))
|
||||
|
||||
client_handler["connecttype"] = "ssh"
|
||||
|
||||
try:
|
||||
self._handle_event("connect", self.client_handlers[channel.getpeername()])
|
||||
except Exception as e:
|
||||
self._handle_event("error", self.client_handlers[channel.getpeername()], e)
|
||||
|
||||
if self.enainputsystem and self.accounts.get_user_enable_inputsystem(self.client_handlers[channel.getpeername()]["current_user"]):
|
||||
try:
|
||||
if self.accounts.get_user_timeout(self.client_handlers[channel.getpeername()]["current_user"]) != None:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(self.accounts.get_user_timeout(self.client_handlers[channel.getpeername()]["current_user"]))
|
||||
|
||||
if echo:
|
||||
channel.send(replace_enter_with_crlf(self.client_handlers[channel.getpeername()]["prompt"] + " "))
|
||||
|
||||
isConnect = True
|
||||
|
||||
while isConnect:
|
||||
isConnect = expect(self, self.client_handlers[channel.getpeername()], echo)
|
||||
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
channel.close()
|
||||
bh_session.close()
|
||||
except KeyboardInterrupt:
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
channel.close()
|
||||
bh_session.close()
|
||||
except Exception as e:
|
||||
self._handle_event("error", client_handler, e)
|
||||
logger.error(e)
|
||||
finally:
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
channel.close()
|
||||
bh_session.close()
|
||||
else:
|
||||
if self.sftpena:
|
||||
logger.info("user is sftp")
|
||||
if self.accounts.get_user_sftp_allow(self.client_handlers[channel.getpeername()]["current_user"]):
|
||||
client_handler["connecttype"] = "sftp"
|
||||
self._handle_event("connectsftp", self.client_handlers[channel.getpeername()])
|
||||
while bh_session.is_active():
|
||||
time.sleep(0.1)
|
||||
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
else:
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
channel.close()
|
||||
else:
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
channel.close()
|
||||
except:
|
||||
bh_session.close()
|
||||
|
||||
def stop_server(self):
|
||||
"""Stop server"""
|
||||
logger.info("Stopping the server...")
|
||||
try:
|
||||
for client_handler in self.client_handlers.values():
|
||||
channel = client_handler.channel
|
||||
if channel:
|
||||
channel.close()
|
||||
self.isrunning = False
|
||||
self.server.close()
|
||||
|
||||
logger.info("Server stopped.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error occurred while stopping the server: {e}")
|
||||
|
||||
def _start_listening_thread(self):
|
||||
try:
|
||||
self.isrunning = True
|
||||
try:
|
||||
logger.info("Listening for connections...")
|
||||
while self.isrunning:
|
||||
client, addr = self.server.accept()
|
||||
if self.__processmode == "thread":
|
||||
logger.info(f"Starting client thread for connection {addr}")
|
||||
client_thread = threading.Thread(target=self._handle_client, args=(client, addr), daemon=True)
|
||||
client_thread.start()
|
||||
else:
|
||||
logger.info(f"Starting client for connection {addr}")
|
||||
self._handle_client(client, addr)
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
self.stop_server()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def run(self, private_key_path=None, host="0.0.0.0", port=2222, waiting_mode="thread", maxuser=0, daemon=False, listen_thread=True, protocol="ssh", custom_server: ITransport = None, custom_server_args: tuple = None, custom_server_require_socket=True):
|
||||
"""mode: single, thread,
|
||||
protocol: ssh, telnet (beta), serial, custom
|
||||
For serial need to set serial port at host (ex. host="com3") and set baudrate at port (ex. port=9600) and change listen_mode to "single".
|
||||
"""
|
||||
if protocol.lower() == "ssh":
|
||||
if private_key_path != None:
|
||||
logger.info("Loading private key")
|
||||
self.private_key = paramiko.RSAKey(filename=private_key_path)
|
||||
else:
|
||||
raise ValueError("No private key")
|
||||
|
||||
self.__processmode = waiting_mode.lower()
|
||||
self.__daemon = daemon
|
||||
|
||||
if custom_server:
|
||||
self.__custom_server = custom_server
|
||||
self.__custom_server_args = custom_server_args
|
||||
|
||||
if ((custom_server and protocol.lower() == "custom") and custom_server_require_socket) or protocol.lower() in ["ssh", "telnet"]:
|
||||
logger.info("Creating server...")
|
||||
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
|
||||
self.server.bind((host, port))
|
||||
|
||||
logger.info("Set listen limit")
|
||||
if maxuser == 0:
|
||||
self.server.listen()
|
||||
else:
|
||||
self.server.listen(maxuser)
|
||||
|
||||
if listen_thread:
|
||||
logger.info("Starting listening in threading")
|
||||
client_thread = threading.Thread(target=self._start_listening_thread, daemon=self.__daemon)
|
||||
client_thread.start()
|
||||
else:
|
||||
print(f"\033[32mServer is running on {host}:{port}\033[0m")
|
||||
self._start_listening_thread()
|
||||
else:
|
||||
client_thread = threading.Thread(target=self._handle_client, args=(None, None), daemon=True)
|
||||
client_thread.start()
|
||||
|
||||
print(f"\033[32mServer is running on {host}:{port}\033[0m")
|
495
src/PyserSSH/system/ProWrapper.py
Normal file
495
src/PyserSSH/system/ProWrapper.py
Normal file
@ -0,0 +1,495 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
#import serial
|
||||
import socket
|
||||
import paramiko
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Union
|
||||
|
||||
from .interface import Sinterface
|
||||
from ..interactive import Send, wait_input
|
||||
|
||||
class ITransport(ABC):
|
||||
@abstractmethod
|
||||
def enable_compression(self, enable: bool) -> None:
|
||||
"""
|
||||
Enables or disables data compression for the transport.
|
||||
|
||||
Args:
|
||||
enable (bool): If True, enable compression. If False, disable it.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def max_packet_size(self, size: int) -> None:
|
||||
"""
|
||||
Sets the maximum packet size for the transport.
|
||||
|
||||
Args:
|
||||
size (int): The maximum packet size in bytes.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def start_server(self) -> None:
|
||||
"""
|
||||
Starts the server for the transport, allowing it to accept incoming connections.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def accept(self, timeout: Union[int, None] = None) -> "IChannel":
|
||||
"""
|
||||
Accepts an incoming connection and returns an IChannel instance for communication.
|
||||
|
||||
Args:
|
||||
timeout (Union[int, None]): The time in seconds to wait for a connection.
|
||||
If None, waits indefinitely.
|
||||
|
||||
Returns:
|
||||
IChannel: An instance of IChannel representing the connection.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_subsystem_handler(self, name: str, handler: callable, *args: any, **kwargs: any) -> None:
|
||||
"""
|
||||
Sets a handler for a specific subsystem in the transport.
|
||||
|
||||
Args:
|
||||
name (str): The name of the subsystem.
|
||||
handler (callable): The handler function to be called for the subsystem.
|
||||
*args: Arguments to pass to the handler.
|
||||
**kwargs: Keyword arguments to pass to the handler.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Closes the transport connection, releasing any resources used.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_authenticated(self) -> bool:
|
||||
"""
|
||||
Checks if the transport is authenticated.
|
||||
|
||||
Returns:
|
||||
bool: True if the transport is authenticated, otherwise False.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def getpeername(self) -> tuple[str, int]: # (host, port)
|
||||
"""
|
||||
Retrieves the peer's address and port.
|
||||
|
||||
Returns:
|
||||
tuple[str, int]: The host and port of the peer.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_username(self) -> str:
|
||||
"""
|
||||
Retrieves the username associated with the transport.
|
||||
|
||||
Returns:
|
||||
str: The username.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_active(self) -> bool:
|
||||
"""
|
||||
Checks if the transport is active.
|
||||
|
||||
Returns:
|
||||
bool: True if the transport is active, otherwise False.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_auth_method(self) -> str:
|
||||
"""
|
||||
Retrieves the authentication method used for the transport.
|
||||
|
||||
Returns:
|
||||
str: The authentication method (e.g., password, public key).
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_username(self, username: str) -> None:
|
||||
"""
|
||||
Sets the username for the transport.
|
||||
|
||||
Args:
|
||||
username (str): The username to be set.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_default_window_size(self) -> int:
|
||||
"""
|
||||
Retrieves the default window size for the transport.
|
||||
|
||||
Returns:
|
||||
int: The default window size.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_connection_type(self) -> str:
|
||||
"""
|
||||
Retrieves the type of connection for the transport.
|
||||
|
||||
Returns:
|
||||
str: The connection type (e.g., TCP, UDP).
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IChannel(ABC):
|
||||
@abstractmethod
|
||||
def send(self, s: Union[bytes, bytearray]) -> None:
|
||||
"""
|
||||
Sends data over the channel.
|
||||
|
||||
Args:
|
||||
s (Union[bytes, bytearray]): The data to send.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def sendall(self, s: Union[bytes, bytearray]) -> None:
|
||||
"""
|
||||
Sends all data over the channel, blocking until all data is sent.
|
||||
|
||||
Args:
|
||||
s (Union[bytes, bytearray]): The data to send.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def getpeername(self) -> tuple[str, int]:
|
||||
"""
|
||||
Retrieves the peer's address and port.
|
||||
|
||||
Returns:
|
||||
tuple[str, int]: The host and port of the peer.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def settimeout(self, timeout: Union[float, None]) -> None:
|
||||
"""
|
||||
Sets the timeout for blocking operations on the channel.
|
||||
|
||||
Args:
|
||||
timeout (Union[float, None]): The timeout in seconds. If None, the operation will block indefinitely.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def setblocking(self, blocking: bool) -> None:
|
||||
"""
|
||||
Sets whether the channel operates in blocking mode or non-blocking mode.
|
||||
|
||||
Args:
|
||||
blocking (bool): If True, the channel operates in blocking mode. If False, non-blocking mode.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def recv(self, nbytes: int) -> bytes:
|
||||
"""
|
||||
Receives data from the channel.
|
||||
|
||||
Args:
|
||||
nbytes (int): The number of bytes to receive.
|
||||
|
||||
Returns:
|
||||
bytes: The received data.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_id(self) -> int:
|
||||
"""
|
||||
Retrieves the unique identifier for the channel.
|
||||
|
||||
Returns:
|
||||
int: The channel's unique identifier.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Closes the channel and releases any resources used.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_out_window_size(self) -> int:
|
||||
"""
|
||||
Retrieves the output window size for the channel.
|
||||
|
||||
Returns:
|
||||
int: The output window size.
|
||||
"""
|
||||
pass
|
||||
|
||||
#--------------------------------------------------------------------------------------------
|
||||
|
||||
class SSHTransport(ITransport):
|
||||
def __init__(self, socketchannel: socket.socket, interface: Sinterface, key):
|
||||
self.socket: socket.socket = socketchannel
|
||||
self.interface: Sinterface = interface
|
||||
self.key = key
|
||||
|
||||
self.bh_session = paramiko.Transport(self.socket)
|
||||
self.bh_session.add_server_key(self.key)
|
||||
self.bh_session.default_window_size = 2147483647
|
||||
|
||||
def enable_compression(self, enable):
|
||||
self.bh_session.use_compression(enable)
|
||||
|
||||
def max_packet_size(self, size):
|
||||
self.bh_session.default_max_packet_size = size
|
||||
|
||||
def start_server(self):
|
||||
self.bh_session.start_server(server=self.interface)
|
||||
|
||||
def accept(self, timeout=None):
|
||||
return SSHChannel(self.bh_session.accept(timeout))
|
||||
|
||||
def set_subsystem_handler(self, name, handler, *args, **kwargs):
|
||||
self.bh_session.set_subsystem_handler(name, handler, *args, **kwargs)
|
||||
|
||||
def close(self):
|
||||
self.bh_session.close()
|
||||
|
||||
def is_authenticated(self):
|
||||
return self.bh_session.is_authenticated()
|
||||
|
||||
def getpeername(self):
|
||||
return self.bh_session.getpeername()
|
||||
|
||||
def get_username(self):
|
||||
return self.bh_session.get_username()
|
||||
|
||||
def is_active(self):
|
||||
return self.bh_session.is_active()
|
||||
|
||||
def get_auth_method(self):
|
||||
return self.bh_session.auth_handler.auth_method
|
||||
|
||||
def set_username(self, username):
|
||||
self.bh_session.auth_handler.username = username
|
||||
|
||||
def get_default_window_size(self):
|
||||
return self.bh_session.default_window_size
|
||||
|
||||
def get_connection_type(self):
|
||||
return "SSH"
|
||||
|
||||
class SSHChannel(IChannel):
|
||||
def __init__(self, channel: paramiko.Channel):
|
||||
self.channel: paramiko.Channel = channel
|
||||
|
||||
def send(self, s):
|
||||
self.channel.send(s)
|
||||
|
||||
def sendall(self, s):
|
||||
self.channel.sendall(s)
|
||||
|
||||
def getpeername(self):
|
||||
return self.channel.getpeername()
|
||||
|
||||
def settimeout(self, timeout):
|
||||
self.channel.settimeout(timeout)
|
||||
|
||||
def setblocking(self, blocking):
|
||||
self.channel.setblocking(blocking)
|
||||
|
||||
def recv(self, nbytes):
|
||||
return self.channel.recv(nbytes)
|
||||
|
||||
def get_id(self):
|
||||
return self.channel.get_id()
|
||||
|
||||
def close(self):
|
||||
self.channel.close()
|
||||
|
||||
def get_out_window_size(self):
|
||||
return self.channel.out_window_size
|
||||
|
||||
#--------------------------------------------------------------------------------------------
|
||||
|
||||
# Telnet command and option codes
|
||||
IAC = 255
|
||||
DO = 253
|
||||
WILL = 251
|
||||
TTYPE = 24
|
||||
ECHO = 1
|
||||
SGA = 3 # Suppress Go Ahead
|
||||
|
||||
def send_telnet_command(sock, command, option):
|
||||
sock.send(bytes([IAC, command, option]))
|
||||
|
||||
class TelnetTransport(ITransport):
|
||||
def __init__(self, socketchannel: socket.socket, interface: Sinterface):
|
||||
self.socket: socket.socket = socketchannel
|
||||
self.interface: Sinterface = interface
|
||||
self.username = None
|
||||
self.isactive = True
|
||||
self.isauth = False
|
||||
self.auth_method = None
|
||||
|
||||
def enable_compression(self, enable):
|
||||
pass
|
||||
|
||||
def max_packet_size(self, size):
|
||||
pass
|
||||
|
||||
def start_server(self):
|
||||
pass
|
||||
|
||||
def set_subsystem_handler(self, name: str, handler: callable, *args: any, **kwargs: any) -> None:
|
||||
pass
|
||||
|
||||
def negotiate_options(self):
|
||||
# Negotiating TTYPE (Terminal Type), ECHO, and SGA (Suppress Go Ahead)
|
||||
send_telnet_command(self.socket, DO, TTYPE)
|
||||
send_telnet_command(self.socket, WILL, ECHO)
|
||||
send_telnet_command(self.socket, WILL, SGA)
|
||||
|
||||
def accept(self, timeout=None):
|
||||
# Perform Telnet negotiation
|
||||
self.negotiate_options()
|
||||
|
||||
# Simple authentication prompt
|
||||
username = wait_input(self.socket, "Login as: ", directchannel=True)
|
||||
|
||||
try:
|
||||
allowauth = self.interface.get_allowed_auths(username).split(',')
|
||||
except:
|
||||
allowauth = self.interface.get_allowed_auths(username)
|
||||
|
||||
if allowauth[0] == "password":
|
||||
password = wait_input(self.socket, "Password", password=True, directchannel=True)
|
||||
result = self.interface.check_auth_password(username, password)
|
||||
|
||||
if result == 0:
|
||||
self.isauth = True
|
||||
self.username = username
|
||||
self.auth_method = "password"
|
||||
return TelnetChannel(self.socket)
|
||||
else:
|
||||
Send(self.socket, "Access denied", directchannel=True)
|
||||
self.close()
|
||||
elif allowauth[0] == "public_key":
|
||||
Send(self.socket, "Public key isn't supported for telnet", directchannel=True)
|
||||
self.close()
|
||||
elif allowauth[0] == "none":
|
||||
result = self.interface.check_auth_none(username)
|
||||
|
||||
if result == 0:
|
||||
self.username = username
|
||||
self.isauth = True
|
||||
self.auth_method = "none"
|
||||
return TelnetChannel(self.socket)
|
||||
else:
|
||||
Send(self.socket, "Access denied", directchannel=True)
|
||||
self.close()
|
||||
else:
|
||||
Send(self.socket, "Access denied", directchannel=True)
|
||||
|
||||
def close(self):
|
||||
self.isactive = False
|
||||
self.socket.close()
|
||||
|
||||
def is_authenticated(self):
|
||||
return self.isauth
|
||||
|
||||
def getpeername(self):
|
||||
return self.socket.getpeername()
|
||||
|
||||
def get_username(self):
|
||||
return self.username
|
||||
|
||||
def is_active(self):
|
||||
return self.isactive
|
||||
|
||||
def get_auth_method(self):
|
||||
return self.auth_method
|
||||
|
||||
def set_username(self, username):
|
||||
self.username = username
|
||||
|
||||
def get_default_window_size(self):
|
||||
return 0
|
||||
|
||||
def get_connection_type(self):
|
||||
return "Telnet"
|
||||
|
||||
|
||||
class TelnetChannel(IChannel):
|
||||
def __init__(self, channel: socket.socket):
|
||||
self.channel: socket.socket = channel
|
||||
|
||||
def send(self, s):
|
||||
self.channel.send(s)
|
||||
|
||||
def sendall(self, s):
|
||||
self.channel.sendall(s)
|
||||
|
||||
def getpeername(self):
|
||||
return self.channel.getpeername()
|
||||
|
||||
def settimeout(self, timeout):
|
||||
self.channel.settimeout(timeout)
|
||||
|
||||
def setblocking(self, blocking):
|
||||
self.channel.setblocking(blocking)
|
||||
|
||||
def recv(self, nbytes):
|
||||
return self.channel.recv(nbytes)
|
||||
|
||||
def get_id(self):
|
||||
return 0
|
||||
|
||||
def close(self) -> None:
|
||||
return self.channel.close()
|
||||
|
||||
def get_out_window_size(self) -> int:
|
||||
return 0
|
230
src/PyserSSH/system/SFTP.py
Normal file
230
src/PyserSSH/system/SFTP.py
Normal file
@ -0,0 +1,230 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import os
|
||||
import paramiko
|
||||
|
||||
class SSHSFTPHandle(paramiko.SFTPHandle):
|
||||
def stat(self):
|
||||
try:
|
||||
return paramiko.SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
|
||||
def chattr(self, attr):
|
||||
# python doesn't have equivalents to fchown or fchmod, so we have to
|
||||
# use the stored filename
|
||||
try:
|
||||
paramiko.SFTPServer.set_file_attr(self.filename, attr)
|
||||
return paramiko.sftp.SFTP_OK
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
|
||||
class SSHSFTPServer(paramiko.SFTPServerInterface):
|
||||
def __init__(self, server: paramiko.ServerInterface, *args, **kwargs):
|
||||
super().__init__(server)
|
||||
self.channel = args[0]
|
||||
self.account = args[1]
|
||||
self.clientH = args[2]
|
||||
|
||||
def _realpath(self, path):
|
||||
root = self.account.get_user_sftp_root_path(self.clientH[self.channel.getpeername()]["current_user"])
|
||||
return root + self.canonicalize(path)
|
||||
|
||||
def list_folder(self, path):
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
out = []
|
||||
flist = os.listdir(path)
|
||||
for fname in flist:
|
||||
attr = paramiko.SFTPAttributes.from_stat(os.stat(os.path.join(path, fname)))
|
||||
attr.filename = fname
|
||||
out.append(attr)
|
||||
return out
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
|
||||
def stat(self, path):
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
return paramiko.SFTPAttributes.from_stat(os.stat(path))
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
|
||||
def lstat(self, path):
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
return paramiko.SFTPAttributes.from_stat(os.lstat(path))
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
|
||||
def open(self, path, flags, attr):
|
||||
# check if write request
|
||||
is_write = (flags & os.O_WRONLY or flags & os.O_RDWR) + (flags & os.O_CREAT) != 0
|
||||
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]) and is_write:
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
binary_flag = getattr(os, 'O_BINARY', 0)
|
||||
flags |= binary_flag
|
||||
mode = getattr(attr, 'st_mode', None)
|
||||
if mode is not None:
|
||||
fd = os.open(path, flags, mode)
|
||||
else:
|
||||
# os.open() defaults to 0777 which is
|
||||
# an odd default mode for files
|
||||
fd = os.open(path, flags, 0o666)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
if (flags & os.O_CREAT) and (attr is not None):
|
||||
attr._flags &= ~attr.FLAG_PERMISSIONS
|
||||
paramiko.SFTPServer.set_file_attr(path, attr)
|
||||
if flags & os.O_WRONLY:
|
||||
if flags & os.O_APPEND:
|
||||
fstr = 'ab'
|
||||
else:
|
||||
fstr = 'wb'
|
||||
elif flags & os.O_RDWR:
|
||||
if flags & os.O_APPEND:
|
||||
fstr = 'a+b'
|
||||
else:
|
||||
fstr = 'r+b'
|
||||
else:
|
||||
# O_RDONLY (== 0)
|
||||
fstr = 'rb'
|
||||
try:
|
||||
f = os.fdopen(fd, fstr)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
fobj = SSHSFTPHandle(flags)
|
||||
fobj.filename = path
|
||||
fobj.readfile = f
|
||||
fobj.writefile = f
|
||||
return fobj
|
||||
|
||||
def remove(self, path):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def rename(self, oldpath, newpath):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
oldpath = self._realpath(oldpath)
|
||||
newpath = self._realpath(newpath)
|
||||
try:
|
||||
os.rename(oldpath, newpath)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def mkdir(self, path, attr):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
os.mkdir(path)
|
||||
if attr is not None:
|
||||
paramiko.SFTPServer.set_file_attr(path, attr)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def rmdir(self, path):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
os.rmdir(path)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def chattr(self, path, attr):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
paramiko.SFTPServer.set_file_attr(path, attr)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def symlink(self, target_path, path):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
root = self.account.get_user_sftp_root_path(self.clientH[self.channel.getpeername()]["current_user"])
|
||||
|
||||
path = self._realpath(path)
|
||||
if (len(target_path) > 0) and (target_path[0] == '/'):
|
||||
# absolute symlink
|
||||
target_path = os.path.join(root, target_path[1:])
|
||||
if target_path[:2] == '//':
|
||||
# bug in os.path.join
|
||||
target_path = target_path[1:]
|
||||
else:
|
||||
# compute relative to path
|
||||
abspath = os.path.join(os.path.dirname(path), target_path)
|
||||
if abspath[:len(root)] != root:
|
||||
# this symlink isn't going to work anyway -- just break it immediately
|
||||
target_path = '<error>'
|
||||
try:
|
||||
os.symlink(target_path, path)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def readlink(self, path):
|
||||
root = self.account.get_user_sftp_root_path(self.clientH[self.channel.getpeername()]["current_user"])
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
symlink = os.readlink(path)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
|
||||
if os.path.isabs(symlink):
|
||||
if symlink[:len(root)] == root:
|
||||
symlink = symlink[len(root):]
|
||||
if (len(symlink) == 0) or (symlink[0] != '/'):
|
||||
symlink = '/' + symlink
|
||||
else:
|
||||
symlink = '<error>'
|
||||
return symlink
|
38
src/PyserSSH/system/__init__.py
Normal file
38
src/PyserSSH/system/__init__.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
"""
|
||||
note
|
||||
|
||||
ansi cursor arrow
|
||||
up - \x1b[A
|
||||
down - \x1b[B
|
||||
left - \x1b[D
|
||||
right - \x1b[C
|
||||
|
||||
https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
"""
|
293
src/PyserSSH/system/clientype.py
Normal file
293
src/PyserSSH/system/clientype.py
Normal file
@ -0,0 +1,293 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
import time
|
||||
|
||||
from ..interactive import Send
|
||||
from .ProWrapper import IChannel, ITransport
|
||||
|
||||
class Client:
|
||||
def __init__(self, channel, transport, peername):
|
||||
"""
|
||||
Initializes a new client instance.
|
||||
|
||||
Args:
|
||||
channel (IChannel): The communication channel for the client.
|
||||
transport (ITransport): The transport layer for the client.
|
||||
peername (tuple): The peer's address and port (host, port).
|
||||
"""
|
||||
self.current_user = None
|
||||
self.transport: ITransport = transport
|
||||
self.channel: IChannel = channel
|
||||
self.subchannel = {}
|
||||
self.connecttype = None
|
||||
self.last_activity_time = None
|
||||
self.last_login_time = None
|
||||
self.windowsize = {}
|
||||
self.x11 = {}
|
||||
self.prompt = None
|
||||
self.inputbuffer = None
|
||||
self.peername = peername
|
||||
self.auth_method = self.transport.get_auth_method()
|
||||
self.session_id = None
|
||||
self.terminal_type = None
|
||||
self.env_variables = {}
|
||||
self.last_error = None
|
||||
self.last_command = None
|
||||
self.isexeccommandrunning = False
|
||||
|
||||
def get_id(self):
|
||||
"""
|
||||
Retrieves the client's session ID.
|
||||
|
||||
Returns:
|
||||
str or None: The session ID of the client.
|
||||
"""
|
||||
return self.session_id
|
||||
|
||||
def get_name(self):
|
||||
"""
|
||||
Retrieves the current username of the client.
|
||||
|
||||
Returns:
|
||||
str: The current username of the client.
|
||||
"""
|
||||
return self.current_user
|
||||
|
||||
def get_peername(self):
|
||||
"""
|
||||
Retrieves the peer's address (host, port) for the client.
|
||||
|
||||
Returns:
|
||||
tuple: The peer's address (host, port).
|
||||
"""
|
||||
return self.peername
|
||||
|
||||
def get_prompt(self):
|
||||
"""
|
||||
Retrieves the prompt string for the client.
|
||||
|
||||
Returns:
|
||||
str: The prompt string for the client.
|
||||
"""
|
||||
return self.prompt
|
||||
|
||||
def get_channel(self):
|
||||
"""
|
||||
Retrieves the communication channel for the client.
|
||||
|
||||
Returns:
|
||||
IChannel: The channel instance for the client.
|
||||
"""
|
||||
return self.channel
|
||||
|
||||
def get_prompt_buffer(self):
|
||||
"""
|
||||
Retrieves the current input buffer for the client as a string.
|
||||
|
||||
Returns:
|
||||
str: The input buffer as a string.
|
||||
"""
|
||||
return str(self.inputbuffer)
|
||||
|
||||
def get_terminal_size(self):
|
||||
"""
|
||||
Retrieves the terminal size (width, height) for the client.
|
||||
|
||||
Returns:
|
||||
tuple[int, int]: The terminal's width and height.
|
||||
"""
|
||||
return self.windowsize["width"], self.windowsize["height"]
|
||||
|
||||
def get_connection_type(self):
|
||||
"""
|
||||
Retrieves the connection type for the client.
|
||||
|
||||
Returns:
|
||||
str: The connection type (e.g., TCP, UDP).
|
||||
"""
|
||||
return self.connecttype
|
||||
|
||||
def get_auth_with(self):
|
||||
"""
|
||||
Retrieves the authentication method used for the client.
|
||||
|
||||
Returns:
|
||||
str: The authentication method (e.g., password, public key).
|
||||
"""
|
||||
return self.auth_method
|
||||
|
||||
def get_session_duration(self):
|
||||
"""
|
||||
Calculates the duration of the current session for the client.
|
||||
|
||||
Returns:
|
||||
float: The duration of the session in seconds.
|
||||
"""
|
||||
return time.time() - self.last_login_time
|
||||
|
||||
def get_environment(self, variable):
|
||||
"""
|
||||
Retrieves the value of an environment variable for the client.
|
||||
|
||||
Args:
|
||||
variable (str): The name of the environment variable.
|
||||
|
||||
Returns:
|
||||
str: The value of the environment variable.
|
||||
"""
|
||||
return self.env_variables.get(variable)
|
||||
|
||||
def get_last_error(self):
|
||||
"""
|
||||
Retrieves the last error message encountered by the client.
|
||||
|
||||
Returns:
|
||||
str: The last error message, or None if no error occurred.
|
||||
"""
|
||||
return self.last_error
|
||||
|
||||
def get_last_command(self):
|
||||
"""
|
||||
Retrieves the last command executed by the client.
|
||||
|
||||
Returns:
|
||||
str: The last command executed.
|
||||
"""
|
||||
return self.last_command
|
||||
|
||||
def set_name(self, name):
|
||||
"""
|
||||
Sets the current username for the client.
|
||||
|
||||
Args:
|
||||
name (str): The username to set for the client.
|
||||
"""
|
||||
self.current_user = name
|
||||
|
||||
def set_prompt(self, prompt):
|
||||
"""
|
||||
Sets the prompt string for the client.
|
||||
|
||||
Args:
|
||||
prompt (str): The prompt string to set for the client.
|
||||
"""
|
||||
self.prompt = prompt
|
||||
|
||||
def set_environment(self, variable, value):
|
||||
"""
|
||||
Sets the value of an environment variable for the client.
|
||||
|
||||
Args:
|
||||
variable (str): The name of the environment variable.
|
||||
value (str): The value to set for the environment variable.
|
||||
"""
|
||||
self.env_variables[variable] = value
|
||||
|
||||
def open_new_subchannel(self, timeout=None):
|
||||
"""
|
||||
Opens a new subchannel for communication with the client.
|
||||
|
||||
Args:
|
||||
timeout (Union[int, None]): The timeout duration in seconds.
|
||||
If None, the operation waits indefinitely.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the subchannel ID and the new subchannel
|
||||
(IChannel). If an error occurs, returns (None, None).
|
||||
"""
|
||||
try:
|
||||
channel = self.transport.accept(timeout)
|
||||
id = channel.get_id()
|
||||
except:
|
||||
return None, None
|
||||
|
||||
self.subchannel[id] = channel
|
||||
return id, channel
|
||||
|
||||
def get_subchannel(self, id):
|
||||
"""
|
||||
Retrieves a subchannel by its ID.
|
||||
|
||||
Args:
|
||||
id (int): The ID of the subchannel to retrieve.
|
||||
|
||||
Returns:
|
||||
IChannel: The subchannel instance.
|
||||
"""
|
||||
return self.subchannel.get(id)
|
||||
|
||||
def switch_user(self, user):
|
||||
"""
|
||||
Switches the current user for the client.
|
||||
|
||||
Args:
|
||||
user (str): The new username to switch to.
|
||||
"""
|
||||
self.current_user = user
|
||||
self.transport.set_username(user)
|
||||
|
||||
def close_subchannel(self, id):
|
||||
"""
|
||||
Closes a specific subchannel by its ID.
|
||||
|
||||
Args:
|
||||
id (int): The ID of the subchannel to close.
|
||||
"""
|
||||
self.subchannel[id].close()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the main communication channel for the client.
|
||||
"""
|
||||
self.channel.close()
|
||||
|
||||
def send(self, data):
|
||||
"""
|
||||
Sends data over the main communication channel.
|
||||
|
||||
Args:
|
||||
data (str): The data to send.
|
||||
"""
|
||||
Send(self.channel, data, directchannel=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"client id: {self.session_id}"
|
||||
|
||||
def __repr__(self):
|
||||
attrs = vars(self) # or self.__dict__
|
||||
|
||||
non_none_attrs = {key: value for key, value in attrs.items() if value is not None}
|
||||
|
||||
attrs_repr = ', '.join(f"{key}={value!r}" for key, value in non_none_attrs.items())
|
||||
return f"Client({attrs_repr})"
|
||||
|
||||
# for backward compatibility only
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self, key, value)
|
56
src/PyserSSH/system/info.py
Normal file
56
src/PyserSSH/system/info.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
version = "5.1.4"
|
||||
|
||||
system_banner = (
|
||||
f"\033[36mPyserSSH V{version} \033[0m"
|
||||
#"\033[33m!!Warning!! This is Testing Version of PyserSSH \033[0m\n"
|
||||
#"\033[35mUse Putty and WinSCP (SFTP) for best experience \033[0m"
|
||||
)
|
||||
|
||||
def Flag_TH(returnlist=False):
|
||||
Flags = [
|
||||
"\n",
|
||||
f"\033[31m ======= == == ====== ======= ====== ====== ====== == == \033[0m\n",
|
||||
f"\033[37m == == == == == === == == == == == == \033[0m\n",
|
||||
f"\033[34m ======= ==== ======= ======= ====== ======= ======= ======== \033[0m\n",
|
||||
f"\033[34m ===== == ===== ==== === == ===== ===== ======== \033[0m\n",
|
||||
f"\033[37m == == === === == == === === == == \033[0m\n",
|
||||
f"\033[31m == == ====== ======= == == ====== ====== == == \033[0m\n",
|
||||
" Made by \033[33mD\033[38;2;255;126;1mP\033[38;2;43;205;150mSoftware\033[0m \033[38;2;204;208;43mFoundation\033[0m from Thailand\n",
|
||||
"\n"
|
||||
]
|
||||
|
||||
if returnlist:
|
||||
return Flags
|
||||
else:
|
||||
exporttext = ""
|
||||
|
||||
for line in Flags:
|
||||
exporttext += line
|
||||
return exporttext
|
218
src/PyserSSH/system/inputsystem.py
Normal file
218
src/PyserSSH/system/inputsystem.py
Normal file
@ -0,0 +1,218 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
import socket
|
||||
import time
|
||||
import logging
|
||||
|
||||
from .sysfunc import replace_enter_with_crlf
|
||||
from .syscom import systemcommand
|
||||
|
||||
logger = logging.getLogger("PyserSSH.InputSystem")
|
||||
|
||||
def expect(self, client, echo=True):
|
||||
buffer = bytearray()
|
||||
cursor_position = 0
|
||||
outindexall = 0
|
||||
history_index_position = 0 # Initialize history index position outside the loop
|
||||
chan = client["channel"]
|
||||
peername = client["peername"]
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
byte = chan.recv(1)
|
||||
except socket.timeout:
|
||||
chan.setblocking(False)
|
||||
chan.settimeout(None)
|
||||
chan.close()
|
||||
raise EOFError()
|
||||
|
||||
self._handle_event("rawtype", self.client_handlers[chan.getpeername()], byte)
|
||||
|
||||
self.client_handlers[chan.getpeername()]["last_activity_time"] = time.time()
|
||||
|
||||
if not byte or byte == b'\x04':
|
||||
raise EOFError()
|
||||
elif byte == b'\x03':
|
||||
pass
|
||||
elif byte == b'\t':
|
||||
pass
|
||||
elif byte == b'\x7f' or byte == b'\x08':
|
||||
if cursor_position > 0:
|
||||
buffer = buffer[:cursor_position - 1] + buffer[cursor_position:]
|
||||
cursor_position -= 1
|
||||
outindexall -= 1
|
||||
if cursor_position != outindexall:
|
||||
chan.sendall(b"\b \b")
|
||||
chan.sendall(buffer[cursor_position:])
|
||||
else:
|
||||
chan.sendall(b"\b \b")
|
||||
else:
|
||||
chan.sendall(b"\x07")
|
||||
elif byte == b"\x1b" and chan.recv(1) == b'[':
|
||||
arrow_key = chan.recv(1)
|
||||
if not self.disable_scroll_with_arrow:
|
||||
if arrow_key == b'C':
|
||||
# Right arrow key, move cursor right if not at the end
|
||||
if cursor_position < len(buffer):
|
||||
chan.sendall(b'\x1b[C')
|
||||
# cursor_position += 1
|
||||
cursor_position = min(len(buffer), cursor_position + 1)
|
||||
|
||||
elif arrow_key == b'D':
|
||||
# Left arrow key, move cursor left if not at the beginning
|
||||
if cursor_position > 0:
|
||||
chan.sendall(b'\x1b[D')
|
||||
# cursor_position -= 1
|
||||
cursor_position = max(0, cursor_position - 1)
|
||||
|
||||
if self.history:
|
||||
if arrow_key == b'A':
|
||||
if history_index_position == 0:
|
||||
command = self.accounts.get_lastcommand(client["current_user"])
|
||||
else:
|
||||
command = self.accounts.get_history(client["current_user"], history_index_position)
|
||||
|
||||
# Clear the buffer
|
||||
for i in range(cursor_position):
|
||||
chan.send(b"\b \b")
|
||||
|
||||
# Update buffer and cursor position with the new command
|
||||
buffer = bytearray(command.encode('utf-8'))
|
||||
cursor_position = len(buffer)
|
||||
outindexall = cursor_position
|
||||
|
||||
# Print the updated buffer
|
||||
chan.sendall(buffer)
|
||||
|
||||
history_index_position += 1
|
||||
elif arrow_key == b'B':
|
||||
if history_index_position != -1:
|
||||
if history_index_position == 0:
|
||||
command = self.accounts.get_lastcommand(client["current_user"])
|
||||
else:
|
||||
command = self.accounts.get_history(client["current_user"], history_index_position)
|
||||
|
||||
# Clear the buffer
|
||||
for i in range(cursor_position):
|
||||
chan.send(b"\b \b")
|
||||
|
||||
# Update buffer and cursor position with the new command
|
||||
buffer = bytearray(command.encode('utf-8'))
|
||||
cursor_position = len(buffer)
|
||||
outindexall = cursor_position
|
||||
|
||||
# Print the updated buffer
|
||||
chan.sendall(buffer)
|
||||
else:
|
||||
history_index_position = 0
|
||||
for i in range(cursor_position):
|
||||
chan.send(b"\b \b")
|
||||
|
||||
buffer.clear()
|
||||
cursor_position = 0
|
||||
outindexall = 0
|
||||
|
||||
history_index_position -= 1
|
||||
|
||||
elif byte in (b'\r', b'\n'):
|
||||
break
|
||||
else:
|
||||
history_index_position = -1
|
||||
|
||||
self._handle_event("type", self.client_handlers[chan.getpeername()], byte)
|
||||
if echo:
|
||||
if outindexall != cursor_position:
|
||||
chan.sendall(b" ")
|
||||
chan.sendall(b'\033[s')
|
||||
chan.sendall(byte + buffer[cursor_position:])
|
||||
chan.sendall(b'\033[u')
|
||||
else:
|
||||
chan.sendall(byte)
|
||||
|
||||
#print(buffer[:cursor_position], byte, buffer[cursor_position:])
|
||||
buffer = buffer[:cursor_position] + byte + buffer[cursor_position:]
|
||||
cursor_position += 1
|
||||
outindexall += 1
|
||||
|
||||
client["inputbuffer"] = buffer
|
||||
|
||||
if echo:
|
||||
chan.sendall(b'\r\n')
|
||||
|
||||
command = str(buffer.decode('utf-8')).strip()
|
||||
|
||||
if self.history and command.strip() != "" and self.accounts.get_lastcommand(client["current_user"]) != command:
|
||||
self.accounts.add_history(client["current_user"], command)
|
||||
client["last_command"] = command
|
||||
|
||||
if command.strip() != "":
|
||||
if self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]) != None:
|
||||
chan.setblocking(False)
|
||||
chan.settimeout(None)
|
||||
|
||||
try:
|
||||
if self.enasyscom:
|
||||
sct = systemcommand(client, command, self)
|
||||
else:
|
||||
sct = False
|
||||
|
||||
if not sct:
|
||||
if self.XHandler != None:
|
||||
self._handle_event("beforexhandler", client, command)
|
||||
|
||||
self.XHandler.call(client, command)
|
||||
|
||||
self._handle_event("afterxhandler", client, command)
|
||||
else:
|
||||
self._handle_event("command", client, command)
|
||||
|
||||
except Exception as e:
|
||||
self._handle_event("error", client, e)
|
||||
if echo:
|
||||
try:
|
||||
chan.send(replace_enter_with_crlf(client["prompt"] + " "))
|
||||
except:
|
||||
logger.error("Send error")
|
||||
|
||||
chan.setblocking(False)
|
||||
chan.settimeout(None)
|
||||
|
||||
if self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]) != None:
|
||||
chan.setblocking(False)
|
||||
chan.settimeout(self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]))
|
||||
except socket.error:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
finally:
|
||||
try:
|
||||
if not byte:
|
||||
return False
|
||||
return True
|
||||
except:
|
||||
return False
|
264
src/PyserSSH/system/interface.py
Normal file
264
src/PyserSSH/system/interface.py
Normal file
@ -0,0 +1,264 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
import time
|
||||
import paramiko
|
||||
import ast
|
||||
|
||||
from .syscom import systemcommand
|
||||
from .RemoteStatus import startremotestatus
|
||||
|
||||
def parse_exec_request(command_string):
|
||||
try:
|
||||
# Remove the leading 'b' and convert bytes to string
|
||||
command_string = command_string.decode('utf-8')
|
||||
|
||||
# Split the string into precommand and env parts
|
||||
try:
|
||||
parts = command_string.split(', ')
|
||||
except:
|
||||
parts = command_string.split(',')
|
||||
|
||||
precommand_str = None
|
||||
env_str = None
|
||||
user_str = None
|
||||
|
||||
for part in parts:
|
||||
if part.startswith('precommand='):
|
||||
precommand_str = part.split('=', 1)[1].strip()
|
||||
elif part.startswith('env='):
|
||||
env_str = part.split('=', 1)[1].strip()
|
||||
elif part.startswith('user='):
|
||||
user_str = part.split('=', 1)[1].strip()
|
||||
|
||||
# Parse precommand using ast.literal_eval if present
|
||||
precommand = ast.literal_eval(precommand_str) if precommand_str else None
|
||||
|
||||
# Parse env using ast.literal_eval if present
|
||||
env = ast.literal_eval(env_str) if env_str else None
|
||||
|
||||
user = ast.literal_eval(user_str) if user_str else None
|
||||
|
||||
return precommand, env, user
|
||||
|
||||
except (ValueError, SyntaxError, TypeError) as e:
|
||||
# Handle parsing errors here
|
||||
print(f"Error parsing SSH command string: {e}")
|
||||
return None, None, None
|
||||
|
||||
def parse_exec_request_kwargs(command_string):
|
||||
try:
|
||||
# Remove the leading 'b' and convert bytes to string
|
||||
command_string = command_string.decode('utf-8')
|
||||
|
||||
# Split the string into key-value pairs
|
||||
try:
|
||||
parts = command_string.split(', ')
|
||||
except:
|
||||
parts = command_string.split(',')
|
||||
|
||||
kwargs = {}
|
||||
|
||||
for part in parts:
|
||||
if '=' in part:
|
||||
key, value = part.split('=', 1)
|
||||
key = key.strip()
|
||||
try:
|
||||
value = ast.literal_eval(value.strip())
|
||||
except (ValueError, SyntaxError):
|
||||
# If literal_eval fails, treat value as string
|
||||
value = value.strip()
|
||||
kwargs[key] = value
|
||||
|
||||
return kwargs
|
||||
|
||||
except (ValueError, SyntaxError, TypeError) as e:
|
||||
# Handle parsing errors here
|
||||
print(f"Error parsing command kwargs: {e}")
|
||||
return {}
|
||||
|
||||
class Sinterface(paramiko.ServerInterface):
|
||||
def __init__(self, serverself):
|
||||
self.serverself = serverself
|
||||
|
||||
def check_channel_request(self, kind, channel_id):
|
||||
if kind == 'session':
|
||||
return paramiko.OPEN_SUCCEEDED
|
||||
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
|
||||
|
||||
def get_allowed_auths(self, username):
|
||||
return self.serverself.accounts.get_allowed_auths(username)
|
||||
|
||||
def check_auth_password(self, username, password):
|
||||
data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"auth_type": "password"
|
||||
}
|
||||
|
||||
if self.serverself.accounts.validate_credentials(username, password) and not self.serverself.usexternalauth:
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
if self.serverself._handle_event("auth", data):
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
return paramiko.AUTH_FAILED
|
||||
|
||||
def check_auth_none(self, username):
|
||||
data = {
|
||||
"username": username,
|
||||
"auth_type": "none"
|
||||
}
|
||||
|
||||
if self.serverself.accounts.validate_credentials(username) and not self.serverself.usexternalauth:
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
if self.serverself._handle_event("auth", data):
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
return paramiko.AUTH_FAILED
|
||||
|
||||
def check_auth_publickey(self, username, key):
|
||||
data = {
|
||||
"username": username,
|
||||
"public_key": key,
|
||||
"auth_type": "key"
|
||||
}
|
||||
|
||||
if self.serverself.accounts.validate_credentials(username, public_key=key) and not self.serverself.usexternalauth:
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
if self.serverself._handle_event("auth", data):
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
return paramiko.AUTH_FAILED
|
||||
|
||||
def get_banner(self):
|
||||
if self.serverself.enaloginbanner:
|
||||
try:
|
||||
banner, lang = self.serverself._handle_event("authbanner", None)
|
||||
return banner, lang
|
||||
except:
|
||||
return "", ""
|
||||
else:
|
||||
return "", ""
|
||||
|
||||
def check_channel_exec_request(self, channel, execommand):
|
||||
if b"##Moba##" in execommand and self.serverself.enaremostatus:
|
||||
startremotestatus(self.serverself, channel)
|
||||
|
||||
client = self.serverself.client_handlers[channel.getpeername()]
|
||||
|
||||
if self.serverself.enasysexec:
|
||||
precommand, env, user = parse_exec_request(execommand)
|
||||
|
||||
if env != None:
|
||||
client.env_variables = env
|
||||
|
||||
if user != None:
|
||||
self.serverself._handle_event("exec", client, user)
|
||||
|
||||
if precommand != None:
|
||||
client.isexeccommandrunning = True
|
||||
try:
|
||||
if self.serverself.enasyscom:
|
||||
sct = systemcommand(client, precommand)
|
||||
else:
|
||||
sct = False
|
||||
|
||||
if not sct:
|
||||
if self.serverself.XHandler != None:
|
||||
self.serverself._handle_event("beforexhandler", client, precommand)
|
||||
|
||||
self.serverself.XHandler.call(client, precommand)
|
||||
|
||||
self.serverself._handle_event("afterxhandler", client, precommand)
|
||||
else:
|
||||
self.serverself._handle_event("command", client, precommand)
|
||||
except Exception as e:
|
||||
self.serverself._handle_event("error", client, e)
|
||||
|
||||
client.isexeccommandrunning = False
|
||||
else:
|
||||
kwargs = parse_exec_request_kwargs(execommand)
|
||||
|
||||
self.serverself._handle_event("exec", client, **kwargs)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes):
|
||||
data = {
|
||||
"term": term,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"pixelwidth": pixelwidth,
|
||||
"pixelheight": pixelheight,
|
||||
"modes": modes
|
||||
}
|
||||
data2 = {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"pixelwidth": pixelwidth,
|
||||
"pixelheight": pixelheight,
|
||||
}
|
||||
try:
|
||||
time.sleep(0.01) # fix waiting windowsize
|
||||
self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data2
|
||||
self.serverself.client_handlers[channel.getpeername()]["terminal_type"] = term
|
||||
self.serverself._handle_event("connectpty", self.serverself.client_handlers[channel.getpeername()], data)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def check_channel_shell_request(self, channel):
|
||||
return True
|
||||
|
||||
def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number):
|
||||
data = {
|
||||
"single_connection": single_connection,
|
||||
"auth_protocol": auth_protocol,
|
||||
"auth_cookie": auth_cookie,
|
||||
"screen_number": screen_number
|
||||
}
|
||||
try:
|
||||
self.serverself.client_handlers[channel.getpeername()]["x11"] = data
|
||||
self.serverself._handle_event("connectx11", self.serverself.client_handlers[channel.getpeername()], data)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def check_channel_window_change_request(self, channel, width: int, height: int, pixelwidth: int, pixelheight: int):
|
||||
data = {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"pixelwidth": pixelwidth,
|
||||
"pixelheight": pixelheight
|
||||
}
|
||||
self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data
|
||||
self.serverself._handle_event("resized", self.serverself.client_handlers[channel.getpeername()], data)
|
272
src/PyserSSH/system/remotestatus.py
Normal file
272
src/PyserSSH/system/remotestatus.py
Normal file
@ -0,0 +1,272 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import psutil
|
||||
from datetime import datetime
|
||||
import platform
|
||||
|
||||
from ..interactive import Send
|
||||
from .info import version
|
||||
|
||||
if platform.system() == "Windows":
|
||||
import ctypes
|
||||
|
||||
logger = logging.getLogger("PyserSSH.RemoteStatus")
|
||||
|
||||
if platform.system() == "Windows":
|
||||
class LASTINPUTINFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('cbSize', ctypes.c_uint),
|
||||
('dwTime', ctypes.c_uint),
|
||||
]
|
||||
|
||||
def get_idle_time():
|
||||
if platform.system() == "Windows":
|
||||
lastInputInfo = LASTINPUTINFO()
|
||||
lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo)
|
||||
ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lastInputInfo))
|
||||
millis = ctypes.windll.kernel32.GetTickCount() - lastInputInfo.dwTime
|
||||
return millis / 1000.0
|
||||
elif platform.system() == "Linux":
|
||||
with open('/proc/stat') as f:
|
||||
for line in f:
|
||||
if line.startswith('btime'):
|
||||
boot_time = float(line.split()[1])
|
||||
break
|
||||
|
||||
with open('/proc/uptime') as f:
|
||||
uptime_seconds = float(f.readline().split()[0])
|
||||
idle_time_seconds = uptime_seconds - (time.time() - boot_time)
|
||||
|
||||
return idle_time_seconds
|
||||
else:
|
||||
return time.time() - psutil.boot_time()
|
||||
|
||||
def get_system_uptime():
|
||||
if platform.system() == "Windows":
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
uptime = kernel32.GetTickCount64() / 1000.0
|
||||
return uptime
|
||||
elif platform.system() == "Linux":
|
||||
with open('/proc/uptime') as f:
|
||||
uptime_seconds = float(f.readline().split()[0])
|
||||
|
||||
return uptime_seconds
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_folder_size(folder_path):
|
||||
total_size = 0
|
||||
for dirpath, _, filenames in os.walk(folder_path):
|
||||
for f in filenames:
|
||||
fp = os.path.join(dirpath, f)
|
||||
total_size += os.path.getsize(fp)
|
||||
return total_size
|
||||
|
||||
def get_folder_usage(folder_path, limit_size):
|
||||
folder_size = get_folder_size(folder_path)
|
||||
used_size = folder_size
|
||||
free_size = limit_size - folder_size
|
||||
percent_used = (folder_size / limit_size) * 100 if limit_size > 0 else 0
|
||||
return used_size, free_size, limit_size, percent_used
|
||||
|
||||
librarypath = os.path.abspath(__file__).replace("\\", "/").split("/system/remotestatus.py")[0]
|
||||
|
||||
def remotestatus(serverself, channel, oneloop=False):
|
||||
try:
|
||||
while True:
|
||||
# Get RAM information
|
||||
mem = psutil.virtual_memory()
|
||||
|
||||
ramoutput = f"""\
|
||||
==> /proc/meminfo <==
|
||||
MemTotal: {mem.total // 1024} kB
|
||||
MemFree: {mem.free // 1024} kB
|
||||
MemAvailable: {mem.available // 1024} kB
|
||||
Buffers: 0 kB
|
||||
Cached: 0 kB
|
||||
SwapCached: 0 kB
|
||||
Active: 0 kB
|
||||
Inactive: 0 kB"""
|
||||
|
||||
cpu_data = []
|
||||
|
||||
#currentprocess = psutil.Process().cpu_times()
|
||||
|
||||
#cpu_data.append(["cpu", int(currentprocess.user), 0, int(currentprocess.system), 0, 0, 0, 0])
|
||||
|
||||
#if platform.system() == "Linux":
|
||||
io_counters = psutil.disk_io_counters(perdisk=False)
|
||||
io_wait_time = io_counters.read_time + io_counters.write_time
|
||||
for idx, cpu_time in enumerate(psutil.cpu_times(True), start=-1):
|
||||
if idx == -1:
|
||||
cpu_data.append(["cpu", int(cpu_time.user), 0, int(cpu_time.system), int(cpu_time.idle), io_wait_time, int(cpu_time.interrupt), 0, 0, 0, 0])
|
||||
else:
|
||||
cpu_data.append(
|
||||
[f"cpu{idx}", int(cpu_time.user), 0, int(cpu_time.system), int(cpu_time.idle), io_wait_time, int(cpu_time.interrupt), 0, 0, 0, 0])
|
||||
|
||||
# Calculate maximum widths for formatting (optional)
|
||||
max_widths = [max(len(str(row[i])) for row in cpu_data) for i in range(len(cpu_data[0]))]
|
||||
|
||||
disk_data = [
|
||||
["Filesystem", "1K-blocks", "Used", "Available", "Use%", "Mounted on"],
|
||||
]
|
||||
|
||||
for disk in psutil.disk_partitions(True):
|
||||
usage = psutil.disk_usage(disk.device)
|
||||
mountpoint = disk.mountpoint
|
||||
|
||||
if mountpoint == "C:\\":
|
||||
mountpoint = "/"
|
||||
|
||||
disk_data.append(
|
||||
[disk.device.replace('\\', '/'), usage.total // 1024, usage.used // 1024, usage.free // 1024, f"{int(usage.percent)}%",
|
||||
mountpoint])
|
||||
|
||||
libused, libfree, libtotal, libpercent = get_folder_usage(librarypath, 1024*1024)
|
||||
|
||||
disk_data.append(["/dev/pyserssh", libtotal // 1024, libused // 1024, libfree // 1024, f"{int(libpercent)}%", "/python/pyserssh"])
|
||||
|
||||
max_widths3 = [max(len(str(row[i])) for row in disk_data) for i in range(len(disk_data[0]))]
|
||||
|
||||
"""
|
||||
network_data = [
|
||||
["Inter-|", " Receive", "", "", "", "", "", "", " |", " Transmit" "", "", "", "", "", "", "", ""],
|
||||
[" face |", "bytes", "packets", "errs", "drop", "fifo", "frame", "compressed", "multicast|", "bytes", "packets",
|
||||
"errs", "drop", "fifo", "colls", "carrier", "compressed"]
|
||||
]
|
||||
|
||||
for interface, stats in psutil.net_io_counters(pernic=True).items():
|
||||
network_data.append(
|
||||
[f"{interface}:", stats.bytes_recv, stats.packets_recv, stats.errin, stats.dropin, 0, 0, 0, 0,
|
||||
stats.bytes_sent, stats.packets_sent, stats.errout, stats.dropout, 0, 0, 0, 0])
|
||||
|
||||
max_widths2 = [max(len(str(row[i])) for row in network_data) for i in range(len(network_data[0]))]
|
||||
|
||||
protocol_names = {
|
||||
(socket.AF_INET, socket.SOCK_STREAM): 'tcp',
|
||||
(socket.AF_INET, socket.SOCK_DGRAM): 'udp',
|
||||
(socket.AF_INET6, socket.SOCK_STREAM): 'tcp6',
|
||||
(socket.AF_INET6, socket.SOCK_DGRAM): 'udp6',
|
||||
}
|
||||
|
||||
netstat_data = [
|
||||
["Proto", "Recv-Q", "Send-Q", "Local Address", "Foreign Address", "State", "PID/Program name"],
|
||||
]
|
||||
|
||||
for conn in psutil.net_connections("all"):
|
||||
if conn.status in ['TIME_WAIT', 'CLOSING', "NONE"]:
|
||||
continue
|
||||
|
||||
laddr_ip, laddr_port = conn.laddr if conn.laddr else ('', '')
|
||||
raddr_ip, raddr_port = conn.raddr if conn.raddr else ('', '')
|
||||
|
||||
protocol = protocol_names.get((conn.family, conn.type), 'Unknown')
|
||||
|
||||
try:
|
||||
process = psutil.Process(conn.pid)
|
||||
processname = f"{conn.pid}/{process.name()}"
|
||||
except psutil.NoSuchProcess:
|
||||
processname = conn.pid
|
||||
|
||||
netstat_data.append(
|
||||
[protocol, 0, 0, f"{laddr_ip}:{laddr_port}", f"{raddr_ip}:{raddr_port}", conn.status, processname])
|
||||
|
||||
max_widths4 = [max(len(str(row[i])) for row in netstat_data) for i in range(len(netstat_data[0]))]
|
||||
"""
|
||||
|
||||
who_data = []
|
||||
|
||||
for idx, client in enumerate(serverself.client_handlers.values()):
|
||||
last_login_date = datetime.utcfromtimestamp(client.last_login_time).strftime('%Y-%m-%d %H:%M')
|
||||
who_data.append([client.current_user, f"pty/{idx}", last_login_date, f"({client.peername[0]})"])
|
||||
|
||||
max_widths5 = [max(len(str(row[i])) for row in who_data) for i in range(len(who_data[0]))]
|
||||
|
||||
Send(channel, ramoutput, directchannel=True)
|
||||
Send(channel, "", directchannel=True)
|
||||
|
||||
# only support for CPU status current python process
|
||||
Send(channel, "==> /proc/stat <==", directchannel=True)
|
||||
for row in cpu_data:
|
||||
Send(channel, " ".join("{:<{width}}".format(item, width=max_widths[i]) for i, item in enumerate(row)), directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/version <==", directchannel=True)
|
||||
Send(channel, f"PyserSSH v{version} run on {platform.platform()} {platform.machine()} {platform.architecture()[0]} with python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} {sys.version_info.releaselevel} {platform.python_build()[0]} {platform.python_build()[1]} {platform.python_compiler()} {platform.python_implementation()} {platform.python_revision()}", directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/uptime <==", directchannel=True)
|
||||
Send(channel, f"{get_system_uptime()} {get_idle_time()}", directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/sys/kernel/hostname <==", directchannel=True)
|
||||
Send(channel, platform.node(), directchannel=True)
|
||||
|
||||
# fixing later for network status
|
||||
#Send(channel, "", directchannel=True)
|
||||
#Send(channel, "==> /proc/net/dev <==", directchannel=True)
|
||||
#for row in network_data:
|
||||
# Send(channel, " ".join("{:<{width}}".format(item, width=max_widths2[i]) for i, item in enumerate(row)), directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/df <==", directchannel=True)
|
||||
for row in disk_data:
|
||||
Send(channel, " ".join("{:<{width}}".format(item, width=max_widths3[i]) for i, item in enumerate(row)), directchannel=True)
|
||||
|
||||
# fixing later for network status
|
||||
#Send(channel, "", directchannel=True)
|
||||
#Send(channel, "==> /proc/netstat <==", directchannel=True)
|
||||
#for row in netstat_data:
|
||||
# Send(channel, " ".join("{:<{width}}".format(item, width=max_widths4[i]) for i, item in enumerate(row)), directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/who <==", directchannel=True)
|
||||
for row in who_data:
|
||||
Send(channel, " ".join("{:<{width}}".format(item, width=max_widths5[i]) for i, item in enumerate(row)), directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/end <==", directchannel=True)
|
||||
Send(channel, "##Moba##", directchannel=True)
|
||||
|
||||
if oneloop:
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
except socket.error:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def startremotestatus(serverself, channel):
|
||||
t = threading.Thread(target=remotestatus, args=(serverself, channel), daemon=True)
|
||||
t.start()
|
84
src/PyserSSH/system/syscom.py
Normal file
84
src/PyserSSH/system/syscom.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WA3RRANTIES 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 shlex
|
||||
|
||||
from ..interactive import Send, Clear, Title, wait_choose
|
||||
|
||||
def system_account_command(client, accounts, action):
|
||||
banner = "accman adduser <username> <password>\naccman deluser <username>\naccman passwd <username> <new password>\naccman list"
|
||||
try:
|
||||
if action[0] == "adduser":
|
||||
accounts.add_account(action[1], action[2])
|
||||
Send(client, f"Added {action[1]}")
|
||||
elif action[0] == "deluser":
|
||||
if accounts.has_user(action[1]):
|
||||
if not accounts.is_user_has_sudo(action[1]):
|
||||
if wait_choose(client, ["No", "Yes"], prompt="Sure? ") == 1:
|
||||
accounts.remove_account(action[1])
|
||||
Send(client, f"Removed {action[1]}")
|
||||
else:
|
||||
Send(client, f"{action[1]} isn't removable.")
|
||||
else:
|
||||
Send(client, f"{action[1]} not found")
|
||||
elif action[0] == "passwd":
|
||||
if accounts.has_user(action[1]):
|
||||
accounts.change_password(action[1], action[2])
|
||||
Send(client, f"Password updated successfully.")
|
||||
else:
|
||||
Send(client, f"{action[1]} not found")
|
||||
elif action[0] == "list":
|
||||
for user in accounts.list_users():
|
||||
Send(client, user)
|
||||
else:
|
||||
Send(client, banner)
|
||||
except:
|
||||
Send(client, banner)
|
||||
|
||||
def systemcommand(client, command, serverself):
|
||||
if command == "whoami":
|
||||
Send(client, client["current_user"])
|
||||
return True
|
||||
elif command.startswith("title"):
|
||||
args = shlex.split(command)
|
||||
title = args[1]
|
||||
Title(client, title)
|
||||
return True
|
||||
elif command.startswith("accman"):
|
||||
args = shlex.split(command)
|
||||
if serverself.accounts.is_user_has_sudo(client.current_user):
|
||||
system_account_command(client, serverself.accounts, args[1:])
|
||||
else:
|
||||
Send(client, "accman: Permission denied.")
|
||||
return True
|
||||
elif command == "exit":
|
||||
client["channel"].close()
|
||||
return True
|
||||
elif command == "clear":
|
||||
Clear(client)
|
||||
return True
|
||||
else:
|
||||
return False
|
60
src/PyserSSH/system/sysfunc.py
Normal file
60
src/PyserSSH/system/sysfunc.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
def replace_enter_with_crlf(input_string):
|
||||
if isinstance(input_string, str):
|
||||
# Replace '\n' with '\r\n' in the string
|
||||
input_string = input_string.replace('\n', '\r\n')
|
||||
# Encode the string to bytes
|
||||
return input_string.encode()
|
||||
elif isinstance(input_string, bytes):
|
||||
# Decode bytes to string
|
||||
decoded_string = input_string.decode()
|
||||
# Replace '\n' with '\r\n' in the string
|
||||
modified_string = decoded_string.replace('\n', '\r\n')
|
||||
# Encode the modified string back to bytes
|
||||
|
||||
return modified_string.encode()
|
||||
else:
|
||||
raise TypeError("Input must be a string or bytes")
|
||||
|
||||
def text_centered_screen(text, screen_width, screen_height, spacecharacter=" "):
|
||||
screen = []
|
||||
lines = text.split("\n")
|
||||
padding_vertical = (screen_height - len(lines)) // 2 # Calculate vertical padding
|
||||
|
||||
for y in range(screen_height):
|
||||
line = ""
|
||||
if padding_vertical <= y < padding_vertical + len(lines): # Check if it's within the range of the text lines
|
||||
index = y - padding_vertical # Get the corresponding line index
|
||||
padding_horizontal = (screen_width - len(lines[index])) // 2 # Calculate horizontal padding for each line
|
||||
line += spacecharacter * padding_horizontal + lines[index] + spacecharacter * padding_horizontal
|
||||
else: # Fill other lines with space characters
|
||||
line += spacecharacter * screen_width
|
||||
screen.append(line)
|
||||
|
||||
return "\n".join(screen)
|
174
src/PyserSSH/utils/ServerManager.py
Normal file
174
src/PyserSSH/utils/ServerManager.py
Normal file
@ -0,0 +1,174 @@
|
||||
import time
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("PyserSSH.Utils.ServerManager")
|
||||
|
||||
class ServerManager:
|
||||
def __init__(self):
|
||||
self.servers = {}
|
||||
|
||||
def add_server(self, name, server, *args, **kwargs):
|
||||
"""
|
||||
Adds a server to the manager with the specified name. Raises an error if a server with the same name already exists.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server.
|
||||
server (object): The server instance to be added.
|
||||
*args: Arguments for server initialization.
|
||||
**kwargs: Keyword arguments for server initialization.
|
||||
|
||||
Raises:
|
||||
ValueError: If a server with the same name already exists.
|
||||
"""
|
||||
if name in self.servers:
|
||||
raise ValueError(f"Server with name '{name}' already exists.")
|
||||
self.servers[name] = {"server": server, "args": args, "kwargs": kwargs, "status": "stopped"}
|
||||
logger.info(f"Server '{name}' added.")
|
||||
|
||||
def remove_server(self, name):
|
||||
"""
|
||||
Removes a server from the manager by name. Raises an error if the server does not exist.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server to be removed.
|
||||
|
||||
Raises:
|
||||
ValueError: If no server with the specified name exists.
|
||||
"""
|
||||
if name not in self.servers:
|
||||
raise ValueError(f"No server found with name '{name}'.")
|
||||
del self.servers[name]
|
||||
logger.info(f"Server '{name}' removed.")
|
||||
|
||||
def get_server(self, name):
|
||||
"""
|
||||
Retrieves a server by its name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server to retrieve.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the server instance, arguments, keyword arguments, and its status, or None if the server is not found.
|
||||
"""
|
||||
return self.servers.get(name, None)
|
||||
|
||||
def start_server(self, name):
|
||||
"""
|
||||
Starts a server with the specified name if it is not already running. Blocks until the server starts.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server to start.
|
||||
|
||||
Raises:
|
||||
ValueError: If no server with the specified name exists or the server cannot be started.
|
||||
"""
|
||||
server_info = self.get_server(name)
|
||||
if not server_info:
|
||||
raise ValueError(f"No server found with name '{name}'.")
|
||||
|
||||
if server_info["status"] == "running":
|
||||
logger.info(f"Server '{name}' is already running.")
|
||||
return
|
||||
|
||||
server = server_info["server"]
|
||||
args, kwargs = server_info["args"], server_info["kwargs"]
|
||||
|
||||
logger.info(f"Starting server '{name}' with arguments {args}...")
|
||||
server_info["status"] = "starting"
|
||||
server.run(*args, **kwargs)
|
||||
|
||||
while not server.isrunning:
|
||||
logger.debug(f"Waiting for server '{name}' to start...")
|
||||
time.sleep(0.1)
|
||||
|
||||
server_info["status"] = "running"
|
||||
logger.info(f"Server '{name}' is now running.")
|
||||
|
||||
def stop_server(self, name):
|
||||
"""
|
||||
Stops a server with the specified name if it is running. Blocks until the server stops.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server to stop.
|
||||
|
||||
Raises:
|
||||
ValueError: If no server with the specified name exists or the server cannot be stopped.
|
||||
"""
|
||||
server_info = self.get_server(name)
|
||||
if not server_info:
|
||||
raise ValueError(f"No server found with name '{name}'.")
|
||||
|
||||
if server_info["status"] == "stopped":
|
||||
logger.info(f"Server '{name}' is already stopped.")
|
||||
return
|
||||
|
||||
server = server_info["server"]
|
||||
|
||||
logger.info(f"Shutting down server '{name}'...")
|
||||
server_info["status"] = "shutting down"
|
||||
server.stop_server()
|
||||
|
||||
while server.isrunning:
|
||||
logger.debug(f"Waiting for server '{name}' to shut down...")
|
||||
time.sleep(0.1)
|
||||
|
||||
server_info["status"] = "stopped"
|
||||
logger.info(f"Server '{name}' has been stopped.")
|
||||
|
||||
def start_all_servers(self):
|
||||
"""
|
||||
Starts all servers managed by the ServerManager. Blocks until each server starts.
|
||||
"""
|
||||
for name, server_info in self.servers.items():
|
||||
if server_info["status"] == "running":
|
||||
logger.info(f"Server '{name}' is already running.")
|
||||
continue
|
||||
server, args, kwargs = server_info["server"], server_info["args"], server_info["kwargs"]
|
||||
logger.info(f"Starting server '{name}' with arguments {args}...")
|
||||
server_info["status"] = "starting"
|
||||
server.run(*args, **kwargs)
|
||||
|
||||
while not server.isrunning:
|
||||
logger.debug(f"Waiting for server '{name}' to start...")
|
||||
time.sleep(0.1)
|
||||
|
||||
server_info["status"] = "running"
|
||||
logger.info(f"Server '{name}' is now running.")
|
||||
|
||||
def stop_all_servers(self):
|
||||
"""
|
||||
Stops all servers managed by the ServerManager. Blocks until each server stops.
|
||||
"""
|
||||
for name, server_info in self.servers.items():
|
||||
if server_info["status"] == "stopped":
|
||||
logger.info(f"Server '{name}' is already stopped.")
|
||||
continue
|
||||
server = server_info["server"]
|
||||
logger.info(f"Shutting down server '{name}'...")
|
||||
server_info["status"] = "shutting down"
|
||||
server.stop_server()
|
||||
|
||||
while server.isrunning:
|
||||
logger.debug(f"Waiting for server '{name}' to shut down...")
|
||||
time.sleep(0.1)
|
||||
|
||||
server_info["status"] = "stopped"
|
||||
logger.info(f"Server '{name}' has been stopped.")
|
||||
|
||||
def get_status(self, name):
|
||||
"""
|
||||
Retrieves the status of a server by name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server to get the status of.
|
||||
|
||||
Returns:
|
||||
str: The current status of the server (e.g., 'running', 'stopped', etc.).
|
||||
|
||||
Raises:
|
||||
ValueError: If no server with the specified name exists.
|
||||
"""
|
||||
server_info = self.get_server(name)
|
||||
if not server_info:
|
||||
raise ValueError(f"No server found with name '{name}'.")
|
||||
return server_info["status"]
|
38
src/PyserSSH/utils/__init__.py
Normal file
38
src/PyserSSH/utils/__init__.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
"""
|
||||
note
|
||||
|
||||
ansi cursor arrow
|
||||
up - \x1b[A
|
||||
down - \x1b[B
|
||||
left - \x1b[D
|
||||
right - \x1b[C
|
||||
|
||||
https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
"""
|
22
src/PyserSSH/utils/keygen.py
Normal file
22
src/PyserSSH/utils/keygen.py
Normal file
@ -0,0 +1,22 @@
|
||||
import paramiko
|
||||
|
||||
def generate_ssh_keypair(private_key_path='id_rsa', public_key_path='id_rsa.pub', key_size=2048):
|
||||
"""
|
||||
Generates an SSH key pair (private and public) and saves them to specified files.
|
||||
|
||||
Args:
|
||||
- private_key_path (str): Path to save the private key.
|
||||
- public_key_path (str): Path to save the public key.
|
||||
- key_size (int): Size of the RSA key (default is 2048).
|
||||
"""
|
||||
# Generate RSA key pair
|
||||
private_key = paramiko.RSAKey.generate(key_size)
|
||||
|
||||
# Save the private key to a file
|
||||
private_key.write_private_key_file(private_key_path)
|
||||
|
||||
# Save the public key to a file
|
||||
with open(public_key_path, 'w') as pub_file:
|
||||
pub_file.write(f"{private_key.get_name()} {private_key.get_base64()}")
|
||||
|
||||
print(f"SSH Key pair generated: {private_key_path} and {public_key_path}")
|
13
upload.bat
Normal file
13
upload.bat
Normal file
@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
|
||||
title change urllib3 to 1.26.15
|
||||
pip install urllib3==1.26.15
|
||||
|
||||
title building dist
|
||||
python setup.py sdist
|
||||
|
||||
title uploading to pypi
|
||||
twine upload -r pypi dist/*
|
||||
|
||||
title done!
|
||||
pause
|
Loading…
x
Reference in New Issue
Block a user