From 77d74b404ee9cd6dd0c41e15789873ff287bca06 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Thu, 14 Oct 2021 14:25:20 +0200
Subject: [PATCH] Permission: add option "host_permissions"

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1115
---
 NEWS                     |  1 +
 doc/user.rst             |  5 +++++
 src/Permission.cxx       | 44 +++++++++++++++++++++++++++++++++++++---
 src/Permission.hxx       |  7 +++++++
 src/client/Listener.cxx  |  8 ++++++--
 src/config/Option.hxx    |  1 +
 src/config/Templates.cxx |  1 +
 src/net/ToString.cxx     | 29 ++++++++++++++++++++++++++
 src/net/ToString.hxx     |  8 ++++++++
 9 files changed, 99 insertions(+), 5 deletions(-)

diff --git a/NEWS b/NEWS
index b85d12751..214e54406 100644
--- a/NEWS
+++ b/NEWS
@@ -25,6 +25,7 @@ ver 0.23 (not yet released)
 * tags
   - new tags "ComposerSort", "Ensemble", "Movement", "MovementNumber", and "Location"
 * split permission "player" from "control"
+* add option "host_permissions"
 * new build-time dependency: libfmt
 
 ver 0.22.11 (2021/08/24)
diff --git a/doc/user.rst b/doc/user.rst
index 4b26ff192..1416b7be8 100644
--- a/doc/user.rst
+++ b/doc/user.rst
@@ -650,6 +650,9 @@ By default, all clients are unauthenticated and have a full set of permissions.
 
 :code:`local_permissions` may be used to assign other permissions to clients connecting on a local socket.
 
+:code:`host_permissions` may be used to assign permissions to clients
+with a certain IP address.
+
 :code:`password` allows the client to send a password to gain other permissions. This option may be specified multiple times with different passwords.
 
 Note that the :code:`password` option is not secure: passwords are sent in clear-text over the connection, and the client cannot verify the server's identity.
@@ -659,6 +662,8 @@ Example:
 .. code-block:: none
 
     default_permissions "read"
+    host_permissions "192.168.0.100 read,add,control,admin"
+    host_permissions "2003:1234:4567::1 read,add,control,admin"
     password "the_password@read,add,control"
     password "the_admin_password@read,add,control,admin"
 
diff --git a/src/Permission.cxx b/src/Permission.cxx
index a76feb195..861f0e1a2 100644
--- a/src/Permission.cxx
+++ b/src/Permission.cxx
@@ -22,6 +22,9 @@
 #include "config/Param.hxx"
 #include "config/Data.hxx"
 #include "config/Option.hxx"
+#include "net/AddressInfo.hxx"
+#include "net/Resolver.hxx"
+#include "net/ToString.hxx"
 #include "util/IterableSplitString.hxx"
 #include "util/RuntimeError.hxx"
 #include "util/StringView.hxx"
@@ -55,6 +58,10 @@ static unsigned permission_default;
 static unsigned local_permissions;
 #endif
 
+#ifdef HAVE_TCP
+static std::map<std::string, unsigned> host_passwords;
+#endif
+
 static unsigned
 ParsePermission(StringView s)
 {
@@ -66,10 +73,9 @@ ParsePermission(StringView s)
 				 int(s.size), s.data);
 }
 
-static unsigned parsePermissions(const char *string)
+static unsigned
+parsePermissions(std::string_view string)
 {
-	assert(string != nullptr);
-
 	unsigned permission = 0;
 
 	for (const auto i : IterableSplitString(string, PERMISSION_SEPARATOR))
@@ -120,8 +126,40 @@ initPermissions(const ConfigData &config)
 			: permission_default;
 	});
 #endif
+
+#ifdef HAVE_TCP
+	for (const auto &param : config.GetParamList(ConfigOption::HOST_PERMISSIONS)) {
+		permission_default = 0;
+
+		param.With([](StringView value){
+			auto [host_sv, permissions_s] = value.Split(' ');
+			unsigned permissions = parsePermissions(permissions_s);
+
+			const std::string host_s{host_sv};
+
+			for (const auto &i : Resolve(host_s.c_str(), 0,
+						     AI_PASSIVE, SOCK_STREAM))
+				host_passwords.emplace(HostToString(i),
+						       permissions);
+		});
+	}
+#endif
 }
 
