Skip to content

Commit 90a7e5d

Browse files
committed
Add --users-directory arguement to server CLIs
1 parent f5b18ad commit 90a7e5d

5 files changed

Lines changed: 81 additions & 38 deletions

File tree

gambaterm/console.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ def pop_console_arguments(
122122
) -> Callable[[], Console]:
123123
romfile: Path = namespace.romfile
124124
input_file: Path | None = namespace.input_file
125+
save_directory: Path | None = namespace.save_directory
125126
force_gameboy: bool = namespace.__dict__.pop("force_gameboy")
126-
save_directory: Path | None = namespace.__dict__.pop("save_directory")
127127
# Save directory defaults to the rom file directory (unless we read the input from a file)
128128
if input_file is None and save_directory is None:
129129
save_directory = romfile.parent

gambaterm/main.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class AppConfig:
2929
speed: float
3030
skip_inputs: int
3131
cpr_sync: bool
32+
save_directory: Path | None
3233

3334

3435
@dataclass
@@ -39,13 +40,6 @@ class LocalAppConfig(AppConfig):
3940

4041
def add_base_arguments(parser: argparse.ArgumentParser) -> None:
4142
parser.add_argument("romfile", metavar="ROM", type=Path, help="Path to a rom file")
42-
parser.add_argument(
43-
"--save-directory",
44-
"--sd",
45-
type=Path,
46-
default=None,
47-
help="Path to the save directory (default to the ROM directory)",
48-
)
4943

5044

5145
def add_input_file_arguments(parser: argparse.ArgumentParser) -> None:
@@ -119,6 +113,13 @@ def add_local_only_arguments(parser: argparse.ArgumentParser) -> None:
119113
type=Path,
120114
help="Record inputs into a file",
121115
)
116+
parser.add_argument(
117+
"--save-directory",
118+
"--sd",
119+
type=Path,
120+
default=None,
121+
help="Path to the save directory (default to the ROM directory)",
122+
)
122123

123124

124125
def main(

gambaterm/remote_terminal.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from __future__ import annotations
66

77
import codecs
8+
import hashlib
89
import contextlib
910
from typing import IO, Generator
1011

@@ -84,3 +85,14 @@ def _height_and_width(self) -> WINSZ:
8485
def update_size(self, rows: int, columns: int) -> None:
8586
self._rows = rows
8687
self._columns = columns
88+
89+
90+
def user_directory_name(username: str | None) -> str:
91+
"""Hash the username into a safe directory name.
92+
93+
:param username: telnet/ssh-negotiated username, or ``None``
94+
:returns: hex digest suitable for use as a directory name
95+
"""
96+
if username is None:
97+
return "_anonymous"
98+
return hashlib.sha256(username.encode("utf-8")).hexdigest()[:16]

gambaterm/ssh.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import os
44
import time
55
import hmac
6-
import hashlib
76
import asyncio
87
import argparse
98
import traceback
@@ -36,7 +35,7 @@
3635
)
3736
from .console import Console, GameboyColor
3837

39-
from .remote_terminal import RemoteTerminal
38+
from .remote_terminal import RemoteTerminal, user_directory_name
4039
from .ssh_app_session import process_to_terminal
4140

4241

@@ -105,6 +104,7 @@ async def ssh_process_handler(process: SSHServerProcess[str]) -> int:
105104
console_cls: type[Console] = process.get_extra_info("console_cls")
106105
namespace: argparse.Namespace = process.get_extra_info("namespace")
107106
command_parser: CommandParser = process.get_extra_info("command_parser")
107+
users_directory: Path = process.get_extra_info("users_directory")
108108
executor: ThreadPoolExecutor = process.get_extra_info("executor")
109109
display = process.channel.get_x11_display()
110110
command = process.channel.get_command()
@@ -126,15 +126,15 @@ async def ssh_process_handler(process: SSHServerProcess[str]) -> int:
126126
)
127127

