Files
ttynvt/ttynvt.c
T
Kim Woelders 0f879c8b61 Fix zero size reads
In certain cases we may get zero size reads (e.g. if the server sends an
rfc2217 notification without any other data).
In this situation we must not propagate the zero size read to user space
as it may be interpreted as end of input.

I haven't found a way to tell fuse that it should just ignore this read
but passing EINTR seems to work.
2018-07-02 12:40:27 +02:00

857 lines
21 KiB
C

/*
* Copyright (c) 2018 Lars Thrane A/S
* SPDX-License-Identifier: GPL-2.0
*/
#include "config.h"
#include <cuse_lowlevel.h>
#include <errno.h>
#include <fuse_opt.h>
#include <netdb.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <termios.h>
#include <unistd.h>
#include <linux/sockios.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include "telnet.h"
#define RX_BUF_SIZE 1024
static struct ttynvt_param {
unsigned int major;
unsigned int minor;
char *dev_name;
char *host;
unsigned int port;
unsigned int debug;
} ttynvt_param;
typedef struct ttynvt {
int nonblock;
pthread_mutex_t tty_lock;
pthread_mutex_t poll_lock;
pthread_t thread_id;
pthread_cond_t cond;
struct fuse_pollhandle *ph;
struct pollfd fds[1];
struct termios tio;
unsigned char mcr;
char buf[RX_BUF_SIZE];
size_t blen;
size_t rxcnt;
int txbuf_size;
int epipe;
volatile sig_atomic_t error;
tn_ctx_t *tn;
} ttynvt_t;
static void ttynvt_log(int prio, const char *fmt, ...)
{
va_list arg;
if (prio >= LOG_DEBUG && ttynvt_param.debug <= 0)
return;
va_start(arg, fmt);
vsyslog(prio, fmt, arg);
va_end(arg);
}
static void ttynvt_release(fuse_req_t req, struct fuse_file_info *info)
{
ttynvt_t *tty = (ttynvt_t *) (uintptr_t) info->fh;
ttynvt_log(LOG_INFO, "connection closed\n");
close(tty->fds[0].fd);
(void)pthread_cancel(tty->thread_id);
(void)pthread_join(tty->thread_id, NULL);
(void)pthread_mutex_destroy(&tty->tty_lock);
(void)pthread_mutex_destroy(&tty->poll_lock);
(void)pthread_cond_destroy(&tty->cond);
if (tty->ph)
fuse_pollhandle_destroy(tty->ph);
free(tty->tn);
free(tty);
fuse_reply_err(req, 0);
}
static void ttynvt_notify(ttynvt_t * tty)
{
pthread_mutex_lock(&tty->poll_lock);
if (tty->ph)
{
fuse_lowlevel_notify_poll(tty->ph);
fuse_pollhandle_destroy(tty->ph);
tty->ph = NULL;
}
pthread_mutex_unlock(&tty->poll_lock);
pthread_cond_signal(&tty->cond);
}
static void ttynvt_update_notify(ttynvt_t * tty, struct fuse_pollhandle *ph)
{
pthread_mutex_lock(&tty->poll_lock);
struct fuse_pollhandle *tmp_ph = tty->ph;
tty->ph = ph;
pthread_mutex_unlock(&tty->poll_lock);
if (tmp_ph)
fuse_pollhandle_destroy(tmp_ph);
}
static unsigned int ttynvt_write_avail(ttynvt_t * tty)
{
size_t write_avail;
int used;
pthread_mutex_lock(&tty->tty_lock);
if (ioctl(tty->fds[0].fd, SIOCOUTQ, &used) < 0)
return 0;
write_avail = tty->txbuf_size - used;
pthread_mutex_unlock(&tty->tty_lock);
return write_avail;
}
static int ttynvt_read_avail(ttynvt_t * tty)
{
int read_avail;
pthread_mutex_lock(&tty->tty_lock);
read_avail = tty->rxcnt;
pthread_mutex_unlock(&tty->tty_lock);
return read_avail;
}
static void *ttynvt_read_net(void *arg)
{
ttynvt_t *tty = (ttynvt_t *) arg;
ssize_t res;
int tn_res = -1;
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
while (1)
{
res = poll(tty->fds, 1, -1);
if (res < 0)
{
if (errno == EINTR)
continue;
else
break;
}
else if (tty->fds[0].revents & (POLLERR | POLLHUP | POLLNVAL))
{
break;
}
else if (tty->fds[0].revents & POLLIN)
{
pthread_mutex_lock(&tty->tty_lock);
int read_avail = RX_BUF_SIZE - tty->blen;
if (read_avail == 0)
{
/*buffer overflow */
tty->blen = 0;
read_avail = RX_BUF_SIZE;
ttynvt_log(LOG_WARNING, "Rx buffer overflow\n");
}
res = read(tty->fds[0].fd, tty->buf + tty->blen, read_avail);
if (res <= 0)
{
pthread_mutex_unlock(&tty->tty_lock);
break;
}
else
{
tty->blen += res;
tn_res = telnet_rx(tty->tn, tty->buf, &tty->blen);
if (tn_res == 0)
{
tty->rxcnt = tty->blen;
ttynvt_notify(tty);
}
pthread_mutex_unlock(&tty->tty_lock);
}
}
}
if (res == 0)
ttynvt_log(LOG_INFO, "Connection closed by remote host\n");
else
ttynvt_log(LOG_ERR, "Rx read error:%s\n", strerror(errno));
tty->error = 1;
ttynvt_notify(tty);
return 0;
}
static int ttynvt_connect(const char *host, unsigned int port)
{
struct sockaddr_in serveraddr;
struct hostent *server;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
ttynvt_log(LOG_ERR, "Socket open failed: %s\n", strerror(errno));
return sockfd;
}
server = gethostbyname(host);
if (server == NULL)
{
ttynvt_log(LOG_ERR, "Cannot resolve host name: %s\n", strerror(errno));
return -1;
}
bzero((char *)&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
bcopy((char *)server->h_addr, (char *)&serveraddr.sin_addr.s_addr,
server->h_length);
serveraddr.sin_port = htons(port);
if (connect
(sockfd, (const struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
ttynvt_log(LOG_ERR, "Failed to connect to: %s:%u:%s\n",
host, port, strerror(errno));
return -1;
}
return sockfd;
}
int srv_write(void *cctx, const void *buf, int len)
{
ttynvt_t *tty = cctx;
return write(tty->fds[0].fd, buf, len);
}
int srv_read(void *cctx, int timeout)
{
ttynvt_t *tty = cctx;
int res;
while (1)
{
res = poll(tty->fds, 1, timeout);
if (res <= 0)
return res;
if (tty->fds[0].revents & POLLIN)
{
res = read(tty->fds[0].fd, tty->buf + tty->blen,
RX_BUF_SIZE - tty->blen);
if (res <= 0)
return res;
tty->blen += (size_t) res;
res = telnet_rx(tty->tn, tty->buf, &tty->blen);
if (res == 0)
{
tty->rxcnt = tty->blen;
break;
}
}
}
return 1;
}
void modem_status_cb(void *cctx, int status)
{
ttynvt_t *tty = cctx;
tty->mcr &= ~(TIOCM_CD | TIOCM_RI | TIOCM_CTS | TIOCM_DSR);
ttynvt_log(LOG_INFO, "Modem status: 0x%02x\n", status);
if (status & TNS_STATE_CD)
tty->mcr |= TIOCM_CD;
if (status & TNS_STATE_RI)
tty->mcr |= TIOCM_RI;
if (status & TNS_STATE_CTS)
tty->mcr |= TIOCM_CTS;
if (status & TNS_STATE_DSR)
tty->mcr |= TIOCM_DSR;
}
static void ttynvt_open(fuse_req_t req, struct fuse_file_info *info)
{
ttynvt_t *tty;
socklen_t len;
int sockfd;
int res;
tty = calloc(1, sizeof(*tty));
if (!tty)
{
fuse_reply_err(req, ENOMEM);
return;
}
tty->tn = telnet_ctx_init(tty, srv_write, srv_read, modem_status_cb);
if (!tty->tn)
{
free(tty);
fuse_reply_err(req, ENOMEM);
return;
}
sockfd = ttynvt_connect(ttynvt_param.host, ttynvt_param.port);
if (sockfd < 0)
{
free(tty->tn);
free(tty);
fuse_reply_err(req, EFAULT);
return;
}
tty->fds[0].fd = sockfd;
tty->fds[0].events = POLLIN;
tty->blen = 0;
tty->rxcnt = 0;
res = telnet_open(tty->tn);
if (res < 0)
{
free(tty->tn);
free(tty);
fuse_reply_err(req, EBADE);
return;
}
len = sizeof(tty->txbuf_size);
if (getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &tty->txbuf_size, &len) < 0)
{
free(tty->tn);
free(tty);
fuse_reply_err(req, EIO);
return;
}
if (pthread_mutex_init(&tty->tty_lock, NULL) < 0 ||
pthread_mutex_init(&tty->poll_lock, NULL) < 0 ||
pthread_cond_init(&tty->cond, NULL) < 0 ||
pthread_create(&tty->thread_id, NULL, &ttynvt_read_net, tty))
{
free(tty->tn);
free(tty);
fuse_reply_err(req, ENOMEM);
return;
}
info->fh = (uintptr_t) tty;
info->nonseekable = 1;
info->direct_io = 1;
ttynvt_log(LOG_INFO, "Open connection to server: %s:%d\n",
ttynvt_param.host, ttynvt_param.port);
fuse_reply_open(req, info);
}
static void
ttynvt_read(fuse_req_t req, size_t size, off_t off,
struct fuse_file_info *info)
{
ttynvt_t *tty = (ttynvt_t *) (uintptr_t) info->fh;
if (tty->error)
{
if (tty->epipe == 0)
{
ttynvt_log(LOG_WARNING, "Rx EPIPE\n");
tty->epipe = 1;
fuse_reply_buf(req, tty->buf, 0);
}
else
fuse_reply_err(req, EBADFD);
return;
}
pthread_mutex_lock(&tty->tty_lock);
if (tty->rxcnt == 0 && !(info->flags & O_NONBLOCK))
{
pthread_cond_wait(&tty->cond, &tty->tty_lock);
}
if (size > tty->rxcnt)
size = tty->rxcnt;
if (size > 0)
{
fuse_reply_buf(req, tty->buf, size);
memmove(tty->buf, tty->buf + size, tty->blen - size);
tty->blen -= size;
tty->rxcnt -= size;
}
else
{
fuse_reply_err(req, EINTR);
}
pthread_mutex_unlock(&tty->tty_lock);
}
static void
ttynvt_write(fuse_req_t req, const char *data, size_t size, off_t off,
struct fuse_file_info *info)
{
int res;
ttynvt_t *tty = (ttynvt_t *) (uintptr_t) info->fh;
if (size == 0)
{
fuse_reply_write(req, 0);
return;
}
if (tty->error)
{
fuse_reply_err(req, EPIPE);
return;
}
if (tty->nonblock || (info->flags & O_NONBLOCK))
{
size_t write_avail = ttynvt_write_avail(tty);
if (size > write_avail)
size = write_avail;
}
res = telnet_tx(tty->tn, data, size);
if (res < 0)
{
tty->error = 1;
fuse_reply_err(req, EPIPE);
return;
}
#if 0
else if (res == 0)
{
fuse_reply_err(req, EAGAIN);
return;
}
#endif
fuse_reply_write(req, size);
}
static int baudrate(speed_t speed)
{
return speed == B50 ? 50 :
speed == B75 ? 75 :
speed == B110 ? 110 :
speed == B300 ? 300 :
speed == B600 ? 600 :
speed == B1200 ? 1200 :
speed == B1800 ? 1800 :
speed == B2400 ? 2400 :
speed == B4800 ? 4800 :
speed == B9600 ? 9600 :
speed == B19200 ? 19200 :
speed == B38400 ? 38400 :
speed == B57600 ? 57600 :
speed == B115200 ? 115200 : speed == B230400 ? 230400 : 0;
}
static const char pstr[] = { ' ', 'N', 'O', 'E', 'M', 'S' };
static void
ttynvt_ioctl(fuse_req_t req, int cmd, void *arg,
struct fuse_file_info *info, unsigned int flags,
const void *in_buf, size_t in_bufsz, size_t out_bufsz)
{
ttynvt_t *tty = (ttynvt_t *) (uintptr_t) info->fh;
unsigned int tiocm = 0;
static unsigned int mcr = 0;
static struct termios tio = {
.c_cflag = B115200 | CS8 | PARODD | CREAD | CLOCAL,
};
int val;
switch (cmd)
{
case TCSETSF:
case TCSETSW:
case TCSETS:
if (!in_bufsz)
{
struct iovec iov = { arg, sizeof(struct termios) };
fuse_reply_ioctl_retry(req, &iov, 1, NULL, 0);
}
else
{
speed_t speed;
int csize, par, sb;
memcpy(&tio, in_buf, in_bufsz);
speed = cfgetospeed(&tio);
val = htonl(baudrate(speed));
telnet_rfc2217_cfg(tty->tn, TNS_SET_BAUDRATE, &val, 4);
if ((tio.c_cflag & CSIZE) == CS5)
csize = 5;
else if ((tio.c_cflag & CSIZE) == CS6)
csize = 6;
else if ((tio.c_cflag & CSIZE) == CS7)
csize = 7;
else
csize = 8;
telnet_rfc2217_cfg(tty->tn, TNS_SET_DATASIZE, &csize, 1);
if (tio.c_cflag & PARENB && tio.c_iflag & IGNPAR)
par = 1;
#ifdef CMSPAR
else if (tio.c_cflag & PARENB && tio.c_iflag & CMSPAR)
{
if (tio.c_cflag & PARODD)
par = 4;
else
par = 5;
}
#endif
else if (tio.c_cflag & PARENB)
par = 3;
else
par = 1;
telnet_rfc2217_cfg(tty->tn, TNS_SET_PARITY, &par, 1);
if (tio.c_cflag & CSTOPB)
sb = 2;
else
sb = 1;
telnet_rfc2217_cfg(tty->tn, TNS_SET_STOPSIZE, &sb, 1);
if (tio.c_cflag & CRTSCTS)
telnet_rfc2217_ctl(tty->tn, TNS_CTL_CTSRTS);
if (!(mcr & TIOCM_DTR) && speed != B0)
{
mcr |= TIOCM_DTR;
telnet_rfc2217_ctl(tty->tn, TNS_CTL_DTR_ON);
}
ttynvt_log
(LOG_DEBUG,
"CFG ioctl: %u%c%u%u %cCTSRTS %cCREAD %cHUPCL %cCLOCAL\n",
baudrate(speed), pstr[par], csize, sb,
tio.c_cflag & CRTSCTS ? '+' : '-',
tio.c_cflag & CREAD ? '+' : '-',
tio.c_cflag & HUPCL ? '+' : '-',
tio.c_cflag & CLOCAL ? '+' : '-');
fuse_reply_ioctl(req, 0, 0, 0);
}
break;
case TCGETS:
if (out_bufsz)
{
fuse_reply_ioctl(req, 0, &tio, sizeof(struct termio));
}
else
{
struct iovec iov = { arg, sizeof(struct termio) };
fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1);
}
break;
case TIOCINQ: /* Get the number of bytes in the input
* buffer */
if (out_bufsz)
{
size_t rx_size = ttynvt_read_avail(tty);
fuse_reply_ioctl(req, 0, &rx_size, sizeof(size_t));
}
else
{
struct iovec iov = { arg, sizeof(size_t) };
fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1);
}
break;
case TIOCOUTQ: /* Get the number of bytes in the output
* buffer */
if (out_bufsz)
{
size_t tx_size = ttynvt_write_avail(tty);
fuse_reply_ioctl(req, 0, &tx_size, sizeof(size_t));
}
else
{
struct iovec iov = { arg, sizeof(size_t) };
fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1);
}
break;
case TCFLSH:
if (arg == TCIFLUSH)
{
/*todo */
}
else if ((size_t) arg == TCOFLUSH)
{
/*todo */
}
else if ((size_t) arg == TCIOFLUSH)
{
/*todo */
}
fuse_reply_ioctl(req, 0, 0, 0);
break;
case TCIFLUSH:
val = 1;
telnet_rfc2217_cfg(tty->tn, TNS_SET_PURGE, &val, 1);
pthread_mutex_lock(&tty->tty_lock);
tty->blen = 0;
pthread_mutex_unlock(&tty->tty_lock);
fuse_reply_ioctl(req, 0, 0, 0);
break;
case TCOFLUSH:
val = 2;
telnet_rfc2217_cfg(tty->tn, TNS_SET_PURGE, &val, 1);
fuse_reply_ioctl(req, 0, 0, 0);
break;
case TCIOFLUSH:
val = 3;
telnet_rfc2217_cfg(tty->tn, TNS_SET_PURGE, &val, 1);
fuse_reply_ioctl(req, 0, 0, 0);
break;
case TIOCMGET:
if (out_bufsz)
{
fuse_reply_ioctl(req, 0, &mcr, sizeof(unsigned int));
}
else
{
struct iovec iov = { arg, sizeof(unsigned int) };
fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1);
}
break;
case TIOCMBIS:
if (!in_bufsz)
{
struct iovec iov = { arg, sizeof(unsigned int) };
fuse_reply_ioctl_retry(req, &iov, 1, NULL, 0);
}
else
{
tiocm = *(unsigned int *)in_buf;
fuse_reply_ioctl(req, 0, 0, 0);
}
if (tiocm & TIOCM_RTS)
{
mcr |= TIOCM_RTS;
telnet_rfc2217_ctl(tty->tn, TNS_CTL_RTS_ON);
}
if (tiocm & TIOCM_DTR)
{
mcr |= TIOCM_DTR;
telnet_rfc2217_ctl(tty->tn, TNS_CTL_DTR_ON);
}
break;
case TIOCMBIC:
if (!in_bufsz)
{
struct iovec iov = { arg, sizeof(unsigned int) };
fuse_reply_ioctl_retry(req, &iov, 1, NULL, 0);
}
else
{
tiocm = *(unsigned int *)in_buf;
fuse_reply_ioctl(req, 0, 0, 0);
}
if (tiocm & TIOCM_RTS)
{
mcr &= ~TIOCM_RTS;
telnet_rfc2217_ctl(tty->tn, TNS_CTL_RTS_OFF);
}
if (tiocm & TIOCM_DTR)
{
mcr &= ~TIOCM_DTR;
telnet_rfc2217_ctl(tty->tn, TNS_CTL_DTR_OFF);
}
break;
case TIOCMSET:
if (!in_bufsz)
{
struct iovec iov = { arg, sizeof(unsigned int) };
fuse_reply_ioctl_retry(req, &iov, 1, NULL, 0);
}
else
{
tiocm = *(unsigned int *)in_buf;
fuse_reply_ioctl(req, 0, 0, 0);
ttynvt_log(LOG_DEBUG, "CFG ioctl: DTR=%c RTS=%c\n",
tiocm & TIOCM_DTR ? '1' : '0',
tiocm & TIOCM_RTS ? '1' : '0');
mcr &= ~(TIOCM_RTS | TIOCM_DTR);
mcr |= tiocm & (TIOCM_RTS | TIOCM_DTR);
telnet_rfc2217_ctl(tty->tn, (tiocm & TIOCM_DTR) ?
TNS_CTL_DTR_ON : TNS_CTL_DTR_OFF);
telnet_rfc2217_ctl(tty->tn, (tiocm & TIOCM_RTS) ?
TNS_CTL_RTS_ON : TNS_CTL_RTS_OFF);
}
break;
case TCSBRK:
fuse_reply_ioctl(req, 0, 0, 0);
break;
case TIOCGWINSZ:
if (out_bufsz)
{
struct winsize ws = {.ws_row = 25,.ws_col = 80 };
fuse_reply_ioctl(req, 0, &ws, sizeof(struct winsize));
}
else
{
struct iovec iov = { arg, sizeof(struct winsize) };
fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1);
}
break;
default:
fuse_reply_err(req, ENOSYS);
return;
}
}
static void
ttynvt_poll(fuse_req_t req, struct fuse_file_info *info,
struct fuse_pollhandle *ph)
{
ttynvt_t *tty = (ttynvt_t *) (uintptr_t) info->fh;
int revents = 0;
ttynvt_update_notify(tty, ph);
if (ttynvt_write_avail(tty) > 0)
revents |= POLLOUT;
if (ttynvt_read_avail(tty) > 0)
revents |= POLLIN;
if (tty->error)
{
if (tty->epipe == 0)
revents = POLLHUP | POLLIN;
else
revents = 0;
}
fuse_reply_poll(req, revents);
}
static const struct cuse_lowlevel_ops ttynvt_op = {
.open = ttynvt_open,
.read = ttynvt_read,
.write = ttynvt_write,
.ioctl = ttynvt_ioctl,
.poll = ttynvt_poll,
.release = ttynvt_release,
};
#define TTYNET_OPT(t, p, v) { t, offsetof(struct ttynvt_param, p), v }
static const struct fuse_opt ttynvt_opts[] = {
TTYNET_OPT("-D", debug, 0),
TTYNET_OPT("-M %u", major, 1),
TTYNET_OPT("--maj=%u", major, 1),
TTYNET_OPT("-m %u", minor, 1),
TTYNET_OPT("--min=%u", minor, 1),
TTYNET_OPT("-n %s", dev_name, 0),
TTYNET_OPT("--name=%s", dev_name, 0),
TTYNET_OPT("-s %s", host, 0),
TTYNET_OPT("--server=%s", host, 0),
TTYNET_OPT("-p %u", port, 0),
TTYNET_OPT("--port=%u", port, 0),
FUSE_OPT_KEY("-h", 0),
FUSE_OPT_KEY("--help", 0),
FUSE_OPT_END
};
static void print_help(void)
{
fprintf(stderr, "ttynvt Usage:\n"
"\t-D\tEnable debug\n"
"\t-M major, --maj=major\n"
"\t-m minor, --min=minor\n"
"\t-n name, --name=name (default: ttyNVT0)\n"
"\t\tDevice will be created in /dev/$name.\n"
"\t-s server, --server=host name or IP address\n"
"\t-p port, --port=host port number\n" "\n");
}
static int
process_arg(void *data, const char *arg, int key, struct fuse_args *outargs)
{
switch (key)
{
case 0:
print_help();
return fuse_opt_add_arg(outargs, "-ho");
default:
return 1;
}
}
int main(int argc, char *argv[])
{
struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
char dev_name[128] = { 0 };
const char *dev_info_argv[] = { dev_name };
openlog("ttynvt", LOG_PID, LOG_DAEMON);
if (fuse_opt_parse(&args, &ttynvt_param, ttynvt_opts, process_arg))
{
fprintf(stderr, "Failed to parse arguments\n");
return 1;
}
snprintf(dev_name, sizeof(dev_name), "DEVNAME=%s",
ttynvt_param.dev_name ? ttynvt_param.dev_name : "ttyNVT0");
struct cuse_info ci = {
.dev_major = ttynvt_param.major,
.dev_minor = ttynvt_param.minor,
.dev_info_argc = 1,
.dev_info_argv = dev_info_argv,
.flags = CUSE_UNRESTRICTED_IOCTL,
};
return cuse_lowlevel_main(args.argc, args.argv, &ci, &ttynvt_op, NULL);
}