+#ifdef HAVE_TCP
+
+int
+GetPermissionsFromAddress(SocketAddress address) noexcept
+{
+	if (auto i = host_passwords.find(HostToString(address));
+	    i != host_passwords.end())
+		return i->second;
+
+	return -1;
+}
+
+#endif
+
 int
 getPermissionFromPassword(const char *password, unsigned *permission) noexcept
 {
diff --git a/src/Permission.hxx b/src/Permission.hxx
index 0161fc8f1..2d9dfb9ae 100644
--- a/src/Permission.hxx
+++ b/src/Permission.hxx
@@ -23,6 +23,7 @@
 #include "config.h"
 
 struct ConfigData;
+class SocketAddress;
 
 static constexpr unsigned PERMISSION_NONE = 0;
 static constexpr unsigned PERMISSION_READ = 1;
@@ -45,6 +46,12 @@ unsigned
 GetLocalPermissions() noexcept;
 #endif
 
+#ifdef HAVE_TCP
+[[gnu::pure]]
+int
+GetPermissionsFromAddress(SocketAddress address) noexcept;
+#endif
+
 void
 initPermissions(const ConfigData &config);
 
diff --git a/src/client/Listener.cxx b/src/client/Listener.cxx
index b71055b86..4a15bf799 100644
--- a/src/client/Listener.cxx
+++ b/src/client/Listener.cxx
@@ -32,8 +32,12 @@ GetPermissions(SocketAddress address, int uid) noexcept
 #ifdef HAVE_UN
 	if (address.GetFamily() == AF_LOCAL)
 		return GetLocalPermissions();
-#else
-	(void)address;
+#endif
+
+#ifdef HAVE_TCP
+	if (int permissions = GetPermissionsFromAddress(address);
+	    permissions >= 0)
+		return permissions;
 #endif
 
 	return getDefaultPermissions();
diff --git a/src/config/Option.hxx b/src/config/Option.hxx
index e05c48fd1..a62fc691e 100644
--- a/src/config/Option.hxx
+++ b/src/config/Option.hxx
@@ -48,6 +48,7 @@ enum class ConfigOption {
 	ZEROCONF_NAME,
 	ZEROCONF_ENABLED,
 	PASSWORD,
+	HOST_PERMISSIONS,
 	LOCAL_PERMISSIONS,
 	DEFAULT_PERMS,
 	AUDIO_OUTPUT_FORMAT,
diff --git a/src/config/Templates.cxx b/src/config/Templates.cxx
index 5f3a34ddd..f3789a0e2 100644
--- a/src/config/Templates.cxx
+++ b/src/config/Templates.cxx
@@ -44,6 +44,7 @@ const ConfigTemplate config_param_templates[] = {
 	{ "zeroconf_name" },
 	{ "zeroconf_enabled" },
 	{ "password", true },
+	{ "host_permissions", true },
 	{ "local_permissions" },
 	{ "default_permissions" },
 	{ "audio_output_format" },
diff --git a/src/net/ToString.cxx b/src/net/ToString.cxx
index 8d595cd3d..b9935ea92 100644
--- a/src/net/ToString.cxx
+++ b/src/net/ToString.cxx
@@ -116,3 +116,32 @@ ToString(SocketAddress address) noexcept
 	result.append(serv);
 	return result;
 }
+
+std::string
+HostToString(SocketAddress address) noexcept
+{
+	if (address.IsNull())
+		return "null";
+
+#ifdef HAVE_UN
+	if (address.GetFamily() == AF_LOCAL)
+		/* return path of local socket */
+		return LocalAddressToString(address.CastTo<struct sockaddr_un>(),
+					    address.GetSize());
+#endif
+
+#if defined(HAVE_IPV6) && defined(IN6_IS_ADDR_V4MAPPED)
+	IPv4Address ipv4_buffer;
+	if (address.IsV4Mapped())
+		address = ipv4_buffer = address.UnmapV4();
+#endif
+
+	char host[NI_MAXHOST], serv[NI_MAXSERV];
+	int ret = getnameinfo(address.GetAddress(), address.GetSize(),
+			      host, sizeof(host), serv, sizeof(serv),
+			      NI_NUMERICHOST|NI_NUMERICSERV);
+	if (ret != 0)
+		return "unknown";
+
+	return host;
+}
diff --git a/src/net/ToString.hxx b/src/net/ToString.hxx
index 78ed421c3..3f67785c9 100644
--- a/src/net/ToString.hxx
+++ b/src/net/ToString.hxx
@@ -42,4 +42,12 @@ class SocketAddress;
 std::string
 ToString(SocketAddress address) noexcept;
 
+/**
+ * Generates the string representation of a #SocketAddress into the
+ * specified buffer, without the port number.
+ */
+[[gnu::pure]]
+std::string
+HostToString(SocketAddress address) noexcept;
+
 #endif