From 48f773b3abbc8524ddb57b4a6a6d37a0749deac6 Mon Sep 17 00:00:00 2001 From: Evan Burkey Date: Tue, 9 Jul 2024 21:03:23 +0000 Subject: [PATCH] Implement Server (#1) - Generic Server struct - TCP and UDP Reviewed-on: https://git.burkey.co/eburk/libflint/pulls/1 --- .gitea/workflows/jobs.yaml | 2 +- .gitignore | 3 + CMakeLists.txt | 22 +++++- Makefile | 32 -------- build.sh | 10 --- clanggen.sh | 17 +++++ docs/input.md | 19 ++++- docs/network.md | 106 ++++++++++++++++++++++++++ include/lfinput.h | 4 + include/lfnetwork.h | 28 +++++++ run_tests.sh | 12 +++ src/input.c | 16 ++++ src/{macos => }/macos.c | 0 src/network.c | 150 +++++++++++++++++++++++++++++++++++++ tests/netmanual.c | 7 ++ tests/tests.c | 61 ++++++++++++++- 16 files changed, 440 insertions(+), 49 deletions(-) delete mode 100644 Makefile delete mode 100755 build.sh create mode 100755 clanggen.sh create mode 100644 docs/network.md create mode 100644 include/lfnetwork.h create mode 100755 run_tests.sh rename src/{macos => }/macos.c (100%) create mode 100644 src/network.c create mode 100644 tests/netmanual.c diff --git a/.gitea/workflows/jobs.yaml b/.gitea/workflows/jobs.yaml index 5cf1842..288ac9d 100644 --- a/.gitea/workflows/jobs.yaml +++ b/.gitea/workflows/jobs.yaml @@ -14,7 +14,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libbsd-dev cmake build-essential + sudo apt-get install -y libbsd-dev cmake build-essential netcat - name: Build and test run: | diff --git a/.gitignore b/.gitignore index c3157f7..221cf55 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ compile_commands.json site libflint.so test +tcptest +testrunner .idea +netmanual diff --git a/CMakeLists.txt b/CMakeLists.txt index a8dc823..fa09448 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.17) project(flint C) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +if ((${CMAKE_SYSTEM_NAME} STREQUAL "Linux")) + add_compile_definitions(flint __USE_XOPEN_EXTENDED) +endif() + set(CMAKE_C_STANDARD 99) include_directories(include) @@ -17,16 +21,17 @@ set(SOURCES src/utility.c src/crypto.c src/parsing.c + src/network.c ) if ((${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")) - add_library(flint ${SOURCES} src/macos/macos.c) + add_library(flint ${SOURCES} src/macos.c) else() add_library(flint ${SOURCES}) endif() if ((${CMAKE_SYSTEM_NAME} STREQUAL "Linux")) - target_link_libraries(flint bsd) + target_link_libraries(flint pthread bsd) endif() if(${CMAKE_PROJECT_NAME} STREQUAL flint) @@ -34,8 +39,17 @@ if(${CMAKE_PROJECT_NAME} STREQUAL flint) target_include_directories(tests PRIVATE include) if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") - target_link_libraries(tests flint bsd) + target_link_libraries(tests flint pthread bsd) else() - target_link_libraries(tests flint) + target_link_libraries(tests flint pthread) + endif() + + add_executable(netmanual tests/netmanual.c) + target_include_directories(netmanual PRIVATE include) + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + target_link_libraries(netmanual flint pthread bsd) + else() + target_link_libraries(netmanual flint pthread) endif() endif() diff --git a/Makefile b/Makefile deleted file mode 100644 index e4a2582..0000000 --- a/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -.PHONY : clean - -CFLAGS = -std=c99 -Iinclude -pedantic -Wall -Wextra -LDFLAGS = -fPIC -shared - -TARGET = libflint.so -SRC != ls src/*.c -OBJ = $(SRC:./src/$.c=./obj/%.o) - -PREFIX = $(DESTDIR)/usr/local -LIBDIR = $(PREFIX)/lib - -all: $(TARGET) - -$(TARGET): $(OBJ) - cc $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(OBJ) - -./obj/%.o: ./src/%.c - cc $(CFLAGS) -c $< -o $@ - -install: $(TARGET) - cp $(TARGET) $(LIBDIR) - -uninstall: - rm -f $(LIBDIR)/$(TARGET) - -clean: - rm -f $(TARGET) - rm -f test - -test: - cc $(CFLAGS) -o test tests/tests.c src/*.c diff --git a/build.sh b/build.sh deleted file mode 100755 index c860355..0000000 --- a/build.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -e - -# For building outside of CLion - -mkdir -p build -cd build -cmake .. -make -cp compile_commands.json .. -cd diff --git a/clanggen.sh b/clanggen.sh new file mode 100755 index 0000000..fbcb531 --- /dev/null +++ b/clanggen.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh + +set -e + +CFLAGS="-I./include -I/usr/local/include" + +rm -f compile_commands.json + +for f in $(find ./src -type f | grep -v macos); do + n=$(echo $f | grep -o '[^/]*$' | sed 's/c$/json/') + cc -MJ $n $CFLAGS -c $f +done + +rm *.o +sed -e '1s/^/[/' -e '$s/,$/]/' *.json > compile_commands.out +rm *.json +mv compile_commands.out compile_commands.json diff --git a/docs/input.md b/docs/input.md index 8537d0e..dae3f84 100644 --- a/docs/input.md +++ b/docs/input.md @@ -92,7 +92,8 @@ printf("%s\n", sp[0]); // Prints "Split" ### del_split -Frees all memory used by `split()`. Just like `split`, it does not touch the original string +Frees all memory used by `split()`. Just like `split`, it does not touch the original string. + ```c void del_split(char **sp); @@ -101,3 +102,19 @@ size_t sp_sz = 0; char **sp = split("Delete Me!", &sp_sz, " "); void del_split(sp); ``` + +### capture_system + +Runs a command on the system shell and returns stdout as a string. `buffsize` is the size of +the returned buffer that holds `stdout`. Passing `0` to `buffsize` will use the default buffer size of `1024`. + +User is responsible for freeing the returned string. + +```c +const char *capture_system(const char *cmd, int buffsize); + +/* Usage */ +const char *cap = capture_system("ls $HOME", 0); +printf("%s\n", cap); +free(cap); +``` \ No newline at end of file diff --git a/docs/network.md b/docs/network.md new file mode 100644 index 0000000..b47a96c --- /dev/null +++ b/docs/network.md @@ -0,0 +1,106 @@ +# network + +This module provides a generic `Server` type that abstracts away the setup and teardown of a socket + +## Enums + +### ServerType + +Types of servers. Currently supports TCP and UDP, will eventually add UNIX sockets. + +```c +typedef enum ServerType { + SERVERTYPE_TCP, + SERVERTYPE_UDP +} ServerType; +``` + +## Structs + +### Server + +Server is a generic abstraction over sockets. The type of the server is defined by `server_type`. + +```c +typedef struct Server { + ServerType server_type; + int fd; + int port; + void (*handler)(struct Server *s); +} Server; +``` + +## Functions + +### new_server + +Create a `Server*`. User is responsible for freeing the memory. + +```c +Server *new_server(ServerType type, const char *port, void(handler)(Server *s)); +``` + +### delete_server + +Frees the memory allocated for `Server*` and sets the pointer to `NULL`. + +```c +void delete_server(Server *s); +``` + +### serve + +Starts up the server. `backlog_size` is the size of the backlog buffer for the underlying socket. Use the macro +`DEFAULT_BACKLOG_SIZE` or pass `0` to use a reasonable default. + +```c +int serve(Server *s, int backlog_size); +``` + +### get_in_addr + +Convenience method to get an IP address from a `struct sockaddr_storage` of either IPV4 or IPV6. + +```c +void *get_in_addr(struct sockaddr *sa); + +/* Usage */ +struct sockaddr_storage client_addr; +socklen_t client_addr_sz = sizeof(client_addr); +char buf[33]; + +if (new_fd = accept(s->fd, (struct sockaddr *)&client_addr, &client_addr_sz) == -1) { + /* error handling */ +} +inet_ntop(client_addr.ss_family, get_in_addr((struct sockaddr *)&client_addr), buf, 32); +printf("Received connection from %s\n", buf); + +``` + +### handler_tcp_echo + +An example handler for a multithreaded tcp echo server. + +```c +void handler_tcp_echo(Server *s); + +/* Usage */ +#include "lfnetwork.h" + +int main(int argc, char **argv) { + Server *server = new_server(SERVERTYPE_TCP, "80", handler_tcp_echo); + serve(server, DEFAULT_BACKLOG); + delete_server(server); +} +``` + +## Macros + +### DEFAULT_BACKLOG_SIZE + +A default size for the socket's backlog buffer. `5` is a standard default size, providing some backlog but not +enough to make huge buffers for each socket + +```c +#define DEFAULT_BACKLOG_SIZE 5 +``` \ No newline at end of file diff --git a/include/lfinput.h b/include/lfinput.h index 53ecb0c..3e5bc23 100644 --- a/include/lfinput.h +++ b/include/lfinput.h @@ -17,4 +17,8 @@ void del_split(char **); void del_lines(char **); +#define DEFAULT_CAPTURE_SYSTEM_BUFSIZE 1024 + +const char *capture_system(const char *cmd, int buf_sz); + #endif // LIBFLINT_INPUT_H diff --git a/include/lfnetwork.h b/include/lfnetwork.h new file mode 100644 index 0000000..b4869f0 --- /dev/null +++ b/include/lfnetwork.h @@ -0,0 +1,28 @@ +#ifndef LIBFLINT_NET_H +#define LIBFLINT_NET_H + +#include + +typedef enum ServerType { + SERVERTYPE_TCP, + SERVERTYPE_UDP +} ServerType; + +typedef struct Server { + ServerType server_type; + int fd; + int port; + void (*handler)(struct Server *s); +} Server; + +#define DEFAULT_BACKLOG 5 + +Server *new_server(ServerType type, const char *port, void(handler)(Server *s)); +void delete_server(Server *s); +int serve(Server *s, int backlog_size); +void *get_in_addr(struct sockaddr *sa); + +// Example handlers +void handler_tcp_echo(Server *s); + +#endif //LIBFLINT_NET_H diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..ba1a860 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,12 @@ +set -e + +#./testrunner +./tcptest & +tcpout=$(echo "hello" | nc localhost 18632) +echo "tcpout: $tcpout" +if [ "$tcpout" != "TEST SEND" ]; then + echo "Error: \"$tcpout\" != \"TEST SEND\"" + exit 1 +fi + +exit 0 diff --git a/src/input.c b/src/input.c index f20b820..23d54c9 100644 --- a/src/input.c +++ b/src/input.c @@ -2,6 +2,7 @@ #include #include #include +#include #ifdef __linux__ @@ -121,3 +122,18 @@ void del_split(char **sp) { void del_lines(char **lines) { del_split(lines); } + +const char *capture_system(const char *cmd, int buf_sz) { + if (buf_sz == 0) { + buf_sz = DEFAULT_CAPTURE_SYSTEM_BUFSIZE; + } + char *buf = malloc(buf_sz); + FILE *tmp = popen(cmd, "r"); + if (tmp == NULL) { + fprintf(stderr, "libflint: failed to open FILE *tmp in capture_system. Errno: %d\n", errno); + free(buf); + return NULL; + } + fgets(buf, buf_sz, tmp); + return buf; +} \ No newline at end of file diff --git a/src/macos/macos.c b/src/macos.c similarity index 100% rename from src/macos/macos.c rename to src/macos.c diff --git a/src/network.c b/src/network.c new file mode 100644 index 0000000..2984122 --- /dev/null +++ b/src/network.c @@ -0,0 +1,150 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lfnetwork.h" + +static void sighandler(int s) { + int saved_errno = errno; + while (waitpid(-1, NULL, WNOHANG) > 0); + errno = saved_errno; +} + +void *get_in_addr(struct sockaddr *sa) +{ + if (sa->sa_family == AF_INET) { + return &(((struct sockaddr_in*)sa)->sin_addr); + } + + return &(((struct sockaddr_in6*)sa)->sin6_addr); +} + +Server *new_server(ServerType type, const char *port, void(handler)(Server *s)) { + Server *s = (Server *)malloc(sizeof(Server)); + if (s == NULL) { + return NULL; + } + s->server_type = type; + s->handler = handler; + + struct addrinfo *addr = NULL; + if (getaddrinfo(NULL, port, NULL, &addr) != 0) { + free(s); + return NULL; + } + s->port = (int)strtol(port, NULL, 10); + + int socktype = 0; + switch (type) { + case SERVERTYPE_TCP: + socktype = SOCK_STREAM; + break; + case SERVERTYPE_UDP: + socktype = SOCK_DGRAM; + break; + } + + struct addrinfo *p; + for (p = addr; p != NULL; p = p->ai_next) { + s->fd = socket(AF_INET, socktype, 0); + if (s->fd == -1) { + continue; + } + + if (bind(s->fd, p->ai_addr, p->ai_addrlen) != 0) { + close(s->fd); + continue; + } + + break; + } + + if (p == NULL) { + fprintf(stderr, "Failed to bind\n"); + free(s); + return NULL; + } + + freeaddrinfo(addr); + return s; +} + +void delete_server(Server *s) { + free(s); + s = NULL; +} + +int serve(Server *s, int backlog_size) { + if (backlog_size == 0) { + backlog_size = DEFAULT_BACKLOG; + } + + if (listen(s->fd, backlog_size) != 0) { + return 1; + } + + // Linux doesn't handle SA_RESTART properly, and I don't know about Windows (nor do I care) + // This is just for macOS and BSDs + #if !defined(__linux__) && !defined(_WIN32) + struct sigaction sa; + sa.sa_handler = sighandler; + sa.sa_flags = SA_RESTART; + if (sigaction(SIGCHLD, &sa, NULL) == -1) { + fprintf(stderr, "Failed to set sigaction\n"); + return 1; + } + #endif + + s->handler(s); + + return 0; +} + +static void *tcp_echo_thread(void *vargp) { + while (1) { + char recv_buf[256]; + int fd = *(int *) vargp; + + int r = (int)recv(fd, recv_buf, 256, 0); + if (r < 1) { + if (r == -1) { + fprintf(stderr, "Failed to recv. Errno: %d\n", errno); + } + close(fd); + break; + } + + if (send(fd, recv_buf, strlen(recv_buf), 0) == -1) { + fprintf(stderr, "Failed to send echo. Errno: %d\n", errno); + close(fd); + break; + } + } +} + +void handler_tcp_echo(Server *s) { + while (1) { + struct sockaddr_storage client_addr; + socklen_t client_addr_sz = sizeof(client_addr); + int new_fd = accept(s->fd, (struct sockaddr *)&client_addr, &client_addr_sz); + if (new_fd == -1) { + fprintf(stderr, "failed to accept. Errno: %d\n", errno); + continue; + } + + pthread_t srv_tid; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_create(&srv_tid, &attr, tcp_echo_thread, &new_fd); + } +} diff --git a/tests/netmanual.c b/tests/netmanual.c new file mode 100644 index 0000000..c1eb3df --- /dev/null +++ b/tests/netmanual.c @@ -0,0 +1,7 @@ +#include "lfnetwork.h" + +int main(int argc, char **argv) { + Server *server = new_server(SERVERTYPE_TCP, "18632", handler_tcp_echo); + serve(server, DEFAULT_BACKLOG); + delete_server(server); +} diff --git a/tests/tests.c b/tests/tests.c index 541550e..663631f 100644 --- a/tests/tests.c +++ b/tests/tests.c @@ -2,8 +2,11 @@ #include #include #include +#include +#include #include "lflinkedlist.h" +#include "lfnetwork.h" #include "lfset.h" #include "lfstack.h" #include "lfbinarytree.h" @@ -12,6 +15,7 @@ #include "lfstring.h" #include "lfcrypto.h" #include "lfparsing.h" +#include "lfinput.h" #if defined(__APPLE__) || defined(__MACH__) #include "lfmacos.h" @@ -395,6 +399,60 @@ void test_parsing() { printf("Passes all parsing tests\n"); } +#define NET_MSG "TEST SEND" + +void tcp_test_handler(Server *s) { + struct sockaddr_storage client_addr; + socklen_t client_addr_sz = sizeof(client_addr); + int new_fd = accept(s->fd, (struct sockaddr *)&client_addr, &client_addr_sz); + assert(new_fd != -1); + assert(send(new_fd, NET_MSG, 10, 0) != -1); + close(new_fd); +} + +void *tcp_server_thread(void *vargp) { + Server *server = new_server(SERVERTYPE_TCP, "18632", tcp_test_handler); + serve(server, DEFAULT_BACKLOG); + delete_server(server); +} + +void udp_test_handler(Server *s) { + struct sockaddr_storage client_addr; + socklen_t client_addr_sz = sizeof(client_addr); + char recv_buf[128]; + + int r = (int)recvfrom(s->fd, recv_buf, 128, 0, (struct sockaddr*)&client_addr, &client_addr_sz); + assert(r > 0); + assert(strcmp(recv_buf, NET_MSG) == 0); +} + +void *udp_server_thread(void *vargp) { + Server *server = new_server(SERVERTYPE_UDP, "18633", udp_test_handler); + serve(server, DEFAULT_BACKLOG); + delete_server(server); +} + +void test_network() { + printf("\n--- NETWORK TEST ---\n"); + pthread_t srv_tid; + pthread_create(&srv_tid, NULL, tcp_server_thread, NULL); + + sleep(1); + const char *s = capture_system("echo hello | nc localhost 18632", 0); + assert(strcmp(s, NET_MSG) == 0); + free((char *)s); + + pthread_join(srv_tid, NULL); + printf("Passed TCP test\n"); + + pthread_create(&srv_tid, NULL, udp_server_thread, NULL); + sleep(1); + system("echo hello | nc localhost 18633"); + + pthread_join(srv_tid, NULL); + printf("Passed UDP test\n"); +} + #if defined(__APPLE__) || defined(__MACH__) void test_macos() { printf("\n--- macOS TEST ---\n"); @@ -420,10 +478,11 @@ int main() { test_string(); test_crypto(); test_parsing(); + test_network(); #if defined(__APPLE__) || defined(__MACH__) test_macos(); #endif return 0; -} \ No newline at end of file +}