2017-11-22 21:13:32 +08:00
|
|
|
#!/usr/bin/env python3
|
2020-11-09 12:23:58 +08:00
|
|
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
2017-11-22 21:13:32 +08:00
|
|
|
#
|
2018-12-10 02:17:33 +08:00
|
|
|
# systemd is free software; you can redistribute it and/or modify it
|
|
|
|
# under the terms of the GNU Lesser General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 2.1 of the License, or
|
|
|
|
# (at your option) any later version.
|
2017-11-22 21:13:32 +08:00
|
|
|
|
2017-11-23 20:56:32 +08:00
|
|
|
import os
|
2017-11-22 21:13:32 +08:00
|
|
|
import sys
|
2017-12-02 04:15:51 +08:00
|
|
|
import socket
|
2017-11-22 21:13:32 +08:00
|
|
|
import subprocess
|
2017-12-02 04:15:51 +08:00
|
|
|
import tempfile
|
|
|
|
import pwd
|
2018-10-13 16:26:48 +08:00
|
|
|
import grp
|
2017-12-02 04:15:51 +08:00
|
|
|
|
|
|
|
try:
|
|
|
|
from systemd import id128
|
|
|
|
except ImportError:
|
|
|
|
id128 = None
|
2017-11-22 21:13:32 +08:00
|
|
|
|
|
|
|
EX_DATAERR = 65 # from sysexits.h
|
2017-12-02 21:00:58 +08:00
|
|
|
EXIT_TEST_SKIP = 77
|
|
|
|
|
|
|
|
try:
|
|
|
|
subprocess.run
|
|
|
|
except AttributeError:
|
|
|
|
sys.exit(EXIT_TEST_SKIP)
|
2017-11-22 21:13:32 +08:00
|
|
|
|
2017-12-07 03:04:45 +08:00
|
|
|
exe_with_args = sys.argv[1:]
|
2017-11-22 21:13:32 +08:00
|
|
|
|
2017-12-02 04:15:51 +08:00
|
|
|
def test_line(line, *, user, returncode=EX_DATAERR, extra={}):
|
2017-11-23 20:56:32 +08:00
|
|
|
args = ['--user'] if user else []
|
2017-12-07 03:04:45 +08:00
|
|
|
print('Running {} on {!r}'.format(' '.join(exe_with_args + args), line))
|
|
|
|
c = subprocess.run(exe_with_args + ['--create', '-'] + args,
|
2017-12-02 21:00:58 +08:00
|
|
|
input=line, stdout=subprocess.PIPE, universal_newlines=True,
|
|
|
|
**extra)
|
2017-11-23 20:56:32 +08:00
|
|
|
assert c.returncode == returncode, c
|
|
|
|
|
|
|
|
def test_invalids(*, user):
|
|
|
|
test_line('asdfa', user=user)
|
|
|
|
test_line('f "open quote', user=user)
|
|
|
|
test_line('f closed quote""', user=user)
|
|
|
|
test_line('Y /unknown/letter', user=user)
|
|
|
|
test_line('w non/absolute/path', user=user)
|
|
|
|
test_line('s', user=user) # s is for short
|
|
|
|
test_line('f!! /too/many/bangs', user=user)
|
|
|
|
test_line('f++ /too/many/plusses', user=user)
|
|
|
|
test_line('f+!+ /too/many/plusses', user=user)
|
|
|
|
test_line('f!+! /too/many/bangs', user=user)
|
2021-03-30 22:37:11 +08:00
|
|
|
test_line('f== /too/many/equals', user=user)
|
2017-11-23 20:56:32 +08:00
|
|
|
test_line('w /unresolved/argument - - - - "%Y"', user=user)
|
|
|
|
test_line('w /unresolved/argument/sandwich - - - - "%v%Y%v"', user=user)
|
|
|
|
test_line('w /unresolved/filename/%Y - - - - "whatever"', user=user)
|
|
|
|
test_line('w /unresolved/filename/sandwich/%v%Y%v - - - - "whatever"', user=user)
|
2019-04-27 08:22:40 +08:00
|
|
|
test_line('w - - - - - "no file specified"', user=user)
|
|
|
|
test_line('C - - - - - "no file specified"', user=user)
|
2017-11-23 20:56:32 +08:00
|
|
|
test_line('C non/absolute/path - - - - -', user=user)
|
|
|
|
test_line('b - - - - - -', user=user)
|
|
|
|
test_line('b 1234 - - - - -', user=user)
|
|
|
|
test_line('c - - - - - -', user=user)
|
|
|
|
test_line('c 1234 - - - - -', user=user)
|
|
|
|
test_line('t - - -', user=user)
|
|
|
|
test_line('T - - -', user=user)
|
|
|
|
test_line('a - - -', user=user)
|
|
|
|
test_line('A - - -', user=user)
|
|
|
|
test_line('h - - -', user=user)
|
|
|
|
test_line('H - - -', user=user)
|
|
|
|
|
2018-06-04 00:57:21 +08:00
|
|
|
def test_uninitialized_t():
|
2017-11-23 20:56:32 +08:00
|
|
|
if os.getuid() == 0:
|
|
|
|
return
|
|
|
|
|
2017-12-02 04:15:51 +08:00
|
|
|
test_line('w /foo - - - - "specifier for --user %t"',
|
2023-01-28 22:09:25 +08:00
|
|
|
user=True, returncode=0, extra={'env':{'HOME': os.getenv('HOME')}})
|
2017-12-02 04:15:51 +08:00
|
|
|
|
2021-03-30 22:37:11 +08:00
|
|
|
def test_content(line, expected, *, user, extra={}, subpath='/arg', path_cb=None):
|
2017-12-02 04:15:51 +08:00
|
|
|
d = tempfile.TemporaryDirectory(prefix='test-systemd-tmpfiles.')
|
2021-03-30 22:37:11 +08:00
|
|
|
if path_cb is not None:
|
|
|
|
path_cb(d.name, subpath)
|
|
|
|
arg = d.name + subpath
|
2017-12-02 04:15:51 +08:00
|
|
|
spec = line.format(arg)
|
|
|
|
test_line(spec, user=user, returncode=0, extra=extra)
|
|
|
|
content = open(arg).read()
|
|
|
|
print('expect: {!r}\nactual: {!r}'.format(expected, content))
|
|
|
|
assert content == expected
|
|
|
|
|
|
|
|
def test_valid_specifiers(*, user):
|
|
|
|
test_content('f {} - - - - two words', 'two words', user=user)
|
2022-10-25 22:02:10 +08:00
|
|
|
if id128 and os.path.isfile('/etc/machine-id'):
|
2017-12-02 22:40:30 +08:00
|
|
|
try:
|
|
|
|
test_content('f {} - - - - %m', '{}'.format(id128.get_machine().hex), user=user)
|
|
|
|
except AssertionError as e:
|
|
|
|
print(e)
|
|
|
|
print('/etc/machine-id: {!r}'.format(open('/etc/machine-id').read()))
|
|
|
|
print('/proc/cmdline: {!r}'.format(open('/proc/cmdline').read()))
|
|
|
|
print('skipping')
|
2017-12-02 04:15:51 +08:00
|
|
|
test_content('f {} - - - - %b', '{}'.format(id128.get_boot().hex), user=user)
|
|
|
|
test_content('f {} - - - - %H', '{}'.format(socket.gethostname()), user=user)
|
|
|
|
test_content('f {} - - - - %v', '{}'.format(os.uname().release), user=user)
|
shared/specifier: fix %u/%U/%g/%G when called as unprivileged user
We would resolve those specifiers to the calling user/group. This is mostly OK
when done in the manager, because the manager generally operates as root
in system mode, and a non-root in user mode. It would still be wrong if
called with --test though. But in systemctl, this would be generally wrong,
since we can call 'systemctl --system' as a normal user, either for testing
or even for actual operation with '--root=…'.
When operating in --global mode, %u/%U/%g/%G should return an error.
The information whether we're operating in system mode, user mode, or global
mode is passed as the data pointer to specifier_group_name(), specifier_user_name(),
specifier_group_id(), specifier_user_id(). We can't use userdata, because
it's already used for other things.
2022-03-10 05:29:19 +08:00
|
|
|
test_content('f {} - - - - %U', '{}'.format(os.getuid() if user else 0), user=user)
|
|
|
|
test_content('f {} - - - - %G', '{}'.format(os.getgid() if user else 0), user=user)
|
2017-12-02 04:15:51 +08:00
|
|
|
|
2023-01-28 22:09:25 +08:00
|
|
|
try:
|
|
|
|
puser = pwd.getpwuid(os.getuid() if user else 0)
|
|
|
|
except KeyError:
|
|
|
|
puser = None
|
2018-10-13 16:26:48 +08:00
|
|
|
|
2023-01-28 22:09:25 +08:00
|
|
|
if puser:
|
|
|
|
test_content('f {} - - - - %u', '{}'.format(puser.pw_name), user=user)
|
|
|
|
|
|
|
|
try:
|
|
|
|
pgroup = grp.getgrgid(os.getgid() if user else 0)
|
|
|
|
except KeyError:
|
|
|
|
pgroup = None
|
|
|
|
|
|
|
|
if pgroup:
|
|
|
|
test_content('f {} - - - - %g', '{}'.format(pgroup.gr_name), user=user)
|
2017-12-04 16:05:05 +08:00
|
|
|
|
|
|
|
# Note that %h is the only specifier in which we look the environment,
|
|
|
|
# because we check $HOME. Should we even be doing that?
|
|
|
|
home = os.path.expanduser("~")
|
|
|
|
test_content('f {} - - - - %h', '{}'.format(home), user=user)
|
2017-12-02 04:15:51 +08:00
|
|
|
|
|
|
|
xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR')
|
|
|
|
if xdg_runtime_dir is not None or not user:
|
|
|
|
test_content('f {} - - - - %t',
|
|
|
|
xdg_runtime_dir if user else '/run',
|
|
|
|
user=user)
|
|
|
|
|
|
|
|
xdg_config_home = os.getenv('XDG_CONFIG_HOME')
|
|
|
|
if xdg_config_home is not None or not user:
|
|
|
|
test_content('f {} - - - - %S',
|
|
|
|
xdg_config_home if user else '/var/lib',
|
|
|
|
user=user)
|
|
|
|
|
|
|
|
xdg_cache_home = os.getenv('XDG_CACHE_HOME')
|
|
|
|
if xdg_cache_home is not None or not user:
|
|
|
|
test_content('f {} - - - - %C',
|
|
|
|
xdg_cache_home if user else '/var/cache',
|
|
|
|
user=user)
|
|
|
|
|
|
|
|
if xdg_config_home is not None or not user:
|
|
|
|
test_content('f {} - - - - %L',
|
|
|
|
xdg_config_home + '/log' if user else '/var/log',
|
|
|
|
user=user)
|
|
|
|
|
|
|
|
test_content('f {} - - - - %%', '%', user=user)
|
2017-11-22 21:13:32 +08:00
|
|
|
|
2021-03-30 22:37:11 +08:00
|
|
|
def mkfifo(parent, subpath):
|
|
|
|
os.makedirs(parent, mode=0o755, exist_ok=True)
|
|
|
|
first_component = subpath.split('/')[1]
|
|
|
|
path = parent + '/' + first_component
|
|
|
|
print('path: {}'.format(path))
|
|
|
|
os.mkfifo(path)
|
|
|
|
|
|
|
|
def mkdir(parent, subpath):
|
|
|
|
first_component = subpath.split('/')[1]
|
|
|
|
path = parent + '/' + first_component
|
|
|
|
os.makedirs(path, mode=0o755, exist_ok=True)
|
|
|
|
os.symlink(path, path + '/self', target_is_directory=True)
|
|
|
|
|
|
|
|
def symlink(parent, subpath):
|
|
|
|
link_path = parent + '/link-target'
|
|
|
|
os.makedirs(parent, mode=0o755, exist_ok=True)
|
|
|
|
with open(link_path, 'wb') as f:
|
|
|
|
f.write(b'target')
|
|
|
|
first_component = subpath.split('/')[1]
|
|
|
|
path = parent + '/' + first_component
|
|
|
|
os.symlink(link_path, path, target_is_directory=True)
|
|
|
|
|
|
|
|
def file(parent, subpath):
|
|
|
|
content = 'file-' + subpath.split('/')[1]
|
|
|
|
path = parent + subpath
|
|
|
|
os.makedirs(os.path.dirname(path), mode=0o755, exist_ok=True)
|
|
|
|
with open(path, 'wb') as f:
|
|
|
|
f.write(content.encode())
|
|
|
|
|
|
|
|
def valid_symlink(parent, subpath):
|
|
|
|
target = 'link-target'
|
|
|
|
link_path = parent + target
|
|
|
|
os.makedirs(link_path, mode=0o755, exist_ok=True)
|
|
|
|
first_component = subpath.split('/')[1]
|
|
|
|
path = parent + '/' + first_component
|
|
|
|
os.symlink(target, path, target_is_directory=True)
|
|
|
|
|
|
|
|
def test_hard_cleanup(*, user):
|
|
|
|
type_cbs = [None, file, mkdir, symlink]
|
|
|
|
if 'mkfifo' in dir(os):
|
|
|
|
type_cbs.append(mkfifo)
|
|
|
|
|
|
|
|
for type_cb in type_cbs:
|
|
|
|
for subpath in ['/shallow', '/deep/1/2']:
|
|
|
|
label = '{}-{}'.format('None' if type_cb is None else type_cb.__name__, subpath.split('/')[1])
|
|
|
|
test_content('f= {} - - - - ' + label, label, user=user, subpath=subpath, path_cb=type_cb)
|
|
|
|
|
|
|
|
# Test the case that a valid symlink is in the path.
|
|
|
|
label = 'valid_symlink-deep'
|
|
|
|
test_content('f= {} - - - - ' + label, label, user=user, subpath='/deep/1/2', path_cb=valid_symlink)
|
|
|
|
|
2022-07-13 05:51:15 +08:00
|
|
|
def test_base64():
|
|
|
|
test_line('f~ /tmp/base64-test - - - - UGlmZgpQYWZmClB1ZmYgCg==', user=False, returncode=0)
|
|
|
|
|
|
|
|
with open("/tmp/base64-test", mode='r') as f:
|
|
|
|
d = f.read()
|
|
|
|
|
|
|
|
assert d == "Piff\nPaff\nPuff \n"
|
|
|
|
|
2017-11-22 21:13:32 +08:00
|
|
|
if __name__ == '__main__':
|
2017-11-23 20:56:32 +08:00
|
|
|
test_invalids(user=False)
|
|
|
|
test_invalids(user=True)
|
2018-06-04 00:57:21 +08:00
|
|
|
test_uninitialized_t()
|
2017-12-02 04:15:51 +08:00
|
|
|
|
|
|
|
test_valid_specifiers(user=False)
|
|
|
|
test_valid_specifiers(user=True)
|
2021-03-30 22:37:11 +08:00
|
|
|
|
|
|
|
test_hard_cleanup(user=False)
|
|
|
|
test_hard_cleanup(user=True)
|
2022-07-13 05:51:15 +08:00
|
|
|
|
|
|
|
test_base64()
|