Add project files
This commit is contained in:
parent
5849bd98b4
commit
4f8419fb51
4 changed files with 162 additions and 1 deletions
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 Julian Müller
|
Copyright (c) 2024 Julian Müller (ChaoticByte)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
139
agent/__init__.py
Executable file
139
agent/__init__.py
Executable file
|
@ -0,0 +1,139 @@
|
||||||
|
# Copyright (c) 2024 Julian Müller (ChaoticByte)
|
||||||
|
# License: MIT
|
||||||
|
|
||||||
|
from concurrent.futures import ThreadPoolExecutor as _ThreadPoolExecutor
|
||||||
|
from datetime import datetime as _datetime
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
from queue import Empty as _Empty
|
||||||
|
from queue import Queue as _Queue
|
||||||
|
from sys import stderr as _stderr
|
||||||
|
from time import time as _time
|
||||||
|
from typing import IO as _IO
|
||||||
|
|
||||||
|
from paramiko import SSHClient
|
||||||
|
|
||||||
|
|
||||||
|
VERSION = "unknown"
|
||||||
|
BASE_VERSION = 1
|
||||||
|
TIME_A = _time()
|
||||||
|
|
||||||
|
|
||||||
|
# helpers & types
|
||||||
|
|
||||||
|
def log(*args):
|
||||||
|
print(_datetime.now().isoformat(), *args, file=_stderr)
|
||||||
|
|
||||||
|
|
||||||
|
class Task:
|
||||||
|
def __init__(self, user: str, command: str, args: str):
|
||||||
|
assert type(user) == str
|
||||||
|
assert type(command) == str
|
||||||
|
assert type(args) == str
|
||||||
|
self.user = user
|
||||||
|
self.command = command
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
|
||||||
|
# commands
|
||||||
|
|
||||||
|
def command_hi(task: Task):
|
||||||
|
writer_queue.put(f"Hi {task.user}! Running agent v{BASE_VERSION}:{VERSION} since {(_time()-TIME_A):.2f} seconds.")
|
||||||
|
|
||||||
|
def command_help(task: Task):
|
||||||
|
writer_queue.put(f"Available commands: {', '.join(commands)}")
|
||||||
|
|
||||||
|
commands = {
|
||||||
|
"hi": command_hi,
|
||||||
|
"help": command_help
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# internal
|
||||||
|
|
||||||
|
_stop = False
|
||||||
|
_worker_queue = _Queue()
|
||||||
|
writer_queue = _Queue()
|
||||||
|
|
||||||
|
def _ssh_listener(stdout: _IO):
|
||||||
|
global _stop
|
||||||
|
log("Starting SSH Listener")
|
||||||
|
while not _stop:
|
||||||
|
try:
|
||||||
|
if stdout.channel.closed:
|
||||||
|
log("Connection lost.")
|
||||||
|
break
|
||||||
|
# parse message
|
||||||
|
l = stdout.readline().strip("\n ")
|
||||||
|
if l == "": continue
|
||||||
|
if not ": " in l: continue
|
||||||
|
username, rest = l.split(": ", 1)
|
||||||
|
if " " in rest:
|
||||||
|
command, args = rest.split(" ", 1)
|
||||||
|
else:
|
||||||
|
command, args = rest, ""
|
||||||
|
# put in queue
|
||||||
|
_worker_queue.put(Task(username, command, args))
|
||||||
|
except Exception as e:
|
||||||
|
log(f"An exception occured: {e.__class__.__name__} {e}")
|
||||||
|
_stop = True
|
||||||
|
log("Stopping SSH Listener")
|
||||||
|
|
||||||
|
def _ssh_writer(stdin: _IO):
|
||||||
|
global _stop
|
||||||
|
log("Starting SSH Writer")
|
||||||
|
while not _stop:
|
||||||
|
try:
|
||||||
|
l = writer_queue.get(timeout=1)
|
||||||
|
if type(l) == str:
|
||||||
|
stdin.write(l.rstrip("\n") + "\n")
|
||||||
|
except _Empty:
|
||||||
|
pass
|
||||||
|
log("Stopping SSH Writer")
|
||||||
|
|
||||||
|
def _worker():
|
||||||
|
global _stop
|
||||||
|
log("Starting SSH Worker")
|
||||||
|
while not _stop:
|
||||||
|
try:
|
||||||
|
t = _worker_queue.get(timeout=1)
|
||||||
|
if t.command in commands:
|
||||||
|
commands[t.command](t)
|
||||||
|
except _Empty:
|
||||||
|
pass
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log("Keyboard Interrupt")
|
||||||
|
_stop = True
|
||||||
|
log("Stopping Worker")
|
||||||
|
|
||||||
|
def main(server_host: str, server_port: int, user: str, key: _Path, known_hosts: _Path):
|
||||||
|
# check config
|
||||||
|
assert type(server_host) == str
|
||||||
|
assert type(server_port) == int
|
||||||
|
assert type(user) == str
|
||||||
|
assert isinstance(key, _Path)
|
||||||
|
assert key.exists()
|
||||||
|
assert isinstance(known_hosts, _Path)
|
||||||
|
assert known_hosts.exists()
|
||||||
|
# start client
|
||||||
|
client = SSHClient()
|
||||||
|
stdin, stdout, stderr = None, None, None
|
||||||
|
tpe = _ThreadPoolExecutor()
|
||||||
|
try:
|
||||||
|
client.get_host_keys().clear()
|
||||||
|
client.load_host_keys(str(known_hosts.resolve()))
|
||||||
|
client.connect(hostname=server_host, port=server_port, username=user, key_filename=str(key.resolve()))
|
||||||
|
stdin, stdout, stderr = client.exec_command("", get_pty=False)
|
||||||
|
tpe.submit(_ssh_listener, stdout)
|
||||||
|
tpe.submit(_ssh_writer, stdin)
|
||||||
|
_worker()
|
||||||
|
except Exception as e:
|
||||||
|
log(f"An exception occured: {e.__class__.__name__} {e}")
|
||||||
|
stop = True
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
if stdin is not None:
|
||||||
|
stdin.close()
|
||||||
|
stdin.channel.shutdown_write() # shutdown channel
|
||||||
|
if stdout is not None: stdout.close()
|
||||||
|
if stderr is not None: stderr.close()
|
||||||
|
tpe.shutdown() # shutdown thread pool
|
21
agent_example.py
Executable file
21
agent_example.py
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import agent
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
agent.VERSION = 1
|
||||||
|
|
||||||
|
def echo(t: agent.Task):
|
||||||
|
agent.writer_queue.put(f"{t.user}: {t.args}")
|
||||||
|
|
||||||
|
agent.commands.update({
|
||||||
|
"echo": echo
|
||||||
|
})
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
agent.main(
|
||||||
|
server_host= "localhost",
|
||||||
|
server_port = 8022,
|
||||||
|
user = "example1",
|
||||||
|
key = Path("../ex1"),
|
||||||
|
known_hosts = Path("../known"))
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
paramiko
|
Reference in a new issue