128128
# Manage save directory — hash username to prevent path traversal
129-
if "save_directory" in namespace.__dict__:
130-
if getattr(namespace, "input_file", False):
131-
setattr(namespace, "save_directory", None)
132-
else:
133-
safe_name = hashlib.sha256(username.encode("utf-8")).hexdigest()[:16]
134-
save_directory = Path("ssh_save") / safe_name
135-
save_directory.mkdir(parents=True, exist_ok=True)
136-
(save_directory / "username").write_text(username)
137-
setattr(namespace, "save_directory", save_directory)
129+
namespace.save_directory = (
130+
None
131+
if getattr(namespace, "input_file", None)
132+
else users_directory / user_directory_name(username)
133+
)
134+
135+
if namespace.save_directory is not None:
136+
namespace.save_directory.mkdir(parents=True, exist_ok=True)
137+
(namespace.save_directory / "username").write_text(username)
138138

139139
# Pop console arguments and extract configuration
140140
console_callback = console_cls.pop_console_arguments(namespace)
@@ -292,11 +292,13 @@ def __init__(
292292
console_cls: type[Console],
293293
namespace: argparse.Namespace,
294294
command_parser: CommandParser,
295+
users_directory: Path,
295296
executor: ThreadPoolExecutor,
296297
):
297298
self._gambaterm_console_cls = console_cls
298299
self._gambaterm_namespace = namespace
299300
self._gambaterm_command_parser = command_parser
301+
self._gambaterm_users_directory = users_directory
300302
self._gambaterm_executor = executor
301303
self._gambaterm_authentication = authentication
302304

@@ -305,6 +307,7 @@ def connection_made(self, conn: asyncssh.SSHServerConnection) -> None:
305307
conn.set_extra_info(executor=self._gambaterm_executor)
306308
conn.set_extra_info(namespace=self._gambaterm_namespace)
307309
conn.set_extra_info(command_parser=self._gambaterm_command_parser)
310+
conn.set_extra_info(users_directory=self._gambaterm_users_directory)
308311

309312
def begin_auth(self, username: str) -> bool:
310313
return not isinstance(self._gambaterm_authentication, NoAuthentication)
@@ -334,6 +337,7 @@ async def run_ssh_server(
334337
console_cls: type[Console],
335338
namespace: argparse.Namespace,
336339
command_parser: CommandParser,
340+
users_directory: Path,
337341
executor: ThreadPoolExecutor,
338342
) -> AsyncIterator[SSHAcceptor]:
339343
# Gambaterm configuration
@@ -389,7 +393,12 @@ async def run_ssh_server(
389393

390394
server = await asyncssh.create_server(
391395
lambda: SSHServer(
392-
authentication, console_cls, namespace, command_parser, executor
396+
authentication,
397+
console_cls,
398+
namespace,
399+
command_parser,
400+
users_directory,
401+
executor,
393402
),
394403
bind,
395404
port,
@@ -465,13 +474,20 @@ def main(
465474
action="store_true",
466475
help="Disable authentication altogether (no password nor public key required)",
467476
)
477+
parser.add_argument(
478+
"--users-directory",
479+
type=Path,
480+
default=Path("users_save"),
481+
help="Directory containing one save directory per user (default is ./users_save)",
482+
)
468483

469484
# Parse arguments
470485
namespace = parser.parse_args(parser_args)
471486
bind: str = namespace.__dict__.pop("bind")
472487
port: int = namespace.__dict__.pop("port")
473488
password: str = namespace.__dict__.pop("password")
474489
no_auth: bool = namespace.__dict__.pop("no_auth")
490+
users_directory: Path = namespace.__dict__.pop("users_directory")
475491

476492
# Determine authentication method
477493
if no_auth and password is None:
@@ -512,6 +528,7 @@ async def async_main() -> None:
512528
console_cls,
513529
namespace,
514530
command_parser,
531+
users_directory,
515532
executor,
516533
):
517534
await asyncio.Future()

gambaterm/telnet.py

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import time
4-
import hashlib
54
import asyncio
65
import argparse
76
import traceback
@@ -40,19 +39,10 @@
4039
console_input_from_keyboard_protocol_context,
4140
is_kitty_keyboard_protocol_supported,
4241
)
43-
from .remote_terminal import RemoteTerminal
44-
from .telnet_app_session import telnet_to_terminal
45-
46-
47-
def _save_dir_name(username: str | None) -> str:
48-
"""Hash the username into a safe directory name.
49-
50-
:param username: telnet-negotiated username, or ``None``
51-
:returns: hex digest suitable for use as a directory name
52-
"""
53-
if username is None:
54-
return "_anonymous"
55-
return hashlib.sha256(username.encode("utf-8")).hexdigest()[:16]
42+
from .remote_terminal import RemoteTerminal, user_directory_name
43+
from .telnet_app_session import (
44+
telnet_to_terminal,
45+
)
5646

