nexserv is a server for the nex protocol written in C and compiled with sheepy (more about sheepy in the links below).

nexserv is a sheepy package and can be installed with spm:

spm -g install nexserv

The server configuration is on top of the source code.

I'm running nexserv in my machine and it serves my gemini capsule. Using nc as a nex client, it can be accessed like this:

echo index.gmi | nc gmi.noulin.net 1900 | less

nex protocol is a simple protocol used in Nightfall city, the request is a path and the response is a file.

Nightfall city Nightfall express protocol (nex://) specification

Nex protocol specification:

echo nex/info/specification.txt | nc nightfall.city 1900 | less

Sheepy (gemini) sheepy (http)

nexserv is my spartan server spartserv with unnecessary code removed, I only kept the code handling the requested path and the connection is closed when an error is detected.

Simple spartan client and server

The source code is around 200 lines:

#! /usr/bin/env sheepy
/* or direct path to sheepy: #! /usr/local/bin/sheepy */

/* Libsheepy documentation: https://spartatek.se/libsheepy/ */
#include "libsheepyObject.h"
#include <sys/socket.h>
#include <netinet/in.h> // sockaddr_in
#include <limits.h> // PATH_MAX

// inet_ntoa
//already included #include <sys/socket.h>
//already included #include <netinet/in.h>
#include <arpa/inet.h>

/* enable/disable logging */
/* #undef pLog */
/* #define pLog(...) */

// server configuration
#define HOSTNAME "localhost"
#define ROOT "."
#define PORT 1900
#define INDEXFILE "index.txt"

int main(int ARGC, char** ARGV) {

  initLibsheepy(ARGV[0]);
  //setLogMode(LOG_VERBOSE);
  setLogSymbols(LOG_UTF8);


  int sock;
  struct sockaddr_in server;
  int mysock;
  char buf[32768];
  int rval;

  // set root as absolute path
  char root[PATH_MAX] = {0};
  if (ROOT[0] == '/') {
    realpath(ROOT, root);
    pErrorNULL(bAppendManyS(root, "/", ""));
  }
  else {
    // create absolute path from relative ROOT path
    char p[PATH_MAX] = {0};
    getcwd(p, PATH_MAX);
    pErrorNULL(bAppendManyS(p, "/", ROOT));
    realpath(p, root);
    pErrorNULL(bAppendManyS(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);

  logP("Serving "HOSTNAME":%d %s index file: "INDEXFILE, PORT, root);

  // date for request print
  char date[50];
  forever {
    // 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;
      // get YMD HMS date
      pErrorNULL(bLGetCurrentDateYMD(date, sizeof(date)));
      if (setsockopt(mysock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
        logE("%s ", inet_ntoa(addr.sin_addr));
        perror("receive timeout failed");
        close(mysock);
        continue;
      }
      if (setsockopt(mysock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
        logE("%s ", inet_ntoa(addr.sin_addr));
        perror("send timeout failed");
        close(mysock);
        continue;
      }

      // new client
      memset(buf, 0, sizeof(buf));
      if ((rval = recv(mysock, buf, sizeof(buf), 0)) < 0) {
        logE("%s ", inet_ntoa(addr.sin_addr));
        perror("reading message");
        close(mysock);
        continue;
      }
      elif (rval == 0) {
        logE("%s ", inet_ntoa(addr.sin_addr));
        puts("Ending connection");
        close(mysock);
        continue;
      }
      elif (rval >= PATH_MAX) {
        logE("%s ", inet_ntoa(addr.sin_addr));
        logE("Request too long, got %d", rval);
        close(mysock);
        continue;
      }

      // request
      cleanCharP(req) = trimS(buf);

      // print request in terminal
      logI("%s '%s'", inet_ntoa(addr.sin_addr), req);

      char *path = req;

      // build server path
      char localPath[PATH_MAX] = {0};
      if (rootLen + strlen(path) >= PATH_MAX) {
        logE("Path too long");
        send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
        close(mysock);
        continue;
      }
      pErrorNULL(bCatS(localPath, root, path));

      // check path
      if (not isPath(localPath)) {
        logE("Not found: %s", localPath);
        send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
        close(mysock);
        continue;
      }
      char realPath[PATH_MAX] = {0};
      realpath(localPath, realPath);
      if (memcmp(realPath, root, rootLen-slash) != 0) {
        logE("Not found: %s", realPath);
        send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
        close(mysock);
        continue;
      }

      size_t pathLen = strlen(realPath);
      char *cursor = realPath + pathLen;
      if (isDir(realPath)) {
        if (pathLen + strlen("/"INDEXFILE) >= PATH_MAX) {
          logE("Path too long: %s/"INDEXFILE, realPath);
          send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
          close(mysock);
          continue;
        }
        memcpy(cursor, "/"INDEXFILE, strlen("/"INDEXFILE));
        cursor += strlen("/"INDEXFILE);
      }

      FILE *f = fopen(realPath, "r");
      if (!f) {
        logE("Page not found: %s", realPath);
        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

      // 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");
          logE("closed socket");
          break;
        }
      }
      fclose(f);
      close(mysock);
    }
  }

  logP("Server stopped.");
}
// vim: set expandtab ts=2 sw=2: