root/usr.sbin/sdpd/server.c
/*      $NetBSD: server.c,v 1.12 2021/08/08 20:54:49 nia Exp $  */

/*-
 * Copyright (c) 2006 Itronix Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of Itronix Inc. may not be used to endorse
 *    or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY ITRONIX INC. ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL ITRONIX INC. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
/*-
 * Copyright (c) 2009 The NetBSD Foundation, Inc.
 * Copyright (c) 2004 Maksim Yevmenkin <m_evmenkin@yahoo.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $FreeBSD: src/usr.sbin/bluetooth/sdpd/server.c,v 1.2 2005/12/06 17:56:36 emax Exp $
 */

#include <sys/cdefs.h>
__RCSID("$NetBSD: server.c,v 1.12 2021/08/08 20:54:49 nia Exp $");

#include <sys/select.h>
#include <sys/stat.h>
#include <sys/ucred.h>
#include <sys/un.h>

#include <assert.h>
#include <bluetooth.h>
#include <errno.h>
#include <grp.h>
#include <pwd.h>
#include <sdp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "sdpd.h"

static bool     server_open_control     (server_t *, char const *);
static bool     server_open_l2cap       (server_t *);
static void     server_accept_client    (server_t *, int);
static bool     server_process_request  (server_t *, int);
static void     server_close_fd         (server_t *, int);
static bool     server_auth_check       (server_t *, void *);

/* number of groups we allocate space for in cmsg */
#define MAX_GROUPS      20

/*
 * Initialize server
 */
bool
server_init(server_t *srv, char const *control, char const *sgroup)
{

        assert(srv != NULL);
        assert(control != NULL);

        memset(srv, 0, sizeof(*srv));
        FD_ZERO(&srv->fdset);
        srv->sgroup = sgroup;

        srv->fdmax = -1;
        srv->fdidx = calloc(FD_SETSIZE, sizeof(fd_idx_t));
        if (srv->fdidx == NULL) {
                log_crit("Failed to allocate fd index");
                goto fail;
        }

        srv->ctllen = CMSG_SPACE(SOCKCREDSIZE(MAX_GROUPS));
        srv->ctlbuf = malloc(srv->ctllen);
        if (srv->ctlbuf == NULL) {
                log_crit("Malloc cmsg buffer (len=%zu) failed.", srv->ctllen);
                goto fail;
        }

        srv->imtu = SDP_LOCAL_MTU - sizeof(sdp_pdu_t);
        srv->ibuf = malloc(srv->imtu);
        if (srv->ibuf == NULL) {
                log_crit("Malloc input buffer (imtu=%d) failed.", srv->imtu);
                goto fail;
        }

        srv->omtu = L2CAP_MTU_DEFAULT - sizeof(sdp_pdu_t);
        srv->obuf = malloc(srv->omtu);
        if (srv->obuf == NULL) {
                log_crit("Malloc output buffer (omtu=%d) failed.", srv->omtu);
                goto fail;
        }

        if (db_init(srv)
            && server_open_control(srv, control)
            && server_open_l2cap(srv))
                return true;

fail:
        server_shutdown(srv);
        return false;
}

/*
 * Open local control socket
 */
static bool
server_open_control(server_t *srv, char const *control)
{
        struct sockaddr_un      un;
        int                     opt, fd;

        if (unlink(control) == -1 && errno != ENOENT) {
                log_crit("Could not unlink(%s). %s (%d)",
                    control, strerror(errno), errno);

                return false;
        }

        fd = socket(PF_LOCAL, SOCK_STREAM, 0);
        if (fd == -1) {
                log_crit("Could not create control socket. %s (%d)",
                    strerror(errno), errno);

                return false;
        }

        opt = 1;
        if (setsockopt(fd, SOL_LOCAL, LOCAL_CREDS, &opt, sizeof(opt)) == -1)
                log_crit("Warning: No credential checks on control socket");

        memset(&un, 0, sizeof(un));
        un.sun_len = sizeof(un);
        un.sun_family = AF_LOCAL;
        strlcpy(un.sun_path, control, sizeof(un.sun_path));

        if (bind(fd, (struct sockaddr *) &un, sizeof(un)) == -1) {
                log_crit("Could not bind control socket. %s (%d)",
                    strerror(errno), errno);

                close(fd);
                return false;
        }

        if (chmod(control, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) == -1) {
                log_crit("Could not set permissions on control socket. %s (%d)",
                    strerror(errno), errno);

                close(fd);
                return false;
        }

        if (listen(fd, 5) == -1) {
                log_crit("Could not listen on control socket. %s (%d)",
                    strerror(errno), errno);

                close(fd);
                return false;
        }

        /* Add control descriptor to index */
        if (fd > srv->fdmax)
                srv->fdmax = fd;

        FD_SET(fd, &srv->fdset);
        srv->fdidx[fd].valid = true;
        srv->fdidx[fd].server = true;
        srv->fdidx[fd].control = true;
        srv->fdidx[fd].priv = false;
        return true;
}

