diff --git a/README.rst b/README.rst index 93cffec..953b728 100644 --- a/README.rst +++ b/README.rst @@ -82,19 +82,31 @@ The following configuration values are available: - ``mpd/hostname``: Which address the MPD server should bind to. - This can be a network address or the path toa Unix socket: + This can be a network address or the path to a Unix socket: - ``127.0.0.1``: Listens only on the IPv4 loopback interface (default). - ``::1``: Listens only on the IPv6 loopback interface. - ``0.0.0.0``: Listens on all IPv4 interfaces. - ``::``: Listens on all interfaces, both IPv4 and IPv6. - - ``unix:/path/to/unix/socket.sock``: Listen on the Unix socket at the + - ``unix:/var/run/mopidy/mpd.sock``: Listen on the Unix socket at the specified path. Must be prefixed with ``unix:``. + If `Mopidy is run as a system service `_, + ``mpd/socket_permissions`` must allow group write access (default) + and users must be added to the ``mopidy`` group (``usermod -a -G mopidy user``) + to communicate with the MPD server over a Unix socket. - ``mpd/port``: Which TCP port the MPD server should listen to. Default: 6600. +- ``mpd/socket_permissions``: + The octal permission value used for the Unix socket created by the MPD server + (only applies if ``mpd/hostname`` is a unix socket). + + - ``775``: rwx for user and group, r-x for others (default). + - ``777``: rwx for all users. + - ``755``: rwx for user, r-x for others. + - ``mpd/password``: The password required for connecting to the MPD server. If blank, no password is required. diff --git a/mopidy_mpd/__init__.py b/mopidy_mpd/__init__.py index 49f47e1..113faf3 100644 --- a/mopidy_mpd/__init__.py +++ b/mopidy_mpd/__init__.py @@ -20,6 +20,7 @@ def get_config_schema(self): schema = super().get_config_schema() schema["hostname"] = config.Hostname() schema["port"] = config.Port(optional=True) + schema["socket_permissions"] = config.String(optional=True) schema["password"] = config.Secret(optional=True) schema["max_connections"] = config.Integer(minimum=1) schema["connection_timeout"] = config.Integer(minimum=1) diff --git a/mopidy_mpd/actor.py b/mopidy_mpd/actor.py index 882a187..514e849 100644 --- a/mopidy_mpd/actor.py +++ b/mopidy_mpd/actor.py @@ -32,6 +32,7 @@ def __init__(self, config, core): self.hostname = network.format_hostname(config["mpd"]["hostname"]) self.port = config["mpd"]["port"] + self.socket_permissions = config["mpd"]["socket_permissions"] self.uri_map = uri_mapper.MpdUriMapper(core) self.zeroconf_name = config["mpd"]["zeroconf"] @@ -52,6 +53,7 @@ def _setup_server(self, config, core): }, max_connections=config["mpd"]["max_connections"], timeout=config["mpd"]["connection_timeout"], + socket_permissions=self.socket_permissions, ) except OSError as exc: raise exceptions.FrontendError(f"MPD server startup failed: {exc}") diff --git a/mopidy_mpd/ext.conf b/mopidy_mpd/ext.conf index ee518a8..017f21a 100644 --- a/mopidy_mpd/ext.conf +++ b/mopidy_mpd/ext.conf @@ -2,6 +2,7 @@ enabled = true hostname = 127.0.0.1 port = 6600 +socket_permissions = 775 password = max_connections = 20 connection_timeout = 60 diff --git a/mopidy_mpd/network.py b/mopidy_mpd/network.py index 63f83d9..e736de9 100644 --- a/mopidy_mpd/network.py +++ b/mopidy_mpd/network.py @@ -36,6 +36,26 @@ def get_socket_address(host, port): else: return (host, port) +def get_socket_umask(perms): + default_umask = 0o002 + if perms is None: + return default_umask + all_perms = 0o777 + mask = all_perms - int(perms, 8) + if mask < 0: + logger.error( + f"Invalid Unix socket permission value: {perms}, " + f"reverting to default permission of 775." + ) + return default_umask + elif mask >= 0o100: + logger.error( + f"Unix socket permission must allow user rwx, " + f"reverting to default permission of 775." + ) + return default_umask + else: + return mask class ShouldRetrySocketCall(Exception): @@ -99,6 +119,7 @@ def format_hostname(hostname): return hostname + class Server: """Setup listener and register it with GLib's event loop.""" @@ -111,21 +132,29 @@ def __init__( protocol_kwargs=None, max_connections=5, timeout=30, + socket_permissions=None, ): self.protocol = protocol self.protocol_kwargs = protocol_kwargs or {} self.max_connections = max_connections self.timeout = timeout - self.server_socket = self.create_server_socket(host, port) + self.server_socket = self.create_server_socket(host, port, socket_permissions) self.address = get_socket_address(host, port) + self.umask = get_socket_umask(socket_permissions) self.watcher = self.register_server_socket(self.server_socket.fileno()) - def create_server_socket(self, host, port): + def create_server_socket(self, host, port, socket_permissions=None): socket_path = get_unix_socket_path(host) if socket_path is not None: # host is a path so use unix socket sock = create_unix_socket() - sock.bind(socket_path) + # apply socket perms from config + socket_umask = get_socket_umask(socket_permissions) + oldmask = os.umask(socket_umask) + try: + sock.bind(socket_path) + finally: + os.umask(oldmask) else: # ensure the port is supplied if not isinstance(port, int): diff --git a/tests/network/test_server.py b/tests/network/test_server.py index 467d979..6e09b6c 100644 --- a/tests/network/test_server.py +++ b/tests/network/test_server.py @@ -21,7 +21,7 @@ def test_init_calls_create_server_socket(self): self.mock, sentinel.host, sentinel.port, sentinel.protocol ) self.mock.create_server_socket.assert_called_once_with( - sentinel.host, sentinel.port + sentinel.host, sentinel.port, None ) self.mock.stop() diff --git a/tests/test_actor.py b/tests/test_actor.py index cb85fad..83a418f 100644 --- a/tests/test_actor.py +++ b/tests/test_actor.py @@ -31,6 +31,7 @@ def test_idle_hooked_up_correctly(event, expected): "mpd": { "hostname": "foobar", "port": 1234, + "socket_permissions": "775", "zeroconf": None, "max_connections": None, "connection_timeout": None,