diff --git a/keepmenu/__init__.py b/keepmenu/__init__.py index 554d636..dc64463 100644 --- a/keepmenu/__init__.py +++ b/keepmenu/__init__.py @@ -8,7 +8,8 @@ import shlex from subprocess import run, DEVNULL import sys -from os.path import exists, expanduser +import tempfile +from os.path import exists, expanduser, join from keepmenu.menu import dmenu_err @@ -22,7 +23,29 @@ # file_handler.setFormatter(formatter) # logger.addHandler(file_handler) -AUTH_FILE = expanduser("~/.cache/.keepmenu-auth") + +def get_runtime_dir(): + """Get the runtime directory for auth file storage. + + Prefers $XDG_RUNTIME_DIR/keepmenu/ for security (tmpfs-backed, auto-cleanup + on logout, proper permissions enforced by systemd). Falls back to + $TMPDIR/keepmenu-/ which is also typically tmpfs and cleared on reboot. + + Returns: str path to runtime directory + + """ + xdg_runtime = os.environ.get('XDG_RUNTIME_DIR') + if xdg_runtime and exists(xdg_runtime): + runtime_dir = join(xdg_runtime, 'keepmenu') + else: + runtime_dir = join(tempfile.gettempdir(), f'keepmenu-{os.getuid()}') + # Ensure directory exists with secure permissions + if not exists(runtime_dir): + os.makedirs(runtime_dir, mode=0o700) + return runtime_dir + + +AUTH_FILE = join(get_runtime_dir(), ".keepmenu-auth") CONF_FILE = expanduser("~/.config/keepmenu/config.ini") SECRET_VALID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" diff --git a/keepmenu/__main__.py b/keepmenu/__main__.py index 8887755..9593b20 100644 --- a/keepmenu/__main__.py +++ b/keepmenu/__main__.py @@ -41,7 +41,10 @@ def port_in_use(port): return s.connect_ex(('127.0.0.1', port)) == 0 def get_auth(): - """Generate and save port and authkey to ~/.cache/.keepmenu-auth + """Generate and save port and authkey to runtime directory. + + Uses $XDG_RUNTIME_DIR/keepmenu/ if available (tmpfs-backed, auto-cleanup), + otherwise falls back to $TMPDIR/keepmenu-/. Returns: int port, bytestring authkey diff --git a/tests/tests.py b/tests/tests.py index eedb8a0..705d2ac 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -19,6 +19,78 @@ SECRET2 = 'PW4YAYYZVDE5RK2AOLKUATNZIKAFQLZO' +class TestRuntimeDir(unittest.TestCase): + """Test get_runtime_dir() function for auth file location + + """ + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + # Save original environment and tempfile cache + self.orig_xdg_runtime = os.environ.get('XDG_RUNTIME_DIR') + self.orig_tmpdir = os.environ.get('TMPDIR') + self.orig_tempfile_tempdir = tempfile.tempdir + + def tearDown(self): + rmtree(self.tmpdir) + # Restore original environment + if self.orig_xdg_runtime is not None: + os.environ['XDG_RUNTIME_DIR'] = self.orig_xdg_runtime + elif 'XDG_RUNTIME_DIR' in os.environ: + del os.environ['XDG_RUNTIME_DIR'] + if self.orig_tmpdir is not None: + os.environ['TMPDIR'] = self.orig_tmpdir + elif 'TMPDIR' in os.environ: + del os.environ['TMPDIR'] + # Restore tempfile cache + tempfile.tempdir = self.orig_tempfile_tempdir + + def test_xdg_runtime_dir_used_when_set(self): + """Test that $XDG_RUNTIME_DIR/keepmenu/ is used when available + """ + xdg_runtime = os.path.join(self.tmpdir, 'runtime') + os.makedirs(xdg_runtime, mode=0o700) + os.environ['XDG_RUNTIME_DIR'] = xdg_runtime + + runtime_dir = KM.get_runtime_dir() + + self.assertEqual(runtime_dir, os.path.join(xdg_runtime, 'keepmenu')) + self.assertTrue(os.path.exists(runtime_dir)) + self.assertEqual(os.stat(runtime_dir).st_mode & 0o777, 0o700) + + def test_tmpdir_fallback_when_xdg_runtime_unset(self): + """Test fallback to $TMPDIR/keepmenu-/ when XDG_RUNTIME_DIR not set + """ + if 'XDG_RUNTIME_DIR' in os.environ: + del os.environ['XDG_RUNTIME_DIR'] + custom_tmpdir = os.path.join(self.tmpdir, 'tmp') + os.makedirs(custom_tmpdir, mode=0o777) + os.environ['TMPDIR'] = custom_tmpdir + # Reset tempfile cache so it picks up new TMPDIR + tempfile.tempdir = None + + runtime_dir = KM.get_runtime_dir() + + expected = os.path.join(custom_tmpdir, f'keepmenu-{os.getuid()}') + self.assertEqual(runtime_dir, expected) + self.assertTrue(os.path.exists(runtime_dir)) + self.assertEqual(os.stat(runtime_dir).st_mode & 0o777, 0o700) + + def test_tmpdir_fallback_when_xdg_runtime_dir_not_exists(self): + """Test fallback when XDG_RUNTIME_DIR is set but doesn't exist + """ + os.environ['XDG_RUNTIME_DIR'] = '/nonexistent/path' + custom_tmpdir = os.path.join(self.tmpdir, 'tmp') + os.makedirs(custom_tmpdir, mode=0o777) + os.environ['TMPDIR'] = custom_tmpdir + # Reset tempfile cache so it picks up new TMPDIR + tempfile.tempdir = None + + runtime_dir = KM.get_runtime_dir() + + expected = os.path.join(custom_tmpdir, f'keepmenu-{os.getuid()}') + self.assertEqual(runtime_dir, expected) + + class TestServer(unittest.TestCase): """Test various BaseManager server functions