Files
ttynvt/ttynvt.c
T
Kim Woelders 8956f3f851 Cleanups
- Consequently use "unsigned int", not "unsigned"
- Indent
- Sort headers
2018-06-28 13:37:25 +02:00

878 lines
22 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 <fcntl.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 <time.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
#define MAX_HOST_NAME 64
static char host_name[MAX_HOST_NAME];
static int host_port;
static int lfd;
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;
void ttynvt_log(const char *fmt, ...)
{
struct tm *ts;
time_t t;
char s[32];
va_list arg;
if (lfd < 0)
return;
t = time(NULL);
ts = gmtime(&t);
strftime(s, 32, "%b %a %T", ts);
dprintf(lfd, "%s ", s);
va_start(arg, fmt);
vdprintf(lfd, 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("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("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("connection closed by remote host\n");
else
ttynvt_log("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("ERROR: opening socket: %s\n", strerror(errno));
return sockfd;
}
server = gethostbyname(host);
if (server == NULL)
{
ttynvt_log("ERROR: can't 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("ERROR: 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("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(host_name, host_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("open connection to server: %s:%d\n", host_name, host_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("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;
fuse_reply_buf(req, tty->buf, size);
memmove(tty->buf, tty->buf + size, tty->blen - size);
tty->blen -= size;
tty->rxcnt -= size;
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;
}
else if (res == 0)
{
fuse_reply_err(req, EAGAIN);
return;
}
fuse_reply_write(req, res);
}
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
("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("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 }
struct ttynvt_param {
unsigned int major;
unsigned int minor;
char *dev_name;
char *logfile;
char *host;
unsigned int port;
};
static const struct fuse_opt ttynvt_opts[] = {
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),
TTYNET_OPT("-l %s", logfile, 0),
TTYNET_OPT("--log=%s", logfile, 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-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-l logfile, --log=file 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);
struct ttynvt_param param = { 0 };
char dev_name[128] = { 0 };
const char *dev_info_argv[] = { dev_name };
if (fuse_opt_parse(&args, &param, ttynvt_opts, process_arg))
{
fprintf(stderr, "Failed to parse arguments\n");
return 1;
}
snprintf(dev_name, sizeof(dev_name), "DEVNAME=%s",
param.dev_name ? param.dev_name : "ttyNVT0");
snprintf(host_name, sizeof(host_name), "%s", param.host);
host_port = param.port;
if (param.logfile)
{
lfd =
open(param.logfile, O_CREAT | O_RDWR | O_APPEND,
S_IWUSR | S_IRUSR);
if (lfd < 0)
{
perror("Failed to open log file");
return 1;
}
ttynvt_log("ttynvt ver:%s started, dev:%s\n",
PACKAGE_VERSION, param.dev_name);
}
else
lfd = -1;
struct cuse_info ci = {
.dev_major = param.major,
.dev_minor = 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);
}