/*
 * Open L2CAP server socket
 */
static bool
server_open_l2cap(server_t *srv)
{
        struct sockaddr_bt      sa;
        int                     fd;

        fd = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
        if (fd == -1) {
                log_crit("Could not create L2CAP socket. %s (%d)",
                    strerror(errno), errno);

                return false;
        }

        if (setsockopt(fd, BTPROTO_L2CAP, SO_L2CAP_IMTU,
            &srv->imtu, sizeof(srv->imtu)) == -1) {
                log_crit("Could not set L2CAP Incoming MTU. %s (%d)",
                    strerror(errno), errno);

                close(fd);
                return false;
        }

        memset(&sa, 0, sizeof(sa));
        sa.bt_len = sizeof(sa);
        sa.bt_family = AF_BLUETOOTH;
        sa.bt_psm = L2CAP_PSM_SDP;
        bdaddr_copy(&sa.bt_bdaddr, BDADDR_ANY);

        if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) == -1) {
                log_crit("Could not bind L2CAP socket. %s (%d)",
                    strerror(errno), errno);

                close(fd);
                return false;
        }

        if (listen(fd, 5) == -1) {
                log_crit("Could not listen on L2CAP socket. %s (%d)",
                    strerror(errno), errno);

                close(fd);
                return false;
        }

        /* Add L2CAP descriptor to index */
        if (fd > srv->fdmax)
                srv->fdmax = fd;

        FD_SET(fd, &srv->fdset);
        srv->fdidx[fd].valid = true;
        srv->fdidx[fd].server = true;
        srv->fdidx[fd].control = false;
        srv->fdidx[fd].priv = false;
        return true;
}

/*
 * Shutdown server
 */
void
server_shutdown(server_t *srv)
{
        record_t *r;
        int     fd;

        assert(srv != NULL);

        while ((r = LIST_FIRST(&srv->rlist)) != NULL) {
                LIST_REMOVE(r, next);
                free(r);
        }

        for (fd = 0; fd < srv->fdmax + 1; fd ++) {
                if (srv->fdidx[fd].valid)
                        server_close_fd(srv, fd);
        }

        free(srv->fdidx);
        free(srv->ctlbuf);
        free(srv->ibuf);
        free(srv->obuf);

        memset(srv, 0, sizeof(*srv));
}

/*
 * Do one server iteration
 */
bool
server_do(server_t *srv)
{
        fd_set  fdset;
        int     n, fd;

        assert(srv != NULL);

        memcpy(&fdset, &srv->fdset, sizeof(fdset));
        n = select(srv->fdmax + 1, &fdset, NULL, NULL, NULL);
        if (n == -1) {
                if (errno == EINTR)
                        return true;

                log_err("Could not select(%d, %p). %s (%d)",
                    srv->fdmax + 1, &fdset, strerror(errno), errno);

                return false;
        }

        for (fd = 0; fd < srv->fdmax + 1 && n > 0; fd++) {
                if (!FD_ISSET(fd, &fdset))
                        continue;

                assert(srv->fdidx[fd].valid);

                if (srv->fdidx[fd].server)
                        server_accept_client(srv, fd);
                else if (!server_process_request(srv, fd))
                        server_close_fd(srv, fd);

                n--;
        }

        return true;

}

/*
 * Accept new client connection and register it with index
 */
