Skip to content

Instantly share code, notes, and snippets.

@YutaroHayakawa
Created July 26, 2020 15:50
Show Gist options
  • Save YutaroHayakawa/4688d8669d40ca25e5954be6ea3917cb to your computer and use it in GitHub Desktop.
Save YutaroHayakawa/4688d8669d40ca25e5954be6ea3917cb to your computer and use it in GitHub Desktop.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <getopt.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netdb.h>
#include <syslog.h>
#include <grp.h>
#include <pwd.h>
#define LINE_BUF_SIZE 512
#define TIME_BUF_SIZE 64
#define BLOCK_BUF_SIZE 16384
#define MAX_REQUEST_BODY_LENGTH 2048
#define HTTP_MINOR_VERSION 0
#define SERVER_NAME "my-httpd"
#define SERVER_VERSION "0.0.1"
#define MAX_BACKLOG 5
#define DEFAULT_PORT "80"
#define USAGE "Usage: %s [--port=n] [--chroot --user=u --group=g] <docroot>\n"
static int debug_mode = 0;
static struct option longopts[] = {
{"debug", no_argument, &debug_mode, 1},
{"chroot", no_argument, NULL, 'c'},
{"user", required_argument, NULL, 'u'},
{"group", required_argument, NULL, 'g'},
{"port", required_argument, NULL, 'p'},
{"help", no_argument, NULL, 'h'},
{0, 0, 0, 0}
};
struct HTTPHeaderField {
char *name;
char *value;
struct HTTPHeaderField *next;
};
struct HTTPRequest {
int protocol_minor_version;
char *method;
char *path;
struct HTTPHeaderField *header;
char *body;
long length;
};
struct FileInfo {
char *path;
long size;
int ok;
};
static void
log_exit(char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
if (debug_mode) {
vfprintf(stderr, fmt, ap);
fputc('\n', stderr);
} else {
vsyslog(LOG_ERR, fmt, ap);
}
va_end(ap);
exit(EXIT_FAILURE);
}
static void *
xmalloc(size_t size)
{
void *p;
p = malloc(size);
if (p == NULL) {
log_exit("failed to allocate memory");
}
return p;
}
static void
upcase(char *s)
{
int c = 0;
while (s[c] != '\0') {
if (s[c] >= 'a' && s[c] <= 'z') {
s[c] = s[c] - 32;
}
c++;
}
}
static void
trap_signal(int sig, void *handler)
{
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART;
if (sigaction(sig, &act, NULL) < 0) {
log_exit("sigaction() failed: %s", strerror(errno));
}
}
static void
signal_exit(int sig)
{
log_exit("exit by signal %d", sig);
}
static void
noop_handler(int sig)
{
;
}
static void
detach_children(void)
{
struct sigaction act;
act.sa_handler = noop_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART | SA_NOCLDWAIT;
if (sigaction(SIGCHLD, &act, NULL) < 0) {
log_exit("sigaction() failed: %s", strerror(errno));
}
}
static void
install_signal_handlers(void)
{
trap_signal(SIGPIPE, signal_exit);
detach_children();
}
static void
free_request(struct HTTPRequest *req)
{
struct HTTPHeaderField *h, *head;
head = req->header;
while (head) {
h = head;
head = head->next;
free(h->name);
free(h->value);
free(h);
}
free(req->method);
free(req->path);
free(req->body);
free(req);
}
static void
read_request_line(struct HTTPRequest *req, FILE *in)
{
char buf[LINE_BUF_SIZE];
char *path, *p;
if (!fgets(buf, LINE_BUF_SIZE, in)) {
log_exit("no request line");
}
p = strchr(buf, ' ');
if (p == NULL) {
log_exit("parse error on request line (1): %s", buf);
}
*p++ = '\0';
req->method = xmalloc(p - buf);
strcpy(req->method, buf);
upcase(req->method);
path = p;
p = strchr(path, ' ');
if (p == NULL) {
log_exit("parse error on request line (2): %s", buf);
}
*p++ = '\0';
req->path = xmalloc(p - path);
strcpy(req->path, path);
if (strncasecmp(p, "HTTP/1.", strlen("HTTP/1.")) != 0) {
log_exit("parse error on request line (3): %s", buf);
}
p += strlen("HTTP/1.");
req->protocol_minor_version = atoi(p);
}
static struct HTTPHeaderField *
read_header_field(FILE *in)
{
struct HTTPHeaderField *h;
char buf[LINE_BUF_SIZE];
char *p;
if (!fgets(buf, LINE_BUF_SIZE, in)) {
log_exit("failed to read request header field: %s", strerror(errno));
}
if ((buf[0] == '\n') || (strcmp(buf, "\r\n") == 0)) {
return NULL;
}
p = strchr(buf, ':');
if (p == NULL) {
log_exit("parse error on request header field: %s", buf);
}
*p++ = '\0';
h = xmalloc(sizeof(*h));
h->name = xmalloc(p - buf);
strcpy(h->name, buf);
p += strspn(p, " \t");
h->value = xmalloc(strlen(p) + 1);
strcpy(h->value, p);
return h;
}
static char *
lookup_header_field_value(struct HTTPRequest *req, char *name)
{
struct HTTPHeaderField *h;
for (h = req->header; h; h = h->next) {
if (strcasecmp(h->name, name) == 0) {
return h->value;
}
}
return NULL;
}
static long
content_length(struct HTTPRequest *req)
{
char *val;
long len;
val = lookup_header_field_value(req, "Content-Length");
if (val == NULL) {
return 0;
}
len = atoi(val);
if (len < 0) {
log_exit("negative Content-Length value");
}
return len;
}
static struct HTTPRequest *
read_request(FILE *in)
{
struct HTTPRequest *req;
struct HTTPHeaderField *h;
req = xmalloc(sizeof(*req));
read_request_line(req, in);
req->header = NULL;
while ((h = read_header_field(in))) {
h->next = req->header;
req->header = h;
}
req->length = content_length(req);
if (req->length != 0) {
if (req->length > MAX_REQUEST_BODY_LENGTH) {
log_exit("request body too long");
}
req->body = xmalloc(req->length);
if (fread(req->body, req->length, 1, in) < 1) {
log_exit("failed to read request body");
}
} else {
req->body = NULL;
}
return req;
}
static char *
build_fspath(char *docroot, char *urlpath)
{
char *path;
path = xmalloc(strlen(docroot) + 1 + strlen(urlpath) + 1);
sprintf(path, "%s/%s", docroot, urlpath);
return path;
}
static struct FileInfo *
get_fileinfo(char *docroot, char *urlpath)
{
struct FileInfo *info;
struct stat st;
info = xmalloc(sizeof(*info));
info->path = build_fspath(docroot, urlpath);
info->ok = 0;
if (lstat(info->path, &st) < 0) {
return info;
}
if (!S_ISREG(st.st_mode)) {
return info;
}
info->ok = 1;
info->size = st.st_size;
return info;
}
static void
free_fileinfo(struct FileInfo *info)
{
free(info->path);
free(info);
}
static void
output_common_header_fields(struct HTTPRequest *req,
FILE *out, char *status)
{
time_t t;
struct tm *tm;
char buf[TIME_BUF_SIZE];
t = time(NULL);
tm = gmtime(&t);
if (tm == NULL) {
log_exit("gmtime() failed: %s", strerror(errno));
}
strftime(buf, TIME_BUF_SIZE, "%a, %d %b %Y %H:%M:%S GMT", tm);
fprintf(out, "HTTP/1.%d %s\r\n", HTTP_MINOR_VERSION, status);
fprintf(out, "Date: %s\r\n", buf);
fprintf(out, "Server: %s/%s\r\n", SERVER_NAME, SERVER_VERSION);
fprintf(out, "Connection: close\r\n");
}
static void
not_found(struct HTTPRequest *req, FILE *out)
{
output_common_header_fields(req, out, "404 Not Found");
fprintf(out, "\r\n");
fflush(out);
}
static char *
guess_content_type(struct FileInfo *info)
{
return "text/plain";
}
static void
do_file_response(struct HTTPRequest *req, FILE *out, char *docroot)
{
struct FileInfo *info;
info = get_fileinfo(docroot, req->path);
if (!info->ok) {
free_fileinfo(info);
not_found(req, out);
return;
}
output_common_header_fields(req, out, "200 OK");
fprintf(out, "Content-Length: %ld\r\n", info->size);
fprintf(out, "Content-Type: %s\r\n", guess_content_type(info));
fprintf(out, "\r\n");
if (strcmp(req->method, "HEAD") != 0) {
int fd;
char buf[BLOCK_BUF_SIZE];
ssize_t n;
fd = open(info->path, O_RDONLY);
if (fd < 0) {
log_exit("failed to open %s: %s", info->path, strerror(errno));
}
for (;;) {
n = read(fd, buf, BLOCK_BUF_SIZE);
if (n < 0) {
log_exit("failed to read %s: %s", info->path, strerror(errno));
}
if (n == 0) {
break;
}
if (fwrite(buf, 1, n, out) < n) {
log_exit("failed to write to socket: %s", strerror(errno));
}
}
close(fd);
}
fflush(out);
free_fileinfo(info);
}
static void
method_not_allowed(struct HTTPRequest *req, FILE *out)
{
output_common_header_fields(req, out, "405 Method Not Allowed");
fprintf(out, "\r\n");
fflush(out);
}
static void
not_implemented(struct HTTPRequest *req, FILE *out)
{
output_common_header_fields(req, out, "501 Not Implemented");
fprintf(out, "\r\n");
fflush(out);
}
static void
respond_to(struct HTTPRequest *req, FILE *out, char *docroot)
{
if (strcmp(req->method, "GET") == 0) {
do_file_response(req, out, docroot);
} else if (strcmp(req->method, "HEAD") == 0) {
do_file_response(req, out, docroot);
} else if (strcmp(req->method, "POST") == 0) {
method_not_allowed(req, out);
} else {
not_implemented(req, out);
}
}
static void
service(FILE *in, FILE *out, char *docroot)
{
struct HTTPRequest *req;
req = read_request(in);
respond_to(req, out, docroot);
free_request(req);
}
static int
listen_socket(char *port)
{
struct addrinfo hints, *res, *ai;
int err;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if ((err = getaddrinfo(NULL, port, &hints, &res)) != 0) {
log_exit("%s", gai_strerror(err));
}
for (ai = res; ai; ai = ai->ai_next) {
int sock;
sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (sock < 0) {
continue;
}
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));
if (bind(sock, ai->ai_addr, ai->ai_addrlen) < 0) {
close(sock);
continue;
}
if (listen(sock, MAX_BACKLOG) < 0) {
close(sock);
continue;
}
freeaddrinfo(res);
return sock;
}
log_exit("failed to listen socket");
return -1;
}
static void
server_main(int server_fd, char *docroot)
{
for (;;) {
struct sockaddr_storage addr;
socklen_t addrlen = sizeof(addr);
int sock;
int pid;
sock = accept(server_fd, (struct sockaddr *)&addr, &addrlen);
if (sock < 0) {
log_exit("accept(2) failed: %s", strerror(errno));
}
pid = fork();
if (pid < 0) {
exit(3);
}
if (pid == 0) {
FILE *inf = fdopen(sock, "r");
FILE *outf = fdopen(sock, "w");
service(inf, outf, docroot);
exit(0);
}
close(sock);
}
}
static void
become_daemon(void)
{
int n;
if (chdir("/") < 0) {
log_exit("chdir(2) failed: %s", strerror(errno));
}
freopen("/dev/null", "r", stdin);
freopen("/dev/null", "w", stdout);
freopen("/dev/null", "w", stderr);
n = fork();
if (n < 0) {
log_exit("fork(2) failed: %s", strerror(errno));
}
if (n != 0) {
_exit(0);
}
if (setsid() < 0) {
log_exit("setsid(2) failed: %s", strerror(errno));
}
}
static void
setup_environment(char *root, char *user, char *group)
{
struct passwd *pw;
struct group *gr;
if (!user || !group) {
fprintf(stderr, "use both of --user and --group\n");
exit(1);
}
gr = getgrnam(group);
if (!gr) {
fprintf(stderr, "no such group: %s\n", group);
exit(1);
}
if (setgid(gr->gr_gid) < 0) {
perror("setgid(2)");
exit(1);
}
if (initgroups(user, gr->gr_gid) < 0) {
perror("initgroups(2)");
exit(1);
}
pw = getpwnam(user);
if (!pw) {
fprintf(stderr, "no such user: %s\n", user);
exit(1);
}
chroot(root);
if (setuid(pw->pw_uid) < 0) {
perror("setuid(2)");
exit(1);
}
}
int
main(int argc, char **argv)
{
int server_fd;
char *port = DEFAULT_PORT;
char *docroot;
int do_chroot = 0;
char *user = NULL;
char *group = NULL;
int opt;
while ((opt = getopt_long(argc, argv, "", longopts, NULL)) != -1) {
switch (opt) {
case 0:
break;
case 'c':
do_chroot = 1;
break;
case 'u':
user = optarg;
break;
case 'g':
group = optarg;
break;
case 'p':
port = optarg;
break;
case 'h':
fprintf(stdout, USAGE, argv[0]);
exit(0);
case '?':
fprintf(stdout, USAGE, argv[0]);
exit(1);
}
}
if (optind != argc - 1) {
fprintf(stderr, USAGE, argv[0]);
exit(1);
}
docroot = argv[optind];
if (do_chroot) {
setup_environment(docroot, user, group);
docroot = "";
}
install_signal_handlers();
server_fd = listen_socket(port);
if (!debug_mode) {
openlog(SERVER_NAME, LOG_PID|LOG_NDELAY, LOG_DAEMON);
become_daemon();
}
server_main(server_fd, docroot);
exit(0);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment