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 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | README.md | | | 22 | ++++++++++++++++++++++ |
| M | build.sh | | | 2 | ++ |
| A | spartclientudp.c | | | 103 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | spartservudp.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.");
+}