static void
server_accept_client(server_t *srv, int fd)
{
        struct sockaddr_bt      sa;
        socklen_t               len;
        int                     cfd;
        uint16_t                omtu;

        do {
                cfd = accept(fd, NULL, NULL);
        } while (cfd == -1 && errno == EINTR);

        if (cfd == -1) {
                log_err("Could not accept connection on %s socket. %s (%d)",
                    srv->fdidx[fd].control ? "control" : "L2CAP",
                    strerror(errno), errno);

                return;
        }

        if (cfd >= FD_SETSIZE) {
                log_crit("File descriptor too large");
                close(cfd);
                return;
        }

        assert(!FD_ISSET(cfd, &srv->fdset));
        assert(!srv->fdidx[cfd].valid);

        memset(&sa, 0, sizeof(sa));
        omtu = srv->omtu;

        if (!srv->fdidx[fd].control) {
                len = sizeof(sa);
                if (getsockname(cfd, (struct sockaddr *)&sa, &len) == -1)
                        log_warning("getsockname failed, using BDADDR_ANY");

                len = sizeof(omtu);
                if (getsockopt(cfd, BTPROTO_L2CAP, SO_L2CAP_OMTU, &omtu, &len) == -1)
                        log_warning("Could not get L2CAP OMTU, using %d", omtu);
                else
                        omtu -= sizeof(sdp_pdu_t);
        }

        /* Add client descriptor to the index */
        if (cfd > srv->fdmax)
                srv->fdmax = cfd;

        FD_SET(cfd, &srv->fdset);
        srv->fdidx[cfd].valid = true;
        srv->fdidx[cfd].server = false;
        srv->fdidx[cfd].control = srv->fdidx[fd].control;
        srv->fdidx[cfd].priv = false;
        srv->fdidx[cfd].omtu = (omtu > srv->omtu) ? srv->omtu : omtu;
        srv->fdidx[cfd].offset = 0;
        bdaddr_copy(&srv->fdidx[cfd].bdaddr, &sa.bt_bdaddr);

        log_debug("new %s client on fd#%d",
            srv->fdidx[cfd].control ? "control" : "L2CAP", cfd);
}

/*
 * Process request from the client
 */
