| /* 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; |
| } |