2024-06-15 19:10:58 +02:00
#!/usr/bin/env python3
# Copyright (c) 2024 Julian Müller (ChaoticByte)
# License: MIT
import asyncio
from argparse import ArgumentParser
2024-06-19 11:35:01 +02:00
from getpass import getpass
2024-06-15 19:10:58 +02:00
from pathlib import Path
from sys import stdout
from sys import stderr
import asyncssh
import yaml
config_host = " "
config_port = 8022
config_clients = {
# username: asyncssh.SSHAuthorizedKeys
}
2024-06-19 11:43:22 +02:00
enable_logging = False
connected_clients = [ ]
2024-06-15 19:10:58 +02:00
class SSHServer ( asyncssh . SSHServer ) :
def host_based_auth_supported ( self ) : return False
def kbdint_auth_supported ( self ) : return False
def password_auth_supported ( self ) : return False
def public_key_auth_supported ( self ) : return True
def begin_auth ( self , username : str ) - > bool : return True # we wanna handle auth
def validate_public_key ( self , username : str , key : asyncssh . SSHKey ) - > bool :
try :
2024-06-16 09:07:57 +02:00
return config_clients [ username ] . validate ( key , " " , " " ) is not None # checks client key
2024-06-15 19:10:58 +02:00
except :
return False
2024-06-16 07:41:36 +02:00
def broadcast ( msg : str , use_stderr : bool = False ) :
2024-06-16 09:07:57 +02:00
# Broadcast a message to all connected clients
2024-06-15 19:10:58 +02:00
assert type ( msg ) == str
2024-06-16 07:41:36 +02:00
msg = msg . strip ( " \r \n " )
if use_stderr :
2024-06-16 09:07:57 +02:00
msg + = " \r \n " # we need CRLF for stderr apparently
2024-06-16 07:41:36 +02:00
for c in connected_clients :
c . stderr . write ( msg )
else :
msg + = " \n "
for c in connected_clients :
c . stdout . write ( msg )
2024-06-15 19:10:58 +02:00
async def handle_connection ( process : asyncssh . SSHServerProcess ) :
connected_clients . append ( process )
username = process . get_extra_info ( " username " )
try :
2024-06-16 09:07:57 +02:00
# hello there
2024-06-15 19:10:58 +02:00
connected_msg = f " [connected] { username } \n "
2024-06-19 11:43:22 +02:00
if enable_logging :
stderr . write ( connected_msg )
2024-06-16 07:41:36 +02:00
broadcast ( connected_msg , True )
2024-06-16 08:55:52 +02:00
if process . command is not None :
2024-06-16 09:07:57 +02:00
# client has provided a command as a ssh commandline argument
2024-06-16 08:55:52 +02:00
line = process . command . strip ( " \r \n " )
msg = f " { username } : { line } \n "
2024-06-19 11:43:22 +02:00
if enable_logging :
stdout . write ( msg )
2024-06-16 08:55:52 +02:00
broadcast ( msg )
else :
2024-06-16 09:07:57 +02:00
# client wants an interactive session
2024-06-16 08:55:52 +02:00
while True :
try :
async for line in process . stdin :
if line == " " : raise asyncssh . BreakReceived ( 0 )
line = line . strip ( ' \r \n ' )
msg = f " { username } : { line } \n "
2024-06-19 11:43:22 +02:00
if enable_logging :
stdout . write ( msg )
2024-06-16 08:55:52 +02:00
broadcast ( msg )
except asyncssh . TerminalSizeChanged :
2024-06-16 09:07:57 +02:00
continue # we don't want to exit when the client changes its terminal size lol
finally : # but otherwise, we do want to
break # exit this loop.
2024-06-15 19:10:58 +02:00
except asyncssh . BreakReceived :
2024-06-16 09:07:57 +02:00
pass # we don't want to write an error message on this exception
2024-06-15 19:10:58 +02:00
except Exception as e :
stderr . write ( f " An error occured: { type ( e ) . __name__ } { e } \n " )
stderr . flush ( )
finally :
2024-06-16 09:07:57 +02:00
# exit process, remove client from list, inform other clients
disconnected_msg = f " [disconnected] { username } \n "
2024-06-15 19:10:58 +02:00
process . exit ( 0 )
connected_clients . remove ( process )
2024-06-19 11:43:22 +02:00
if enable_logging :
stderr . write ( disconnected_msg )
2024-06-16 07:41:36 +02:00
broadcast ( disconnected_msg , True )
2024-06-15 19:10:58 +02:00
if __name__ == " __main__ " :
# commandline arguments
argp = ArgumentParser ( )
argp . add_argument ( " config " , type = Path , help = " The path to the config file " )
2024-06-16 08:42:50 +02:00
argp . add_argument ( " pkey " , type = Path , help = " The path to the ssh private key " )
2024-06-19 11:43:22 +02:00
argp . add_argument ( " --log " , action = " store_true " , help = " Enable logging to stdout and stderr " )
2024-06-15 19:10:58 +02:00
args = argp . parse_args ( )
# read config
config = yaml . safe_load ( args . config . read_text ( ) )
config_host = str ( config [ " host " ] )
config_port = int ( config [ " port " ] )
2024-06-19 11:43:22 +02:00
enable_logging = args . log
2024-06-19 11:35:01 +02:00
try :
config_private_key = asyncssh . import_private_key ( args . pkey . read_text ( ) )
except asyncssh . public_key . KeyImportError as e :
e_str = str ( e ) . lower ( )
if " passphrase " in e_str or " encyrpted " in e_str : # this is unstable af!
config_private_key = asyncssh . import_private_key ( args . pkey . read_text ( ) , passphrase = getpass ( " Private Key Passphrase: " ) )
else :
raise e
2024-06-16 09:07:57 +02:00
for c in config [ " clients " ] :
config_clients [ str ( c ) ] = asyncssh . import_authorized_keys ( str ( config [ " clients " ] [ c ] ) )
# read private key
2024-06-15 19:10:58 +02:00
server_public_key = config_private_key . export_public_key ( " openssh " ) . decode ( ) . strip ( " \n \r " )
stderr . write ( f " Server public key is \" { server_public_key } \" \n " )
stderr . flush ( )
# start server
loop = asyncio . get_event_loop ( )
loop . run_until_complete (
asyncssh . create_server (
SSHServer ,
config_host ,
config_port ,
server_host_keys = [ config_private_key ] ,
process_factory = handle_connection
) )
loop . run_forever ( )