swps

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

commit e60326b350f234ad7dbf93be8b4a943d38935f7c
Author: Chris Noxz <chris@noxz.tech>
Date:   Sun,  4 Aug 2019 15:20:59 +0200

Initial commit

Diffstat:
A.gitignore | 2++
ALICENSE | 21+++++++++++++++++++++
AMakefile | 42++++++++++++++++++++++++++++++++++++++++++
Aconfig.mk | 8++++++++
Aswps.1 | 26++++++++++++++++++++++++++
Aswps.c | 277+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 376 insertions(+), 0 deletions(-)

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; +}