spartserv

Simple client and server for the spartan protocol
git clone https://noulin.net/git/spartserv.git
Log | Files | Refs | README

commit 858eeb9a627173d4a239c14e5c1bd1e51c3d82c2
parent 4c4e0cf470bf8cf85f773e828370777032796484
Author: Remy Noulin <loader2x@gmail.com>
Date:   Mon, 10 Jul 2023 21:09:05 +0200

add spartan over UDP

.gitignore       |  63 +++++++++++
README.md        |  22 ++++
build.sh         |   2 +
spartclientudp.c | 103 ++++++++++++++++++
spartservudp.c   | 315 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 505 insertions(+)

Diffstat:
A.gitignore | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MREADME.md | 22++++++++++++++++++++++
Mbuild.sh | 2++
Aspartclientudp.c | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aspartservudp.c | 315+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 505 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,63 @@ +# Vim +*.sw* + +# Debug +.gdb_history + +# Coverage +*.gcov +*.gcda +*.gcno + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf diff --git a/README.md b/README.md @@ -1,5 +1,12 @@ +# Spartserv + This repository has 2 simple clients and 2 simple servers for the spartan protocol written in C and x64 assembly. +Additionally, there is a client and a server using the spartan protocol over UDP: + +- spartservudp +- spartclientudp + About the spartan protocol: [Spartan on the web](https://portal.mozz.us/spartan/spartan.mozz.us/) [Spartan on gemini](gemini://spartan.mozz.us) @@ -7,6 +14,7 @@ About the spartan protocol: `spartasm` is the server written in assembly and the information about `spartasm` is in `spartasm/README.md`. +# Build To build the clients and server written in C, you need a shell and the GCC C compiler and run: ``` apt-get install gcc @@ -43,6 +51,20 @@ The line type `=:` is not supported, to upload data run `sparline` with the URL ./sparline spartan://hostname/path --infile afile.txt ``` +# Spartan over UDP +To run spartan over UDP, you need 2 machines (it is not possible to run the server and client on the same machine) and these machines have to be to send and received UDP packets. +If you have router with NAT, you need to setup port forwarding or port triggering. + +The define `HOSTNAME` in `spartservudp.c` has to be set to the address used on the client machine. + +`spartclientudp` works in the same way as `spartclient`: + +``` +./spartclient 192.168.1.2 3000 / +``` + +A request is one UDP packet and a response is also one UDP packet, so the maximum document length is 63000 bytes. + # Running on port 300 The default port for spartan is port 300 and in general only root processes can open a listening socket on port under 1024. diff --git a/build.sh b/build.sh @@ -2,6 +2,8 @@ gcc -std=gnu11 -g3 spartserv.c -o spartserv gcc -std=gnu11 -g3 spartclient.c -o spartclient gcc -std=gnu11 -g3 sparline.c -o sparline +gcc -std=gnu11 -g3 spartservudp.c -o spartservudp +gcc -std=gnu11 -g3 spartclientudp.c -o spartclientudp echo Run to start spartan server: echo ./spartserv diff --git a/spartclientudp.c b/spartclientudp.c @@ -0,0 +1,103 @@ +// Usage: spartclient hostname port path +#include <stdio.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +#include <netinet/in.h> +#include <string.h> +#include <stdlib.h> +#include <ctype.h> +#include <unistd.h> + +int64_t parseInt(const char *string) { + while (!isdigit(*string) && *string != '-' && *string != 0) { + string++; + } + int64_t r = strtoll(string, NULL, 10); + return(r); +} + +int main(int ac, char **av){ + int sock; + struct sockaddr_in server; + struct hostent *hp; + int mysock; + char buf[65536] = {0}; + int rval; + int i; + + if (ac < 4) { + puts("Usage: spartclient hostname port path"); + return 0; + } + + sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0){ + perror("Failed to create socket"); + } + + server.sin_family = AF_INET; + + hp = gethostbyname(av[1]); + if (hp==0) { + perror("gethostbyname failed"); + close(sock); + exit(1); + } + + memcpy(&server.sin_addr, hp->h_addr, hp->h_length); + + int64_t port = parseInt(av[2]); + + if (port < 1 || port > 65000) { + printf("Invalid port %d.\n", port); + return 1; + } + + server.sin_port = htons(port); + + // open socket for response + struct sockaddr_in respaddr; + int rsock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (rsock < 0){ + perror("Failed to create socket"); + } + + respaddr.sin_family = AF_INET; + respaddr.sin_addr.s_addr = INADDR_ANY; + respaddr.sin_port = htons(port); + + if (bind(rsock, (struct sockaddr *) &respaddr, sizeof(respaddr))){ + perror("bind failed"); + exit(1); + } + + // build request + size_t len = strlen(av[1]); + memcpy(buf, av[1], len); + buf[len] = ' '; + char *cursor = buf + len + 1; + len = strlen(av[3]); + memcpy(cursor, av[3], len); + memcpy(cursor + len, " 0\r\n", strlen(" 0\r\n")); + cursor += len + strlen(" 0\r\n"); + + // send request + if (sendto(sock, buf, cursor - buf, 0, (const struct sockaddr *)&server, sizeof(server)) < 0) { + perror("send failed"); + close(sock); + exit(1); + } + close(sock); + + // reveice server response + struct sockaddr_in addr; + socklen_t ln = sizeof(addr); + rval = recvfrom(rsock, buf, sizeof(buf)-1/* -1 to add end of string before printing */, 0, (struct sockaddr *) &addr, &ln); + close(rsock); + if (addr.sin_addr.s_addr == server.sin_addr.s_addr && rval != -1 && rval != 0) { + buf[rval] = 0; + puts(buf); + } +} + diff --git a/spartservudp.c b/spartservudp.c @@ -0,0 +1,315 @@ +/* +This server creates a socket listening to PORT and +starts the event loop +2 mimetypes are set depending on file extension: +text/gemini .gmi +text/markdown .md .markdown +*/ + +#define _GNU_SOURCE +#include <string.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <stdbool.h> +#include <ctype.h> +#include <stdarg.h> +#include <limits.h> +#include <time.h> +#include <fcntl.h> // access + +// inet_ntoa +//already included #include <sys/socket.h> +//already included #include <netinet/in.h> +#include <arpa/inet.h> + + +// server configuration +// TODO set HOSTNAME +#define HOSTNAME "" +//#define HOSTNAME "192.168.1.2" +#define ROOT "." +#define PORT 3000 + +bool isDir(const char *path) { + struct stat st; + + if (stat(path, &st) == -1) { + return(false); + } + + if (!S_ISDIR(st.st_mode)) { + return(false); + } + return(true); +} + +int main(int ac, char **av){ + int sock; + struct sockaddr_in server; + int mysock; + char buf[63000]; + int rval; + + char root[PATH_MAX] = {0}; + if (ROOT[0] == '/') { + realpath(ROOT, root); + } + else { + // create absolute path from relative ROOT path + char p[PATH_MAX] = {0}; + getcwd(p, PATH_MAX); + strcat(p, "/"); + strcat(p, ROOT); + realpath(p, root); + strcat(root, "/"); + } + + size_t rootLen = strlen(root); + size_t slash = 0; + + // count slashes at the end of root + // to compare paths with memcmp correctly + // since realpath removes the slashes at the + // end of the path from client. + while(root[rootLen-1-slash] == '/') { + slash++; + } + + server.sin_family = AF_INET; + server.sin_addr.s_addr = INADDR_ANY; + server.sin_port = htons(PORT); + + /* setsockopt: Handy debugging trick that lets + * us rerun the server immediately after we kill it; + * otherwise we have to wait about 20 secs. + * Eliminates "ERROR on binding: Address already in use" error. + */ + int optval = 1; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int)); + + printf("Serving "HOSTNAME":%d %s\n", PORT, root); + + // date for request print + char date[50]; + do { + sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0){ + perror("Failed to create socket"); + } + + if (bind(sock, (struct sockaddr *) &server, sizeof(server))){ + perror("bind failed"); + exit(1); + } + listen(sock, SOMAXCONN); + + // struct for printing client ip in terminal with inet_ntoa + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + memset(buf, 0, sizeof(buf)); + ssize_t r = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len); + close(sock); + if (r == -1) + perror("recvfrom failed"); + else { + // new client + // get YMD HMS date + time_t clk = time(NULL); + struct tm *pClk = localtime(&clk); + strftime(date, sizeof(date), "%Y-%m-%d:%H:%M:%S", pClk); + printf("%s %s ",date, inet_ntoa(addr.sin_addr)); + + int mysock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (mysock < 0){ + perror("Failed to create socket"); + } + addr.sin_port = htons(PORT); + + // validate request then scan hostname path and content length + + // find request end + char *reqEnd = memmem(buf, sizeof(buf), "\r\n", 2); + if (!reqEnd || buf[0] == ' ') { + puts("4 Invalid request"); + // add MSG_NOSIGNAL flag to ignore SIGPIPE when the client closes the socket early + sendto(mysock, "4 Invalid request\r\n", sizeof("4 Invalid request\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + + // check ascii + char *cursor = buf; + bool isBad = false; + while (cursor < reqEnd) { + if (*cursor < 32 || *cursor == 127) { + isBad = true; + break; + } + cursor++; + } + if (isBad) { + puts("4 Non ASCII"); + sendto(mysock, "4 Non ASCII\r\n", sizeof("4 Non ASCII\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + + // print request in terminal + *reqEnd = 0; + printf("%s ", buf); + *reqEnd = '\r'; + + // parse hostname, path and content_length in request + char *hostname; + char *path; + char *content_length; + + hostname = buf; + + // hostname must match HOSTNAME + // comment out this test to accept nay hostname + int c = memcmp(hostname, HOSTNAME, strlen(HOSTNAME)); + + if (c != 0) { + puts("4 Hostname"); + sendto(mysock, "4 Hostname\r\n", sizeof("4 Hostname\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + + // get path + cursor = buf; + while (*cursor != ' ' && cursor < reqEnd) { + cursor++; + } + + cursor++; + if (cursor >= reqEnd || *cursor == ' ') { + puts("4 Path"); + sendto(mysock, "4 Path\r\n", sizeof("4 Path\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + + path = cursor; + + // get content_length + while (*cursor != ' ' && cursor < reqEnd) { + cursor++; + } + + cursor++; + if (cursor >= reqEnd || *cursor == ' ') { + puts("4 Length"); + sendto(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + + content_length = cursor; + while(cursor < reqEnd) { + if (!isdigit(*cursor)) { + isBad = true; + break; + } + cursor++; + } + + // the request must not have any content + // content_length = 0 + if (isBad || reqEnd - content_length > 1 || *content_length != '0') { + puts("4 Length"); + sendto(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + + // replace SPC with 0 at the end of path + *(content_length-1) = 0; + + // build server path + char localPath[PATH_MAX] = {0}; + if (rootLen + strlen(path) >= PATH_MAX) { + puts("5 Path too long"); + sendto(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + memcpy(localPath, root, rootLen); + cursor = localPath + rootLen; + memcpy(cursor, path, strlen(path)); + + // check path + if (access(localPath, R_OK) == -1) { + puts("4 Not found"); + sendto(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + char realPath[PATH_MAX] = {0}; + realpath(localPath, realPath); + if (memcmp(realPath, root, rootLen-slash) != 0) { + puts("4 Not found"); + sendto(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + + size_t pathLen = strlen(realPath); + cursor = realPath + pathLen; + if (isDir(realPath)) { + if (pathLen + strlen("/index.gmi") >= PATH_MAX) { + puts("5 Path too long"); + sendto(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + memcpy(cursor, "/index.gmi", strlen("/index.gmi")); + cursor += strlen("/index.gmi"); + } + + FILE *f = fopen(realPath, "r"); + if (!f) { + puts("4 Page not found"); + sendto(mysock, "4 Page not found\r\n", sizeof("4 Page not found\r\n"), 0, (const struct sockaddr *)&addr, sizeof(addr)); + close(mysock); + continue; + } + + // request in buf is not needed anymore, reuse buf for response + + // check gemini extension + char *mimetype = "application/octet-stream"; + if (strlen(realPath) > 4 && memcmp(cursor-2, "md", 2) == 0) { + mimetype = "text/markdown"; + } + else if (strlen(realPath) > 5 && memcmp(cursor-3, "gmi", 3) == 0) { + mimetype = "text/gemini"; + } + else if (strlen(realPath) > 10 && memcmp(cursor-2, "markdown", 8) == 0) { + mimetype = "text/markdown"; + } + + int len = sprintf(buf, "2 %s\r\n", mimetype); + // print response in terminal + // remove \r\n + buf[len-2] = 0; + puts(buf); + buf[len-2] = '\r'; + len += fread(buf, 1, (size_t)sizeof(buf) , f); + if (sendto(mysock, buf, len, 0, (const struct sockaddr *)&addr, sizeof(addr)) == -1) { + perror("write failed"); + } + fclose(f); + close(mysock); + } + } while(1); + + puts("Server stopped."); +}