diff options
-rw-r--r-- | .gitignore | 64 | ||||
-rw-r--r-- | Makefile.am | 5 | ||||
-rw-r--r-- | configure.ac | 23 | ||||
-rw-r--r-- | src/main.c | 440 | ||||
-rwxr-xr-x | tests/run-test.sh | 4 |
5 files changed, 536 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ffbbf4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# http://www.gnu.org/software/automake + +Makefile.in +.deps/ +.dirstamp + +# http://www.gnu.org/software/autoconf + +autom4te.cache +/autoscan.log +/autoscan-*.log +/aclocal.m4 +/compile +/config.cache +/config.guess +/config.h.in +/config.log +/config.status +/config.sub +/configure +/configure~ +/configure.scan +/depcomp +/install-sh +/missing +/stamp-h1 +/build-aux/ + +# https://www.gnu.org/software/libtool/ + +/ltmain.sh + +# http://www.gnu.org/software/texinfo + +/texinfo.tex + +# http://www.gnu.org/software/m4/ + +m4/libtool.m4 +m4/ltoptions.m4 +m4/ltsugar.m4 +m4/ltversion.m4 +m4/lt~obsolete.m4 + +# Generated Makefile +# (meta build system like autotools, +# can automatically generate from config.status script +# (which is called by configure script)) +Makefile + +# Object files +*.o +*.ko +*.obj +*.elf + +# Executables +tiptorrent + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..d252360 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,5 @@ +sbin_PROGRAMS = tiptorrent-client + +AM_CFLAGS = ${LIBEVENT_CFLAGS} -g -Wall + +tiptorrent_client_SOURCES = src/main.c diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..e78ac0e --- /dev/null +++ b/configure.ac @@ -0,0 +1,23 @@ +AC_INIT(tiptorrent-client, 1.0, info@soleta.eu) +AC_CONFIG_AUX_DIR([build-aux]) +AC_PREFIX_DEFAULT(/usr) + +AC_CANONICAL_HOST +AC_CONFIG_MACRO_DIR([m4]) +AM_INIT_AUTOMAKE([-Wall foreign subdir-objects tar-pax no-dist-gzip dist-bzip2 1.6]) + +m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) + +AC_PROG_CC +AC_PROG_INSTALL +AC_PROG_LN_S + +case "$host" in +*-*-linux*) ;; +*) AC_MSG_ERROR([Linux only, sorry!]);; +esac + +AC_CHECK_LIB([ev], [ev_loop_new], , AC_MSG_ERROR([libev not found])) + +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..d08336b --- /dev/null +++ b/src/main.c @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2021 Soleta Networks <info@soleta.eu> + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + */ + +#include <stdlib.h> +#include <stdio.h> +#include <ev.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <errno.h> +#include <string.h> +#include <sys/fcntl.h> +#include <unistd.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <syslog.h> +#include <stddef.h> +#include <stdbool.h> +#include <sys/time.h> +#include <limits.h> +#include <syslog.h> + +#define TIP_TORRENT_PORT 9999 + +/* number of chunks for files. */ +#define MAX_CHUNKS 4 + +#define container_of(ptr, type, member) ({ \ + typeof( ((type *)0)->member ) *__mptr = (ptr); \ + (type *)( (char *)__mptr - offsetof(type,member) );}) + +static const char *filename; +static const char *addr; +struct ev_loop *tip_main_loop; + +enum { + TIP_CLIENT_RECEIVING_HEADER, + TIP_CLIENT_RECEIVING_PAYLOAD, + TIP_CLIENT_NOTIFY_REDIRECT, + TIP_CLIENT_DONE, +}; + +struct tip_client { + ev_io io; + struct sockaddr_in addr; + char buf[10240000]; + uint32_t buf_len; + uint64_t data_len; + uint64_t content_len; + int state; + int fd; + bool error; + bool redirected; + const char *payload; +}; + +static struct tip_client _cli = { + .fd = -1, +}; + +struct { + uint32_t direct_from_server; + uint32_t redirects; +} tip_client_stats; + +static int tip_client_recv(struct tip_client *cli, int events) +{ + struct ev_io *io = &cli->io; + int ret; + + ret = recv(io->fd, cli->buf + cli->buf_len, + sizeof(cli->buf) - cli->buf_len, 0); + if (ret < 0) { + syslog(LOG_ERR, "error reading from server %s:%hu (%s)\n", + inet_ntoa(cli->addr.sin_addr), ntohs(cli->addr.sin_port), + strerror(errno)); + } + + return ret; +} + +static void tip_client_close(struct tip_client *cli) +{ + ev_io_stop(tip_main_loop, &cli->io); + shutdown(cli->io.fd, SHUT_RDWR); + close(cli->io.fd); + cli->buf_len = 0; +} + +static void tip_client_error(struct tip_client *cli) +{ + cli->error = true; + tip_client_close(cli); +} + +static int tip_client_connect(const char *addr); + +static int tip_client_state_recv_hdr(struct tip_client *cli) +{ + char *ptr, *trailer, *payload; + char redirect_addr[32]; + uint32_t payload_len; + uint32_t header_len; + int ret; + + ptr = strstr(cli->buf, "\r\n\r\n"); + if (!ptr) + return 0; + + if (!strncmp(cli->buf, "HTTP/1.1 404 Not Found", strlen("HTTP/1.1 404 Not Found"))) { + syslog(LOG_ERR, "server says file `%s' not found\n", filename); + return -1; + } + if (!strncmp(cli->buf, "HTTP/1.1 301 Moves Permanently", strlen("HTTP/1.1 301 Moves Permanently"))) { + ptr = strstr(cli->buf, "Location:"); + if (!ptr) + return -1; + + ret = sscanf(ptr, "Location: http://%31s[^\r\n]", + redirect_addr); + if (ret != 1) + return -1; + + ptr = strchr(redirect_addr, ':'); + if (!ptr) + return -1; + + ptr[0] = '\0'; + + syslog(LOG_INFO, "Redirected to %s to fetch file %s\n", + redirect_addr, filename); + + cli->redirected = true; + tip_client_close(cli); + tip_client_connect(redirect_addr); + cli->state = TIP_CLIENT_RECEIVING_HEADER; + + return 0; + } + + trailer = ptr + 4; + + ptr = strstr(cli->buf, "Content-Length: "); + if (!ptr) + return -1; + + if (sscanf(ptr, "Content-Length: %lu[^\r\n]", &cli->content_len) != 1) + return -1; + if (cli->content_len < 0) + return -1; + + if (cli->content_len == 0) { + cli->buf_len = 0; + return 1; + } + + if (cli->redirected) + tip_client_stats.redirects++; + else + tip_client_stats.direct_from_server++; + + cli->fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (cli->fd < 0) + return ret; + + header_len = trailer - cli->buf; + payload = cli->buf + header_len; + payload_len = cli->buf_len - header_len; + cli->data_len += cli->buf_len; + cli->buf_len = 0; + + if (payload_len > 0) { + ret = write(cli->fd, payload, payload_len); + if (ret < 0) + return ret; + } + + if (payload_len >= cli->content_len) + return 0; + + return 1; +} + +static int tip_client_state_recv_payload(struct tip_client *cli) +{ + int ret; + + cli->data_len += cli->buf_len; + + ret = write(cli->fd, cli->buf, cli->buf_len); + if (ret < 0) + return ret; + + cli->buf_len = 0; + + if (cli->data_len >= cli->content_len) { + if (cli->redirected) { + tip_client_close(cli); + tip_client_connect(addr); + cli->state = TIP_CLIENT_NOTIFY_REDIRECT; + + return 1; + } + cli->state = TIP_CLIENT_DONE; + return 0; + } + + return 1; +} + +static int tip_client_state_notify_redirect(struct tip_client *cli) +{ + char *ptr; + + ptr = strstr(cli->buf, "\r\n\r\n"); + if (!ptr) + return 0; + + if (strncmp(cli->buf, "HTTP/1.1 200 OK", strlen("HTTP/1.1 200 OK"))) + return -1; + + ptr = strstr(cli->buf, "Content-Length: "); + if (!ptr) + return -1; + + if (sscanf(ptr, "Content-Length: %lu[^\r\n]", &cli->content_len) != 1) + return -1; + if (cli->content_len < 0) + return -1; + + if (cli->content_len != 0) + return -1; + + cli->state = TIP_CLIENT_DONE; + + return 0; +} + +static void tip_client_read_cb(struct ev_loop *loop, struct ev_io *io, int events) +{ + struct tip_client *cli; + int ret; + + cli = container_of(io, struct tip_client, io); + + ret = tip_client_recv(cli, events); + if (ret < 0) + goto error; + + cli->buf_len += ret; + + switch (cli->state) { + case TIP_CLIENT_RECEIVING_HEADER: + ret = tip_client_state_recv_hdr(cli); + if (ret < 0) + goto error; + if (!ret) + return; + + cli->state = TIP_CLIENT_RECEIVING_PAYLOAD; + /* Fall through. */ + case TIP_CLIENT_RECEIVING_PAYLOAD: + ret = tip_client_state_recv_payload(cli); + if (ret < 0) + goto error; + if (ret == 0) + goto close; + break; + case TIP_CLIENT_NOTIFY_REDIRECT: + ret = tip_client_state_notify_redirect(cli); + if (ret < 0) + goto error; + if (ret == 0) + goto close; + break; + } + return; +error: + tip_client_error(cli); + return; +close: + tip_client_close(cli); + return; +} + +static void tip_client_connect_cb(struct ev_loop *loop, struct ev_io *io, int events) +{ + struct tip_client *cli; + char buf[PATH_MAX + 1]; + int ret, len; + + cli = container_of(io, struct tip_client, io); + + if (events & EV_ERROR) + return; + + len = sizeof(cli->addr); + ret = connect(cli->io.fd, (struct sockaddr *)&cli->addr, len); + if (ret < 0) { + if (errno != EINPROGRESS) { + perror("connect"); + tip_client_error(cli); + return; + } + } + + if (cli->state == TIP_CLIENT_NOTIFY_REDIRECT) + snprintf(buf, sizeof(buf), "POST /%s HTTP/1.1\r\n\r\n", filename); + else + snprintf(buf, sizeof(buf), "GET /%s HTTP/1.1\r\n\r\n", filename); + + ret = send(cli->io.fd, buf, strlen(buf), 0); + if (ret < 0) { + tip_client_error(cli); + return; + } + + ev_io_stop(tip_main_loop, &cli->io); + ev_io_init(&cli->io, tip_client_read_cb, cli->io.fd, EV_READ); + ev_io_start(tip_main_loop, &cli->io); +} + +static int tip_client_connect(const char *addr) +{ + struct tip_client *cli = &_cli; + int remote_fd; + int flags; + int len; + int ret; + + remote_fd = socket(AF_INET, SOCK_STREAM, 0); + if (remote_fd < 0) + return -1; + + flags = fcntl(remote_fd, F_GETFL); + flags |= O_NONBLOCK; + ret = fcntl(remote_fd, F_SETFL, flags); + if (ret < 0) + return ret; + + cli->addr.sin_family = AF_INET; + cli->addr.sin_addr.s_addr = inet_addr(addr); + cli->addr.sin_port = htons(TIP_TORRENT_PORT); + + len = sizeof(cli->addr); + ret = connect(remote_fd, (struct sockaddr *)&cli->addr, len); + if (ret < 0 && errno != EINPROGRESS) { + perror("connect"); + return ret; + } + + ev_io_init(&cli->io, tip_client_connect_cb, remote_fd, EV_WRITE); + ev_io_start(tip_main_loop, &cli->io); + + syslog(LOG_INFO, "connecting to %s to fetch file %s\n", addr, filename); + + return 0; +} + +static uint32_t select_file_chunk(bool *file_chunk) +{ + struct timeval tv; + uint32_t k; + int i; + + gettimeofday(&tv, NULL); + srand(tv.tv_usec); + + k = rand() % MAX_CHUNKS; + for (i = 0; i < MAX_CHUNKS; i++) { + if (!file_chunk[k]) + break; + + k++; + if (k == MAX_CHUNKS) + k = 0; + } + + return k; +} + +static char _filename[PATH_MAX + 1]; + +int main(int argc, char *argv[]) +{ + struct timeval tv_start, tv_stop, tv; + bool file_chunk[MAX_CHUNKS] = {}; + int i, k; + + if (argc != 3) { + printf("%s [ip] [file]\n", argv[0]); + return EXIT_FAILURE; + } + addr = argv[1]; + + openlog("tiptorrent-client", LOG_PID, LOG_DAEMON); + + signal(SIGPIPE, SIG_IGN); + + tip_main_loop = ev_default_loop(0); + + gettimeofday(&tv_start, NULL); + + for (i = 0; i < MAX_CHUNKS; i++) { + memset(&_cli, 0, sizeof(_cli)); + + k = select_file_chunk(file_chunk); + snprintf(_filename, sizeof(_filename), "%s.%u", argv[2], k); + filename = _filename; + + syslog(LOG_INFO, "Requesting file %s to server\n", filename); + + tip_client_connect(argv[1]); + _cli.state = TIP_CLIENT_RECEIVING_HEADER; + + while (_cli.state != TIP_CLIENT_DONE || _cli.error) + ev_loop(tip_main_loop, 0); + + file_chunk[k] = true; + } + + if (_cli.state == TIP_CLIENT_DONE) { + gettimeofday(&tv_stop, NULL); + timersub(&tv_stop, &tv_start, &tv); + printf("Done in %lus.%lums. " + "Direct from server: %u Redirected: %u\n", + tv.tv_sec, tv.tv_usec, + tip_client_stats.direct_from_server, + tip_client_stats.redirects); + return EXIT_SUCCESS; + } + + printf("Failure, see syslog for details.\n"); + return EXIT_FAILURE; +} diff --git a/tests/run-test.sh b/tests/run-test.sh new file mode 100755 index 0000000..afda064 --- /dev/null +++ b/tests/run-test.sh @@ -0,0 +1,4 @@ +for ((i=1;i<=18;i++)) +do + ip netns exec c$i .././tiptorrent-client 10.141.10.1 TEST & +done |