text: /*
   * Copyright 2014 Google Inc. All rights reserved.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  /*
   * ssdptax (SSDP Taxonomy)
   *
   * A client implementing the API described in
   * http://miniupnp.free.fr/minissdpd.html
   *
   * Requests the list of all known SSDP nodes, requests
   * device info from them, and tries to figure out what
   * they are.
   */
  
  #include <arpa/inet.h>
  #include <asm/types.h>
  #include <ctype.h>
  #include <curl/curl.h>
  #include <getopt.h>
  #include <net/if.h>
  #include <netinet/in.h>
  #include <regex.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <sys/socket.h>
  #include <sys/time.h>
  #include <sys/types.h>
  #include <sys/un.h>
  #include <unistd.h>
  
  #include <iostream>
  #include <set>
  #include <tr1/unordered_map>
  
  #include "l2utils.h"
  
  /* Encode length by using 7bit per Byte :
   * Most significant bit of each byte specifies that the
   * following byte is part of the code */
  #define DECODELENGTH(n, p) { \
    n = 0; \
    do { n = (n << 7) | (*p & 0x7f); } \
    while (*(p++)&0x80); \
  }
  
  #define CODELENGTH(n, p) { \
    if(n>=0x10000000) *(p++) = (n >> 28) | 0x80; \
    if(n>=0x200000) *(p++) = (n >> 21) | 0x80; \
    if(n>=0x4000) *(p++) = (n >> 14) | 0x80; \
    if(n>=0x80) *(p++) = (n >> 7) | 0x80; \
    *(p++) = n & 0x7f; \
  }
  
  #define SOCK_PATH "/var/run/minissdpd.sock"
  
  
  typedef struct ssdp_info {
    ssdp_info(): srv_type(), url(), friendlyName(), ipaddr(),
      manufacturer(), model(), buffer(), failed(0) {}
    ssdp_info(const ssdp_info& s): srv_type(s.srv_type), url(s.url),
      friendlyName(s.friendlyName), ipaddr(s.ipaddr),
      manufacturer(s.manufacturer), model(s.model),
      buffer(s.buffer), failed(s.failed) {}
    std::string srv_type;
    std::string url;
    std::string friendlyName;
    std::string ipaddr;
    std::string manufacturer;
    std::string model;
  
    std::string buffer;
    int failed;
  } ssdp_info_t;
  
  
  typedef std::tr1::unordered_map<std::string, ssdp_info_t*> ResponsesMap;
  
  
  int ssdp_loop = 0;
  
  
  /* SSDP Discover packet */
  #define SSDP_PORT 1900
  #define SSDP_IP4  "239.255.255.250"
  #define SSDP_IP6  "ff02::c"
  const char discover_template[] = "M-SEARCH * HTTP/1.1\r\n"
                                   "HOST: %s:%d\r\n"
                                   "MAN: \"ssdp:discover\"\r\n"
                                   "MX: 2\r\n"
                                   "USER-AGENT: ssdptax/1.0\r\n"
                                   "ST: %s\r\n\r\n";
  
  
  static void strncpy_limited(char *dst, size_t dstlen,
      const char *src, size_t srclen)
  {
    size_t i;
    size_t lim = (srclen >= (dstlen - 1)) ? (dstlen - 2) : srclen;
  
    for (i = 0; i < lim; ++i) {
      unsigned char s = src[i];
      if (isspace(s) || s == ';') {
        dst[i] = ' ';  // deliberately convert newline to space
      } else if (isprint(s)) {
        dst[i] = s;
      } else {
        dst[i] = '_';
      }
    }
    dst[lim] = '\0';
  }
  
  
  static time_t monotime(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec;
  }
  
  
  /*
   * Send a request to minissdpd. Returns a std::string containing
   * minissdpd's response.
   */
  std::string request_from_ssdpd(const char *sock_path,
      int reqtype, const char *device)
  {
    int s = socket(AF_UNIX, SOCK_STREAM, 0);
    struct sockaddr_un addr;
    char *buffer;
    ssize_t len;
    size_t siz = 256 * 1024;
    char *p;
    int device_len = (int)strlen(device);
    fd_set readfds;
    struct timeval tv;
    std::string rc;
  
    if (s < 0) {
      perror("socket AF_UNIX failed");
      return rc;
    }
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path));
    if (connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
      perror("connect to minisspd failed");
      return rc;
    }
  
    if ((buffer = (char *)malloc(siz)) == NULL) {
      fprintf(stderr, "malloc(%zu) failed\n", siz);
      return rc;
    }
    memset(buffer, 0, siz);
  
    buffer[0] = reqtype;
    p = buffer + 1;
    CODELENGTH(device_len, p);
    memcpy(p, device, device_len);
    p += device_len;
    if (write(s, buffer, p - buffer) < 0) {
      perror("write to minissdpd failed");
      free(buffer);
      return rc;
    }
  
    FD_ZERO(&readfds);
    FD_SET(s, &readfds);
    memset(&tv, 0, sizeof(tv));
    tv.tv_sec = 2;
  
    if (select(s + 1, &readfds, NULL, NULL, &tv) < 1) {
      fprintf(stderr, "select failed\n");
      free(buffer);
      return rc;
    }
  
    if ((len = read(s, buffer, siz)) < 0) {
      perror("read from minissdpd failed");
      free(buffer);
      return rc;
    }
  
    close(s);
    rc = std::string(buffer, len);
    free(buffer);
    return rc;
  }
  
  
  int get_ipv4_ssdp_socket()
  {
    int s;
    int reuse = 1;
    struct sockaddr_in sin;
    struct ip_mreq mreq;
    struct ip_mreqn mreqn;
  
    if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
      perror("socket SOCK_DGRAM");
      exit(1);
    }
  
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse))) {
      perror("setsockopt SO_REUSEADDR");
      exit(1);
    }
  
    if (setsockopt(s, IPPROTO_IP, IP_MULTICAST_LOOP,
          &ssdp_loop, sizeof(ssdp_loop))) {
      perror("setsockopt IP_MULTICAST_LOOP");
      exit(1);
    }
  
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SSDP_PORT);
    sin.sin_addr.s_addr = INADDR_ANY;
    if (bind(s, (struct sockaddr*)&sin, sizeof(sin))) {
      perror("bind");
      exit(1);
    }
  
    memset(&mreqn, 0, sizeof(mreqn));
    mreqn.imr_ifindex = if_nametoindex("br0");
    if (setsockopt(s, IPPROTO_IP, IP_MULTICAST_IF, &mreqn, sizeof(mreqn))) {
      perror("IP_MULTICAST_IF");
      exit(1);
    }
  
    memset(&mreq, 0, sizeof(mreq));
    mreq.imr_multiaddr.s_addr = inet_addr(SSDP_IP4);
    if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP,
          (char *)&mreq, sizeof(mreq))) {
      perror("IP_ADD_MEMBERSHIP");
      exit(1);
    }
  
    return s;
  }
  
  
  void send_ssdp_ip4_request(int s, const char *search)
  {
    struct sockaddr_in sin;
    char buf[1024];
    ssize_t len;
  
    snprintf(buf, sizeof(buf), discover_template, SSDP_IP4, SSDP_PORT, search);
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SSDP_PORT);
    sin.sin_addr.s_addr = inet_addr(SSDP_IP4);
    len = strlen(buf);
    if (sendto(s, buf, len, 0, (struct sockaddr*)&sin, sizeof(sin)) != len) {
      perror("sendto multicast IPv4");
      exit(1);
    }
  }
  
  
  int get_ipv6_ssdp_socket()
  {
    int s;
    int reuse = 1;
    struct sockaddr_in6 sin6;
    struct ipv6_mreq mreq;
    int idx;
    int hops;
  
    if ((s = socket(AF_INET6, SOCK_DGRAM, 0)) < 0) {
      perror("socket SOCK_DGRAM");
      exit(1);
    }
  
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse))) {
      perror("setsockopt SO_REUSEADDR");
      exit(1);
    }
  
    if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_LOOP,
          &ssdp_loop, sizeof(ssdp_loop))) {
      perror("setsockopt IPV6_MULTICAST_LOOP");
      exit(1);
    }
  
    memset(&sin6, 0, sizeof(sin6));
    sin6.sin6_family = AF_INET6;
    sin6.sin6_port = htons(SSDP_PORT);
    sin6.sin6_addr = in6addr_any;
    if (bind(s, (struct sockaddr*)&sin6, sizeof(sin6))) {
      perror("bind");
      exit(1);
    }
  
    idx = if_nametoindex("br0");
    if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_IF, &idx, sizeof(idx))) {
      perror("IP_MULTICAST_IF");
      exit(1);
    }
  
    hops = 2;
    if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops, sizeof(hops))) {
      perror("IPV6_MULTICAST_HOPS");
      exit(1);
    }
  
    memset(&mreq, 0, sizeof(mreq));
    mreq.ipv6mr_interface = idx;
    if (inet_pton(AF_INET6, SSDP_IP6, &mreq.ipv6mr_multiaddr) != 1) {
      fprintf(stderr, "ERR: inet_pton(%s) failed", SSDP_IP6);
      exit(1);
    }
    if (setsockopt(s, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) < 0) {
      perror("ERR: setsockopt(IPV6_JOIN_GROUP)");
      exit(1);
    }
  
    return s;
  }
  
  
  void send_ssdp_ip6_request(int s, const char *search)
  {
    struct sockaddr_in6 sin6;
    char buf[1024];
    ssize_t len;
  
    snprintf(buf, sizeof(buf), discover_template, SSDP_IP6, SSDP_PORT, search);
    memset(&sin6, 0, sizeof(sin6));
    sin6.sin6_family = AF_INET6;
    sin6.sin6_port = htons(SSDP_PORT);
    if (inet_pton(AF_INET6, SSDP_IP6, &sin6.sin6_addr) != 1) {
      fprintf(stderr, "ERR: inet_pton(%s) failed", SSDP_IP6);
      exit(1);
    }
    len = strlen(buf);
    if (sendto(s, buf, len, 0, (struct sockaddr*)&sin6, sizeof(sin6)) != len) {
      perror("sendto multicast IPv6");
      exit(1);
    }
  }
  
  
  /*
   * Returns true if the friendlyName appears to include an email address.
   */
  bool contains_email_address(const std::string &friendlyName)
  {
    regex_t r_email;
    int rc;
  
    if (regcomp(&r_email, ".+@[a-z0-9.-]+\\.[a-z0-9.-]+",
          REG_EXTENDED | REG_ICASE | REG_NOSUB)) {
      fprintf(stderr, "%s: regcomp failed!\n", __FUNCTION__);
      exit(1);
    }
  
    rc = regexec(&r_email, friendlyName.c_str(), 0, NULL, 0);
    regfree(&r_email);
  
    return (rc == 0);
  }
  
  
  /*
   * Combine the manufacturer and model. If the manufacturer name
   * is already present in the model string, don't duplicate it.
   */
  const std::string unfriendly_name(const std::string &manufacturer,
      const std::string &model)
  {
    if (strcasestr(model.c_str(), manufacturer.c_str()) != NULL) {
      return model;
    }
  
    return manufacturer + " " + model;
  }
  
  
  std::string format_response(const ssdp_info_t *info, L2Map *l2map)
  {
    std::string mac;
    std::string ipaddr;
    std::string result;
  
    if (info->failed) {
      /*
       * We could not fetch information from this client. That often means that
       * the device was powered off recently. minissdpd still remembers that
       * it is there, but we cannot contact it.
       *
       * Don't print anything for these, as we'd end up calling them "Unknown"
       * and that is misleading. We only report information about devices which
       * are active right now.
       */
      return result;
    }
  
    mac = get_l2addr_for_ip(info->ipaddr);
    if (contains_email_address(info->friendlyName)) {
      result = "ssdp " + mac + " REDACTED;" + info->srv_type;
    } else if (info->friendlyName.length() > 0) {
      result = "ssdp " + mac + " " + info->friendlyName + ";" +
        unfriendly_name(info->manufacturer, info->model);
    } else {
      result = "ssdp " + mac + " Unknown;" + info->srv_type;
    }
  
    return result;
  }
  
  
  void parse_minissdpd_response(std::string &response,
      std::string &url, std::string &srv_type)
  {
    size_t slen;
    const char *p;
    const char *end = response.c_str() + response.length();
    char urlbuf[256];
    char srv_type_buf[256];
  
    memset(urlbuf, 0, sizeof(urlbuf));
    memset(srv_type_buf, 0, sizeof(srv_type_buf));
  
    p = response.c_str();
    DECODELENGTH(slen, p);
    if ((p + slen) > end) {
      fprintf(stderr, "Unable to parse SSDP response\n");
      return;
    }
    strncpy_limited(urlbuf, sizeof(urlbuf), p, slen);
    p += slen;
  
    DECODELENGTH(slen, p);
    if ((p + slen) > end) {
      fprintf(stderr, "Unable to parse SSDP response\n");
      return;
    }
    strncpy_limited(srv_type_buf, sizeof(srv_type_buf), p, slen);
    p += slen;
  
    DECODELENGTH(slen, p);
    if ((p + slen) > end) {
      fprintf(stderr, "Unable to parse SSDP response\n");
      return;
    }
    /* Skip over the UUID without processing it. */
    p += slen;
  
    url = urlbuf;
    srv_type = srv_type_buf;
  
    response.erase(0, (p - response.c_str()));
  }
  
  
  const char *findXmlField(const char *ptr, const char *label, ssize_t *len)
  {
    char openlabel[64], closelabel[64];
    const char *start, *end;
  
    snprintf(openlabel, sizeof(openlabel), "<%s>", label);
    snprintf(closelabel, sizeof(closelabel), "</%s>", label);
  
    start = strcasestr(ptr, openlabel) + strlen(openlabel);
    end = strcasestr(ptr, closelabel);
  
    if (start < end) {
      *len = end - start;
      return start;
    }
  
    return NULL;
  }
  
  
  /*
   * Expected value in buffer is an XML blob of
   * http://upnp.org/specs/basic/UPnP-basic-Basic-v1-Device.pdf
   *
   * Like this (a Samsung TV):
   *  <?xml version="1.0"?>
   *  <root xmlns='urn:schemas-upnp-org:device-1-0' ...
   *   <device>
   *    <deviceType>urn:dial-multiscreen-org:device:dialreceiver:1</deviceType>
   *    <friendlyName>[TV]Samsung LED60</friendlyName>
   *    <manufacturer>Samsung Electronics</manufacturer>
   *    <manufacturerURL>http://www.samsung.com/sec</manufacturerURL>
   *    <modelDescription>Samsung TV NS</modelDescription>
   *    <modelName>UN60F6300</modelName>
   *    <modelNumber>1.0</modelNumber>
   *    <modelURL>http://www.samsung.com/sec</modelURL>
   * ... etc, etc ...
   */
  void extract_fields_from_buffer(ssdp_info_t *info)
  {
    const char *ptr = info->buffer.c_str();
    const char *p;
    ssize_t len;
  
    if ((p = findXmlField(ptr, "friendlyName", &len)) == NULL) {
      p = findXmlField(ptr, "modelDescription", &len);
    }
    if (p && len > 0) {
      info->friendlyName = std::string(p, len);
    }
  
    p = findXmlField(ptr, "manufacturer", &len);
    if (p && len > 0) {
      info->manufacturer = std::string(p, len);
    }
  
    p = findXmlField(ptr, "modelName", &len);
    if (p && len > 0) {
      info->model = std::string(p, len);
    }
  }
  
  
  size_t callback(const char *ptr, size_t size, size_t nmemb, void *userdata)
  {
    ssdp_info_t *info = (ssdp_info_t *)userdata;
    info->buffer.append(ptr, size * nmemb);
    return size * nmemb;
  }
  
  
  /*
   * SSDP returned an endpoint URL, use curl to GET its contents.
   */
  void fetch_device_info(const std::string &url, ssdp_info_t *info)
  {
    CURL *curl = curl_easy_init();
    char *ip;
  
    if (!curl) {
      fprintf(stderr, "curl_easy_init failed\n");
      return;
    }
    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_PATH_AS_IS, 1L);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, info);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "ssdptaxonomy/1.0");
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1L);
    curl_easy_setopt(curl, CURLOPT_FAILONERROR, true);
    if (curl_easy_perform(curl) == CURLE_OK) {
      extract_fields_from_buffer(info);
    } else {
      info->failed = 1;
    }
    if (curl_easy_getinfo(curl, CURLINFO_PRIMARY_IP, &ip) == CURLE_OK) {
      info->ipaddr = ip;
    }
  
    info->buffer.clear();
    curl_easy_cleanup(curl);
  }
  
  
  std::string trim(std::string s)
  {
    size_t start = s.find_first_not_of(" \t\v\f\b\r\n");
    if (std::string::npos != start && 0 != start) s = s.erase(0, start);
  
    size_t end = s.find_last_not_of(" \t\v\f\b\r\n");
    if (std::string::npos != end) s = s.substr(0, end + 1);
  
    return s;
  }
  
  
  void parse_ssdp_response(int s, ResponsesMap &responses)
  {
    ssdp_info_t *info = new ssdp_info_t;
    char buffer[4096];
    char *p, *saveptr, *strtok_pos;
    ssize_t pktlen;
  
    memset(buffer, 0, sizeof(buffer));
    pktlen = recv(s, buffer, sizeof(buffer) - 1, 0);
    if (pktlen < 0 || (size_t)pktlen >= sizeof(buffer)) {
      fprintf(stderr, "error receiving SSDP response, pktlen=%zd\n", pktlen);
      delete info;
      /* not fatal, just return */
      return;
    }
    buffer[pktlen] = '\0';
    strtok_pos = buffer;
  
    while ((p = strtok_r(strtok_pos, "\r\n", &saveptr)) != NULL) {
      if (strlen(p) > 9 && strncasecmp(p, "location:", 9) == 0) {
        char urlbuf[512];
        p += 9;
        strncpy_limited(urlbuf, sizeof(urlbuf), p, strlen(p));
        info->url = trim(std::string(urlbuf, strlen(urlbuf)));
      } else if (strlen(p) > 7 && strncasecmp(p, "server:", 7) == 0) {
        char srv_type_buf[256];
        p += 7;
        strncpy_limited(srv_type_buf, sizeof(srv_type_buf), p, strlen(p));
        info->srv_type = trim(std::string(srv_type_buf, strlen(srv_type_buf)));
      }
      strtok_pos = NULL;
    }
  
    if (info->url.length() && responses.find(info->url) == responses.end()) {
      fetch_device_info(info->url, info);
      responses[info->url] = info;
    } else {
      delete info;
    }
  }
  
  
  /* Wait for SSDP NOTIFY messages to arrive. */
  #define TIMEOUT_SECS  5
  void listen_for_responses(int s4, int s6, ResponsesMap &responses)
  {
    struct timeval tv;
    fd_set rfds;
    int maxfd = (s4 > s6) ? s4 : s6;
    time_t start = monotime();
  
    memset(&tv, 0, sizeof(tv));
    tv.tv_sec = TIMEOUT_SECS;
    tv.tv_usec = 0;
  
    FD_ZERO(&rfds);
    FD_SET(s4, &rfds);
    FD_SET(s6, &rfds);
  
    while (select(maxfd + 1, &rfds, NULL, NULL, &tv) > 0) {
      time_t end = monotime();
      if (FD_ISSET(s4, &rfds)) {
        parse_ssdp_response(s4, responses);
      }
      if (FD_ISSET(s6, &rfds)) {
        parse_ssdp_response(s6, responses);
      }
  
      FD_ZERO(&rfds);
      FD_SET(s4, &rfds);
      FD_SET(s6, &rfds);
  
      if ((end - start) > TIMEOUT_SECS) {
        /* even on a network filled with SSDP packets,
         * return after TIMEOUT_SECS. */
        break;
      }
    }
  }
  
  
  void usage(char *progname) {
    printf("usage: %s [-t /path/to/fifo] [-s search]\n", progname);
    printf("\t-s\tserver type to search for (default ssdp:all)\n");
    printf("\t-t\ttest mode, use a fake path instead of minissdpd.\n");
    exit(1);
  }
  
  
  int main(int argc, char **argv)
  {
    std::string buffer;
    ResponsesMap responses;
    L2Map l2map;
    int c, s4, s6;
    const char *sock_path = SOCK_PATH;
    const char *search = "ssdp:all";
  
    setlinebuf(stdout);
    alarm(30);
  
    if (curl_global_init(CURL_GLOBAL_NOTHING)) {
      fprintf(stderr, "curl_global_init failed\n");
      exit(1);
    }
  
    while ((c = getopt(argc, argv, "s:t:")) != -1) {
      switch(c) {
        case 's': search = optarg; break;
        case 't':
          sock_path = optarg;
          ssdp_loop = 1;
          break;
        default: usage(argv[0]); break;
      }
    }
  
    /* Request the list from MiniSSDPd */
    buffer = request_from_ssdpd(sock_path, 3, search);
    if (!buffer.empty()) {
      int num = buffer.c_str()[0];
      buffer.erase(0, 1);
      while ((num-- > 0) && buffer.length() > 0) {
        ssdp_info_t *info = new ssdp_info_t;
  
        parse_minissdpd_response(buffer, info->url, info->srv_type);
        if (info->url.length() && responses.find(info->url) == responses.end()) {
          fetch_device_info(info->url, info);
          responses[info->url] = info;
        } else {
          delete info;
        }
      }
  
      /* Capture the ARP table in its current state. */
      get_l2_map(&l2map);
    }
  
    /* Supplement what we got from MiniSSDPd by sending
     * our own M-SEARCH and listening for responses. */
    s4 = get_ipv4_ssdp_socket();
    send_ssdp_ip4_request(s4, search);
    s6 = get_ipv6_ssdp_socket();
    send_ssdp_ip6_request(s6, search);
    listen_for_responses(s4, s6, responses);
    close(s4);
    s4 = -1;
    close(s6);
    s6 = -1;
  
    /* Capture any new ARP table entries which appeared after sending
     * our own M-SEARCH. */
    get_l2_map(&l2map);
  
    typedef std::set<std::string> ResultsSet;
    ResultsSet results;
    for (ResponsesMap::const_iterator ii = responses.begin();
        ii != responses.end(); ++ii) {
      std::string r = format_response(ii->second, &l2map);
      if (r.length() > 0) {
        results.insert(r);
      }
    }
  
    /* Many devices advertise multiple URLs with the same
     * model information in all of them. Suppress duplicate
     * output using the set. */
    for (ResultsSet::const_iterator ii = results.begin();
        ii != results.end(); ++ii) {
      std::cout << *ii << std::endl;
    }
  
    curl_global_cleanup();
    exit(0);
  }
  