swps

Static Web Page Server
git clone https://noxz.tech/git/swps.git
Log | Files | LICENSE

commit: e60326b350f234ad7dbf93be8b4a943d38935f7c
parent: 
author: Chris Noxz <chris@noxz.tech>
date:   Sun, 4 Aug 2019 15:20:59 +0200
Initial commit
A.gitignore2+
ALICENSE21++
AMakefile42+++
Aconfig.mk8+
Aswps.126++
Aswps.c277++++++++++++++++++++
6 files changed, 376 insertions(+)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+swps
+swps.o
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+© 2019 Chris Noxz <chris@noxz.tech>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,42 @@
+.POSIX:
+
+include config.mk
+
+SRC = swps.c
+OBJ = $(SRC:.c=.o)
+
+all: options swps
+
+options:
+	@echo swps build options:
+	@echo "VERSION = $(VERSION)"
+	@echo "PREFIX  = $(PREFIX)"
+	@echo "CFLAGS  = $(STCFLAGS)"
+	@echo "CC      = $(CC)"
+
+.c.o:
+	$(CC) $(STCFLAGS) -c $<
+
+$(OBJ): config.mk
+
+swps: $(OBJ)
+	$(CC) -o $@ $(OBJ) $(STCFLAGS)
+
+clean:
+	rm -f swps $(OBJ)
+
+install: swps
+	@echo installing executable to ${PREFIX}/bin
+	mkdir -p $(PREFIX)/bin
+	cp -f swps $(PREFIX)/bin
+	chmod 755 $(PREFIX)/bin/swps
+	@echo installing manual page to ${MANPREFIX}/man1
+	mkdir -p $(MANPREFIX)/man1
+	cp -f swps.1 $(MANPREFIX)/man1/swps.1
+	chmod 644 $(MANPREFIX)/man1/swps.1
+
+uninstall:
+	rm -f $(PREFIX)/bin/swps
+	rm -f $(MANPREFIX)/man1/swps.1
+
+.PHONY: all options clean install uninstall
diff --git a/config.mk b/config.mk
@@ -0,0 +1,8 @@
+VERSION = 0.0.1
+
+PREFIX = /usr/local
+MANPREFIX = $(PREFIX)/share/man
+
+CFLAGS = -Wall -pedantic -std=c99
+CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=600
+STCFLAGS = $(CPPFLAGS) $(CFLAGS)
diff --git a/swps.1 b/swps.1
@@ -0,0 +1,26 @@
+.Dd $Mdocdate$
+.Dt SWPS 1
+.Os
+.Sh NAME
+.Nm swps
+.Nd Static Web Page Server
+.Sh SYNOPSIS
+.Nm
+.Ar port
+.Ar file
+.Op Ar -h
+.Sh DESCRIPTION
+The purpose of
+.Nm
+is to serve a single static
+.Ar file
+as a web server. Besides serving a
+file in the root, swps also handles 301, 403 and 404.
+.Pp
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Ar port
+Specifies the port on where to serve the file.
+.It Ar file
+Specifies what file to serve.
+.El
diff --git a/swps.c b/swps.c
@@ -0,0 +1,277 @@
+/* swps - Static Web Page Server
+ * -----------------------------
+ * This programs purpose is to serve a single static file as a web server.
+ * Besides serving a file in the root, swps also handles 301, 403 and 404.
+ *
+ *
+ * MIT License
+ *
+ * © 2019 Chris Noxz <chris@noxz.tech>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+
+#define BUFSIZE     8096
+#define DATESIZE    64
+#define SUCCESS     200
+#define REDIRECT    301
+#define FORBIDDEN   403
+#define NOTFOUND    404
+
+/* functions */
+static void die(int, const char *, ...);
+static int handle(int, unsigned char *);
+static int respond(int, int, unsigned char *, const char *);
+static void serve(int);
+static void stopserver(int);
+static int toint(const char *);
+
+static int serverfd;
+static const char *server = "swps 0.1";
+static const char *usage ="usage: swps PORT FILE\n"
+    "Serve html FILE as a web page on specified PORT\n\n"
+    "Options\n"
+    "  -h   display this help and exit\n\n"
+    "Full documentation and source code <https://noxz.tech/software/swps>\n";
+static const char *path = NULL;
+static const char *response_200 =
+    "HTTP/1.1 200 OK\n"
+    "Date: %s\n"
+    "Server: %s\n"
+    "Content-Length: %ld\n"
+    "Content-Type: text/html\n"
+    "Connection: closed\n\n";
+static const char *response_301 =
+    "HTTP/1.1 301 Moved Permanently\n"
+    "Location: /\n";
+static const char *response_403 =
+    "HTTP/1.1 403 Forbidden\n"
+    "Date: %s\n"
+    "Server: %s\n"
+    "Content-Length: 153\n"
+    "Content-Type: text/html\n"
+    "Connection: closed\n\n"
+
+    "<html><head>\n"
+    "<title>403 Forbidden</title>\n"
+    "</head><body>\n"
+    "<h1>Forbidden</h1>\n"
+    "The requested URL, or operation is not allowed on this server.\n"
+    "</body></html>\n";
+static const char *response_404 =
+    "HTTP/1.1 404 Not Found\n"
+    "Date: %s\n"
+    "Server: %s\n"
+    "Content-Length: 140\n"
+    "Content-Type: text/html\n"
+    "Connection: closed\n\n"
+
+    "<html><head>\n"
+    "<title>404 Not Found</title>\n"
+    "</head><body>\n"
+    "<h1>Not Found</h1>\n"
+    "The requested URL / was not found on this server.\n"
+    "</body></html>\n";
+
+int
+toint(const char *s)
+{
+    const char *p = s;
+    int n = -1;
+
+    while (*p && (n >= 0 || (n = 0) == 0)) {
+        if (*p >= 48 && *p <= 57)
+            n = (n * 10) + ((*p) - 48);
+        else
+            return -1;
+        p++;
+    }
+
+    return n;
+}
+
+void
+die(int n, const char *msg, ...)
+{
+    va_list arguments;
+
+    va_start(arguments, msg);
+    if (msg)
+        vfprintf(stderr, msg, arguments);
+    va_end(arguments);
+
+    if (n >= 0)
+        exit(n);
+}
+
+int
+respond(int type, int fd, unsigned char *ip, const char *pt)
+{
+    static char buffer[BUFSIZE + 1];
+    static char date[DATESIZE + 1];
+    time_t t = time(NULL);
+    struct tm *tm = gmtime(&t);
+    int filefd;
+    long l;
+
+    strftime(date, DATESIZE, "%a, %d %b %Y %H:%M:%S GMT", tm);
+
+    switch (type) {
+    case SUCCESS:
+        if ((filefd = open(path, O_RDONLY)) == -1)
+            return respond(NOTFOUND, fd, ip, pt);
+        l = (long)lseek(filefd, (off_t)0, SEEK_END);
+        lseek(filefd, (off_t)0, SEEK_SET);
+        sprintf(buffer, response_200, date, server, l);
+        write(fd, buffer, strlen(buffer));
+        while ((l = read(filefd, buffer, BUFSIZE)) > 0)
+            write(fd, buffer, l);
+        close(filefd);
+        break;
+    case REDIRECT:
+        sprintf(buffer, response_301, date, server);
+        write(fd, buffer, strlen(buffer));
+        break;
+    case FORBIDDEN:
+        sprintf(buffer, response_403, date, server);
+        write(fd, buffer, strlen(buffer));
+        break;
+    case NOTFOUND:
+        sprintf(buffer, response_404, date, server);
+        write(fd, buffer, strlen(buffer));
+        break;
+    }
+
+    die(-1, "[%s] %d %d.%d.%d.%d %s\n",
+        date, type, ip[0], ip[1], ip[2], ip[3], pt);
+    return type;
+}
+
+int
+handle(int clientfd, unsigned char *ip)
+{
+    static char buffer[BUFSIZE + 1], *pt;
+    long r;
+
+    if ((r = read(clientfd, buffer, BUFSIZE)) <= 0)
+        return respond(FORBIDDEN, clientfd, ip, NULL);
+
+    /* verify operation to be GET */
+    if (strncmp(buffer,"GET ", 4))
+        return respond(FORBIDDEN, clientfd, ip, NULL);
+
+    /* get requested uri */
+    pt = buffer + 4;
+    while (*pt) {
+        if (*pt == ' ') {
+            *pt = 0;
+            break;
+        }
+        pt++;
+    }
+    pt = buffer + 4;
+
+    /* redirect everything but '/' */
+    if (strncmp(pt, "/", strlen(pt)))
+        return respond(REDIRECT, clientfd, ip, pt);
+
+    /* serve the path */
+    return respond(SUCCESS, clientfd, ip, pt);
+}
+
+void
+stopserver(int signum)
+{
+    if (signum == SIGINT || signum == SIGTERM) {
+        close(serverfd);
+        die(0, "\nShutting down...\n");
+    }
+}
+
+void
+serve(int serverfd)
+{
+    static struct sockaddr_in c_addr;
+    unsigned char *ip;
+    unsigned int l;
+    int clientfd;
+
+    /* define the only way out... */
+    if (signal(SIGINT, &stopserver) == SIG_ERR)
+        die(1, "Error: system call 'signal'\n");
+    if (signal(SIGTERM, &stopserver) == SIG_ERR)
+        die(1, "Error: system call 'signal'\n");
+
+    /* ...and server forever */
+    for (;;) {
+        l = sizeof(c_addr);
+        if ((clientfd = accept(serverfd, (struct sockaddr *)&c_addr, &l)) < 0)
+            die(1, "Error: system call 'accept'\n");
+        ip = (unsigned char *)(&c_addr.sin_addr.s_addr);
+        handle(clientfd, ip);
+        close(clientfd);
+    }
+}
+
+int
+main(int argc, char **argv)
+{
+    static struct sockaddr_in s_addr;
+    int port, filefd;
+
+    /* validate arguments */
+    if (argc != 3 || !strcmp(argv[1], "-h"))
+        die(0, usage);
+    if ((port = toint(argv[1])) < 1 || port > 65535)
+        die(1, "Invalid port number: %s\n", argv[1]);
+    if ((filefd = open(argv[2], O_RDONLY)) == -1)
+        die(1, "Cannot open file: %s\n", argv[2]);
+    close(filefd);
+
+    /* define server socket address */
+    s_addr.sin_family       = AF_INET;
+    s_addr.sin_addr.s_addr  = htonl(INADDR_ANY);
+    s_addr.sin_port         = htons(port);
+
+    /* setup the network socket */
+    if ((serverfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
+        die(1, "Error: system call 'socket'\n");
+    if (bind(serverfd, (struct sockaddr *)&s_addr, sizeof(s_addr)) < 0)
+        die(1, "Error: system call 'bind'\n");
+    if (listen(serverfd, 64) < 0)
+        die(1, "Error: system call 'listen'\n");
+
+    path = argv[2];
+
+    die(-1, "Serving '%s' on 0.0.0.0:%d...\n\n", path, port);
+    serve(serverfd);
+
+    return 0;
+}