簡易 HTTP サーバいじり

IPA の「ネットワークサービスは必ずforkしよう 」記載の簡易 HTTP サーバを、ホーム・ディレクトリで一時的に使う HTTP サーバ向けに手を加えてみました。

ホーム・ディレクトリで一時的に使う HTTP サーバでは、SIGINT でサーバを停止できるようにしておくと、何かと便利です。ですが、 linux の accept、read、write システムコールはデフォルトでは、SIGINT のシグナル・ハンドラを呼んで即座に accept の受付待ちに戻ってしまい、システムコールを EINTR でエラー中断しません。シグナル・ハンドラで g_singal_status に設定した値をチェックする機会を作るために、sigaction システムコールを使ってシステム・コールのリスタートなしでシグナルを処理させます。

もうひとつ、元のサーバにはタイムアウト処理がないので、 SIGALRM で read を中断させるようにします。

//@<シグナル・ハンドラを定義します@>=
namespace {
    volatile std::sig_atomic_t g_signal_status = 0;
    volatile std::sig_atomic_t g_alarm_status = 0;
}

static void
hundle_sigint (int sig)
{
    g_signal_status = sig;
}

static void
hundle_sigalrm (int sig)
{
    g_alarm_status = sig;
}

static void
hundle_sigchld (int sig)
{
    int saved_errno = errno;
    int status;
    while (waitpid (-1, &status, WNOHANG) > 0)
        ;
    errno = saved_errno;
}

static void
signal_restart (int sig, void (*handler)(int))
{
    struct sigaction sa;
    sa.sa_handler = handler;
    sa.sa_flags = SA_RESTART;
    sigemptyset (&sa.sa_mask);
    sigaction (sig, &sa, nullptr);
}

static void
signal_norestart (int sig, void (*handler)(int))
{
    struct sigaction sa;
    sa.sa_handler = handler;
    sa.sa_flags = 0;
    sigemptyset (&sa.sa_mask);
    sigaction (sig, &sa, nullptr);
}

このサーバは、簡易とあるだけあって accept するごとに子プロセスを個数制限なしで生成していく、牧歌的な時代の作り方になっています。リンク先の記事はサービス拒否攻撃対策を目的にしていますが、このような作りのサーバをインターネットへ公開してしまうと、それこそサービス拒否攻撃の餌食になるので、 公開してはいけません。もちろん、この稿のサーバにも同じ脆弱性があるので、公開してはいけません。

#include <string>
#include <clocale>
#include <cstdio>
#include <cstdlib>
#include <csignal>
#include <ctime>
#include <cerrno>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

enum {PORT = 10080, BACKLOG = 5, TIMEOUT = 10};

static const std::string SERVER = "chiken/1.0";

void hundle_http (int const sock, std::string const& remote_addr);
void fixup_mock_page (std::string& response);
bool sock_read (int const sock);
bool sock_write (int const sock, std::string const& octets);
void sock_teardown (int const sock);
int listen_socket_create (int const port, int const backlog);
int accept_client (int const listen_sock, std::string& remote_addr);
std::string to_string_time (std::string const& fmt, std::time_t const t);

//@<シグナル・ハンドラを定義します@>

int
main ()
{
    std::setlocale (LC_ALL, "C");

    signal_restart (SIGCHLD, hundle_sigchld);
    signal_norestart (SIGINT, hundle_sigint);
    signal_norestart (SIGALRM, hundle_sigalrm);
    std::signal (SIGPIPE, SIG_IGN);

    int srv = listen_socket_create (PORT, BACKLOG);
    if (srv < 0)
        exit (EXIT_FAILURE);
    std::printf ("listening http://localhost:%d/\n", PORT);
    std::printf ("^C to shutdown\n");
    while (! g_signal_status) {
        std::string remote_addr;
        int sock = accept_client (srv, remote_addr);
        if (sock < 0 && EINTR == errno)
            continue;
        if (sock < 0)
            break;
        pid_t pid = fork ();
        if (pid < 0) {
            perror ("fork");
            close (sock);
            break;
        }
        else if (0 == pid) {
            close (srv);
            hundle_http (sock, remote_addr);
            close (sock);
            exit (EXIT_SUCCESS);
        }
        close (sock);
    }
    close (srv);
    std::signal (SIGCHLD, SIG_IGN);
    for (;;) {
        int status;
        if (wait (&status) < 0) {
            if (ECHILD == errno)
                break;
            else if (EINTR == errno)
                ;
            else
                perror ("wait");
        }
    }
    std::printf ("shutdown\n");
    return EXIT_SUCCESS;
}

//@<hundle_http 関数を定義します@>
//@<listen_socket_create 関数を定義します@>
//@<accept_client 関数を定義します@>
//@<to_string_time 関数を定義します@>

子プロセスは、 exec 系統をおこなわない限り、親プロセスのシグナル・ハンドラ処理を受け継ぐので、SIGINT で g_signal_status に値をセットする方式はそのまま利用することができます。

void
hundle_http (int const sock, std::string const& remote_addr)
{
    std::string response;
    if (sock_read (sock)) {
        fixup_mock_page (response);
        sock_write (sock, response);
    }
    sock_teardown (sock);
}