static bool
server_process_request(server_t *srv, int fd)
{
        struct msghdr   msg;
        struct iovec    iov[2];
        struct cmsghdr  *cmsg;
        ssize_t         len;
        uint16_t        error;

        assert(FD_ISSET(fd, &srv->fdset));
        assert(srv->fdidx[fd].valid);
        assert(!srv->fdidx[fd].server);

        iov[0].iov_base = &srv->pdu;
        iov[0].iov_len = sizeof(srv->pdu);
        iov[1].iov_base = srv->ibuf;
        iov[1].iov_len = srv->imtu;

        msg.msg_name = NULL;
        msg.msg_namelen = 0;
        msg.msg_iov = iov;
        msg.msg_iovlen = __arraycount(iov);
        msg.msg_control = srv->ctlbuf;
        msg.msg_controllen = srv->ctllen;
        msg.msg_flags = 0;

        do {
                len = recvmsg(fd, &msg, 0);
        } while (len == -1 && errno == EINTR);

        if (len == -1) {
                log_err("Could not receive SDP request on %s socket. %s (%d)",
                    srv->fdidx[fd].control ? "control" : "L2CAP",
                    strerror(errno), errno);

                return false;
        }

        if (len == 0) {
                log_info("Client on %s socket has disconnected",
                    srv->fdidx[fd].control ? "control" : "L2CAP");

                return false;
        }

        if (msg.msg_flags & MSG_TRUNC)
                log_info("Truncated message on %s socket",
                    srv->fdidx[fd].control ? "control" : "L2CAP");

        if ((cmsg = CMSG_FIRSTHDR(&msg)) != NULL
            && cmsg->cmsg_level == SOL_SOCKET
            && cmsg->cmsg_type == SCM_CREDS
            && cmsg->cmsg_len >= CMSG_LEN(SOCKCREDSIZE(0)))
                srv->fdidx[fd].priv = server_auth_check(srv, CMSG_DATA(cmsg));

        srv->pdu.len = be16toh(srv->pdu.len);

        if ((uint32_t)len < sizeof(srv->pdu)
            || (uint32_t)len != sizeof(srv->pdu) + srv->pdu.len) {
                error = SDP_ERROR_CODE_INVALID_PDU_SIZE;
        } else {
                switch (srv->pdu.pid) {
                case SDP_PDU_SERVICE_SEARCH_REQUEST:
                        error = service_search_request(srv, fd);
                        break;

                case SDP_PDU_SERVICE_ATTRIBUTE_REQUEST:
                        error = service_attribute_request(srv, fd);
                        break;

                case SDP_PDU_SERVICE_SEARCH_ATTRIBUTE_REQUEST:
                        error = service_search_attribute_request(srv, fd);
                        break;

#ifdef SDP_COMPAT
                case SDP_PDU_SERVICE_REGISTER_REQUEST:
                        error = compat_register_request(srv, fd);
                        break;

                case SDP_PDU_SERVICE_CHANGE_REQUEST:
                        error = compat_change_request(srv, fd);
                        break;
#endif

                case SDP_PDU_RECORD_INSERT_REQUEST:
                        error = record_insert_request(srv, fd);
                        break;

                case SDP_PDU_RECORD_UPDATE_REQUEST:
                        error = record_update_request(srv, fd);
                        break;

                case SDP_PDU_RECORD_REMOVE_REQUEST:
                        error = record_remove_request(srv, fd);
                        break;

                default:
                        error = SDP_ERROR_CODE_INVALID_REQUEST_SYNTAX;
                        break;
                }
        }

        if (error != 0) {
                srv->fdidx[fd].offset = 0;
                db_unselect(srv, fd);
                srv->pdu.pid = SDP_PDU_ERROR_RESPONSE;
                srv->pdu.len = sizeof(error);
                be16enc(srv->obuf, error);
                log_debug("sending ErrorResponse (error=0x%04x)", error);
        }

        iov[0].iov_base = &srv->pdu;
        iov[0].iov_len = sizeof(srv->pdu);
        iov[1].iov_base = srv->obuf;
        iov[1].iov_len = srv->pdu.len;

        srv->pdu.len = htobe16(srv->pdu.len);

        msg.msg_name = NULL;
        msg.msg_namelen = 0;
        msg.msg_iov = iov;
        msg.msg_iovlen = __arraycount(iov);
        msg.msg_control = NULL;
        msg.msg_controllen = 0;
        msg.msg_flags = 0;

        do {
                len = sendmsg(fd, &msg, 0);
        } while (len == -1 && errno == EINTR);

        if (len == -1) {
                log_err("Could not send SDP response on %s socket. %s (%d)",
                    srv->fdidx[fd].control ? "control" : "L2CAP",
                    strerror(errno), errno);

                return false;
        }

        return true;
}

/*
 * Close descriptor and remove it from index
 */
static void
server_close_fd(server_t *srv, int fd)
{

        assert(FD_ISSET(fd, &srv->fdset));
        assert(srv->fdidx[fd].valid);

        db_unselect(srv, fd);   /* release selected records */
        db_release(srv, fd);    /* expire owned records */

        close(fd);
        FD_CLR(fd, &srv->fdset);
        srv->fdidx[fd].valid = false;

        log_debug("client on fd#%d closed", fd);

        if (fd == srv->fdmax) {
                while (fd > 0 && !srv->fdidx[fd].valid)
                        fd--;

                srv->fdmax = fd;
        }
}

/*
 * check credentials, return true when permitted to modify service records
 */
static bool
server_auth_check(server_t *srv, void *data)
{
        struct sockcred *cred = data;
        struct group *grp;
        int n;

        if (cred == NULL)
                return false;

        if (cred->sc_uid == 0 || cred->sc_euid == 0)
                return true;

        if (srv->sgroup == NULL)
                return false;

        grp = getgrnam(srv->sgroup);
        if (grp == NULL) {
                log_err("No gid for group '%s'", srv->sgroup);
                srv->sgroup = NULL;
                return false;
        }

        if (cred->sc_gid == grp->gr_gid || cred->sc_egid == grp->gr_gid)
                return true;

        if (cred->sc_ngroups > MAX_GROUPS) {
                log_info("Credentials truncated, lost %d groups",
                    MAX_GROUPS - cred->sc_ngroups);

                cred->sc_ngroups = MAX_GROUPS;
        }

        for (n = 0 ; n < cred->sc_ngroups ; n++) {
                if (cred->sc_groups[n] == grp->gr_gid)
                        return true;
        }

        return false;
}