Mini Web ServerをC言語で書いた

自作OSでopenとかreadのSystemCallを実装していましたが、何か面白いものを作りたくなったので、最近の自作OSでは一つの目標(?)とされているWebサーバを動かすことを目標とすることにしました。
とりあえず使用するシステムコールを探るためにC言語で超簡易版のWeb Serverを書いてみました。

使用するSystemCallを最小限にするためにfopenやmallocなどは使用せず、open/readなどのSystemCallに近い関数のみ使用するようにしました。本当はmallocは既にOS側でメモリ管理ができているので使いたかったのですがこの世のLinux向けのlibc実装はメモリを確保する際にbrkやmprotectを使用するらしく、現在の自分のOSでは紆余曲折あってLinux ABI互換で、これらの実装がやや面倒だったので(頑張ればできないことはないが)、とりあえず端折ることにしました。

コードは以下のとおりです。

#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

//#define DEBUG

#ifdef DEBUG
#include <arpa/inet.h>
#endif

#define HTTP_VERSION "HTTP/1.1"
#define GET_METHOD "GET"
#define SERVER_HEADER_ENTRY "Server: Mini Web Server"
#define HTTP_200 HTTP_VERSION " 200 OK"
#define HTTP_404 HTTP_VERSION " 404 Not Found"
#define HTTP_500 HTTP_VERSION " 500 Internal Server Error"

int main() {
  puts("Mini Web Server");

  int receive_socket = socket(AF_INET, SOCK_STREAM, 0);
  if (receive_socket < 0) {
    fprintf(stderr, "Failed to open the socket(ret: %d)\n", receive_socket);
    return 1;
  }

  struct sockaddr_in host;
  host.sin_family = AF_INET;
  host.sin_port = htons(8080);
  host.sin_addr.s_addr = INADDR_ANY;
  if (bind(receive_socket, (struct sockaddr *)&host, sizeof(host)) < 0) {
    fprintf(stderr, "Failed to bind\n");
    return 1;
  }

  if (listen(receive_socket, 10) < 0) {
    fprintf(stderr, "Failed to listen the socket.\n");
    return 1;
  }

  while (1) {
    struct sockaddr_in client_address;
    socklen_t client_address_length;
    int client_socket =
        accept(receive_socket, (struct sockaddr *)&client_address,
               &client_address_length);
#ifdef DEBUG
    printf("Client: {\"Address\": \"%s\", \"Port\": %d}\n",
           inet_ntoa(client_address.sin_addr), client_address.sin_port);
#endif
    char buffer[0x1000];
    buffer[sizeof(buffer) - 1] = '\0';

    int received = recv(client_socket, buffer, sizeof(buffer), 0);
    if (received < 0) {
      fprintf(stderr, "Failed to receive data(ret: %d)\n", received);
      return 1;
    }

    /* Check method */
    if (received <= (sizeof(GET_METHOD) - 1) ||
        strncmp(buffer, GET_METHOD, sizeof(GET_METHOD) - 1) != 0) {
      close(client_socket);
      continue;
    }
    size_t pointer = sizeof(GET_METHOD) - 1;

    if (buffer[pointer] != ' ' || buffer[pointer + 1] != '/') {
      close(client_socket);
      continue;
    }
    pointer += 1;
    /* remove "/"(root) */
    pointer += 1;
    char *file_name = buffer + pointer;
    size_t file_name_size = 0;
    for (; (pointer + file_name_size) < received &&
           buffer[pointer + file_name_size] != ' ';
         file_name_size++)
      ;
    pointer += file_name_size + 1;

    if (received <= (pointer + sizeof(HTTP_VERSION) - 1) ||
        strncmp(buffer + pointer, HTTP_VERSION, sizeof(HTTP_VERSION) - 1) !=
            0) {
      close(client_socket);
      continue;
    }
    if (file_name_size == 0) {
      const char index_name[] = "index.htm";
      strcpy(file_name, index_name);
      file_name_size = sizeof(index_name) - 1;
    }
    file_name[file_name_size] = '\0';
#ifdef DEBUG
    printf("URL: %s\n", file_name);
#endif
    /* Create Response */
    int fd = open(file_name, O_RDONLY);
    if (fd < 0) {
      const char not_found_text[] =
          HTTP_404 "\r\n" SERVER_HEADER_ENTRY "\r\n\r\nFile is not found.";
      send(client_socket, not_found_text, sizeof(not_found_text) - 1, 0);
    } else {
      size_t size = lseek(fd, 0, SEEK_END);
      lseek(fd, 0, SEEK_SET);
      if (size > sizeof(buffer)) {
        const char error_text[] =
            HTTP_500 "\r\n" SERVER_HEADER_ENTRY
                     "\r\n\r\nFile size is exceeded the buffer size.";
        send(client_socket, error_text, sizeof(error_text) - 1, 0);
      } else {
        sprintf(buffer,
                HTTP_200 "\r\n" SERVER_HEADER_ENTRY
                         "\r\nContent-Length: %zu\r\n\r\n",
                size);
        send(client_socket, buffer, strlen(buffer), 0);
        read(fd, buffer, size);
        send(client_socket, buffer, size, 0);
      }
    }
    close(client_socket);
  }
  return 0;
}

straceで呼ばれているシステムコールを確認すると起動処理以外では、

  • wrtiev(puts, fprintf)
  • socket
  • bind
  • listen
  • accept
  • recvfrom
  • open
  • lseek
  • sendto
  • read
  • close

のみとなってます。

とりあえずLinuxでビルドしたところまあまあ動いているようのなのでこれが動くように頑張ります。

と言っても、まずはネットワークデバイスのドライバ書くところから始めないといけないのですが…

追記: 一応動きました。 https://twitter.com/PG_MANA_/status/1533812030582292481

投稿者: PG_MANA

支離滅裂な自称プログラマー。 C,C++,Rust,JavaScript,PHP,HTML,CSS,OS自作,openSUSE,Arch,旅行 なんか色々してる人 #seccamp 17 19 20 23 #OtakuAssembly