Skip to content

Commit 6021d4b

Browse files
committed
Add --users-directory arguement to server CLIs
1 parent 2c345d0 commit 6021d4b

5 files changed

Lines changed: 78 additions & 37 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

@@ -67,3 +68,14 @@ def _height_and_width(self) -> WINSZ:
6768
def update_size(self, rows: int, columns: int) -> None:
6869
self._rows = rows
6970
self._columns = columns
71+
72+
73+
def user_directory_name(username: str | None) -> str:
74+
"""Hash the username into a safe directory name.
75+
76+
:param username: telnet/ssh-negotiated username, or ``None``
77+
:returns: hex digest suitable for use as a directory name
78+
"""
79+
if username is None:
80+
return "_anonymous"
81+
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)
@@ -286,11 +286,13 @@ def __init__(
286286
console_cls: type[Console],
287287
namespace: argparse.Namespace,
288288
command_parser: CommandParser,
289+
users_directory: Path,
289290
executor: ThreadPoolExecutor,
290291
):
291292
self._gambaterm_console_cls = console_cls
292293
self._gambaterm_namespace = namespace
293294
self._gambaterm_command_parser = command_parser
295+
self._gambaterm_users_directory = users_directory
294296
self._gambaterm_executor = executor
295297
self._gambaterm_authentication = authentication
296298

@@ -299,6 +301,7 @@ def connection_made(self, conn: asyncssh.SSHServerConnection) -> None:
299301
conn.set_extra_info(executor=self._gambaterm_executor)
300302
conn.set_extra_info(namespace=self._gambaterm_namespace)
301303
conn.set_extra_info(command_parser=self._gambaterm_command_parser)
304+
conn.set_extra_info(users_directory=self._gambaterm_users_directory)
302305

303306
def begin_auth(self, username: str) -> bool:
304307
return not isinstance(self._gambaterm_authentication, NoAuthentication)
@@ -328,6 +331,7 @@ async def run_ssh_server(
328331
console_cls: type[Console],
329332
namespace: argparse.Namespace,
330333
command_parser: CommandParser,
334+
users_directory: Path,
331335
executor: ThreadPoolExecutor,
332336
) -> AsyncIterator[SSHAcceptor]:
333337
# Gambaterm configuration
@@ -383,7 +387,12 @@ async def run_ssh_server(
383387

384388
server = await asyncssh.create_server(
385389
lambda: SSHServer(
386-
authentication, console_cls, namespace, command_parser, executor
390+
authentication,
391+
console_cls,
392+
namespace,
393+
command_parser,
394+
users_directory,
395+
executor,
387396
),
388397
bind,
389398
port,
@@ -459,13 +468,20 @@ def main(
459468
action="store_true",
460469
help="Disable authentication altogether (no password nor public key required)",
461470
)
471+
parser.add_argument(
472+
"--users-directory",
473+
type=Path,
474+
default=Path("users_save"),
475+
help="Directory containing one save directory per user (default is ./users_save)",
476+
)
462477

463478
# Parse arguments
464479
namespace = parser.parse_args(parser_args)
465480
bind: str = namespace.__dict__.pop("bind")
466481
port: int = namespace.__dict__.pop("port")
467482
password: str = namespace.__dict__.pop("password")
468483
no_auth: bool = namespace.__dict__.pop("no_auth")
484+
users_directory: Path = namespace.__dict__.pop("users_directory")
469485

470486
# Determine authentication method
471487
if no_auth and password is None:
@@ -506,6 +522,7 @@ async def async_main() -> None:
506522
console_cls,
507523
namespace,
508524
command_parser,
525+
users_directory,
509526
executor,
510527
):
511528
await asyncio.Future()

gambaterm/telnet.py

Lines changed: 28 additions & 17 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,23 +39,12 @@
4039
console_input_from_keyboard_protocol_context,
4140
is_kitty_keyboard_protocol_supported,
4241
)
43-
from .remote_terminal import RemoteTerminal
42+
from .remote_terminal import RemoteTerminal, user_directory_name
4443
from .telnet_app_session import (
4544
telnet_to_terminal,
4645
)
4746

