#include <sys/stat.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <unistd.h>
#include "sws.h"
#include "utils.h"
#ifdef __linux__
#ifndef PATH_MAX
#include <linux/limits.h> /* for PATH_MAX */
#endif
#endif
static const char* const opts = "dhc:i:l:p:";
static void
usage(void)
{
/*
* Credit to this stackoverflow post for this neat trick to break a
* multiline print statement into multiple lines.
* https://stackoverflow.com/a/1752091
*/
(void)fprintf(stderr, "usage %s [options] /dir\n", getprogname());
(void)fprintf(
stderr,
"options:\n"
"-c dir\t\tAllow execution of CGIs from given directory\n"
"-d\t\tDebugging mode (no daemonization, 1 max connection, stdout "
"logging)\n"
"-h\t\tPrint usage summary and exit.\n"
"-i addr\t\tBind to given IPv4/IPv6 address. Otherwise, listen on "
"all IPv4 & IPv6 addresses on this host.\n"
"-l file\t\tLog requests to given file. Created if it doesn't exist\n"
"-p port\t\tListen on given port. Default port is 8080.\n");
}
int
main(int argc, char* argv[])
{
#ifdef __linux__
process_name = basename(argv[0]);
#endif
extern char* optarg;
extern int optind;
int ch, fd;
long int port_num;
char* ep;
char cgi_dir[PATH_MAX];
char log_file[PATH_MAX];
char main_dir[PATH_MAX];
struct flag_control fc = {0};
fc.c = false;
fc.cgi_dir = cgi_dir;
fc.d = false;
fc.h = false;
fc.i = false;
fc.ip_addr = NULL;
fc.l = false;
fc.log_file = log_file;
fc.p = false;
fc.port = 8080;
while ((ch = getopt(argc, argv, opts)) != -1)
{
switch (ch)
{
case 'd':
fc.d = true;
break;
case 'h':
usage();
return EXIT_FAILURE; /* Most programs return 1 on -h (e.g. cp) */
case 'c':
fc.c = true;
if ((fd = open(optarg, O_RDONLY | O_DIRECTORY, 0)) < 0)
{
(void)fprintf(stderr, "%s: -%c %s: %s\n", getprogname(), ch,
optarg, strerror(errno));
return EX_USAGE;
}
(void)close(fd);
if (realpath(optarg, fc.cgi_dir) == NULL)
{
(void)fprintf(stderr, "%s: -%c %s: %s\n", getprogname(), ch,
optarg, strerror(errno));
return EX_IOERR;
}
break;
case 'i':
fc.i = true;
/* IP address is resolved in sws.c, nothing to do here */
fc.ip_addr = optarg;
break;
case 'l':
fc.l = true;
if ((fd = open(optarg, O_RDONLY | O_CREAT | O_APPEND,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0)
{
(void)fprintf(stderr, "%s: -%c %s: %s\n", getprogname(), ch,
optarg, strerror(errno));
return EX_USAGE;
}
struct stat sb;
if (fstat(fd, &sb) < 0)
{
(void)close(fd);
(void)fprintf(stderr, "%s: -%c %s: %s\n", getprogname(), ch,
optarg, strerror(errno));
return EX_IOERR;
}
if (!S_ISREG(sb.st_mode))
{
(void)close(fd);
(void)fprintf(stderr,
"%s: -%c %s: Log file must be a regular file\n",
getprogname(), ch, optarg);
return EX_USAGE;
}
(void)close(fd);
/* fc.log_file = optarg; */
if (realpath(optarg, fc.log_file) == NULL)
{
(void)fprintf(stderr, "%s: -%c %s: %s\n", getprogname(), ch,
optarg, strerror(errno));
return EX_IOERR;
}
break;
case 'p':
fc.p = true;
/* Manually set errno to check if port conversion works */
errno = 0;
port_num = strtol(optarg, &ep, 10);
if (ep == optarg)
{
(void)fprintf(stderr, "%s: -%c %s: not a valid port.\n",
getprogname(), ch, optarg);
return EX_USAGE;
}
if (*ep != '\0')
{
(void)fprintf(
stderr, "%s: -%c %s: trailing garbage in port argument.\n",
getprogname(), ch, optarg);
return EX_USAGE;
}
if (errno && (errno == ERANGE || port_num == LONG_MAX ||
port_num == LONG_MIN))
{
(void)fprintf(stderr, "%s: -%c %s: port too small/big.\n",
getprogname(), ch, optarg);
return EX_USAGE;
}
if (port_num < 1 || port_num > USHRT_MAX)
{
(void)fprintf(
stderr,
"%s: -%c %s: port must be between 1 and %d inclusive.\n",
getprogname(), ch, optarg, USHRT_MAX);
return EX_USAGE;
}
/* Port cannot be < 1024 if euid is not root */
if (port_num < 1024 && geteuid() != 0)
{
(void)fprintf(stderr,
"%s: -%c %s: cannot bind to well-known port as a "
"non-root user.\n",
getprogname(), ch, optarg);
return EX_USAGE;
}
fc.port = port_num;
break;
default:
usage();
return EX_USAGE;
}
}
argc -= optind;
argv += optind;
if (argc != 1)
{
usage();
return EX_USAGE;
}
if (realpath(argv[0], main_dir) == NULL)
{
(void)fprintf(stderr, "%s: %s: %s\n", getprogname(), argv[0],
strerror(errno));
return EX_IOERR;
}
if (fc.d)
{
(void)printf("[Debug] Current %s PID: %d\n", getprogname(), getpid());
}
else
{
/* We do not want to have the call to daemon(3) close all the file
* descriptors per the assignment spec; see below
* nochdir to root as logging relies on knowing the process' cwd.
*/
if (daemon(true, true) == -1)
{
(void)fprintf(
stderr,
"%s: could not detach from the controlling terminal: %s\n",
getprogname(), strerror(errno));
exit(EXIT_FAILURE);
}
/* Redirect stdout and stdin to /dev/null (redirection and not close(2)
* as that's what daemon(3) claims to do) */
int dev_null;
if ((dev_null = open("/dev/null", O_RDWR)) == -1)
{
fprintf(stderr,
"%s: Warning: could not locate '/dev/null'. Output to "
"stdout will not be suppressed.\n",
getprogname());
/* Early breakout */
goto skip_stdio_redir;
}
/* replace stdin with /dev/null */
if ((dup2(dev_null, STDIN_FILENO)) == -1)
{
fprintf(
stderr,
"%s: Warning: dup2 (stdin): %s. Input from stdin will *not* "
"be ignored.\n",
getprogname(), strerror(errno));
}
/* replace stdout with /dev/null */
if ((dup2(dev_null, STDOUT_FILENO)) == -1)
{
fprintf(
stderr,
"%s: Warning: dup2 (stdout): %s. Output to stdout will not be "
"suppressed.\n",
getprogname(), strerror(errno));
}
/* assignment never said to close/suppress stderr; just ensure nothing
* is printed to stdout. */
/* close /dev/null now that it has been placed in stdin and stdout's
* location */
(void)close(dev_null);
skip_stdio_redir:
/* This will not do anything if all of the above succeeded */
(void)printf("[Debug] New %s PID after daemonizing: %d\n",
getprogname(), getpid());
}
return start_server(main_dir, &fc);
}