--- /dev/null +++ b/minissdpd/ssdpd.c @@ -0,0 +1,626 @@ +/* + * Copyright (C) 2022 iopsys Software Solutions AB + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation + * + * Author: Amin Ben Romdhane + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "codelength.h" + +struct UPNPDev { + struct list_head list; + char *descURL; + char *st; + char *usn; +}; + +struct desc_list_elt { + struct list_head list; + char *url; + char *desc_path; + bool is_device_desc; +}; + +#define UPNP_DESC_PATH "/etc/upnp/description" +#define UPNP_DISCOVER_TIMEOUT (30 * 1000) + +#ifndef MIN +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#endif /* MIN */ + +/* macros used to read from unix socket */ +#define READ_BYTE_BUFFER(c) \ + if ((int)bufferindex >= n) { \ + n = read(s, buffer, sizeof(buffer)); \ + if (n <= 0) break; \ + bufferindex = 0; \ + } \ + c = buffer[bufferindex++]; + +#define READ_COPY_BUFFER(dst, len) \ + for (l = len, p = (unsigned char *)dst; l > 0; ) { \ + unsigned int lcopy; \ + if ((int)bufferindex >= n) { \ + n = read(s, buffer, sizeof(buffer)); \ + if ( n<= 0) break; \ + bufferindex = 0; \ + } \ + lcopy = MIN(l, (n - bufferindex)); \ + memcpy(p, buffer + bufferindex, lcopy); \ + l -= lcopy; \ + p += lcopy; \ + bufferindex += lcopy; \ + } + +LIST_HEAD(dev_list); +LIST_HEAD(desc_list); + +char *ssdp_sockpath = NULL; + +static void upnp_discover_devices(struct uloop_timeout *timeout); +static struct uloop_timeout upnpdiscover_timer = { .cb = upnp_discover_devices }; + +static void add_dev_to_dev_list(char *descURL, char *st, char *usn) +{ + struct UPNPDev *dev = NULL; + + dev = calloc(1, sizeof(struct UPNPDev)); + list_add_tail(&dev->list, &dev_list); + + dev->descURL = descURL; + dev->st = st; + dev->usn = usn; +} + +void free_all_dev_list(void) +{ + struct UPNPDev *dev = NULL; + + while (dev_list.next != &dev_list) { + dev = list_entry(dev_list.next, struct UPNPDev, list); + free(dev->descURL); + free(dev->st); + free(dev->usn); + free(dev); + list_del(&dev->list); + } +} + +static int connectToMiniSSDPD(void) +{ + int s = 0; + struct sockaddr_un addr; + + s = socket(AF_UNIX, SOCK_STREAM, 0); + if(s < 0) + return -1; + + char *ssdp_s = ssdp_sockpath ? ssdp_sockpath : "/var/run/minissdpd.sock"; + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + + strncpy(addr.sun_path, ssdp_s, sizeof(addr.sun_path)); + + if(connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) { + close(s); + return -1; + } + + return s; +} + +static int disconnectFromMiniSSDPD(int s) +{ + if (close(s) < 0) + return -1; + return 0; +} + +static int requestDevicesFromMiniSSDPD(int s) +{ + unsigned char buffer[256]; + unsigned char *p = NULL; + unsigned int stsize = 0, l = 0; + char *devtype = "ssdp:all"; + + buffer[0] = 3; /* request type 3 : everything */ + stsize = strlen(devtype); + + p = buffer + 1; + l = stsize; CODELENGTH(l, p); + if (p + stsize > buffer + sizeof(buffer)) + return -1; + + memcpy(p, devtype, stsize); + p += stsize; + if (write(s, buffer, p - buffer) < 0) + return -1; + + return 0; +} + +static int receiveDevicesFromMiniSSDPD(int s) +{ + unsigned char buffer[256]; + ssize_t n; + unsigned char *p; + unsigned int bufferindex; + unsigned int i, ndev; + unsigned int urlsize, stsize, usnsize, l; + char *url, *st, *usn; + + n = read(s, buffer, sizeof(buffer)); + if (n <= 0) + return -1; + + ndev = buffer[0]; + bufferindex = 1; + for (i = 0; i < ndev; i++) { + DECODELENGTH_READ(urlsize, READ_BYTE_BUFFER); + if (n <= 0) + return -1; + + url = (char *)malloc(urlsize); + if (url == NULL) + return -1; + + READ_COPY_BUFFER(url, urlsize); + if (n <= 0) + return -1; + + DECODELENGTH_READ(stsize, READ_BYTE_BUFFER); + if (n <= 0) + goto free_url_and_return; + + st = (char *)malloc(stsize); + if (st == NULL) + goto free_url_and_return; + + READ_COPY_BUFFER(st, stsize); + if (n <= 0) + goto free_url_and_st_and_return; + + DECODELENGTH_READ(usnsize, READ_BYTE_BUFFER); + if (n <= 0) + goto free_url_and_st_and_return; + + usn = (char *)malloc(usnsize); + if (usn == NULL) + goto free_url_and_st_and_return; + + READ_COPY_BUFFER(usn, usnsize); + if (n <= 0) + goto free_url_and_st_and_usn_and_return; + + add_dev_to_dev_list(url, st, usn); + } + + return 0; + +free_url_and_st_and_usn_and_return: + free(usn); +free_url_and_st_and_return: + free(st); +free_url_and_return: + free(url); + return -1; +} + +static int getDevicesFromMiniSSDPD(void) +{ + int s = 0; + int res = 0; + + s = connectToMiniSSDPD(); + if (s < 0) + return -1; + + res = requestDevicesFromMiniSSDPD(s); + if (res < 0) + goto close_socket_and_return; + + res = receiveDevicesFromMiniSSDPD(s); + +close_socket_and_return: + disconnectFromMiniSSDPD(s); + + return res; +} + +static void download_file(char *file_path, const char *url) +{ + CURL *curl = curl_easy_init(); + if (curl) { + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 500); + + FILE *fp = fopen(file_path, "wb"); + if (fp) { + curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); + curl_easy_perform(curl); + fclose(fp); + } + + curl_easy_cleanup(curl); + } +} + +static bool is_desc_exist(const char *desc_url) +{ + struct desc_list_elt *desc_elt = NULL; + + if (!desc_url) + return false; + + list_for_each_entry(desc_elt, &desc_list, list) { + if (strcmp(desc_elt->url, desc_url) == 0) + return true; + } + + return false; +} + +static void get_desc_name(const char *desc_url, char *str, size_t len) +{ + if (!desc_url || !str || len == 0) + return; + + char *p = strstr(desc_url, "://"); + + snprintf(str, len, "%s", p ? p + 3 : desc_url); + + for (int i = 0; str[i]; i++) { + if (str[i] == '/') + str[i] = '_'; + } +} + +static void add_desc_to_desc_list(const char *desc_path, const char *url, int is_device_desc) +{ + struct desc_list_elt *desc_elt; + + desc_elt = calloc(1, sizeof(struct desc_list_elt)); + list_add_tail(&desc_elt->list, &desc_list); + + desc_elt->desc_path = strdup(desc_path); + desc_elt->url = strdup(url); + desc_elt->is_device_desc = is_device_desc; +} + +static void free_all_desc_list(void) +{ + struct desc_list_elt *desc_elt = NULL; + + while (desc_list.next != &desc_list) { + desc_elt = list_entry(desc_list.next, struct desc_list_elt, list); + free(desc_elt->desc_path); + free(desc_elt->url); + free(desc_elt); + list_del(&desc_elt->list); + } +} + +static void __upnp_discover_devices(void) +{ + struct UPNPDev *dev = NULL; + char desc_name[128] = {0}; + char file_path[256] = {0}; + int res = 0, is_device_desc = 0; + + /* + * Discover devices + */ + if (!list_empty(&dev_list)) + free_all_dev_list(); + + res = getDevicesFromMiniSSDPD(); + if (res) + goto end; + + /* + * Download description files + */ + list_for_each_entry_reverse(dev, &dev_list, list) { + + if (is_desc_exist(dev->descURL)) + continue; + + get_desc_name(dev->descURL, desc_name, sizeof(desc_name)); + snprintf(file_path, sizeof(file_path), "%s/%s", UPNP_DESC_PATH, desc_name); + is_device_desc = (dev->usn && strstr(dev->usn, ":service:")) ? 0 : 1; + + // Download Description + download_file(file_path, dev->descURL); + + // Add description to descriptions list + add_desc_to_desc_list(file_path, dev->descURL, is_device_desc); + } + +end: + uloop_timeout_set(&upnpdiscover_timer, UPNP_DISCOVER_TIMEOUT); +} + +static int upnp_discovery_res(struct ubus_context *ctx, struct ubus_object *obj __attribute__((unused)), + struct ubus_request_data *req, const char *method __attribute__((unused)), struct blob_attr *msg __attribute__((unused))) +{ + struct blob_buf bb = {0}; + struct UPNPDev *dev = NULL; + + memset(&bb,0,sizeof(struct blob_buf)); + blob_buf_init(&bb, 0); + + void *devices_array = blobmsg_open_array(&bb, "devices"); + list_for_each_entry_reverse(dev, &dev_list, list) { + // Parse Root device and devices + if ((dev->st && strstr(dev->st, ":rootdevice") != NULL) || (dev->usn && strstr(dev->usn, ":device:") != NULL)) { + void *device_obj = blobmsg_open_table(&bb, NULL); + blobmsg_add_string(&bb, "descurl", dev->descURL); + blobmsg_add_string(&bb, "st", dev->st); + blobmsg_add_string(&bb, "usn", dev->usn); + blobmsg_add_string(&bb, "is_root_device", dev->st && strstr(dev->st, ":rootdevice") ? "1" : "0"); + blobmsg_close_table(&bb, device_obj); + } + } + blobmsg_close_array(&bb, devices_array); + + void *services_array = blobmsg_open_array(&bb, "services"); + list_for_each_entry_reverse(dev, &dev_list, list) { + // Parse Services + if (dev->usn && strstr(dev->usn, ":service:") != NULL) { + void *service_obj = blobmsg_open_table(&bb, NULL); + blobmsg_add_string(&bb, "descurl", dev->descURL); + blobmsg_add_string(&bb, "st", dev->st); + blobmsg_add_string(&bb, "usn", dev->usn); + blobmsg_close_table(&bb, service_obj); + } + } + blobmsg_close_array(&bb, services_array); + + ubus_send_reply(ctx, req, bb.head); + blob_buf_free(&bb); + return 0; +} + +static void fill_device_instances(struct blob_buf *bb, mxml_node_t *device) +{ + void *device_obj = NULL; + mxml_node_t *b = device; + char buf[64] = {0}; + bool new_device_discovery = false; + + while (b) { + + if (mxmlGetType(b) != MXML_ELEMENT) { + b = mxmlWalkNext(b, device, MXML_DESCEND); + continue; + } + + const char *elm_name = mxmlGetElement(b); + const char *elm_val = mxmlGetOpaque(b); + + if (elm_name && strcmp(elm_name, "device") == 0) { + + if (new_device_discovery && device_obj) + blobmsg_close_table(bb, device_obj); + + device_obj = blobmsg_open_table(bb, NULL); + blobmsg_add_string(bb, "parent_dev", buf); + new_device_discovery = true; + } + + if (elm_name && strcmp(elm_name, "deviceType") == 0 && new_device_discovery) + blobmsg_add_string(bb, "deviceType", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "friendlyName") == 0 && new_device_discovery) + blobmsg_add_string(bb, "friendlyName", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "manufacturer") == 0 && new_device_discovery) + blobmsg_add_string(bb, "manufacturer", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "manufacturerURL") == 0 && new_device_discovery) + blobmsg_add_string(bb, "manufacturerURL", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "modelDescription") == 0 && new_device_discovery) + blobmsg_add_string(bb, "modelDescription", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "modelName") == 0 && new_device_discovery) + blobmsg_add_string(bb, "modelName", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "modelNumber") == 0 && new_device_discovery) + blobmsg_add_string(bb, "modelNumber", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "modelURL") == 0 && new_device_discovery) + blobmsg_add_string(bb, "modelURL", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "serialNumber") == 0 && new_device_discovery) + blobmsg_add_string(bb, "serialNumber", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "UDN") == 0 && new_device_discovery) { + snprintf(buf, sizeof(buf), "%s", elm_val ? elm_val : ""); + blobmsg_add_string(bb, "UDN", buf); + } + + if (elm_name && strcmp(elm_name, "UPC") == 0 && new_device_discovery) + blobmsg_add_string(bb, "UPC", elm_val ? elm_val : ""); + + b = mxmlWalkNext(b, device, MXML_DESCEND); + } + + if (new_device_discovery && device_obj) + blobmsg_close_table(bb, device_obj); +} + +static void fill_service_element(struct blob_buf *bb, mxml_node_t *service) +{ + mxml_node_t *b = service; + void *service_obj = NULL; + char buf[64] = {0}; + bool new_srv_discovery = false; + + while (b) { + + if (mxmlGetType(b) != MXML_ELEMENT) { + b = mxmlWalkNext(b, service, MXML_DESCEND); + continue; + } + + const char *elm_name = mxmlGetElement(b); + const char *elm_val = mxmlGetOpaque(b); + + if (elm_name && strcmp(elm_name, "UDN") == 0) + snprintf(buf, sizeof(buf), "%s", elm_val ? elm_val : ""); + + + if (elm_name && strcmp(elm_name, "service") == 0) { + + if (new_srv_discovery && service_obj) + blobmsg_close_table(bb, service_obj); + + service_obj = blobmsg_open_table(bb, NULL); + blobmsg_add_string(bb, "parent_dev", buf); + new_srv_discovery = true; + } + + if (elm_name && strcmp(elm_name, "serviceType") == 0 && new_srv_discovery) + blobmsg_add_string(bb, "serviceType", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "serviceId") == 0 && new_srv_discovery) + blobmsg_add_string(bb, "serviceId", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "SCPDURL") == 0 && new_srv_discovery) + blobmsg_add_string(bb, "SCPDURL", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "controlURL") == 0 && new_srv_discovery) + blobmsg_add_string(bb, "controlURL", elm_val ? elm_val : ""); + + if (elm_name && strcmp(elm_name, "eventSubURL") == 0 && new_srv_discovery) + blobmsg_add_string(bb, "eventSubURL", elm_val ? elm_val : ""); + + b = mxmlWalkNext(b, service, MXML_DESCEND); + } + + if (new_srv_discovery && service_obj) + blobmsg_close_table(bb, service_obj); +} + +static int upnp_description_res(struct ubus_context *ctx, struct ubus_object *obj __attribute__((unused)), + struct ubus_request_data *req, const char *method __attribute__((unused)), struct blob_attr *msg __attribute__((unused))) +{ + struct desc_list_elt *desc_elt = NULL; + struct blob_buf bb = {0}; + + memset(&bb,0,sizeof(struct blob_buf)); + blob_buf_init(&bb, 0); + + void *desc_array = blobmsg_open_array(&bb, "descriptions"); + list_for_each_entry(desc_elt, &desc_list, list) { + void *device_obj = blobmsg_open_table(&bb, NULL); + blobmsg_add_string(&bb, "desc_url", desc_elt->url); + blobmsg_add_u32(&bb, "is_device_desc", desc_elt->is_device_desc); + blobmsg_close_table(&bb, device_obj); + + } + blobmsg_close_array(&bb, desc_array); + + list_for_each_entry(desc_elt, &desc_list, list) { + + FILE *fp = fopen(desc_elt->desc_path, "r"); + if (!fp) + continue; + + mxml_node_t *tree = mxmlLoadFile(NULL, fp, MXML_OPAQUE_CALLBACK); + fclose(fp); + + if (tree) { + void *devices_array = blobmsg_open_array(&bb, "devices"); + mxml_node_t *device = mxmlFindElement(tree, tree, "device", NULL, NULL, MXML_DESCEND); + fill_device_instances(&bb, device); + blobmsg_close_array(&bb, devices_array); + + void *services_array = blobmsg_open_array(&bb, "services"); + mxml_node_t *service = mxmlFindElement(tree, tree, "device", NULL, NULL, MXML_DESCEND); + fill_service_element(&bb, service); + blobmsg_close_array(&bb, services_array); + + mxmlDelete(tree); + } + } + + ubus_send_reply(ctx, req, bb.head); + blob_buf_free(&bb); + return 0; +} + +static struct ubus_method upnp_methods[] = { + UBUS_METHOD_NOARG("discovery", upnp_discovery_res), + UBUS_METHOD_NOARG("description", upnp_description_res), +}; + +static struct ubus_object_type upnp_type = UBUS_OBJECT_TYPE("upnp", upnp_methods); + +static void upnp_discover_devices(struct uloop_timeout *timeout __attribute__((unused))) +{ + __upnp_discover_devices(); +} + +static struct ubus_object upnp_object = { + .name = "upnp", + .type = &upnp_type, + .methods = upnp_methods, + .n_methods = ARRAY_SIZE(upnp_methods), +}; + +void upnp_thread_discover_devices(void) +{ + struct ubus_context *ctx = NULL; + const char *ubus_socket = NULL; + int ret = 0; + + uloop_init(); + + ctx = ubus_connect(ubus_socket); + if (!ctx) { + syslog(LOG_ERR, "Failed to connect to ubus\n"); + return; + } + + ubus_add_uloop(ctx); + + __upnp_discover_devices(); + + ret = ubus_add_object(ctx, &upnp_object); + if (ret) { + syslog(LOG_ERR, "Failed to add 'upnp' ubus object: %s\n", ubus_strerror(ret)); + goto end; + } + + uloop_run(); + +end: + free_all_desc_list(); + free_all_dev_list(); + uloop_done(); + ubus_free(ctx); +}