4847

49-
def _save_dir_name(username: str | None) -> str:
50-
"""Hash the username into a safe directory name.
51-
52-
:param username: telnet-negotiated username, or ``None``
53-
:returns: hex digest suitable for use as a directory name
54-
"""
55-
if username is None:
56-
return "_anonymous"
57-
return hashlib.sha256(username.encode("utf-8")).hexdigest()[:16]
58-
59-
6048
def thread_target(
6149
terminal: RemoteTerminal,
6250
console_callback: Callable[[], Console],
@@ -123,14 +111,21 @@ def make_telnet_shell(
123111
app_config: argparse.Namespace,
124112
console_cls: Type[Console],
125113
idle_timeout: float | None,
114+
users_directory: Path,
126115
executor: ThreadPoolExecutor,
127116
) -> ShellCallback:
128117
"""Create a telnet shell callback with app_config and executor bound."""
129118

130119
async def telnet_shell(reader: TelnetReader, writer: TelnetWriter) -> None:
131120
try:
132121
await _telnet_shell(
133-
reader, writer, app_config, console_cls, idle_timeout, executor
122+
reader,
123+
writer,
124+
app_config,
125+
console_cls,
126+
idle_timeout,
127+
users_directory,
128+
executor,
134129
)
135130
except (KeyboardInterrupt, EOFError):
136131
pass
@@ -237,6 +232,7 @@ async def _telnet_shell(
237232
app_config: argparse.Namespace,
238233
console_cls: type[Console],
239234
idle_timeout: float | None,
235+
users_directory: Path,
240236
executor: ThreadPoolExecutor,
241237
) -> int:
242238
peername = writer.get_extra_info("peername")
@@ -281,12 +277,12 @@ async def _telnet_shell(
281277
)
282278

283279
try:
284-
# Copy namespace and set telnet-specific save directory
280+
# Copy namespace and set save directory
285281
namespace = argparse.Namespace(**vars(app_config))
286282
save_directory = (
287283
None
288284
if getattr(namespace, "input_file", None)
289-
else Path("telnet_save") / _save_dir_name(username)
285+
else users_directory / user_directory_name(username)
290286
)
291287
namespace.save_directory = save_directory
292288
if save_directory is not None:
@@ -323,11 +319,18 @@ async def run_telnet_server(
323319
idle_timeout: float | None,
324320
console_cls: type[Console],
325321
namespace: argparse.Namespace,
322+
users_directory: Path,
326323
executor: ThreadPoolExecutor,
327324
) -> AsyncIterator[telnetlib3.Server]:
328325
import telnetlib3
329326

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

332335
if robot_check or max_players > 0:
333336
from telnetlib3.guard_shells import ConnectionCounter, busy_shell
@@ -430,13 +433,20 @@ def main(
430433
default=None,
431434
help="Idle timeout in seconds (default is disabled)",
432435
)
436+
parser.add_argument(
437+
"--users-directory",
438+
type=Path,
439+
default=Path("users_save"),
440+
help="Directory containing one save directory per user (default is ./users_save)",
441+
)
433442

434443
namespace = parser.parse_args(parser_args)
435444
bind: str = namespace.__dict__.pop("bind")
436445
port: int = namespace.__dict__.pop("port")
437446
robot_check: bool = namespace.__dict__.pop("robot_check")
438447
max_players: int = namespace.__dict__.pop("max_players")
439448
idle_timeout: float | None = namespace.__dict__.pop("idle_timeout")
449+
users_directory: Path = namespace.__dict__.pop("users_directory")
440450

441451
try:
442452
with ThreadPoolExecutor(max_workers=32) as executor:
@@ -450,6 +460,7 @@ async def async_main() -> None:
450460
idle_timeout,
451461
console_cls,
452462
namespace,
463+
users_directory,
453464
executor,
454465
):
455466
await asyncio.Future()

0 commit comments

Comments
 (0)