5747

5848
def thread_target(
@@ -129,14 +119,21 @@ def make_telnet_shell(
129119
app_config: argparse.Namespace,
130120
console_cls: Type[Console],
131121
idle_timeout: float | None,
122+
users_directory: Path,
132123
executor: ThreadPoolExecutor,
133124
) -> ShellCallback:
134125
"""Create a telnet shell callback with app_config and executor bound."""
135126

136127
async def telnet_shell(reader: TelnetReader, writer: TelnetWriter) -> None:
137128
try:
138129
await _telnet_shell(
139-
reader, writer, app_config, console_cls, idle_timeout, executor
130+
reader,
131+
writer,
132+
app_config,
133+
console_cls,
134+
idle_timeout,
135+
users_directory,
136+
executor,
140137
)
141138
except (KeyboardInterrupt, EOFError):
142139
pass
@@ -243,6 +240,7 @@ async def _telnet_shell(
243240
app_config: argparse.Namespace,
244241
console_cls: type[Console],
245242
idle_timeout: float | None,
243+
users_directory: Path,
246244
executor: ThreadPoolExecutor,
247245
) -> int:
248246
peername = writer.get_extra_info("peername")
@@ -277,12 +275,12 @@ async def _telnet_shell(
277275
print(f"[Terminal Info] {peer_host}: ttype={terminal_type}, {cols}x{rows}")
278276

279277
try:
280-
# Copy namespace and set telnet-specific save directory
278+
# Copy namespace and set save directory
281279
namespace = argparse.Namespace(**vars(app_config))
282280
save_directory = (
283281
None
284282
if getattr(namespace, "input_file", None)
285-
else Path("telnet_save") / _save_dir_name(username)
283+
else users_directory / user_directory_name(username)
286284
)
287285
namespace.save_directory = save_directory
288286
if save_directory is not None:
@@ -320,11 +318,18 @@ async def run_telnet_server(
320318
idle_timeout: float | None,
321319
console_cls: type[Console],
322320
namespace: argparse.Namespace,
321+
users_directory: Path,
323322
executor: ThreadPoolExecutor,
324323
) -> AsyncIterator[telnetlib3.Server]:
325324
import telnetlib3
326325

327-
shell = make_telnet_shell(namespace, console_cls, idle_timeout, executor)
326+
shell = make_telnet_shell(
327+
namespace,
328+
console_cls,
329+
idle_timeout,
330+
users_directory,
331+
executor,
332+
)
328333

329334
if robot_check or max_players > 0:
330335
from telnetlib3.guard_shells import ConnectionCounter, busy_shell
@@ -427,13 +432,20 @@ def main(
427432
default=None,
428433
help="Idle timeout in seconds (default is disabled)",
429434
)
435+
parser.add_argument(
436+
"--users-directory",
437+
type=Path,
438+
default=Path("users_save"),
439+
help="Directory containing one save directory per user (default is ./users_save)",
440+
)
430441

431442
namespace = parser.parse_args(parser_args)
432443
bind: str = namespace.__dict__.pop("bind")
433444
port: int = namespace.__dict__.pop("port")
434445
robot_check: bool = namespace.__dict__.pop("robot_check")
435446
max_players: int = namespace.__dict__.pop("max_players")
436447
idle_timeout: float | None = namespace.__dict__.pop("idle_timeout")
448+
users_directory: Path = namespace.__dict__.pop("users_directory")
437449

438450
try:
439451
with ThreadPoolExecutor(max_workers=32) as executor:
@@ -447,6 +459,7 @@ async def async_main() -> None:
447459
idle_timeout,
448460
console_cls,
449461
namespace,
462+
users_directory,
450463
executor,
451464
):
452465
await asyncio.Future()

0 commit comments

Comments
 (0)