blob: 06f6f2cd9529dafd45a56fc7b6b98ee52adf91b0 [file] [log] [blame] [raw]
/* PPP over WebSocket server
* Copyright 2015-2019 Rivoreo
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <sys/param.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#ifdef __SVR4
#include <stropts.h>
#include <sys/sockio.h>
#else
#include <sys/ioctl.h>
#endif
#include <net/if.h>
#include <termios.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <cli/fio_cli.h>
#include <http/http.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define USE_PTY 1
#if defined __sun && defined __SVR4
#define DEFAULT_PPPD_PATH "/usr/bin/pppd"
#else
#define DEFAULT_PPPD_PATH "/usr/sbin/pppd"
#endif
#ifdef BSD
#define setpgrp() setpgid(0,0)
#endif
struct session_udata {
pid_t pppd_pid;
#ifdef USE_PTY
int master_fd;
#else
int write_fd;
int read_fd;
#endif
int host_number;
};
static const char *accept_path;
static size_t accept_path_len;
static const char *pppd_path;
static ws_s *websocket_instance_list_by_fd[256];
static const char *ppp_local_address;
static size_t ppp_local_address_string_length;
static const char *ppp_remote_address_network_number;
static size_t ppp_remote_address_network_number_string_length;
static unsigned char allocated_remote_host_numbers[254];
static void on_ppp_data(intptr_t uuid, fio_protocol_s *protocol) {
//fprintf(stderr, "function: on_ppp_data(%ld, %p)\n", (long int)uuid, protocol);
ws_s *ws = websocket_instance_list_by_fd[fio_uuid2fd(uuid)];
if(!ws) {
fprintf(stderr, "on_ppp_data: error: websocket instance pointer is null!\n");
abort();
}
char buffer[4096];
while(1) {
errno = 0;
int s = fio_read(uuid, buffer, sizeof buffer);
if(s < 0) {
if(errno) {
perror("fio_read");
exit(1);
} else {
exit(0);
}
}
if(!s) return;
fio_str_info_s buffer_info = {
.capa = sizeof buffer,
.len = s,
.data = buffer
};
if(websocket_write(ws, buffer_info, 0) < 0) {
perror("websocket_write");
return;
}
}
}
static void on_ppp_close(intptr_t uuid, fio_protocol_s *protocol) {
fprintf(stderr, "function: on_ppp_close(%ld, %p)\n", (long int)uuid, protocol);
ws_s *ws = websocket_instance_list_by_fd[fio_uuid2fd(uuid)];
if(ws) websocket_close(ws);
}
static fio_protocol_s ppp_in_protocol = {
.on_data = on_ppp_data,
.on_close = on_ppp_close
};
#ifdef USE_PTY
static void terminal_disable_echo(int fd) {
struct termios attr;
if(tcgetattr(fd, &attr) < 0) {
perror("tcgetattr");
return;
}
attr.c_lflag &= ~(ICANON | ECHO);
if(tcsetattr(fd, TCSANOW, &attr) < 0) {
perror("tcsetattr");
}
}
static int allocate_pty(int *master_fd, int *slave_fd) {
int fd = open("/dev/ptmx", O_RDWR);
if(fd == -1) {
perror("allocate_pty: open: /dev/ptmx");
return -1;
}
if(grantpt(fd) < 0) {
perror("grantpt");
close(fd);
return -1;
}
if(unlockpt(fd) < 0) {
perror("unlockpt");
close(fd);
return -1;
}
char *name = ptsname(fd);
if(!name) {
perror("ptsname");
close(fd);
return -1;
}
*slave_fd = open(name, O_RDWR);
if(*slave_fd == -1) {
perror(name);
close(fd);
return -1;
}
*master_fd = fd;
#ifdef __SVR4
ioctl(*slave_fd, I_PUSH, "ptem");
ioctl(*slave_fd, I_PUSH, "ldterm");
#endif
terminal_disable_echo(*slave_fd);
return 0;
}
#endif
// Return boolean
static int check_peer_address(unsigned char i) {
static int s = -1;
static struct in_addr addr;
if(s == -1) {
s = socket(AF_INET, SOCK_DGRAM, 0);
if(s == -1) {
perror("check_peer_address: socket");
return 1;
}
}
if(addr.s_addr) {
addr.s_addr = ((i + 2) << 24) | (addr.s_addr & 0xffffff);
} else {
char buffer[16];
sprintf(buffer, "%s.%hhu", ppp_remote_address_network_number, i + 2);
if(inet_pton(AF_INET, buffer, &addr) < 1) {
fputs("inet_pton failed\n", stderr);
return 1;
}
}
//fprintf(stderr, "check_peer_address: checking address '%s'\n", inet_ntoa(addr));
struct ifconf ifc;
int n;
#ifdef SIOCGIFNUM
if(ioctl(s, SIOCGIFNUM, &n) < 0) {
perror("check_peer_address: ioctl: SIOCGIFNUM");
return 1;
}
ifc.ifc_len = sizeof(struct ifreq) * n;
#else
ifc.ifc_len = 0;
ifc.ifc_buf = NULL;
if(ioctl(s, SIOCGIFCONF, &ifc) < 0) {
perror("check_peer_address: ioctl: SIOCGIFCONF");
return 1;
//die("SIOCGIFCOUNT");
}
n = ifc.ifc_len / sizeof(struct ifreq);
#endif
ifc.ifc_buf = malloc(ifc.ifc_len);
if(!ifc.ifc_buf) {
perror("check_peer_address: malloc");
return 1;
}
//fprintf(stderr, "n = %d, ifc_len = %d, ifc_buf = %p\n", n, ifc.ifc_len, ifc.ifc_buf);
if(ioctl(s, SIOCGIFCONF, &ifc) < 0) {
perror("check_peer_address: ioctl: SIOCGIFCONF");
free(ifc.ifc_buf);
return 1;
}
while(n > 0) {
struct ifreq *ifr = ifc.ifc_req + --n;
if(ioctl(s, SIOCGIFFLAGS, ifr) < 0) {
perror("ioctl: SIOCGIFFLAGS");
continue;
}
if(!(ifr->ifr_flags & IFF_POINTOPOINT)) continue;
if(ioctl(s, SIOCGIFDSTADDR, ifr) < 0) {
perror("ioctl: SIOCGIFDSTADDR");
continue;
}
if(ifr->ifr_dstaddr.sa_family != AF_INET) continue;
struct in_addr dstaddr = ((struct sockaddr_in *)&ifr->ifr_dstaddr)->sin_addr;
//fprintf(stderr, "check_peer_address: checking existing peer address '%s'\n", inet_ntoa(dstaddr));
//if(addr == dstaddr) {
if(memcmp(&addr, &dstaddr, sizeof addr) == 0) {
//fputs("found existing address!\n", stderr);
free(ifc.ifc_buf);
return 0;
}
}
free(ifc.ifc_buf);
return 1;
}
//static char *allocate_remote_address() {
//static char buffer[16];
static int allocate_remote_host_number() {
for(int i = 0; i < sizeof allocated_remote_host_numbers; i++) {
if(allocated_remote_host_numbers[i]) continue;
if(!check_peer_address(i)) continue;
allocated_remote_host_numbers[i] = 1;
//memcpy(buffer, ppp_remote_address_network_number, ppp_remote_address_network_number_string_length);
//sprintf(buffer + ppp_remote_address_network_number_string_length, ".%hhu", i + 2);
//return buffer;
return i;
}
return -1;
}
static void on_ws_open(ws_s *ws) {
// TODO: add ipparam
fprintf(stderr, "function: on_ws_open(%p)\n", ws);
int remote_host_number = allocate_remote_host_number();
if(remote_host_number < 0) {
fprintf(stderr, "on_ws_open: no remote address available\n");
return;
}
#ifdef USE_PTY
int master_fd, slave_fd;
if(allocate_pty(&master_fd, &slave_fd) < 0) {
perror("on_ws_open: allocate_pty");
allocated_remote_host_numbers[remote_host_number] = 0;
return;
}
if(master_fd >= sizeof websocket_instance_list_by_fd / sizeof *websocket_instance_list_by_fd) {
fprintf(stderr, "on_ws_open: got master_fd = %d: too many open files\n", master_fd);
allocated_remote_host_numbers[remote_host_number] = 0;
close(master_fd);
close(slave_fd);
return;
}
#else
int pipe_fds_0[2], pipe_fds_1[2];
if(pipe(pipe_fds_0) < 0) {
allocated_remote_host_numbers[remote_host_number] = 0;
perror("pipe");
return;
}
if(pipe(pipe_fds_1) < 0) {
perror("pipe");
allocated_remote_host_numbers[remote_host_number] = 0;
close(pipe_fds_0[0]);
close(pipe_fds_0[1]);
return;
}
if(pipe_fds_1[0] >= sizeof websocket_instance_list_by_fd / sizeof *websocket_instance_list_by_fd) {
fprintf(stderr, "on_ws_open: got fd %d: too many open files\n", pipe_fds_1[0]);
allocated_remote_host_numbers[remote_host_number] = 0;
close(pipe_fds_0[0]);
close(pipe_fds_0[1]);
close(pipe_fds_1[0]);
close(pipe_fds_1[1]);
return;
}
#endif
pid_t pid = fio_fork();
if(pid < 0) {
perror("fork");
allocated_remote_host_numbers[remote_host_number] = 0;
#ifdef USE_PTY
close(master_fd);
close(slave_fd);
#else
close(pipe_fds_0[0]);
close(pipe_fds_0[1]);
close(pipe_fds_1[0]);
close(pipe_fds_1[1]);
#endif
return;
}
if(pid) {
#ifdef USE_PTY
close(slave_fd);
#else
close(pipe_fds_0[0]);
close(pipe_fds_1[1]);
#endif
struct session_udata *data = malloc(sizeof(struct session_udata));
if(!data) {
fprintf(stderr, "on_ws_open: malloc: out of memory\n");
kill(pid, SIGTERM);
allocated_remote_host_numbers[remote_host_number] = 0;
return;
}
fprintf(stderr, "on_ws_open debug: data = %p\n", data);
data->pppd_pid = pid;
#ifdef USE_PTY
data->master_fd = master_fd;
#define read_fd data->master_fd
#else
data->write_fd = pipe_fds_0[1];
data->read_fd = pipe_fds_1[0];
#define read_fd data->read_fd
#endif
data->host_number = remote_host_number;
websocket_udata_set(ws, data);
#if 0
fio_protocol_s *ppp_in_protocol = malloc(sizeof(fio_protocol_s));
if(!ppp_in_protocol) {
// TODO
return;
}
memset(ppp_in_protocol, 0, (sizeof(fio_protocol_s));
ppp_in_protocol->on_data = on_ppp_data;
ppp_in_protocol->on_close = on_ppp_close;
#endif
fio_set_non_block(read_fd);
fio_attach_fd(read_fd, &ppp_in_protocol);
websocket_instance_list_by_fd[read_fd] = ws;
#undef read_fd
} else {
#ifdef USE_PTY
close(master_fd);
#else
close(pipe_fds_0[1]);
close(pipe_fds_1[0]);
#endif
close(0);
close(1);
#ifdef USE_PTY
if(dup2(slave_fd, 0) == -1 || dup2(slave_fd, 1) == -1) {
perror("dup2");
_exit(1);
}
if(slave_fd != 0 && slave_fd != 1) close(slave_fd);
#else
if(dup2(pipe_fds_0[0], 0) == -1) {
perror("dup2");
_exit(1);
}
if(dup2(pipe_fds_1[1], 1) == -1) {
perror("dup2");
_exit(1);
}
#endif
//char addresses[32];
char addresses[ppp_local_address_string_length + 1 + ppp_remote_address_network_number_string_length + 1];
memcpy(addresses, ppp_local_address, ppp_local_address_string_length);
addresses[ppp_local_address_string_length] = ':';
memcpy(addresses + ppp_local_address_string_length + 1, ppp_remote_address_network_number, ppp_remote_address_network_number_string_length);
sprintf(addresses + ppp_local_address_string_length + 1 + ppp_remote_address_network_number_string_length, ".%hhu", (unsigned char)remote_host_number + 2);
fio_str_info_s peer_addr = fio_peer_addr(websocket_uuid(ws));
if(setpgrp() < 0) perror("setpgrp");
execl(pppd_path, pppd_path, "file", "/etc/ppp/options.ws",
addresses, "ipparam", peer_addr.data,
#ifndef USE_PTY
"notty",
#endif
NULL);
perror(pppd_path);
_exit(127);
}
}
static void on_ws_close(intptr_t uuid, void *udata) {
fprintf(stderr, "function: on_ws_close(%ld, %p)\n", (long int)uuid, udata);
struct session_udata *data = (struct session_udata *)udata;
fprintf(stderr, "wspppd debug: on_ws_close: data = %p\n", data);
if(!data) return;
fprintf(stderr, "wspppd debug: on_ws_close: data->pppd_pid = %d\n", (int)data->pppd_pid);
kill(data->pppd_pid, SIGTERM);
#ifdef USE_PTY
fprintf(stderr, "wspppd debug: on_ws_close: data->master_fd = %d\n", data->master_fd);
close(data->master_fd);
websocket_instance_list_by_fd[data->master_fd] = NULL;
#else
close(data->write_fd);
close(data->read_fd);
websocket_instance_list_by_fd[data->read_fd] = NULL;
#endif
allocated_remote_host_numbers[data->host_number] = 0;
free(data);
}
static void on_ws_message(ws_s *ws, fio_str_info_s msg, uint8_t is_text) {
struct session_udata *data = (struct session_udata *)websocket_udata_get(ws);
if(!data) return;
if(is_text) {
//fprintf(stderr, "wspppd warning: on_ws_message: ws instance %p: received text message, length %zu\n", ws, msg.len);
return;
}
write(
#ifdef USE_PTY
data->master_fd
#else
data->write_fd
#endif
, msg.data, msg.len);
}
static void on_request(http_s *req) {
fprintf(stderr, "function: on_request(%p)\n", req);
}
static void on_upgrade(http_s *req, char *target, size_t len) {
fprintf(stderr, "function: on_upgrade(%p, %p<%s>, %zu)\n", req, target, target, len);
if(len != 9 || memcmp(target, "websocket", 9)) {
http_send_error(req, 400);
return;
}
fio_str_info_s path = fiobj_obj2cstr(req->path);
if(path.len != accept_path_len || memcmp(path.data, accept_path, accept_path_len)) {
http_send_error(req, 400);
return;
}
http_upgrade2ws(req, .on_open = on_ws_open, .on_close = on_ws_close, .on_message = on_ws_message);
}
static void signal_handler(int sig) {
if(sig != SIGCHLD) return;
while(1) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if(pid == -1) {
if(errno == EINTR) continue;
if(errno == ECHILD) return;
perror("waitpid");
return;
}
if(!pid) return;
fprintf(stderr, "signal_handler: process %d terminated\n", (int)pid);
}
}
int main(int argc, char **argv) {
FIO_LOG_LEVEL = FIO_LOG_LEVEL_INFO;
fio_cli_start(argc, argv, 2, 2, "PPP over WebSocket server",
FIO_CLI_PRINT("Usage: wspppd [<options>] <local-address> <remote-network-bits-with-prefix-length-24>"),
FIO_CLI_PRINT_HEADER("Options:"),
FIO_CLI_STRING("-b Bind to address"),
FIO_CLI_INT("-p Bind to TCP port"),
FIO_CLI_STRING("-h HTTP document root directory"),
FIO_CLI_STRING("-r Virtual path to accept PPP request"),
FIO_CLI_STRING("-e Path to pppd(8) program"));
fio_cli_set_default("-p", "8080");
accept_path = fio_cli_get("-r");
if(!accept_path) accept_path = "/";
accept_path_len = strlen(accept_path);
pppd_path = fio_cli_get("-e");
if(!pppd_path) pppd_path = DEFAULT_PPPD_PATH;
ppp_local_address = fio_cli_unnamed(0);
ppp_local_address_string_length = strlen(ppp_local_address);
if(ppp_local_address_string_length > 15) {
fprintf(stderr, "%s: Local IP address too long\n", argv[0]);
return -1;
}
ppp_remote_address_network_number = fio_cli_unnamed(1);
char *dot = strchr(ppp_remote_address_network_number, '.');
if(!dot) goto invalid_remote_address;
dot = strchr(dot + 1, '.');
if(!dot) goto invalid_remote_address;
dot = strchr(dot + 1, '.');
if(!dot) {
invalid_remote_address:
fprintf(stderr, "%s: Invalid IP address '%s'\n", argv[0], ppp_remote_address_network_number);
return -1;
}
*dot = 0;
ppp_remote_address_network_number_string_length = strlen(ppp_remote_address_network_number);
if(ppp_remote_address_network_number_string_length > 11) {
fprintf(stderr, "%s: Remote IP address too long\n", argv[0]);
return -1;
}
puts(ppp_remote_address_network_number);
struct sigaction act = { .sa_handler = SIG_IGN };
if(sigaction(SIGPIPE, &act, NULL) < 0) {
perror("sigaction: SIGPIPE");
return 1;
}
act.sa_handler = signal_handler;
if(sigaction(SIGCHLD, &act, NULL) < 0) {
perror("sigaction: SIGCHLD");
return 1;
}
/*
for(unsigned int i = 0; i < sizeof websocket_instance_list_by_fd / sizeof *websocket_instance_list_by_fd; i++) {
websocket_instance_list_by_fd[i] = NULL;
}
*/
if(http_listen(fio_cli_get("-p"), fio_cli_get("-b"),
.on_request = on_request, .on_upgrade = on_upgrade,
.log = 1, .public_folder = fio_cli_get("-h")) == -1) {
perror("http_listen");
return 1;
}
fio_start(.threads = 1, .workers = 1);
fio_cli_end();
return 0;
}