//@<fixup_mock_page 関数を定義します@>
//@<sock_read 関数を定義します@>
//@<sock_write 関数を定義します@>
//@<sock_teardown 関数を定義します@>

fixup_mock_page ダミーの HTML からレスポンスを作ります。レスポンスはソケットへ出力するオクテット列そのものです。

//@<fixup_mock_page 関数を定義します@>=
void
fixup_mock_page (std::string& response)
{
    static const std::string body =
    "<!DOCTYPE html>\n"
    "<html>\n"
    "<head><title>It works</title></head>\n"
    "<body>\n"
    "<h1>It works</h1>\n"
    "<p>If you see the current page, the server works correctly.</p>\n"
    "</body>\n"
    "</html>\n"
    ;
    response  = "HTTP/1.1 200 Ok\r\n";
    response += "Date:" + to_string_time ("%a, %d %b %Y %H:%M:%S GMT", std::time (nullptr)) + "\r\n";
    response += "Server:" + SERVER + "\r\n";
    response += "Connection:close\r\n";
    response += "Content-Type:text/html; charset=UTF-8\r\n";
    response += "Content-Length:" + std::to_string (body.size ()) + "\r\n";
    response += "\r\n";
    response += body;
}

簡易サーバでは、リクエストを解読せずに読み飛ばします。

//@<sock_read 関数を定義します@>=
bool
sock_read (int const sock)
{
    char buf[1024];
    g_alarm_status = 0;
    alarm (TIMEOUT);
    for (;;) {
        ssize_t n = read (sock, buf, sizeof (buf));
        if (g_signal_status || g_alarm_status)
            return false;
        if (n < 0 && EINTR == errno)
            continue;
        if (n < 0) {
            perror ("read");
            return false;
        }
        if (n >= 0)
            break;
    }
    alarm (0);
    return true;
}

レスポンスの書き出しはオクテット列を送り込みます。こちらは一回の write で全部送りだせなかったときに残りの送りだしを続けるようになっています。

//@<sock_write 関数を定義します@>=
bool
sock_write (int const sock, std::string const& octets)
{
    ssize_t const len = octets.size ();
    ssize_t pos = 0;
    for (;;) {
        ssize_t n = write (sock, &octets[pos], len - pos);
        if (g_signal_status)
            return false;
        if (n < 0 && EINTR == errno)
            continue;
        if (n < 0 && EPIPE != errno)
            perror ("write");
        if (n < 0)
            return false;
        pos += n;
        if (pos >= len)
            break;
    }
    return true;
}

TCP ソケットをいきなり close で閉じてしまうと、クライアントがレスポンスを全部受け取る前にソケットが閉じてしまうことがあります。そうならないように、行儀良く書き込み側を shutdown システムコールで閉じてから、読み込み待ちのオクテット列を空読みします。

//@<sock_teardown 関数を定義します@>=
void
sock_teardown (int const sock)
{
    char buf[1024];
    shutdown (sock, SHUT_WR);
    g_alarm_status = 0;
    alarm (TIMEOUT);
    for (;;) {
        ssize_t n = read (sock, buf, sizeof (buf));
        if (g_alarm_status)
            break;
        if (n < 0 && EINTR == errno)
            continue;
        if (n < 0)
            perror ("read");
        if (n <= 0)
            break;
    }
    alarm (0);
}

listen_socket_create、 accept_client はいつもと同じです。to_string_time は、時刻をテンプレートにしたがって文字列にします。

//@<listen_socket_create 関数を定義します@>=
static inline struct sockaddr *
sockaddr_ptr (struct sockaddr_in& addr)
{
    return reinterpret_cast<struct sockaddr *> (&addr);
}

int
listen_socket_create (int const port, int const backlog)
{
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl (INADDR_ANY);
    addr.sin_port = htons (port);
    int yes = 1;
    int const sock = socket (PF_INET, SOCK_STREAM, 0);
    if (sock < 0)
        std::perror ("socket");
    else if (setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) < 0)
        std::perror ("setsockopt");
    else if (bind (sock, sockaddr_ptr (addr), sizeof addr) < 0)
        std::perror ("bind");
    else if (listen (sock, backlog) < 0)
        std::perror ("listen");
    else
        return sock;
    if (sock >= 0)
        close (sock);
    return -1;
}

//@<accept_client 関数を定義します@>=
int
accept_client (int const listen_sock, std::string& remote_addr)
{
    struct sockaddr_in addr;
    socklen_t len = sizeof addr;
    int const sock = accept (listen_sock, sockaddr_ptr (addr), &len);
    if (sock < 0 && EINTR == errno)
        ;
    else if (sock < 0)
        std::perror ("accept");
    else
        remote_addr = inet_ntoa (addr.sin_addr);
    return sock;
}

//@<to_string_time 関数を定義します@>=
std::string
to_string_time (std::string const& fmt, std::time_t const t)
{
    char str[256];
    auto const npos = std::string::npos;
    if (fmt.find ("GMT") != npos || fmt.find ("+0000") != npos)
        std::strftime (str, sizeof (str), fmt.c_str (), std::gmtime (&t));
    else
        std::strftime (str, sizeof (str), fmt.c_str (), std::localtime (&t));
    return str;
}