--- /dev/null
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2020 Seungbae Shin
+ based on module-zeroconf-discover.c
+
+ PulseAudio 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.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <dns_sd.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include <pulse/xmalloc.h>
+
+#include <pulsecore/core-util.h>
+#include <pulsecore/log.h>
+#include <pulsecore/hashmap.h>
+#include <pulsecore/modargs.h>
+#include <pulsecore/namereg.h>
+
+PA_MODULE_AUTHOR("Seungbae Shin");
+PA_MODULE_DESCRIPTION("mDNS/DNS-SD Service Discovery");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(true);
+
+#define SERVICE_TYPE_SINK "_pulse-sink._tcp"
+#define SERVICE_TYPE_SOURCE "_pulse-source._tcp"
+#define MAX_IP_ADDR 32
+#define MAX_RECORD_LEN 256
+
+static const char * const valid_modargs[] = {
+ NULL
+};
+
+typedef struct dns_request_data {
+ DNSServiceRef sd_ref;
+ pa_io_event *io;
+} dns_request_data_t;
+
+typedef struct service_data {
+ struct userdata *u;
+
+ int protocol;
+ dns_request_data_t resolve;
+ dns_request_data_t addr;
+
+ /* browse */
+ int flags;
+ unsigned int interface;
+ char *service_name;
+ char *service_type;
+ char *domain;
+
+ /* resolve */
+ char *host_name;
+ unsigned short port;
+
+ char *device;
+ pa_sample_spec ss;
+ pa_channel_map cm;
+ bool channel_map_set;
+
+ /* address */
+ char *ip_address;
+} service_data_t;
+
+struct tunnel {
+ unsigned int interface;
+ int protocol;
+ char *name, *type, *domain;
+ uint32_t module_index;
+};
+
+struct userdata {
+ pa_core *core;
+ pa_module *module;
+
+ dns_request_data_t browse_sink;
+ dns_request_data_t browse_source;
+
+ pa_hashmap *tunnels;
+};
+
+static unsigned tunnel_hash(const void *p) {
+ const struct tunnel *t = p;
+
+ return
+ (unsigned) t->interface +
+ (unsigned) t->protocol +
+ pa_idxset_string_hash_func(t->name) +
+ pa_idxset_string_hash_func(t->type) +
+ pa_idxset_string_hash_func(t->domain);
+}
+
+static int tunnel_compare(const void *a, const void *b) {
+ const struct tunnel *ta = a, *tb = b;
+ int r;
+
+ if (ta->interface != tb->interface)
+ return 1;
+ if (ta->protocol != tb->protocol)
+ return 1;
+ if ((r = strcmp(ta->name, tb->name)))
+ return r;
+ if ((r = strcmp(ta->type, tb->type)))
+ return r;
+ if ((r = strcmp(ta->domain, tb->domain)))
+ return r;
+
+ return 0;
+}
+
+static struct tunnel *tunnel_new(
+ unsigned int interface, int protocol,
+ const char *name, const char *type, const char *domain) {
+ struct tunnel *t;
+ t = pa_xnew0(struct tunnel, 1);
+ t->interface = interface;
+ t->protocol = protocol;
+ t->name = pa_xstrdup(name);
+ t->type = pa_xstrdup(type);
+ t->domain = pa_xstrdup(domain);
+ t->module_index = PA_IDXSET_INVALID;
+ return t;
+}
+
+static void tunnel_free(struct tunnel *t) {
+ pa_assert(t);
+ pa_xfree(t->name);
+ pa_xfree(t->type);
+ pa_xfree(t->domain);
+ pa_xfree(t);
+}
+
+static service_data_t *service_data_new() {
+ return pa_xnew0(service_data_t, 1);
+}
+
+static void service_data_free(service_data_t *data) {
+ pa_assert(data);
+
+ pa_xfree(data->service_name);
+ pa_xfree(data->service_type);
+ pa_xfree(data->domain);
+ pa_xfree(data->host_name);
+ pa_xfree(data->ip_address);
+ pa_xfree(data->device);
+
+ pa_xfree(data);
+}
+
+
+static int request_set_io_callback(pa_mainloop_api *m_api, dns_request_data_t *request, pa_io_event_cb_t cb, void *user_data) {
+ int fd = -1;
+
+ pa_assert(request);
+ pa_assert(request->sd_ref);
+
+ fd = DNSServiceRefSockFD(request->sd_ref);
+
+ if (fd < 0) {
+ pa_log_error("DNSServiceRefSockFD(%p) error", request->sd_ref);
+ return -1;
+ }
+
+ request->io = m_api->io_new(m_api, fd,
+ PA_IO_EVENT_INPUT|PA_IO_EVENT_HANGUP,
+ cb, user_data);
+
+ pa_log_info("io(%p) with cb(%p) added for fd(%d)", request->io, cb, fd);
+
+ return 0;
+}
+
+static void request_clear(pa_mainloop_api *m_api, dns_request_data_t *request) {
+ if (request->sd_ref) {
+ DNSServiceRefDeallocate(request->sd_ref);
+ request->sd_ref = NULL;
+ }
+
+ if (request->io) {
+ m_api->io_free(request->io);
+ request->io = NULL;
+ }
+}
+
+static char *get_ip_str(const struct sockaddr *sa) {
+ char addr_str[MAX_IP_ADDR] = { 0, };
+
+ switch(sa->sa_family) {
+ case AF_INET:
+ inet_ntop(AF_INET, &(((struct sockaddr_in *)sa)->sin_addr), addr_str, MAX_IP_ADDR);
+ break;
+
+ case AF_INET6:
+ inet_ntop(AF_INET6, &(((struct sockaddr_in6 *)sa)->sin6_addr), addr_str, MAX_IP_ADDR);
+ break;
+
+ default:
+ pa_strlcpy(addr_str, "Unknown AF", MAX_IP_ADDR);
+ break;
+ }
+
+ return pa_xstrdup(addr_str);
+}
+
+static void parse_txt_record(service_data_t *data,
+ const unsigned char *txt_record,
+ unsigned short txt_len) {
+ char record_str[MAX_RECORD_LEN] = { 0, };
+ int record_len;
+ const char *key_str = NULL;
+ const char *val_str = NULL;
+ const char *state = NULL;
+ size_t val_len;
+ const char *delimiter = "=";
+ const unsigned char *cur = txt_record;
+
+ pa_assert(data);
+ pa_assert(txt_record);
+ pa_assert(txt_len > 0);
+
+ while (cur < txt_record + txt_len) {
+ record_len = *cur;
+ state = NULL;
+
+ pa_snprintf(record_str, record_len + 1, "%s", ++cur);
+
+ key_str = pa_split_in_place(record_str, delimiter, &val_len, &state);
+ if (pa_strneq(key_str, "device", val_len)) {
+ data->device = pa_split(record_str, delimiter, &state);
+ } else if (pa_strneq(key_str, "rate", val_len)) {
+ data->ss.rate = (uint32_t)atoi(pa_split_in_place(record_str, delimiter, &val_len, &state));
+ } else if (pa_strneq(key_str, "channels", val_len)) {
+ data->ss.channels = (uint8_t)atoi(pa_split_in_place(record_str, delimiter, &val_len, &state));
+ } else if (pa_strneq(key_str, "format", val_len)) {
+ val_str = pa_split_in_place(record_str, delimiter, &val_len, &state);
+ data->ss.format = pa_parse_sample_format(val_str);
+ } else if (pa_strneq(key_str, "channel_map", val_len)) {
+ val_str = pa_split_in_place(record_str, delimiter, &val_len, &state);
+ pa_channel_map_parse(&data->cm, val_str);
+ }
+
+ cur += record_len;
+ }
+ pa_log_info("-----------------------------------");
+ pa_log_info("device : [%s]", data->device);
+ pa_log_info("rate : [%d]", data->ss.rate);
+ pa_log_info("channels : [%d]", data->ss.channels);
+ pa_log_info("format : [%s]", pa_sample_format_to_string(data->ss.format));
+ pa_log_info("channel_map : [%s]", pa_channel_map_to_pretty_name(&data->cm));
+ pa_log_info("-----------------------------------");
+}
+
+static void tunnel_add(service_data_t *data) {
+ struct tunnel *tnl;
+ char *dname, *module_name, *args;
+ const char *t;
+ char cmt[PA_CHANNEL_MAP_SNPRINT_MAX];
+ pa_module *m;
+
+ pa_assert(data);
+ pa_assert(data->u);
+
+ tnl = tunnel_new(data->interface, data->protocol, data->service_name, data->service_type, data->domain);
+
+ data->ss = data->u->core->default_sample_spec;
+ data->cm = data->u->core->default_channel_map;
+
+ if (!data->channel_map_set && data->cm.channels != data->ss.channels)
+ pa_channel_map_init_extend(&data->cm, data->ss.channels, PA_CHANNEL_MAP_DEFAULT);
+
+ if (!pa_sample_spec_valid(&data->ss)) {
+ pa_log("Service '%s' contains an invalid sample specification.", data->service_name);
+ goto finish;
+ }
+
+ if (!pa_channel_map_valid(&data->cm) || data->cm.channels != data->ss.channels) {
+ pa_log("Service '%s' contains an invalid channel map.", data->service_name);
+ goto finish;
+ }
+
+ if (data->device)
+ dname = pa_sprintf_malloc("tunnel.%s.%s", data->host_name, data->device);
+ else
+ dname = pa_sprintf_malloc("tunnel.%s", data->host_name);
+
+ if (!pa_namereg_is_valid_name(dname)) {
+ pa_log("Cannot construct valid device name from credentials of service '%s'.", dname);
+ pa_xfree(dname);
+ goto finish;
+ }
+
+ t = strstr(data->service_type, "sink") ? "sink" : "source";
+
+ module_name = pa_sprintf_malloc("module-tunnel-%s", t);
+ args = pa_sprintf_malloc("server=[%s]:%u "
+ "%s=%s "
+ "format=%s "
+ "channels=%u "
+ "rate=%u "
+ "%s_name=%s "
+ "channel_map=%s",
+ data->ip_address,
+ data->port,
+ t, data->device,
+ pa_sample_format_to_string(data->ss.format),
+ data->ss.channels,
+ data->ss.rate,
+ t, dname,
+ pa_channel_map_snprint(cmt, sizeof(cmt), &data->cm));
+
+ pa_log_debug("Loading %s with arguments '%s'", module_name, args);
+
+ if (pa_module_load(&m, data->u->core, module_name, args) >= 0) {
+ tnl->module_index = m->index;
+ pa_hashmap_put(data->u->tunnels, tnl, tnl);
+ pa_log_info("Module(%u) loaded, Tunnel(%p) added success", m->index, tnl);
+ tnl = NULL;
+ } else {
+ pa_log_error("Failed to load Module(%s)", module_name);
+ }
+
+ pa_xfree(module_name);
+ pa_xfree(dname);
+ pa_xfree(args);
+
+finish:
+
+ if (tnl)
+ tunnel_free(tnl);
+}
+
+
+static void dnssd_getaddrinfo_reply_cb(DNSServiceRef sd_ref,
+ unsigned int flags, unsigned int if_index,
+ DNSServiceErrorType error_code, const char *host_name,
+ const struct sockaddr *address, unsigned int ttl,
+ void *user_data) {
+ service_data_t *data = (service_data_t *)user_data;
+
+ pa_assert(data);
+ pa_assert(data->u);
+
+ if (flags & kDNSServiceFlagsMoreComing) {
+ pa_log_warn("More results are queued, No need to send callback to "
+ "application at this stage");
+ return;
+ }
+
+ if (data->interface != if_index)
+ pa_log_warn("diff interface %d <=> %d", data->interface, if_index);
+ if (!pa_safe_streq(data->host_name, host_name))
+ pa_log_warn("diff host_name %s <=> %s", data->host_name, host_name);
+
+ data->ip_address = get_ip_str(address);
+
+ pa_log_info("-----------------------------------");
+ pa_log_info("Address Reply : ref(%p), userdata(%p), error(%d), flags(0x%x)",
+ sd_ref, user_data, error_code, flags);
+ pa_log_info(" Interface Index : %d", if_index);
+ pa_log_info(" Host Name : %s", data->host_name);
+ pa_log_info(" IP Address : %s", data->ip_address);
+ pa_log_info(" TTL : %d", ttl);
+
+ pa_log_info("-----------------------------------");
+
+ tunnel_add(data);
+
+ /* cleanup addr */
+ request_clear(data->u->module->core->mainloop, &data->addr);
+
+ /* cleanup data */
+ service_data_free(data);
+}
+
+static void io_addr_event_callback(pa_mainloop_api *io, pa_io_event *e,
+ int fd, pa_io_event_flags_t events,
+ void *user_data) {
+ service_data_t *data = (service_data_t *)user_data;
+ DNSServiceErrorType err;
+
+ pa_assert(io);
+ pa_assert(data);
+ pa_assert(data->u);
+
+ if (events & (PA_IO_EVENT_HANGUP|PA_IO_EVENT_ERROR)) {
+ pa_log_warn("Lost connection fd : %d", fd);
+ goto fail;
+ }
+
+ if (events & PA_IO_EVENT_INPUT) {
+ err = DNSServiceProcessResult(data->addr.sd_ref);
+ if (err == 0)
+ pa_log_info("DNSServiceProcessResult(%p, fd:%d) success", data->addr.sd_ref, fd);
+ else
+ pa_log_info("DNSServiceProcessResult(%p, fd:%d) error(%d)", data->addr.sd_ref, fd, err);
+ }
+
+ return;
+
+fail:
+ request_clear(data->u->module->core->mainloop, &data->addr);
+}
+
+static int dnssd_getaddrinfo(service_data_t *data) {
+ int ret;
+
+ ret = DNSServiceGetAddrInfo(&(data->addr.sd_ref), data->flags, data->interface,
+ data->protocol, data->host_name,
+ dnssd_getaddrinfo_reply_cb, data);
+ if (ret != kDNSServiceErr_NoError) {
+ pa_log_error("DNSServiceGetAddrInfo() error(%d)", ret);
+ return -1;
+ }
+
+ pa_log_info("DNSServiceGetAddrInfo(%p) success", data->addr.sd_ref);
+
+ return request_set_io_callback(data->u->module->core->mainloop, &data->addr, io_addr_event_callback, data);
+}
+
+static void dnssd_resolve_reply_cb(DNSServiceRef sd_ref, unsigned int flags,
+ unsigned int if_index, DNSServiceErrorType error_code,
+ const char *fullname, const char *host_name,
+ unsigned short port, unsigned short txt_len,
+ const unsigned char *txt_record, void *user_data) {
+ int ret;
+ service_data_t *data = (service_data_t *)user_data;
+
+ pa_assert(data);
+ pa_assert(data->u);
+
+ if (flags & kDNSServiceFlagsMoreComing) {
+ pa_log_warn("More results are queued");
+ return;
+ }
+
+ if (data->interface != if_index)
+ pa_log_warn("diff interface %d <=> %d", data->interface, if_index);
+
+ data->flags = flags;
+ data->host_name = pa_xstrdup(host_name);
+ data->port = ntohs(port);
+
+ pa_log_info("-----------------------------------");
+ pa_log_info("Resolve Reply : ref(%p), userdata(%p), error(%d), flags(0x%x)",
+ sd_ref, user_data, error_code, flags);
+ pa_log_info(" Interface Index : %d", if_index);
+ pa_log_info(" Full Name : %s", fullname);
+ pa_log_info(" Host Name : %s", data->host_name);
+ pa_log_info(" Port : %d", data->port);
+ pa_log_info(" Text Record : [%p][%d]", txt_record, txt_len);
+ pa_log_info("-----------------------------------");
+
+ parse_txt_record(data, txt_record, txt_len);
+
+ /* cleanup resolve */
+ request_clear(data->u->module->core->mainloop, &data->resolve);
+
+ /* Get Address Info details and send browse callback */
+ ret = dnssd_getaddrinfo(data);
+ if (ret < 0) {
+ pa_log_error("failed to get address info!");
+ service_data_free(data);
+ }
+}
+
+
+static void io_resolve_event_callback(pa_mainloop_api *io, pa_io_event *e, int fd,
+ pa_io_event_flags_t events, void *user_data) {
+ service_data_t *data = (service_data_t *)user_data;
+ DNSServiceErrorType err;
+
+ pa_assert(io);
+ pa_assert(data);
+ pa_assert(data->u);
+
+ if (events & (PA_IO_EVENT_HANGUP|PA_IO_EVENT_ERROR)) {
+ pa_log_warn("Lost connection fd : %d", fd);
+ goto fail;
+ }
+
+ if (events & PA_IO_EVENT_INPUT) {
+ err = DNSServiceProcessResult(data->resolve.sd_ref);
+ if (err == 0)
+ pa_log_info("DNSServiceProcessResult(%p, fd:%d) success", data->resolve.sd_ref, fd);
+ else
+ pa_log_info("DNSServiceProcessResult(%p, fd:%d) error(%d)", data->resolve.sd_ref, fd, err);
+ }
+
+ return;
+
+fail:
+ request_clear(data->u->module->core->mainloop, &data->resolve);
+}
+
+
+static int dnssd_resolve_dns_service(struct userdata *u,
+ unsigned int flags, unsigned int if_index, int protocol,
+ const char *service_name, const char *service_type,
+ const char *domain) {
+ int ret;
+
+ service_data_t *data = service_data_new();
+ data->u = u;
+ data->flags = flags;
+ data->interface = if_index;
+ data->protocol = protocol;
+ data->service_name = pa_xstrdup(service_name);
+ data->service_type = pa_xstrdup(service_type);
+ data->domain = pa_xstrdup(domain);
+
+ ret = DNSServiceResolve(&(data->resolve.sd_ref),
+ flags, if_index,
+ service_name, service_type,
+ domain,
+ dnssd_resolve_reply_cb,
+ data);
+ if (ret != kDNSServiceErr_NoError) {
+ pa_log_error("DNSServiceResolve() error(%d)", ret);
+ goto fail;
+ }
+
+ pa_log_info("DNSServiceResolve(%p) success", data->resolve.sd_ref);
+
+ ret = request_set_io_callback(u->module->core->mainloop, &data->resolve, io_resolve_event_callback, data);
+ if (ret < 0)
+ goto fail;
+
+ return 0;
+
+fail:
+ service_data_free(data);
+ return -1;
+}
+
+static void dnssd_browse_reply_cb(DNSServiceRef sd_ref, unsigned int flags,
+ unsigned int if_index, DNSServiceErrorType error_code,
+ const char *service_name, const char *service_type,
+ const char *domain, void *user_data) {
+ struct userdata *u = user_data;
+ struct tunnel *t;
+ int protocol = kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6;
+
+ pa_log_info("-----------------------------------");
+ pa_log_info("Browse Reply : ref(%p), userdata(%p), error(%d), flags(0x%x)",
+ sd_ref, user_data, error_code, flags);
+ pa_log_info(" Interface Index : %d", if_index);
+ pa_log_info(" Service Name : %s", service_name);
+ pa_log_info(" Service Type : %s", service_type);
+ pa_log_info(" Domain : %s", domain);
+ pa_log_info("-----------------------------------");
+
+ t = tunnel_new(if_index, protocol, service_name, service_type, domain);
+
+ if (flags & kDNSServiceFlagsAdd) {
+ if (!pa_hashmap_get(u->tunnels, t))
+ dnssd_resolve_dns_service(u, flags, if_index, protocol,
+ service_name, service_type, domain);
+ else
+ pa_log_warn("already exists %p", t);
+ } else {
+ struct tunnel *t2;
+
+ if ((t2 = pa_hashmap_get(u->tunnels, t))) {
+ pa_module_unload_request_by_index(u->core, t2->module_index, true);
+ pa_hashmap_remove(u->tunnels, t2);
+ pa_log_info("Module(%u) Unloaded, Tunnel(%p) removed success", t2->module_index, t2);
+ tunnel_free(t2);
+ } else {
+ pa_log_warn("Not found %p", t);
+ }
+ }
+}
+
+static void io_browse_handle_event(pa_mainloop_api *io, pa_io_event *e, int fd,
+ pa_io_event_flags_t events, struct userdata *u, dns_request_data_t *request) {
+ DNSServiceErrorType err;
+
+ pa_assert(io);
+ pa_assert(u);
+
+ if (events & (PA_IO_EVENT_HANGUP|PA_IO_EVENT_ERROR)) {
+ pa_log_warn("Lost connection fd : %d", fd);
+ goto fail;
+ }
+
+ if (events & PA_IO_EVENT_INPUT) {
+ err = DNSServiceProcessResult(request->sd_ref);
+ if (err == 0)
+ pa_log_info("DNSServiceProcessResult(%p, fd:%d) success", request->sd_ref, fd);
+ else
+ pa_log_info("DNSServiceProcessResult(%p, fd:%d) error(%d)", request->sd_ref, fd, err);
+ }
+
+ return;
+
+fail:
+ request_clear(u->module->core->mainloop, request);
+ pa_module_unload_request(u->module, true);
+}
+
+static void io_browse_sink_event_callback(pa_mainloop_api *io, pa_io_event *e, int fd,
+ pa_io_event_flags_t events, void *user_data) {
+ struct userdata *u = user_data;
+
+ io_browse_handle_event(io, e, fd, events, u, &u->browse_sink);
+}
+
+static void io_browse_source_event_callback(pa_mainloop_api *io, pa_io_event *e, int fd,
+ pa_io_event_flags_t events, void *user_data) {
+ struct userdata *u = user_data;
+
+ io_browse_handle_event(io, e, fd, events, u, &u->browse_source);
+}
+
+static int dnssd_browse_dns_service(struct userdata *u, const char *service_type) {
+ int ret;
+ dns_request_data_t *request;
+ pa_io_event_cb_t cb;
+
+ if (pa_safe_streq(service_type, SERVICE_TYPE_SINK)) {
+ request = &u->browse_sink;
+ cb = io_browse_sink_event_callback;
+ } else if (pa_safe_streq(service_type, SERVICE_TYPE_SOURCE)) {
+ request = &u->browse_source;
+ cb = io_browse_source_event_callback;
+ } else {
+ pa_log_error("invalid service type %s", pa_strnull(service_type));
+ return -1;
+ }
+
+ ret = DNSServiceBrowse(&request->sd_ref, 0,
+ kDNSServiceInterfaceIndexAny, service_type,
+ NULL, dnssd_browse_reply_cb,
+ u);
+ if (ret != kDNSServiceErr_NoError) {
+ pa_log_error("DNSServiceBrowse() with %s error(%d)", service_type, ret);
+ return -1;
+ }
+
+ pa_log_info("DNSServiceBrowse(%p) success", request->sd_ref);
+
+ ret = request_set_io_callback(u->module->core->mainloop, request, cb, u);
+ if (ret < 0) {
+ request_clear(u->module->core->mainloop, request);
+ return -1;
+ }
+
+ return 0;
+}
+
+void pa__done(pa_module *m) {
+ struct userdata *u;
+ pa_assert(m);
+
+ if (!(u = m->userdata))
+ return;
+
+ if (u->tunnels) {
+ struct tunnel *t;
+
+ while ((t = pa_hashmap_steal_first(u->tunnels))) {
+ pa_module_unload_request_by_index(u->core, t->module_index, true);
+ tunnel_free(t);
+ }
+
+ pa_hashmap_free(u->tunnels);
+ }
+
+ request_clear(u->module->core->mainloop, &u->browse_sink);
+ request_clear(u->module->core->mainloop, &u->browse_source);
+
+ pa_xfree(u);
+}
+
+int pa__init(pa_module *m) {
+
+ struct userdata *u;
+ pa_modargs *ma = NULL;
+ int ret;
+
+ if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
+ pa_log("Failed to parse module arguments.");
+ goto fail;
+ }
+
+ m->userdata = u = pa_xnew(struct userdata, 1);
+ u->core = m->core;
+ u->module = m;
+ u->tunnels = pa_hashmap_new(tunnel_hash, tunnel_compare);
+
+ ret = dnssd_browse_dns_service(u, SERVICE_TYPE_SINK);
+ if (ret < 0)
+ goto fail;
+
+ ret = dnssd_browse_dns_service(u, SERVICE_TYPE_SOURCE);
+ if (ret < 0)
+ goto fail;
+
+ pa_modargs_free(ma);
+
+ return 0;
+
+fail:
+ pa__done(m);
+
+ if (ma)
+ pa_modargs_free(ma);
+
+ return -1;
+}
--- /dev/null
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2020 Seungbae Shin
+ based on module-bonjour-publish.c
+
+ PulseAudio 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.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <dns_sd.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include <pulse/xmalloc.h>
+#include <pulse/util.h>
+
+#include <pulsecore/parseaddr.h>
+#include <pulsecore/sink.h>
+#include <pulsecore/source.h>
+#include <pulsecore/native-common.h>
+#include <pulsecore/core-util.h>
+#include <pulsecore/log.h>
+#include <pulsecore/dynarray.h>
+#include <pulsecore/modargs.h>
+#include <pulsecore/protocol-native.h>
+
+PA_MODULE_AUTHOR("Seungbae Shin");
+PA_MODULE_DESCRIPTION("mDNS/DNS-SD Service Publisher");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(true);
+
+#define SERVICE_TYPE_SINK "_pulse-sink._tcp"
+#define SERVICE_TYPE_SOURCE "_pulse-source._tcp"
+#define SERVICE_TYPE_SERVER "_pulse-server._tcp"
+
+static const char* const valid_modargs[] = {
+ NULL
+};
+
+enum service_subtype {
+ SUBTYPE_HARDWARE,
+ SUBTYPE_VIRTUAL,
+ SUBTYPE_MONITOR
+};
+
+struct service {
+ struct userdata *userdata;
+ DNSServiceRef service;
+ DNSRecordRef rec, rec2;
+ char *service_name;
+ pa_object *device;
+ enum service_subtype subtype;
+};
+
+struct userdata {
+ pa_core *core;
+ pa_module *module;
+
+ pa_hashmap *services;
+ char *service_name;
+
+ pa_hook_slot *sink_new_slot, *source_new_slot, *sink_unlink_slot, *source_unlink_slot, *sink_changed_slot, *source_changed_slot;
+
+ pa_native_protocol *native;
+ DNSServiceRef main_service;
+};
+
+static void get_service_data(struct service *s, pa_sample_spec *ret_ss, pa_channel_map *ret_map, const char **ret_name, pa_proplist **ret_proplist, enum service_subtype *ret_subtype) {
+ pa_assert(s);
+ pa_assert(ret_ss);
+ pa_assert(ret_proplist);
+ pa_assert(ret_subtype);
+
+ if (pa_sink_isinstance(s->device)) {
+ pa_sink *sink = PA_SINK(s->device);
+
+ *ret_ss = sink->sample_spec;
+ *ret_map = sink->channel_map;
+ *ret_name = sink->name;
+ *ret_proplist = sink->proplist;
+ *ret_subtype = sink->flags & PA_SINK_HARDWARE ? SUBTYPE_HARDWARE : SUBTYPE_VIRTUAL;
+
+ } else if (pa_source_isinstance(s->device)) {
+ pa_source *source = PA_SOURCE(s->device);
+
+ *ret_ss = source->sample_spec;
+ *ret_map = source->channel_map;
+ *ret_name = source->name;
+ *ret_proplist = source->proplist;
+ *ret_subtype = source->monitor_of ? SUBTYPE_MONITOR : (source->flags & PA_SOURCE_HARDWARE ? SUBTYPE_HARDWARE : SUBTYPE_VIRTUAL);
+
+ } else {
+ pa_assert_not_reached();
+ }
+}
+
+static void txt_record_server_data(pa_core *c, TXTRecordRef *txt) {
+ char s[128];
+ char *t;
+
+ pa_assert(c);
+
+ TXTRecordSetValue(txt, "server-version", strlen(PACKAGE_NAME" "PACKAGE_VERSION), PACKAGE_NAME" "PACKAGE_VERSION);
+
+ t = pa_get_user_name_malloc();
+ TXTRecordSetValue(txt, "user-name", strlen(t), t);
+ pa_xfree(t);
+
+ t = pa_machine_id();
+ TXTRecordSetValue(txt, "machine-id", strlen(t), t);
+ pa_xfree(t);
+
+ t = pa_uname_string();
+ TXTRecordSetValue(txt, "uname", strlen(t), t);
+ pa_xfree(t);
+
+ t = pa_get_fqdn(s, sizeof(s));
+ TXTRecordSetValue(txt, "fqdn", strlen(t), t);
+
+ snprintf(s, sizeof(s), "0x%08x", c->cookie);
+ TXTRecordSetValue(txt, "cookie", strlen(s), s);
+}
+
+static void service_free(struct service *s);
+
+static void dns_service_register_reply(DNSServiceRef sdRef,
+ DNSServiceFlags flags,
+ DNSServiceErrorType errorCode,
+ const char *name,
+ const char *regtype,
+ const char *domain,
+ void *context) {
+ struct service *s = context;
+
+ pa_assert(s);
+
+ switch (errorCode) {
+ case kDNSServiceErr_NameConflict:
+ pa_log("DNS service reported kDNSServiceErr_NameConflict\n");
+ service_free(s);
+ break;
+
+ case kDNSServiceErr_NoError:
+ /* fall through */
+ default:
+ break;
+ }
+}
+
+static uint16_t compute_port(struct userdata *u) {
+ pa_strlist *i;
+
+ pa_assert(u);
+
+ for (i = pa_native_protocol_servers(u->native); i; i = pa_strlist_next(i)) {
+ pa_parsed_address a;
+
+ if (pa_parse_address(pa_strlist_data(i), &a) >= 0 &&
+ (a.type == PA_PARSED_ADDRESS_TCP4 ||
+ a.type == PA_PARSED_ADDRESS_TCP6 ||
+ a.type == PA_PARSED_ADDRESS_TCP_AUTO) &&
+ a.port > 0) {
+
+ pa_xfree(a.path_or_host);
+ return htons(a.port);
+ }
+
+ pa_xfree(a.path_or_host);
+ }
+
+ return htons(PA_NATIVE_DEFAULT_PORT);
+}
+
+static int publish_service(struct service *s) {
+ int r = -1;
+ TXTRecordRef txt;
+ DNSServiceErrorType err;
+ const char *name = NULL, *t;
+ pa_proplist *proplist = NULL;
+ pa_sample_spec ss;
+ pa_channel_map map;
+ char cm[PA_CHANNEL_MAP_SNPRINT_MAX], tmp[64];
+ enum service_subtype subtype;
+
+ const char * const subtype_text[] = {
+ [SUBTYPE_HARDWARE] = "hardware",
+ [SUBTYPE_VIRTUAL] = "virtual",
+ [SUBTYPE_MONITOR] = "monitor"
+ };
+
+ pa_assert(s);
+
+ if (s->service) {
+ DNSServiceRefDeallocate(s->service);
+ s->service = NULL;
+ }
+
+ TXTRecordCreate(&txt, 0, NULL);
+
+ txt_record_server_data(s->userdata->core, &txt);
+
+ get_service_data(s, &ss, &map, &name, &proplist, &subtype);
+ TXTRecordSetValue(&txt, "device", strlen(name), name);
+
+ snprintf(tmp, sizeof(tmp), "%u", ss.rate);
+ TXTRecordSetValue(&txt, "rate", strlen(tmp), tmp);
+
+ snprintf(tmp, sizeof(tmp), "%u", ss.channels);
+ TXTRecordSetValue(&txt, "channels", strlen(tmp), tmp);
+
+ t = pa_sample_format_to_string(ss.format);
+ TXTRecordSetValue(&txt, "format", strlen(t), t);
+
+ t = pa_channel_map_snprint(cm, sizeof(cm), &map);
+ TXTRecordSetValue(&txt, "channel_map", strlen(t), t);
+
+ t = subtype_text[subtype];
+ TXTRecordSetValue(&txt, "subtype", strlen(t), t);
+
+ if ((t = pa_proplist_gets(proplist, PA_PROP_DEVICE_DESCRIPTION)))
+ TXTRecordSetValue(&txt, "description", strlen(t), t);
+ if ((t = pa_proplist_gets(proplist, PA_PROP_DEVICE_ICON_NAME)))
+ TXTRecordSetValue(&txt, "icon-name", strlen(t), t);
+ if ((t = pa_proplist_gets(proplist, PA_PROP_DEVICE_VENDOR_NAME)))
+ TXTRecordSetValue(&txt, "vendor-name", strlen(t), t);
+ if ((t = pa_proplist_gets(proplist, PA_PROP_DEVICE_PRODUCT_NAME)))
+ TXTRecordSetValue(&txt, "product-name", strlen(t), t);
+ if ((t = pa_proplist_gets(proplist, PA_PROP_DEVICE_CLASS)))
+ TXTRecordSetValue(&txt, "class", strlen(t), t);
+ if ((t = pa_proplist_gets(proplist, PA_PROP_DEVICE_FORM_FACTOR)))
+ TXTRecordSetValue(&txt, "form-factor", strlen(t), t);
+
+ err = DNSServiceRegister(&s->service,
+ 0, /* flags */
+ kDNSServiceInterfaceIndexAny,
+ s->service_name,
+ pa_sink_isinstance(s->device) ? SERVICE_TYPE_SINK : SERVICE_TYPE_SOURCE,
+ NULL, /* domain */
+ NULL, /* host */
+ compute_port(s->userdata),
+ TXTRecordGetLength(&txt),
+ TXTRecordGetBytesPtr(&txt),
+ dns_service_register_reply, s);
+
+ if (err != kDNSServiceErr_NoError) {
+ pa_log("DNSServiceRegister() returned err %d", err);
+ goto finish;
+ }
+
+ pa_log_debug("Successfully registered mDNS services for >%s<.", s->service_name);
+ return 0;
+
+finish:
+
+ /* Remove this service */
+ if (r < 0)
+ service_free(s);
+
+ TXTRecordDeallocate(&txt);
+
+ return r;
+}
+
+static struct service *get_service(struct userdata *u, pa_object *device) {
+ struct service *s;
+ char *hn, *un;
+ const char *n;
+
+ pa_assert(u);
+ pa_object_assert_ref(device);
+
+ if ((s = pa_hashmap_get(u->services, device)))
+ return s;
+
+ s = pa_xnew0(struct service, 1);
+ s->userdata = u;
+ s->device = device;
+
+ if (pa_sink_isinstance(device)) {
+ if (!(n = pa_proplist_gets(PA_SINK(device)->proplist, PA_PROP_DEVICE_DESCRIPTION)))
+ n = PA_SINK(device)->name;
+ } else {
+ if (!(n = pa_proplist_gets(PA_SOURCE(device)->proplist, PA_PROP_DEVICE_DESCRIPTION)))
+ n = PA_SOURCE(device)->name;
+ }
+
+ hn = pa_get_host_name_malloc();
+ un = pa_get_user_name_malloc();
+
+ s->service_name = pa_truncate_utf8(pa_sprintf_malloc("%s@%s: %s", un, hn, n), kDNSServiceMaxDomainName-1);
+
+ pa_xfree(un);
+ pa_xfree(hn);
+
+ pa_hashmap_put(u->services, s->device, s);
+
+ return s;
+}
+
+static void service_free(struct service *s) {
+ pa_assert(s);
+
+ pa_hashmap_remove(s->userdata->services, s->device);
+
+ if (s->service)
+ DNSServiceRefDeallocate(s->service);
+
+ pa_xfree(s->service_name);
+ pa_xfree(s);
+}
+
+static bool shall_ignore(pa_object *o) {
+ pa_object_assert_ref(o);
+
+ if (pa_sink_isinstance(o))
+ return !!(PA_SINK(o)->flags & PA_SINK_NETWORK);
+
+ if (pa_source_isinstance(o))
+ return PA_SOURCE(o)->monitor_of || (PA_SOURCE(o)->flags & PA_SOURCE_NETWORK);
+
+ pa_assert_not_reached();
+}
+
+static pa_hook_result_t device_new_or_changed_cb(pa_core *c, pa_object *o, struct userdata *u) {
+ pa_assert(c);
+ pa_object_assert_ref(o);
+
+ if (!shall_ignore(o))
+ publish_service(get_service(u, o));
+
+ return PA_HOOK_OK;
+}
+
+static pa_hook_result_t device_unlink_cb(pa_core *c, pa_object *o, struct userdata *u) {
+ struct service *s;
+
+ pa_assert(c);
+ pa_object_assert_ref(o);
+
+ if ((s = pa_hashmap_get(u->services, o)))
+ service_free(s);
+
+ return PA_HOOK_OK;
+}
+
+static int publish_main_service(struct userdata *u) {
+ DNSServiceErrorType err;
+ TXTRecordRef txt;
+
+ pa_assert(u);
+
+ if (u->main_service) {
+ DNSServiceRefDeallocate(u->main_service);
+ u->main_service = NULL;
+ }
+
+ TXTRecordCreate(&txt, 0, NULL);
+ txt_record_server_data(u->core, &txt);
+
+ err = DNSServiceRegister(&u->main_service,
+ 0, /* flags */
+ kDNSServiceInterfaceIndexAny,
+ u->service_name,
+ SERVICE_TYPE_SERVER,
+ NULL, /* domain */
+ NULL, /* host */
+ compute_port(u),
+ TXTRecordGetLength(&txt),
+ TXTRecordGetBytesPtr(&txt),
+ NULL, NULL);
+
+ if (err != kDNSServiceErr_NoError) {
+ pa_log("DNSServiceRegister() returned err %d", err);
+ return err;
+ }
+
+ TXTRecordDeallocate(&txt);
+
+ return 0;
+}
+
+static int publish_all_services(struct userdata *u) {
+ pa_sink *sink;
+ pa_source *source;
+ uint32_t idx;
+
+ pa_assert(u);
+
+ pa_log_debug("Publishing services in mDNS");
+
+ for (sink = PA_SINK(pa_idxset_first(u->core->sinks, &idx)); sink; sink = PA_SINK(pa_idxset_next(u->core->sinks, &idx)))
+ if (!shall_ignore(PA_OBJECT(sink)))
+ publish_service(get_service(u, PA_OBJECT(sink)));
+
+ for (source = PA_SOURCE(pa_idxset_first(u->core->sources, &idx)); source; source = PA_SOURCE(pa_idxset_next(u->core->sources, &idx)))
+ if (!shall_ignore(PA_OBJECT(source)))
+ publish_service(get_service(u, PA_OBJECT(source)));
+
+ return publish_main_service(u);
+}
+
+static void unpublish_all_services(struct userdata *u) {
+ void *state = NULL;
+ struct service *s;
+
+ pa_assert(u);
+
+ pa_log_debug("Unpublishing services in mDNS");
+
+ while ((s = pa_hashmap_iterate(u->services, &state, NULL)))
+ service_free(s);
+
+ if (u->main_service)
+ DNSServiceRefDeallocate(u->main_service);
+}
+
+void pa__done(pa_module *m) {
+ struct userdata *u;
+ pa_assert(m);
+
+ if (!(u = m->userdata))
+ return;
+
+ unpublish_all_services(u);
+
+ if (u->services)
+ pa_hashmap_free(u->services);
+
+ if (u->sink_new_slot)
+ pa_hook_slot_free(u->sink_new_slot);
+ if (u->source_new_slot)
+ pa_hook_slot_free(u->source_new_slot);
+ if (u->sink_changed_slot)
+ pa_hook_slot_free(u->sink_changed_slot);
+ if (u->source_changed_slot)
+ pa_hook_slot_free(u->source_changed_slot);
+ if (u->sink_unlink_slot)
+ pa_hook_slot_free(u->sink_unlink_slot);
+ if (u->source_unlink_slot)
+ pa_hook_slot_free(u->source_unlink_slot);
+
+ if (u->native)
+ pa_native_protocol_unref(u->native);
+
+ pa_xfree(u->service_name);
+ pa_xfree(u);
+}
+
+int pa__init(pa_module *m) {
+ struct userdata *u;
+ pa_modargs *ma = NULL;
+ char *hn, *un;
+
+ if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
+ pa_log("Failed to parse module arguments.");
+ goto fail;
+ }
+
+ m->userdata = u = pa_xnew0(struct userdata, 1);
+ u->core = m->core;
+ u->module = m;
+ u->native = pa_native_protocol_get(u->core);
+
+ u->services = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ u->sink_new_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_PUT], PA_HOOK_LATE, (pa_hook_cb_t) device_new_or_changed_cb, u);
+ u->sink_changed_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_PROPLIST_CHANGED], PA_HOOK_LATE, (pa_hook_cb_t) device_new_or_changed_cb, u);
+ u->sink_unlink_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_UNLINK], PA_HOOK_LATE, (pa_hook_cb_t) device_unlink_cb, u);
+ u->source_new_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_PUT], PA_HOOK_LATE, (pa_hook_cb_t) device_new_or_changed_cb, u);
+ u->source_changed_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_PROPLIST_CHANGED], PA_HOOK_LATE, (pa_hook_cb_t) device_new_or_changed_cb, u);
+ u->source_unlink_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_UNLINK], PA_HOOK_LATE, (pa_hook_cb_t) device_unlink_cb, u);
+
+ un = pa_get_user_name_malloc();
+ hn = pa_get_host_name_malloc();
+ u->service_name = pa_truncate_utf8(pa_sprintf_malloc("%s@%s", un, hn), kDNSServiceMaxDomainName-1);
+ pa_xfree(un);
+ pa_xfree(hn);
+
+ if (publish_all_services(u) != 0)
+ goto fail;
+
+ pa_modargs_free(ma);
+
+ return 0;
+
+fail:
+ pa__done(m);
+
+ if (ma)
+ pa_modargs_free(ma);
+
+ return -1;
+}