105 lines
3.0 KiB
Python
Executable File
105 lines
3.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
gn-daemon: Gorillanest daemon accepting commands over SSH
|
|
|
|
Commands:
|
|
git-upload-pack
|
|
git-receive-pack
|
|
new-repo [--migrate URL] [--pull-mirror]
|
|
git config remote.$1.$2 $3
|
|
/* NOTE:
|
|
* git actually allows adding arbitrary bullshit,
|
|
* therefor the maximum number of remotes must be limited
|
|
* and the fields must be on a whitelist;
|
|
* a special field should be added called sync,
|
|
* which determines mirror frequency
|
|
*/
|
|
"""
|
|
import asyncio
|
|
import os
|
|
import asyncssh
|
|
import subprocess
|
|
import logging
|
|
from sys import exit
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
log = logging.getLogger("gn-daemon")
|
|
|
|
HOST_KEY_PATH = "gn_host_key" # XXX
|
|
LISTEN_ADDR = "0.0.0.0"
|
|
LISTEN_PORT = 2222
|
|
|
|
def validate_password(username, password):
|
|
return username == "anon" and password == "passwd"
|
|
|
|
# Implementing AsyncSSH functionality is done though primarily polymorphism.
|
|
# This is the class it expects.
|
|
# Complete function definitions are moved out so that
|
|
# this class doesn't dominate the script,
|
|
# and its never in question what should and should not be inside it.
|
|
class GNServer(asyncssh.SSHServer):
|
|
def begin_auth(self, username): return True
|
|
def password_auth_supported(self): return True
|
|
|
|
def validate_password(self, username, password):
|
|
return validate_password(username, password)
|
|
# ---
|
|
|
|
def ensure_host_key(path: str):
|
|
if os.path.exists(path): return path
|
|
|
|
log.info("Generating ephemeral host key (saved to %s)", path)
|
|
key = asyncssh.generate_private_key('ssh-rsa')
|
|
with open(path, "wb") as f: f.write(key.export_private_key())
|
|
os.chmod(path, 0o600)
|
|
|
|
return path
|
|
|
|
async def gn_process(proc : asyncssh.SSHServerProcess):
|
|
cmd = proc.command
|
|
|
|
async def pump(reader, writer):
|
|
while True:
|
|
data = await reader.read(4096)
|
|
if not data: break
|
|
if isinstance(data, bytes):
|
|
# do this or we get "utf_8_encode() argument 1 must be str, not bytes"
|
|
data = data.decode('utf-8')
|
|
log.info(data)
|
|
writer.write(data)
|
|
await writer.drain()
|
|
|
|
sub = await asyncio.create_subprocess_exec(
|
|
'/bin/sh', '-c', cmd, # XXX
|
|
stdin=asyncio.subprocess.PIPE,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
|
|
await asyncio.gather(
|
|
pump(proc.stdin, sub.stdin),
|
|
pump(sub.stdout, proc.stdout),
|
|
pump(sub.stderr, proc.stderr)
|
|
)
|
|
|
|
rc = await sub.wait()
|
|
proc.exit(rc)
|
|
|
|
async def start_server():
|
|
host_key_path = ensure_host_key(HOST_KEY_PATH)
|
|
|
|
await asyncssh.create_server(
|
|
GNServer,
|
|
LISTEN_ADDR,
|
|
LISTEN_PORT,
|
|
server_host_keys=[host_key_path],
|
|
process_factory=gn_process,
|
|
)
|
|
|
|
await asyncio.Future()
|
|
|
|
if __name__ == "__main__":
|
|
try: asyncio.run(start_server())
|
|
except (OSError, asyncssh.Error) as exc:
|
|
log.error("Error starting server: %s", exc)
|