mirror of
https://github.com/tio/tio.git
synced 2026-05-01 14:57:59 +02:00
368 lines
9.4 KiB
C
368 lines
9.4 KiB
C
/*
|
|
* tio - a serial device I/O tool
|
|
*
|
|
* Copyright (c) 2014-2022 Martin Lund
|
|
* Copyright (c) 2022 Google LLC
|
|
*
|
|
* 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 2
|
|
* 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, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301, USA.
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <arpa/inet.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <netinet/in.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
|
|
#include "socket.h"
|
|
#include "options.h"
|
|
#include "print.h"
|
|
#include "tty.h"
|
|
|
|
#define MAX_SOCKET_CLIENTS 16
|
|
#define SOCKET_PORT_DEFAULT 3333
|
|
|
|
static int sockfd;
|
|
static int clientfds[MAX_SOCKET_CLIENTS];
|
|
static int socket_family = AF_UNSPEC;
|
|
static int port_number = SOCKET_PORT_DEFAULT;
|
|
|
|
static const char *socket_filename(void)
|
|
{
|
|
/* skip 'unix:' */
|
|
return option.socket + 5;
|
|
}
|
|
|
|
static int socket_inet_port(void)
|
|
{
|
|
/* skip 'inet:' */
|
|
int port = atoi(option.socket + 5);
|
|
if (port == 0)
|
|
{
|
|
port = SOCKET_PORT_DEFAULT;
|
|
}
|
|
return port;
|
|
}
|
|
|
|
static int socket_inet6_port(void)
|
|
{
|
|
/* skip 'inet6:' */
|
|
int port = atoi(option.socket + 6);
|
|
if (port == 0)
|
|
{
|
|
port = SOCKET_PORT_DEFAULT;
|
|
}
|
|
return port;
|
|
}
|
|
|
|
static void socket_exit(void)
|
|
{
|
|
if (socket_family == AF_UNIX)
|
|
{
|
|
unlink(socket_filename());
|
|
}
|
|
}
|
|
|
|
static bool socket_stale(const char *path)
|
|
{
|
|
struct sockaddr_un addr;
|
|
bool stale = false;
|
|
int sfd;
|
|
|
|
/* Test if socket file exists */
|
|
if (access(path, F_OK) == 0)
|
|
{
|
|
/* Create test socket */
|
|
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (sfd < 0)
|
|
{
|
|
tio_warning_printf("Failure opening socket (%s)", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
/* Prepare address */
|
|
addr.sun_family = AF_UNIX;
|
|
strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
|
|
|
|
/* Perform connect to test if socket is active */
|
|
if (connect(sockfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
|
|
{
|
|
if (errno == ECONNREFUSED)
|
|
{
|
|
// No one is listening on socket file
|
|
stale = true;
|
|
}
|
|
}
|
|
|
|
/* Cleanup */
|
|
close(sockfd);
|
|
}
|
|
|
|
return stale;
|
|
}
|
|
|
|
void socket_configure(void)
|
|
{
|
|
struct sockaddr_un sockaddr_unix = {};
|
|
struct sockaddr_in sockaddr_inet = {};
|
|
struct sockaddr_in6 sockaddr_inet6 = {};
|
|
struct sockaddr *sockaddr_p;
|
|
socklen_t socklen;
|
|
|
|
/* Parse socket string */
|
|
|
|
if (strncmp(option.socket, "unix:", 5) == 0)
|
|
{
|
|
socket_family = AF_UNIX;
|
|
|
|
if (strlen(socket_filename()) == 0)
|
|
{
|
|
tio_error_printf("Missing socket filename");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
if (strlen(socket_filename()) > sizeof(sockaddr_unix.sun_path) - 1)
|
|
{
|
|
tio_error_printf("Socket file path %s too long", option.socket);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
if (strncmp(option.socket, "inet:", 5) == 0)
|
|
{
|
|
socket_family = AF_INET;
|
|
|
|
port_number = socket_inet_port();
|
|
|
|
if (port_number < 0)
|
|
{
|
|
tio_error_printf("Invalid port number: %d", port_number);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
if (strncmp(option.socket, "inet6:", 6) == 0)
|
|
{
|
|
socket_family = AF_INET6;
|
|
|
|
port_number = socket_inet6_port();
|
|
|
|
if (port_number < 0)
|
|
{
|
|
tio_error_printf("Invalid port number: %d", port_number);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
if (socket_family == AF_UNSPEC)
|
|
{
|
|
tio_error_printf("%s: Invalid socket scheme, must be prefixed with 'unix:', 'inet:', or 'inet6:'", option.socket);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/* Configure socket */
|
|
|
|
switch (socket_family)
|
|
{
|
|
case AF_UNIX:
|
|
sockaddr_unix.sun_family = AF_UNIX;
|
|
strncpy(sockaddr_unix.sun_path, socket_filename(), sizeof(sockaddr_unix.sun_path) - 1);
|
|
sockaddr_p = (struct sockaddr *) &sockaddr_unix;
|
|
socklen = sizeof(sockaddr_unix);
|
|
|
|
/* Test for stale unix socket file */
|
|
if (socket_stale(socket_filename()))
|
|
{
|
|
tio_printf("Cleaning up old socket file");
|
|
unlink(socket_filename());
|
|
}
|
|
|
|
break;
|
|
|
|
case AF_INET:
|
|
sockaddr_inet.sin_family = AF_INET;
|
|
sockaddr_inet.sin_addr.s_addr = INADDR_ANY;
|
|
sockaddr_inet.sin_port = htons(port_number);
|
|
sockaddr_p = (struct sockaddr *) &sockaddr_inet;
|
|
socklen = sizeof(sockaddr_inet);
|
|
break;
|
|
|
|
case AF_INET6:
|
|
sockaddr_inet6.sin6_family = AF_INET6;
|
|
sockaddr_inet6.sin6_addr = in6addr_any;
|
|
sockaddr_inet6.sin6_port = htons(port_number);
|
|
sockaddr_p = (struct sockaddr *) &sockaddr_inet6;
|
|
socklen = sizeof(sockaddr_inet6);
|
|
break;
|
|
|
|
default:
|
|
tio_error_printf("Invalid socket family (%d)", socket_family);
|
|
exit(EXIT_FAILURE);
|
|
break;
|
|
}
|
|
|
|
/* Create socket */
|
|
sockfd = socket(socket_family, SOCK_STREAM, 0);
|
|
if (sockfd < 0)
|
|
{
|
|
tio_error_printf("Failed to create socket (%s)", strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/* Bind */
|
|
if (bind(sockfd, sockaddr_p, socklen) < 0)
|
|
{
|
|
tio_error_printf("Failed to bind to socket (%s)", strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/* Listen */
|
|
if (listen(sockfd, MAX_SOCKET_CLIENTS) < 0)
|
|
{
|
|
tio_error_printf("Failed to listen on socket (%s)", strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
memset(clientfds, -1, sizeof(clientfds));
|
|
atexit(socket_exit);
|
|
|
|
if (socket_family == AF_UNIX)
|
|
{
|
|
tio_printf("Listening on socket %s", socket_filename());
|
|
}
|
|
else
|
|
{
|
|
tio_printf("Listening on socket port %d", port_number);
|
|
}
|
|
}
|
|
|
|
void socket_write(char input_char)
|
|
{
|
|
if (!option.socket)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i != MAX_SOCKET_CLIENTS; ++i)
|
|
{
|
|
if (clientfds[i] != -1)
|
|
{
|
|
if (write(clientfds[i], &input_char, 1) <= 0)
|
|
{
|
|
tio_error_printf_silent("Failed to write to socket (%s)", strerror(errno));
|
|
close(clientfds[i]);
|
|
clientfds[i] = -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int socket_add_fds(fd_set *rdfs, bool connected)
|
|
{
|
|
if (!option.socket)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int numclients = 0, maxfd = 0;
|
|
for (int i = 0; i != MAX_SOCKET_CLIENTS; ++i)
|
|
{
|
|
if (clientfds[i] != -1)
|
|
{
|
|
/* let clients block if they try to send while we're disconnected */
|
|
if (connected)
|
|
{
|
|
FD_SET(clientfds[i], rdfs);
|
|
maxfd = MAX(maxfd, clientfds[i]);
|
|
}
|
|
numclients++;
|
|
}
|
|
}
|
|
/* don't bother to accept clients if we're already full */
|
|
if (numclients != MAX_SOCKET_CLIENTS)
|
|
{
|
|
FD_SET(sockfd, rdfs);
|
|
maxfd = MAX(maxfd, sockfd);
|
|
}
|
|
return maxfd;
|
|
}
|
|
|
|
bool socket_handle_input(fd_set *rdfs, char *output_char)
|
|
{
|
|
if (!option.socket)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (FD_ISSET(sockfd, rdfs))
|
|
{
|
|
int clientfd = accept(sockfd, NULL, NULL);
|
|
/* this loop should always succeed because we don't select on sockfd when full */
|
|
for (int i = 0; i != MAX_SOCKET_CLIENTS; ++i)
|
|
{
|
|
if (clientfds[i] == -1)
|
|
{
|
|
clientfds[i] = clientfd;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
for (int i = 0; i != MAX_SOCKET_CLIENTS; ++i)
|
|
{
|
|
if (clientfds[i] != -1 && FD_ISSET(clientfds[i], rdfs))
|
|
{
|
|
int status = read(clientfds[i], output_char, 1);
|
|
if (status == 0)
|
|
{
|
|
close(clientfds[i]);
|
|
clientfds[i] = -1;
|
|
continue;
|
|
}
|
|
if (status < 0)
|
|
{
|
|
tio_error_printf_silent("Failed to read from socket (%s)", strerror(errno));
|
|
close(clientfds[i]);
|
|
clientfds[i] = -1;
|
|
continue;
|
|
}
|
|
|
|
/* If INLCR is set, a received NL character shall be translated into a CR character */
|
|
if (*output_char == '\n' && map_i_nl_cr)
|
|
{
|
|
*output_char = '\r';
|
|
}
|
|
else if (*output_char == '\r')
|
|
{
|
|
/* If IGNCR is set, a received CR character shall be ignored (not read). */
|
|
if (map_ign_cr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/* If IGNCR is not set and ICRNL is set, a received CR character shall be translated into an NL character. */
|
|
if (map_i_cr_nl)
|
|
{
|
|
*output_char = '\n';
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|