$ man tshabbic.projects.sws
SWS(8)Simple Web ServerSWS(8)
Name

sws -- simple web server

Synopsis

sws [ -dh] [ -c dir] [ -i address] [ -l file] [ -p port] dir

Description

[See the Source Code!] [See the actual Manual Page]


For my Advanced Programming in the UNIX Environment course at Stevens, I worked with two teammates to build an HTTP/1.0 web server from scratch in C, following RFC 1945 (among other RFCs). I took on ownership of the networking core: dual-stack IPv4/IPv6 TCP socket support, multiplexed I/O for handling concurrent clients, daemonization, signal handling - along with cross OS/platform compatibility, conditional-GET support via If-Modified-Since and ~user directory translation. I also spent time reviewing and refactoring many other parts of the codebase alongside my teammates, correcting for style, functionality, and more. We split the work by feature across separate branches and merged everything through GitHub PRs, talking through design decisions and reviewing each other's code over Discord along the way.

Some unique challenges and discussions we had that I ultimately were responsible for were signal handling; I quickly realized that if our client was in the middle of sending data to a remote client, and for whatever reason the connection was severed (e.g., client disconnected before all the data was sent), our program would be terminated. Using gdb(1), I quickly discovered that it was ungracefully terminating due to SIGPIPE, whose default behavior is to terminate the process. This occurred because the moment (technically a bit later due to kernel buffering) our server attempted write to a remote client's socket/file descriptor that had its connection closed, the kernel raised SIGPIPE, and since we hadn't touched its default disposition, it simply terminated the entire process and all other connections being served with it.
This necessitated a signal handler to simply ignore SIGPIPE, which did work fine; however, since our sws server also runs CGIs, which involves calls to fork(2) and exec(2) to execute other programs, our fork(2)'d child would also inherit our custom handler for a brief window before exec(2) replaces it, which may have unintended side effects if exec(2) fails or during that setup window. As such, before we run any CGIs, we temporarily replace the signal handler with its original/default handler, then, after successfully running the CGI, put back our own custom signal handler that ignores SIGPIPE.

Note that our server uses multiplexed I/O, meaning it serves the connections that send data to our server first -- our sws webserver does not support true simultaneous connection serving. This is a design decision we as a team agreed upon, because using POSIX Threads and/or a combination of fork(2) and exec(2) to have a "high performance" webserver seemed out of scope of what we felt the assignment required.

If this project really does interest you, much more can be found within the source code comments which explain why some code is written the way it is; not what it does, as any good comment should.

Usage

The following options are available:

-c dir

Allow execution of CGIs from the given directory.

-d

Enter debugging mode, do not daemonize, only accept one connection at a time, and enable logging to stdout.

-h

Print a short usage summary and exit.

-i address

Bind to the given IPv4 or IPv6 address. If not provided, sws will listen on all IPv4 and IPv6 addresses on the host.

-l file

Log all requests to the given file.

-p port

Listen for incoming connections on the given port. If not provided, sws will listen on port 8080.

Bugs

> #include "/opt/magic/include/magic.h" For compiling on OmniOS we used the system specific location for the library when we should have instead solely relied on passing the compiler the -l flag to tell it where it should look for files/libraries.
> int n = snprintf(tmp, PATH_MAX, "/home/%s/sws/", user); wrongfully assumes the user's home directory is mounted/found under /home/ whereas we should have called getpwent(3) to find their actual home directory.
> Idle connections are never closed, otherwise they could eventually consume all of the process' open file descriptors; a denial-of-service attack vector.
> Some HTTP responses codes could be more accurate.


This is of course a non-exhaustive list of bugs, some further documented by my group members in our README.md, comments in the source code, etc.

History

This project was done during Fall of 2025 for CS 631 - Advanced Programming in the Unix Environment @ Stevens Institute of Technology.

Fall 2026 - Fixed a bug where daemonization mode would fail if running on OmniOS; Linux and NetBSD not affected. This was due to sysconf(_SC_OPEN_MAX); returning 65536 on OmniOS, but 1024 on Linux/NetBSD, for the maximum number of file descriptors any one process can have, which led to a stack smash on FD_ZERO(&myset); according to gdb(1).