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:
A | README.md | | | 42 | ++++++++++++++++++++++++++++++++++++++++++ |
A | build.sh | | | 11 | +++++++++++ |
A | index.gmi | | | 6 | ++++++ |
A | sparline.c | | | 707 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | spartclient.c | | | 91 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | spartserv.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.");
+}