diff --git a/.gitignore b/.gitignore index cbe0733..10c6143 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ src/libhagelbarger/libhagelbarger.a src/libdtmf/libdtmf.a src/libtimer/libtimer.a src/libselect/libselect.a +src/libph_socket/libph_socket.a src/libsamplerate/libsamplerate.a src/libscrambler/libscrambler.a src/libemphasis/libemphasis.a diff --git a/configure.ac b/configure.ac index 0cab408..90cabee 100644 --- a/configure.ac +++ b/configure.ac @@ -75,6 +75,7 @@ AC_OUTPUT( src/libdtmf/Makefile src/libtimer/Makefile src/libselect/Makefile + src/libph_socket/Makefile src/libsamplerate/Makefile src/libscrambler/Makefile src/libemphasis/Makefile diff --git a/src/Makefile.am b/src/Makefile.am index 3cf15bd..a3fbe66 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -16,6 +16,7 @@ SUBDIRS = \ libdtmf \ libtimer \ libselect \ + libph_socket \ libsamplerate \ libscrambler \ libemphasis \ diff --git a/src/libph_socket/Makefile.am b/src/libph_socket/Makefile.am new file mode 100644 index 0000000..60fa592 --- /dev/null +++ b/src/libph_socket/Makefile.am @@ -0,0 +1,7 @@ +AM_CPPFLAGS = -Wall -Wextra -g $(all_includes) + +noinst_LIBRARIES = libph_socket.a + +libph_socket_a_SOURCES = \ + ph_socket.c + diff --git a/src/libph_socket/ph_socket.c b/src/libph_socket/ph_socket.c new file mode 100644 index 0000000..28ec30f --- /dev/null +++ b/src/libph_socket/ph_socket.c @@ -0,0 +1,339 @@ +/* PH-socket, a lightweight ISDN physical layer interface + * + * (C) 2022 by Andreas Eversberg + * All Rights Reserved + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../libtimer/timer.h" +#include "../libselect/select.h" +#include "../libdebug/debug.h" +#include "ph_socket.h" + +static int ph_socket_listen_cb(struct osmo_fd *ofd, unsigned int __attribute__((unused)) what); +static int ph_socket_connect_cb(struct osmo_fd *ofd, unsigned int __attribute__((unused)) what); +static void ph_socket_timeout_cb(void *data); + +static void open_connection(ph_socket_t *s) +{ + uint8_t enable = PH_CTRL_UNBLOCK; + int rc, flags; + + if (s->connect_ofd.fd > 0) + return; + + LOGP(DPH, LOGL_DEBUG, "Trying to connect to PH-socket server.\n"); + rc = socket(PF_UNIX, SOCK_STREAM, 0); + if (rc < 0) { + LOGP(DPH, LOGL_ERROR, "Failed to create UNIX socket.\n"); + osmo_timer_schedule(&s->retry_timer, SOCKET_RETRY_TIMER, 0); + return; + } + s->connect_ofd.fd = rc; + s->connect_ofd.data = s; + s->connect_ofd.when = BSC_FD_READ; + s->connect_ofd.cb = ph_socket_connect_cb; + osmo_fd_register(&s->connect_ofd); + /* set nonblocking io, because we do multiple reads when handling read event */ + flags = fcntl(s->connect_ofd.fd, F_GETFL); + flags |= O_NONBLOCK; + fcntl(s->connect_ofd.fd, F_SETFL, flags); + /* connect */ + rc = connect(s->connect_ofd.fd, (struct sockaddr *)&s->sock_address, sizeof(s->sock_address)); + if (rc < 0 && errno != EAGAIN) { + if (!s->connect_failed) + LOGP(DPH, LOGL_NOTICE, "Failed to connect UNIX socket, retrying...\n"); + close(s->connect_ofd.fd); + s->connect_failed = 1; + osmo_fd_unregister(&s->connect_ofd); + s->connect_ofd.fd = 0; + osmo_timer_schedule(&s->retry_timer, SOCKET_RETRY_TIMER, 0); + return; + } + s->connect_failed = 0; + LOGP(DPH, LOGL_INFO, "Connection to PH-socket server.\n"); + /* reset rx buffer */ + s->rx_header_index = 0; + s->rx_data_index = 0; + /* indicate established socket connection */ + s->ph_socket_rx_msg(s, 0, PH_PRIM_CTRL_IND, &enable, 1); +} + +static void close_connection(ph_socket_t *s) +{ + struct socket_msg_list *ml; + uint8_t disable = PH_CTRL_BLOCK; + + if (s->connect_ofd.fd <= 0) + return; + + LOGP(DPH, LOGL_INFO, "Connection from PH-socket closed.\n"); + + /* indicate loss of socket connection */ + s->ph_socket_rx_msg(s, 0, (s->listen_ofd.fd > 0) ? PH_PRIM_CTRL_REQ : PH_PRIM_CTRL_IND, &disable, 1); + + osmo_fd_unregister(&s->connect_ofd); + close(s->connect_ofd.fd); + s->connect_ofd.fd = 0; + + while ((ml = s->tx_list)) { + s->tx_list = ml->next; + free(ml); + } + s->tx_list_tail = &s->tx_list; + if (s->rx_msg) { + free(s->rx_msg); + s->rx_msg = NULL; + } + + if (s->listen_ofd.fd <= 0) { + /* set timer, so that retry is delayed */ + osmo_timer_schedule(&s->retry_timer, SOCKET_RETRY_TIMER, 0); + } +} + +int ph_socket_init(ph_socket_t *s, void (*ph_socket_rx_msg)(ph_socket_t *s, int channel, uint8_t prim, uint8_t *data, + int length), void *priv, const char *socket_name, int server) +{ + int rc; + + memset(s, 0, sizeof(*s)); + s->name = socket_name; + s->ph_socket_rx_msg = ph_socket_rx_msg; + s->priv = priv; + s->tx_list_tail = &s->tx_list; + + memset(&s->sock_address, 0, sizeof(s->sock_address)); + s->sock_address.sun_family = AF_UNIX; + strcpy(s->sock_address.sun_path+1, socket_name); + + s->retry_timer.data = s; + s->retry_timer.cb = ph_socket_timeout_cb; + + if (server) { + rc = socket(PF_UNIX, SOCK_STREAM, 0); + if (rc < 0) { + LOGP(DPH, LOGL_ERROR, "Failed to create UNIX socket.\n"); + return rc; + } + s->listen_ofd.fd = rc; + s->listen_ofd.data = s; + s->listen_ofd.when = BSC_FD_READ; + s->listen_ofd.cb = ph_socket_listen_cb; + osmo_fd_register(&s->listen_ofd); + + rc = bind(s->listen_ofd.fd, (struct sockaddr *)(&s->sock_address), sizeof(s->sock_address)); + if (rc < 0) { + LOGP(DPH, LOGL_ERROR, "Failed to bind UNIX socket with path '%s' (errno = %d (%s)).\n", + s->name, errno, strerror(errno)); + return rc; + } + + rc = listen(s->listen_ofd.fd, 1); + if (rc < 0) { + LOGP(DPH, LOGL_ERROR, "Failed to listen to UNIX socket with path '%s' (errno = %d (%s)).\n", + s->name, errno, strerror(errno)); + return rc; + } + } else + open_connection(s); + + LOGP(DPH, LOGL_INFO, "Created PH-socket at '%s'.\n", s->name); + + return 0; +} + +void ph_socket_exit(ph_socket_t *s) +{ + LOGP(DPH, LOGL_INFO, "Destroyed PH-socket.\n"); + + close_connection(s); + if (s->listen_ofd.fd > 0) { + osmo_fd_unregister(&s->listen_ofd); + close(s->listen_ofd.fd); + s->listen_ofd.fd = 0; + } + + if (osmo_timer_pending(&s->retry_timer)) + osmo_timer_del(&s->retry_timer); +} + +static int ph_socket_listen_cb(struct osmo_fd *ofd, unsigned int __attribute__((unused)) what) +{ + ph_socket_t *s = ofd->data; + struct sockaddr_un sock_address; + uint8_t enable = PH_CTRL_UNBLOCK; + int rc, flags; + + socklen_t sock_len = sizeof(sock_address); + /* see if there is an incoming connection */ + rc = accept(s->listen_ofd.fd, (struct sockaddr *)&sock_address, &sock_len); + if (rc > 0) { + if (s->connect_ofd.fd > 0) { + LOGP(DPH, LOGL_ERROR, "Rejecting incoming connection, because we already have a client " + "connected!\n"); + close(rc); + } else { + LOGP(DPH, LOGL_INFO, "Connection from PH-socket client.\n"); + s->connect_ofd.fd = rc; + s->connect_ofd.data = s; + s->connect_ofd.when = BSC_FD_READ; + s->connect_ofd.cb = ph_socket_connect_cb; + osmo_fd_register(&s->connect_ofd); + /* set nonblocking io, because we do multiple reads when handling read event */ + flags = fcntl(s->connect_ofd.fd, F_GETFL); + flags |= O_NONBLOCK; + fcntl(s->connect_ofd.fd, F_SETFL, flags); + /* reset rx buffer */ + s->rx_header_index = 0; + s->rx_data_index = 0; + /* indicate established socket connection */ + s->ph_socket_rx_msg(s, 0, PH_PRIM_CTRL_REQ, &enable, 1); + } + } + + return 0; +} + +static int ph_socket_connect_cb(struct osmo_fd *ofd, unsigned __attribute__((unused)) int what) +{ + ph_socket_t *s = ofd->data; + int rc; + + if (what & BSC_FD_READ) { +rx_again: + if (!s->rx_msg) + s->rx_msg = calloc(1, sizeof(*s->rx_msg)); + if (s->rx_header_index < (int)sizeof(s->rx_msg->msg.header)) { + /* read header until complete */ + rc = recv(s->connect_ofd.fd, ((uint8_t *)&s->rx_msg->msg.header) + s->rx_header_index, + sizeof(s->rx_msg->msg.header) - s->rx_header_index, 0); + if (rc > 0) { + s->rx_header_index += rc; + goto rx_again; + } else if (rc == 0 || errno != EAGAIN) { + close_connection(s); + return 0; + } + } else if (s->rx_data_index < s->rx_msg->msg.header.length) { + /* read data until complete */ + rc = recv(s->connect_ofd.fd, s->rx_msg->msg.data + s->rx_data_index, + s->rx_msg->msg.header.length - s->rx_data_index, 0); + if (rc > 0) { + s->rx_data_index += rc; + goto rx_again; + } else if (rc == 0 || errno != EAGAIN) { + close_connection(s); + return 0; + } + } else { + /* process and free message */ + if (s->rx_msg->msg.header.prim != PH_PRIM_DATA_REQ + && s->rx_msg->msg.header.prim != PH_PRIM_DATA_IND + && s->rx_msg->msg.header.prim != PH_PRIM_DATA_CNF) { + LOGP(DPH, LOGL_DEBUG, "message 0x%02x channel %d from socket\n", + s->rx_msg->msg.header.prim, s->rx_msg->msg.header.channel); + if (s->rx_msg->msg.header.length) + LOGP(DPH, LOGL_DEBUG, " -> data:%s\n", osmo_hexdump(s->rx_msg->msg.data, + s->rx_msg->msg.header.length)); + } + s->ph_socket_rx_msg(s, s->rx_msg->msg.header.channel, s->rx_msg->msg.header.prim, + s->rx_msg->msg.data, s->rx_msg->msg.header.length); + free(s->rx_msg); + s->rx_msg = NULL; + /* reset rx buffer */ + s->rx_header_index = 0; + s->rx_data_index = 0; + } + } + + if (what & BSC_FD_WRITE) { + if (s->tx_list) { + /* some frame in tx list, so try sending it */ + rc = send(s->connect_ofd.fd, ((uint8_t *)&s->tx_list->msg.header), + sizeof(s->tx_list->msg.header) + s->tx_list->msg.header.length, 0); + if (rc > 0) { + struct socket_msg_list *ml; + if (rc != (int)sizeof(s->tx_list->msg.header) + s->tx_list->msg.header.length) { + LOGP(DPH, LOGL_ERROR, "Short write, please fix handling!\n"); + } + /* remove list entry */ + ml = s->tx_list; + s->tx_list = ml->next; + if (s->tx_list == NULL) + s->tx_list_tail = &s->tx_list; + free(ml); + } else if (rc == 0 || errno != EAGAIN) { + close_connection(s); + return 0; + } + } else + s->connect_ofd.when &= ~BSC_FD_WRITE; + } + + return 0; +} + +static void ph_socket_timeout_cb(void *data) +{ + ph_socket_t *s = data; + + open_connection(s); +} + +void ph_socket_tx_msg(ph_socket_t *s, int channel, uint8_t prim, uint8_t *data, int length) +{ + struct socket_msg_list *tx_msg; + + if (prim != PH_PRIM_DATA_REQ + && prim != PH_PRIM_DATA_IND + && prim != PH_PRIM_DATA_CNF) { + LOGP(DPH, LOGL_DEBUG, "message 0x%02x channel %d to socket\n", prim, channel); + if (length) + LOGP(DPH, LOGL_DEBUG, " -> data:%s\n", osmo_hexdump(data, length)); + } + + if (length > (int)sizeof(tx_msg->msg.data)) { + LOGP(DPH, LOGL_NOTICE, "Frame from HDLC process too large for socket, dropping!\n"); + return; + } + + if (s->connect_ofd.fd <= 0) { + LOGP(DPH, LOGL_NOTICE, "Dropping message for socket, socket is closed!\n"); + return; + } + + tx_msg = calloc(1, sizeof(*tx_msg)); + tx_msg->msg.header.channel = channel; + tx_msg->msg.header.prim = prim; + if (length) { + tx_msg->msg.header.length = length; + memcpy(tx_msg->msg.data, data, length); + } + /* move message to list */ + *s->tx_list_tail = tx_msg; + s->tx_list_tail = &tx_msg->next; + s->connect_ofd.when |= BSC_FD_WRITE; +} + diff --git a/src/libph_socket/ph_socket.h b/src/libph_socket/ph_socket.h new file mode 100644 index 0000000..97293c3 --- /dev/null +++ b/src/libph_socket/ph_socket.h @@ -0,0 +1,107 @@ + +#include + +/* + * Procedure: + * + * If socket connection is establised, a PH_PRIM_CTRL_REQ message with + * PH_CTRL_ENABLE information is received by the socket server user. + * If the socket connection is lost, a PH_PRIM_CTRL_REQ message with + * PH_CTRL_DISABLE information is received by the user. + * + * If socket connection is establised, a PH_PRIM_CTRL_IND message with + * PH_CTRL_ENABLE information is received by the socket client user. + * If the socket connection is lost, a PH_PRIM_CTRL_IND message with + * PH_CTRL_DISABLE information is received by the user. + * + * The socket server should enable or disable interface depending on + * the PH_CTRL_ENABLE / PH_CTRL_DISABLE information. + * + * The socket client user shall keep track of last PH_PRIM_ACT_IND / + * PH_PRIM_DEACT_IND message and treat a PH_PRIM_CTRL_IND message with + * PH_CTRL_DISABLE information as a deactivation of all channels that + * were activated. Also it shall reject every PH_RIM_ACT_REQ with a + * PH_PRIM_DACT_IND, if the socket is currently unavailable. + * + * PH_PRIM_CTRL_REQ and PH_PRIM_CTRL_IND messages with PH_CTRL_ENABLE + * and PH_CTRL_DISABLE informations are not assoicated with a channel + * number. The socket sender shall set it to 0, the receiver shall + * ignore it. + * + * A missing MODE in PH_PRIM_ACT_REQ is interepreted as default: + * HDLC on D-channel, TRANS on B-channel. + * + * Each packet on the socket shall have the follwoing header: + * uint8_t channel; + * uint8_t prim; + * uint16_t length; + * + * The length shall be in host's endian on UN sockets and in network + * endian on TCP sockets and not being transmitted on UDP sockets. + * + * 0 to 65535 bytes shall follow the header, depending on the length + * information field. + */ + +/* all primitives */ +#define PH_PRIM_DATA_REQ 0x00 /* any data sent to channel from upper layer */ +#define PH_PRIM_DATA_IND 0x01 /* any data received from channel to upper layer */ +#define PH_PRIM_DATA_CNF 0x02 /* confirm data sent to channel */ + +#define PH_PRIM_CTRL_REQ 0x04 /* implementation specific requests towards interface */ +#define PH_PRIM_CTRL_IND 0x05 /* implementation specific indications from interface */ + +#define PH_PRIM_ACT_REQ 0x08 /* activation request of channel, mode is given as payload */ +#define PH_PRIM_ACT_IND 0x09 /* activation indication that the channel is now active */ + +#define PH_PRIM_DACT_REQ 0x0c /* deactivation request of channel */ +#define PH_PRIM_DACT_IND 0x0d /* deactivation indication that the channel is now inactive */ + +/* one byte sent activation request */ +#define PH_MODE_TRANS 0x00 /* raw data is sent via B-channel */ +#define PH_MODE_HDLC 0x01 /* HDLC transcoding is performed via B-channel */ + +/* one byte sent with control messages */ +#define PH_CTRL_BLOCK 0x00 /* disable (block) interface, when socket is disconnected */ +#define PH_CTRL_UNBLOCK 0x01 /* enable (unblock) interface, when socket is connected */ +#define PH_CTRL_LOOP_DISABLE 0x04 /* disable loopback */ +#define PH_CTRL_LOOP1_ENABLE 0x05 /* enable LT transceier loopback */ +#define PH_CTRL_LOOP2_ENABLE 0x06 /* enable NT transceier loopback */ +#define PH_CTRL_LOOP_ERROR 0x10 /* frame error report (loopback test) */ +#define PH_CTRL_VIOLATION_LT 0x11 /* code violation received by LT */ +#define PH_CTRL_VIOLATION_NT 0x12 /* code violation received by NT */ + +struct socket_msg { + struct { + uint8_t channel; + uint8_t prim; + uint16_t length; + } header; + uint8_t data[65536]; +} __attribute__((packed)); + +struct socket_msg_list { + struct socket_msg_list *next; + struct socket_msg msg; +}; + +#define SOCKET_RETRY_TIMER 2 + +typedef struct ph_socket { + const char *name; + void (*ph_socket_rx_msg)(struct ph_socket *s, int channel, uint8_t prim, uint8_t *data, int length); + void *priv; + struct sockaddr_un sock_address; + struct osmo_fd listen_ofd; /* socket to listen to incoming connections */ + struct osmo_fd connect_ofd; /* socket of incoming connection */ + int connect_failed; /* used to print a failure only once */ + struct osmo_timer_list retry_timer; /* timer to connect again */ + struct socket_msg_list *tx_list, **tx_list_tail; + struct socket_msg_list *rx_msg; + int rx_header_index; + int rx_data_index; +} ph_socket_t; + +int ph_socket_init(ph_socket_t *s, void (*ph_socket_rx_msg)(ph_socket_t *s, int channel, uint8_t prim, uint8_t *data, int length), void *priv, const char *socket_name, int server); +void ph_socket_exit(ph_socket_t *s); +void ph_socket_tx_msg(ph_socket_t *s, int channel, uint8_t prim, uint8_t *data, int length);