spartserv

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

commit 0157c674bc97f02949fa8557d2d34577448c0a4e
Author: Remy Noulin <loader2x@gmail.com>
Date:   Tue,  3 Jan 2023 14:14:55 +0200

Simple client and server for spartan protocol

README.md     |  42 ++++
build.sh      |  11 +
index.gmi     |   6 +
sparline.c    | 707 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
spartclient.c |  91 ++++++++
spartserv.c   | 337 ++++++++++++++++++++++++++++
6 files changed, 1194 insertions(+)

Diffstat:
AREADME.md | 42++++++++++++++++++++++++++++++++++++++++++
Abuild.sh | 11+++++++++++
Aindex.gmi | 6++++++
Asparline.c | 707+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aspartclient.c | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aspartserv.c | 337+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 1194 insertions(+), 0 deletions(-)

diff --git a/README.md b/README.md @@ -0,0 +1,42 @@ +This repository has 2 simple clients and a simple server for the spartan protocol written in C. + +About the spartan protocol: +[Spartan on the web](https://portal.mozz.us/spartan/spartan.mozz.us/) +[Spartan on gemini](gemini://spartan.mozz.us) +[Spartan on spartan](spartan://spartan.mozz.us) + +To build this, you need a shell and the GCC C compiler and run: +``` +apt-get install gcc +./build.sh +``` + +`spartserv.c` is a server handling one request at a time. The configurations for the hostname and server root are on the top of `spartserv.c`. + +By default, `spartserv` opens port 3000 and serves on hostname `localhost`. + +Start `spartserv` with: +``` +./spartserv +``` + +`spartclient.c` is a client that downloads one page (maximum 8kb) and exits. It doesn't take a URL as argument, run it like this: +``` +./spartclient hostname port path +./spartclient localhost 3000 / +``` + +The `sparline.c` client takes a URL as argument and is able to browse in the terminal. The default port is 300 when no port is specified. + +``` +./sparline spartan://hostname:port/path +./sparline spartan://localhost:3000 +``` + +Each link on the page gets a number, enter the link number to open the link. +Enter 'b' to go back to previous page. + +The line type `=:` is not supported, to upload data run `sparline` with the URL on the `=:` line and the `--infile` option: +``` +./sparline spartan://hostname/path --infile afile.txt +``` diff --git a/build.sh b/build.sh @@ -0,0 +1,11 @@ +gcc -g3 spartserv.c -o spartserv +gcc -g3 spartclient.c -o spartclient +gcc -g3 sparline.c -o sparline + + +echo Run to start spartan server: +echo ./spartserv +echo +echo In another terminal, connect to spartan server with the client: +echo ./spartclient localhost 3000 / +echo ./sparline spartan://localhost:3000 diff --git a/index.gmi b/index.gmi @@ -0,0 +1,6 @@ +# Welcome + +Spartserv is up and running. + +=> spartan://mozz.us/index.gmi Spartan Home +=> spartan://spartan.mozz.us Spartan Specification diff --git a/sparline.c b/sparline.c @@ -0,0 +1,707 @@ +// Usage: sparline spartan://hostname:port/path --infile filename +// Prompt: Enter link number, b, - or + for back and q for quit + +#define _GNU_SOURCE +#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> +#include <sys/stat.h> +#include <sys/time.h> +#include <stdbool.h> + +// links in page +// elements are offsets in page +// [][0] is link start +// [][1] is link end +// linkCount is the number of links on the page +size_t links[100000][2] = {0}; +size_t linkCount = 0; +char *page = NULL; +size_t pageBufSize = 0; + +// history is a dynamic vector of historyEt element +// it is used as a stack +// pop is free string and decrease vector count +typedef struct { + char *hostname; + char *port; + char *path; + char *link; +} historyEt; + +#define sliceT(typeName, elementType)\ + typedef struct {\ + size_t count;\ + elementType *array;\ + } typeName + +sliceT(historyt, historyEt); + +#define var __auto_type +#define TOKENPASTE2(a, b) a ## b +#define TOKENPASTE(a, b) TOKENPASTE2(a, b) +#define UNIQVAR(name) TOKENPASTE(name, __LINE__) + +#define sliceInitCount(name, countInt) do{\ + var UNIQVAR(c) = countInt;\ + (name)->array = malloc(UNIQVAR(c) * sizeof (name)->array[0]);\ + (name)->count = 0;\ + } while(0) + +#define sliceSz 1 + +#define sliceAlloc(name) do{\ + if (!(name)->array) {\ + (name)->array = malloc(sliceSz * sizeof (name)->array[0]);\ + }\ + else {\ + (name)->array = realloc((name)->array, ((name)->count + sliceSz) * sizeof (name)->array[0]);\ + }\ + } while(0) + +#define slicePush(name) do {\ + sliceAlloc(name);\ + (name)->count++;\ + } while(0) + +#define sliceAt(name, index) ((name)->array[index]) +#define sliceLast(name) ((name)->array[(name)->count-1]) + +#define sliceAppend(name, v) do{\ + slicePush(name);\ + sliceLast(name) = v;\ + } while(0) + +/** + * convert string to decimal integer + * + * \param + * string + * \return + * int64_t + * 0 when string represents 0 or doesnt represent a number or the input is NULL + */ +int64_t parseInt(const char *string) { + while (!isdigit(*string) && *string != '-' && *string != 0) { + string++; + } + int64_t r = strtoll(string, NULL, 10); + return(r); +} + +#define startMax 20 + +/** + * read String + * read user input (one line) as a string + * + * there is no size limit and the buffer expands as needed + * + * \return + * line from the user (you must free the pointer) + * NULL when buffer allocation failed + */ +char *readS(void) { + int max = startMax; + + char *s = malloc((size_t)max); + if (!s) { + return(NULL); + } + + int i = 0; + while (1) { + int c = getchar(); + if (c == '\n') { + s[i] = 0; + break; + } + s[i] = (char)c; + if (i == max-1) { + // buffer full + max += max; + char *tmp = realloc(s, (size_t)max); + if (!tmp) { + free(s); + return(NULL); + } + s = tmp; + } + i++; + } + return(s); +} + +// makeRoom is dynamic memory allocation algorithm +// given a length, an allocated size and the additionnal length, +// makeRoom returns the new allocated size for realloc +// when the new allocated size equals alloc value, there is no need to realloc the memory, enough space is already available +#define prealloc (1024*1024) +#define funcbegin ({ +#define funcend }) +#define makeRoom(length, alloc, addlength) funcbegin\ + typeof(alloc) r;\ + typeof(alloc) newlen = (length) + (addlength);\ + if (newlen < (alloc)) {\ + r = alloc;\ + } \ + else {\ + if (newlen < prealloc) {\ + r = newlen * 2;\ + }\ + else {\ + r = newlen + prealloc;\ + }\ + }\ + r;\ + funcend + +bool getPage(char *hostname, char *ports, char *path, size_t content_length, void *senddata) { + int sock; + struct sockaddr_in server; + struct hostent *hp; + int mysock; + char buf[4096] = {0}; + char redirectPath[4096] = {0}; + int rval; + int i; + bool r = true; + + openSocket: + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0){ + perror("Failed to create socket"); + r = false; + goto showPage; + } + + // Set 10s timeouts for receive and send (SO_RCVTIMEO and SO_SNDTIMEO) + struct timeval timeout; + timeout.tv_sec = 10; + timeout.tv_usec = 0; + if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) { + perror("receive timeout failed"); + close(sock); + r = false; + goto showPage; + } + if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) { + perror("send timeout failed"); + close(sock); + r = false; + goto showPage; + } + + server.sin_family = AF_INET; + + hp = gethostbyname(hostname); + if (hp==0) { + perror("gethostbyname failed"); + close(sock); + r = false; + goto showPage; + } + + memcpy(&server.sin_addr, hp->h_addr, hp->h_length); + + int64_t port = parseInt(ports); + + if (port < 1 || port > 65000) { + close(sock); + printf("Invalid port %d.\n", port); + r = false; + goto showPage; + } + + server.sin_port = htons(port); + + if (connect(sock,(struct sockaddr *) &server, sizeof(server))){ + perror("connect failed"); + close(sock); + r = false; + goto showPage; + } + + // build request + // hostname SPC path SPC content_length\r\n + size_t len = strlen(hostname); + memcpy(buf, hostname, len); + buf[len] = ' '; + char *cursor = buf + len + 1; + len = strlen(path); + memcpy(cursor, path, len); + char lenstr[50]; + int ln = sprintf(lenstr, " %d\r\n", content_length); + memcpy(cursor + len, lenstr, ln); + cursor += len + ln; + + // show request + *cursor = 0; + puts(buf); + printf("Page URL: spartan://%s:%s%s\n", hostname, ports, path); + + // send request + if(send(sock, buf, cursor - buf, 0) < 0){ + perror("send failed"); + close(sock); + r = false; + goto showPage; + } + + if (content_length > 0) { + // send data with request + size_t offset = 0; + while (content_length) { + size_t tosend = content_length > 2048 ? 2048 : content_length; + if(send(sock, senddata + offset, tosend, 0) < 0){ + perror("send failed"); + close(sock); + r = false; + goto showPage; + } + content_length -= tosend; + offset += tosend; + } + } + + // receive server response + size_t pageSize = 4096; + size_t offset = 0; + free(page); + page = malloc(pageSize); + page[0] = 0; + do { + rval = recv(sock, buf, sizeof(buf), 0); + if (rval != -1 && rval != 0) { + size_t newSize = makeRoom(offset, pageSize, rval); + if (newSize > pageSize) { + char *tmp = realloc(page, newSize); + if (!tmp) { + // memory allocation error + // crash + exit(1); + } + page = tmp; + } + memcpy(page+offset, buf, rval); + offset += rval; + } + } while (rval != -1 && rval != 0); + close(sock); + + if (buf[0] == '3' && buf[1] == ' ' && buf[2] != ' ' && offset <= sizeof(redirectPath)) { + // redirect + // scan path and reopen socket + cursor = buf + 2; + while (cursor < buf + offset && + !isspace(*cursor)) { + cursor++; + } + *cursor = 0; + puts(buf); + memcpy(redirectPath, buf+2, cursor - buf - 2); + redirectPath[cursor - buf - 2] = 0; + path = redirectPath; + free(page); + goto openSocket; + } + + // save page size for reuse + // when a link fails to load, the current page is reused + pageBufSize = offset; + + // show page + // highlight headers, lists, blockquote, link and fixedwidth blocks + // collect links in links array + showPage: + if (page == NULL) return false; + linkCount = 0; + enum {normal, header, link, list, fixedwidth, blockquote}; + int state = normal; + #define RST "\x1B[0m" + #define BLD "\x1B[1m" + #define RED "\x1B[31m" + #define GRN "\x1B[32m" + #define YLW "\x1B[33m" + #define BLU "\x1B[34m" + #define MGT "\x1B[35m" + #define CYN "\x1B[36m" + #define WHT "\x1B[37m" + puts(RED"──────────────────────────────────────────────────────────────────────"RST); + for (int i = 0; i < pageBufSize; i++) { + if (state != fixedwidth) { + if (page[i] == '#' && (i == 0 || page[i-1] == '\n')) { + state = header; + printf(BLD YLW); + } + if (page[i] == '*' && (i == 0 || page[i-1] == '\n')) { + state = list; + printf(CYN); + } + if (page[i] == '>' && (i == 0 || page[i-1] == '\n')) { + state = blockquote; + printf(BLD WHT); + } + if (page[i] == '=' && page[i+1] == '>' && (i == 0 || page[i-1] == '\n')) { + state = link; + links[linkCount][0] = i; + links[linkCount][1] = 0; // invalid + printf(BLD GRN "%d " RST BLD BLU, linkCount); + } + } + if (page[i] == '`' && page[i+1] == '`' && page[i+2] == '`' && (i == 0 || page[i-1] == '\n')) { + if (state == normal) { + state = fixedwidth; + printf(BLD MGT); + } + else if (state == fixedwidth) { + state = normal; + printf(RST); + } + } + if (state != normal && state != fixedwidth && page[i] == '\n') { + if (state == link) { + links[linkCount++][1] = i; + } + state = normal; + printf(RST); + } + putchar(page[i]); + } + return true; +} + +historyEt parseURL(char *url) { + size_t len = strlen(url); + + char *cursor = strstr(url, "spartan://"); + if (!cursor) return (historyEt){0}; + + char *s = cursor + strlen("spartan://"); + cursor = s; + // scan hostname + while(*cursor != '/' && + *cursor != ' ' && + *cursor != '\t' && + *cursor != ':' && + *cursor != '\n' && + *cursor != 0 && + cursor < url + len) { + cursor++; + } + char *hostname = malloc(cursor-s+1); + memcpy(hostname, s, cursor-s); + hostname[cursor-s] = 0; + + char *port; + if (*cursor == ':') { + // port is specified + cursor++; + port = cursor; + while(isdigit(*cursor)) { + cursor++; + } + char c = *cursor; + port = strdup(port); + *cursor = c; + } + else { + port = strdup("300"); + } + + char *path; + if (*cursor == ' ' || + *cursor == '\t' || + *cursor == '\n' || + *cursor == 0) { + // path is empty + path = strdup("/"); + } + else { + // *cursor == '/' + // scan path + s = cursor; + while(!isspace(*cursor)) { + cursor++; + } + + if (cursor == s+1) { + // path is / + path = strdup("/"); + } + else { + path = malloc(cursor-s+1); + memcpy(path, s, cursor-s); + path[cursor-s] = 0; + } + } + + return (historyEt){.hostname = hostname, .port = port, .path = path}; +} + +/** + * get file size + * + * \param + * filePath: path to file + * \return + * ssize_t >= 0 size + * -1 an error occured or filePath is NULL or empty string + */ +ssize_t fileSize(const char *filePath) { + struct stat st; + + int r = stat(filePath, &st); + if (r) { + printf("Error, the path was: \"%s\"\n", filePath); + return(-1); + } + + // macOS returns a varying number a number above the constant below + // when the file doesnt exists + if ((uint64_t)(st.st_size) > 140734000000000) { + return(-1);//LCOV_EXCL_LINE + } + return(st.st_size); +} + +int main(int ac, char **av){ + if (ac < 2) { + puts("Usage: sparline spartan://hostname:port/path --infile filename\n" + "Default port is 300\n" + "Prompt: Enter link number, b, - or + for back and q for quit"); + return 0; + } + + historyt history; + sliceInitCount(&history, 16); + + historyEt e = parseURL(av[1]); + if (!e.hostname) { + puts("Error: Could not parse the url in argument 1"); + puts(av[1]); + return 1; + } + sliceAppend(&history, e); + + if (ac > 3) { + // check for --infile + if (strcmp(av[2], "--infile") == 0) { + puts(av[3]); + ssize_t size = fileSize(av[3]); + if (size < 0) return 1; + char *data = malloc(size); + FILE *f = fopen(av[3], "r"); + size_t sz = fread(data, 1, size, f); + fclose(f); + if (sz < size) { + puts("Could not read complete file."); + return 1; + } + getPage(e.hostname, e.port, e.path, size /*content length*/, data /*send data*/); + free(data); + free(page); + return 0; + } + } + + bool r = getPage(e.hostname, e.port, e.path, 0 /*content length*/, NULL /*send data*/); + if (!r) { + // get page failed + free(page); + return 1; + } + + char *userInput = strdup(""); + while(strcmp(userInput, "q") != 0) { + // print prompt with link count on current page + if (!linkCount) + printf("> "); + else + printf("%d links > ", linkCount); + free(userInput); + // userInput is allocated in readS + userInput = readS(); + puts(userInput); + if (isdigit(userInput[0])) { + // go to link + int link = parseInt(userInput); + if (link >= linkCount) { + printf("Link number too high: %d, link count: %d\n", link, linkCount); + continue; + } + // build a valid link string + page[links[link][1]] = 0; + char *s = &page[links[link][0]]; + char *hostname; + char *port; + char *path; + + char *cursor = strstr(s, "spartan://"); + if (!cursor) { + if (strstr(s, "://")) { + puts("Only spartan links are supported."); + continue; + } + // this is a path + // sometimes there is no space after => + cursor = isspace(*(s+2)) ? s+3 : s+2; + // p is link start + char *p = cursor; + // search for link end and skip the title + while (!isspace(*cursor) && cursor < &page[links[link][1]]) { + cursor++; + } + *cursor = 0; + // there are no hostname and port in link, use the previous ones + hostname = strdup(sliceLast(&history).hostname); + port = strdup(sliceLast(&history).port); + if (*p != '/') { + // relative path + // check if current path is a file like index.gmi + // to avoid creating a path like: + // /index.gmi/p + size_t len = strlen(sliceLast(&history).path); + if (len > 4 && memcmp(sliceLast(&history).path + len - 4, ".gmi", 4) == 0) { + // when path is .. remove filename only + if (strcmp(p, "..") == 0) + p = ""; + else if (strcmp(p, ".") == 0) { + // path is ., reload same page as previous one + path = strdup(sliceLast(&history).path); + goto downloadPage; + } + // previous has a gmi extension + // find dirname, can be empty string + char *hp = sliceLast(&history).path + len; + while(hp > sliceLast(&history).path) { + hp--; + if (*hp == '/') { + *hp = 0; + asprintf(&path, "%s/%s", sliceLast(&history).path, p); + *hp = '/'; + goto downloadPage; + } + } + // no slash found in history path + asprintf(&path, "/%s", sliceLast(&history).path, p); + } + else + // add path to previous path + asprintf(&path, "%s/%s", sliceLast(&history).path, p); + } + else { + // absolute path starting with / + path = strdup(p); + } + goto downloadPage; + } + + // this part is the same as the parseURL function + s = cursor + strlen("spartan://"); + cursor = s; + // scan hostname + while(*cursor != '/' && + *cursor != ' ' && + *cursor != '\t' && + *cursor != ':' && + *cursor != '\n' && + *cursor != 0 && + cursor < &page[links[link][1]]) { + cursor++; + } + hostname = malloc(cursor-s+1); + memcpy(hostname, s, cursor-s); + hostname[cursor-s] = 0; + + if (*cursor == ':') { + // port is specified + cursor++; + port = cursor; + while(isdigit(*cursor)) { + cursor++; + } + char c = *cursor; + port = strdup(port); + *cursor = c; + } + else { + // default port is 300 + port = strdup("300"); + } + + if (*cursor == ' ' || + *cursor == '\t' || + *cursor == '\n' || + *cursor == 0) { + // path is empty + path = strdup("/"); + } + else { + // *cursor == '/' + // scan path + s = cursor; + while(!isspace(*cursor)) { + cursor++; + } + + if (cursor == s+1) { + // path is / + path = strdup("/"); + } + else { + path = malloc(cursor-s+1); + memcpy(path, s, cursor-s); + path[cursor-s] = 0; + } + } + // ^^ this part is the same as the parseURL function ^^ + + downloadPage: + printf(BLD GRN "%s" RST "\n", &page[links[link][0]]); + + // store hostname, port, path and link in history + e = (historyEt){.hostname = hostname, .port = port, .path = path, .link = strdup(&page[links[link][0]])}; + if (getPage(hostname, port, path, 0 /*content length*/, NULL /*send data*/)) { + // success + sliceAppend(&history, e); + } + } + else if (userInput[0] == 'b' || userInput[0] == '-' || userInput[0] == '+') { + // go back in history + if (history.count < 2) { + puts("First page in history."); + continue; + } + else { + free(sliceLast(&history).hostname); + free(sliceLast(&history).port); + free(sliceLast(&history).path); + free(sliceLast(&history).link); + history.count--; + if (sliceLast(&history).link) + puts(sliceLast(&history).link); + getPage(sliceLast(&history).hostname, sliceLast(&history).port, sliceLast(&history).path, 0 /*content length*/, NULL /*send data*/); + } + } + else if (userInput[0] == 'z') { + // used for debug + puts("break"); + } + } + + free(page); + for (int i = 0; i < history.count ; i++) { + free(sliceAt(&history, i).hostname); + free(sliceAt(&history, i).port); + free(sliceAt(&history, i).path); + free(sliceAt(&history, i).link); + } +} + diff --git a/spartclient.c b/spartclient.c @@ -0,0 +1,91 @@ +// 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[8192] = {0}; + int rval; + int i; + + if (ac < 4) { + puts("Usage: spartclient hostname port path"); + return 0; + } + + sock = socket(AF_INET, SOCK_STREAM, 0); + 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); + + if (connect(sock,(struct sockaddr *) &server, sizeof(server))){ + perror("connect failed"); + close(sock); + 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(send(sock, buf, cursor - buf, 0) < 0){ + perror("send failed"); + close(sock); + exit(1); + } + + // reveice server response + rval = recv(sock, buf, sizeof(buf)-1 /* -1 to add end of string before printing */, 0); + if (rval != -1 && rval != 0) { + buf[rval] = 0; + puts(buf); + } + + close(sock); +} + diff --git a/spartserv.c b/spartserv.c @@ -0,0 +1,337 @@ +/* +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> + +// inet_ntoa +//already included #include <sys/socket.h> +//already included #include <netinet/in.h> +#include <arpa/inet.h> + + +// server configuration +#define HOSTNAME "localhost" +#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[4096]; + 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++; + } + + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0){ + perror("Failed to create socket"); + } + + 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)); + + if (bind(sock, (struct sockaddr *) &server, sizeof(server))){ + perror("bind failed"); + exit(1); + } + + listen(sock, SOMAXCONN); + + printf("Serving "HOSTNAME":%d %s\n", PORT, root); + + // date for request print + char date[50]; + do { + // struct for printing client ip in terminal with inet_ntoa + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + mysock = accept(sock, &addr, &len); + if (mysock == -1) + perror("accept failed"); + else { + // Set 10s timeouts for receive and send (SO_RCVTIMEO and SO_SNDTIMEO) + struct timeval timeout; + timeout.tv_sec = 10; + timeout.tv_usec = 0; + if (setsockopt(mysock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) { + perror("receive timeout failed"); + close(mysock); + continue; + } + if (setsockopt(mysock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) { + perror("send timeout failed"); + close(mysock); + continue; + } + + // new client + memset(buf, 0, sizeof(buf)); + if ((rval = recv(mysock, buf, sizeof(buf), 0)) < 0) { + perror("reading message"); + close(mysock); + continue; + } + else if (rval == 0) { + puts("Ending connection"); + close(mysock); + continue; + } + + // 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)); + + // 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 + send(mysock, "4 Invalid request\r\n", sizeof("4 Invalid request\r\n"), MSG_NOSIGNAL); + 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"); + send(mysock, "4 Non ASCII\r\n", sizeof("4 Non ASCII\r\n"), MSG_NOSIGNAL); + 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"); + send(mysock, "4 Hostname\r\n", sizeof("4 Hostname\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + + // get path + cursor = buf; + while (*cursor != ' ' && cursor < reqEnd) { + cursor++; + } + + cursor++; + if (cursor >= reqEnd || *cursor == ' ') { + puts("4 Path"); + send(mysock, "4 Path\r\n", sizeof("4 Path\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + + path = cursor; + + // get content_length + while (*cursor != ' ' && cursor < reqEnd) { + cursor++; + } + + cursor++; + if (cursor >= reqEnd || *cursor == ' ') { + puts("4 Length"); + send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL); + 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"); + send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL); + 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"); + send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + memcpy(localPath, root, rootLen); + cursor = localPath + rootLen; + memcpy(cursor, path, strlen(path)); + + // check path + char realPath[PATH_MAX] = {0}; + realpath(localPath, realPath); + if (memcmp(realPath, root, rootLen-slash) != 0) { + puts("4 Not found"); + send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL); + 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"); + send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL); + 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"); + send(mysock, "4 Page not found\r\n", sizeof("4 Page not found\r\n"), MSG_NOSIGNAL); + 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); + if (send(mysock, buf, len, MSG_NOSIGNAL) != -1) { + // print response in terminal + // remove \r\n + buf[len-2] = 0; + puts(buf); + + // send file + size_t fsz; + while (fsz = fread(buf, 1, (size_t)sizeof(buf) , f)) { + ssize_t r = send(mysock, buf, fsz, MSG_NOSIGNAL); + if (r == -1) { + perror("write failed"); + puts("closed socket"); + break; + } + } + } + else { + perror("write failed"); + } + fclose(f); + close(mysock); + } + } while(1); + + puts("Server stopped."); +}