gorillanest/gn-daemon
2025-08-21 22:50:23 +02:00

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)