Skip to content

Instantly share code, notes, and snippets.

@frodo821
Created July 31, 2022 00:29
Show Gist options
  • Save frodo821/20d3165505aa07243aaf3ed753781913 to your computer and use it in GitHub Desktop.
Save frodo821/20d3165505aa07243aaf3ed753781913 to your computer and use it in GitHub Desktop.
システムプログラミング 第8週 練習問題802 のコードを供養
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#define HASHTABLE_SIZE 128
#define SERVER_BUFFER 1024
#define SOCK_BACKLOG 10
#define MAX_CLIENTS 10
// ハンドラプロセスのPIDを格納する
pid_t handlers[MAX_CLIENTS];
/**
* @brief ハッシュマップのノード。この構造体にキーと値を格納する。
*/
typedef struct hashmap_node
{
char key[512];
char value[SERVER_BUFFER];
struct hashmap_node *next;
} hashmap_node_t;
/**
* @brief ハッシュマップ。文字列に対応する文字列値を格納する。
*/
typedef struct hashmap
{
hashmap_node_t *table[HASHTABLE_SIZE];
} hashmap_t;
/**
* @brief 与えられた文字列のハッシュ値を計算する
*
* @param key ハッシュ値を計算する文字列
* @return int 1~HASHTABLE_SIZEまでのハッシュ値
*/
int calc_hash(const char *key)
{
int hash = 0;
while (*key != '\0')
{
hash = (hash * 33 + *key) % HASHTABLE_SIZE;
key++;
}
return hash % HASHTABLE_SIZE;
}
/**
* @brief 与えられた文字列をすべて小文字に変換する
*
* @param str 小文字に変換する文字列
* @return char* 元の引数のポインタ
*/
char *to_lower_case(char *str)
{
for (char *cptr = str; *cptr; cptr++)
{
*cptr = tolower(*cptr);
}
return str;
}
/**
* @brief 新しくハッシュマップを生成する
*
* @return hashmap_t* 生成したハッシュマップ
*/
hashmap_t *new_hashmap()
{
hashmap_t *map = malloc(sizeof(hashmap_t));
memset(map, 0, sizeof(hashmap_t));
return map;
}
/**
* @brief ハッシュマップをメモリ上から削除する
*
* @param map 削除したいハッシュマップ
*/
void finalize_hashmap(hashmap_t *map)
{
for (int i = 0; i < HASHTABLE_SIZE; i++)
{
hashmap_node_t *node = map->table[i];
hashmap_node_t *next;
while (node != NULL)
{
next = node->next;
free(node);
node = next;
}
}
free(map);
}
/**
* @brief あるハッシュマップに対して、キーに対応する値を格納する。すでにキーが存在する場合は上書き。
*
* @param self 格納したいハッシュマップ
* @param key キーとなる文字列
* @param value 格納したい値
* @return hashmap_t* ハッシュマップへのポインタ。正しく格納できた場合はselfと一致する。そうでなければNULL。
*/
hashmap_t *set_item(hashmap_t *self, const char *key, const char *value)
{
if (strlen(key) > 512 || strlen(value) > SERVER_BUFFER)
{
return NULL;
}
char *key_cpy = malloc(strlen(key) + 1);
strcpy(key_cpy, key);
key_cpy[strlen(key)] = '\0';
to_lower_case(key_cpy);
int hash = calc_hash(key_cpy);
hashmap_node_t *node = self->table[hash];
if (node == NULL)
{
self->table[hash] = malloc(sizeof(hashmap_node_t));
memset(self->table[hash], 0, sizeof(hashmap_node_t));
strcpy(self->table[hash]->key, key_cpy);
strcpy(self->table[hash]->value, value);
return self;
}
while (node->next != NULL)
{
if (strcmp(node->key, key_cpy) == 0)
{
memset(node->value, 0, SERVER_BUFFER);
strcpy(node->value, value);
return self;
}
node = node->next;
}
node->next = malloc(sizeof(hashmap_node_t));
memset(node->next, 0, sizeof(hashmap_node_t));
strcpy(node->next->key, key_cpy);
strcpy(node->next->value, value);
return self;
}
/**
* @brief ハッシュマップから値を取得する。取得した値は読み取り専用。
*
* @param self 値を取得したいハッシュマップ
* @param key 取得したい値に対応するキー。
* @return char* キーに対応する値。キーが存在しない場合はNULL。
*/
char *get_value(hashmap_t *self, const char *key)
{
char *key_cpy = malloc(strlen(key) + 1);
strcpy(key_cpy, key);
to_lower_case(key_cpy);
int hash = calc_hash(key_cpy);
hashmap_node_t *node = self->table[hash];
if (node == NULL)
{
return NULL;
}
while (node != NULL)
{
if (strcmp(node->key, key_cpy) == 0)
{
return node->value;
}
node = node->next;
}
return NULL;
}
/**
* @brief HTTPのレスポンスを格納する構造体
*/
typedef struct http_response
{
hashmap_t *headers;
int status_code;
size_t body_length;
char body[];
} http_response_t;
/**
* @brief HTTPのリクエストを格納する構造体
*/
typedef struct http_request
{
char method[10];
char path[SERVER_BUFFER];
char version[10];
size_t body_length;
hashmap_t *headers;
char body[];
} http_request_t;
/**
* @brief 空のHTTPリクエストを生成する
*
* @return http_request_t* 生成したHTTPリクエスト
*/
http_request_t *initialize_http_request()
{
http_request_t *request = malloc(sizeof(http_request_t));
memset(request, 0, sizeof(http_request_t));
request->headers = new_hashmap();
return request;
}
/**
* @brief HTTPリクエストをメモリ上から削除する
*
* @param request 削除したいHTTPリクエスト
*/
void finalize_http_request(http_request_t *request)
{
finalize_hashmap(request->headers);
free(request);
}
/**
* @brief 空のHTTPレスポンスを生成する
*
* @return http_response_t* 生成したHTTPレスポンス
*/
http_response_t *initialize_http_response()
{
http_response_t *response = malloc(sizeof(http_response_t));
memset(response, 0, sizeof(http_response_t));
response->headers = new_hashmap();
return response;
}
/**
* @brief HTTPレスポンスをメモリ上から削除する
*
* @param response 削除したいHTTPレスポンス
*/
void finalize_http_response(http_response_t *response)
{
finalize_hashmap(response->headers);
free(response);
}
/**
* @brief ファイルパスからファイルのMIMEタイプを推定する
*
* @param path MIMEタイプを取得したいファイルのパス
* @return const char* 推定したMIMEタイプ。推定できなかった場合は application/x-octet-stream が返る。
*/
const char *assume_file_type(const char *path)
{
char *ext = strrchr(path, '.');
if (ext == NULL)
{
return "application/x-octet-stream";
}
if (strcmp(ext, ".html") == 0)
{
return "text/html";
}
if (strcmp(ext, ".css") == 0)
{
return "text/css";
}
if (strcmp(ext, ".js") == 0)
{
return "application/javascript";
}
if (strcmp(ext, ".png") == 0)
{
return "image/png";
}
if (strcmp(ext, ".jpg") == 0)
{
return "image/jpeg";
}
if (strcmp(ext, ".gif") == 0)
{
return "image/gif";
}
if (strcmp(ext, ".ico") == 0)
{
return "image/x-icon";
}
if (strcmp(ext, ".svg") == 0)
{
return "image/svg+xml";
}
if (strcmp(ext, ".json") == 0)
{
return "application/json";
}
if (strcmp(ext, ".txt") == 0)
{
return "text/plain";
}
return "application/x-octet-stream";
}
/**
* @brief ステータスコードに対応するステータステキストを取得する
*
* @param status_code HTTPステータスコード
* @return const char* HTTPステータスコードに対応するステータステキスト。対応するものがない場合は Unknown Status が返る。
*/
const char *get_status_text(int status_code)
{
switch (status_code)
{
case 100:
return "Continue";
case 101:
return "Switching Protocols";
case 103:
return "Early Hints";
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Non-Authoritative Information";
case 204:
return "No Content";
case 205:
return "Reset Content";
case 206:
return "Partial Content";
case 300:
return "Multiple Choices";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 303:
return "See Other";
case 304:
return "Not Modified";
case 307:
return "Temporary Redirect";
case 308:
return "Permanent Redirect";
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 402:
return "Payment Required";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 406:
return "Not Acceptable";
case 407:
return "Proxy Authentication Required";
case 408:
return "Request Timeout";
case 409:
return "Conflict";
case 410:
return "Gone";
case 411:
return "Length Required";
case 412:
return "Precondition Failed";
case 413:
return "Payload Too Large";
case 414:
return "URI Too Long";
case 415:
return "Unsupported Media Type";
case 416:
return "Range Not Satisfiable";
case 417:
return "Expectation Failed";
case 418:
return "I'm a teapot";
case 421:
return "Misdirected Request";
case 425:
return "Too Early";
case 426:
return "Upgrade Required";
case 428:
return "Precondition Required";
case 429:
return "Too Many Requests";
case 431:
return "Request Header Fields Too Large";
case 451:
return "Unavailable For Legal Reasons";
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
case 505:
return "HTTP Version Not Supported";
case 506:
return "Variant Also Negotiates";
case 510:
return "Not Extended";
case 511:
return "Network Authentication Required";
default:
return "Unknown Status";
}
}
/**
* @brief HTTPのレスポンスを送信する
*
* @param sock_fd 送信するファイル記述子
* @param response 送信するHTTPレスポンス
*/
void send_http_response(int sock_fd, http_response_t *response)
{
char *status_line = malloc(SERVER_BUFFER);
sprintf(status_line, "HTTP/1.1 %d %s\r\n", response->status_code, get_status_text(response->status_code));
send(sock_fd, status_line, strlen(status_line), 0);
free(status_line);
for (int i = 0; i < HASHTABLE_SIZE; i++)
{
hashmap_node_t *node = response->headers->table[i];
char *header = malloc(SERVER_BUFFER * 2);
while (node != NULL)
{
sprintf(header, "%s: %s\r\n", node->key, node->value);
send(sock_fd, header, strlen(header), 0);
node = node->next;
}
free(header);
}
send(sock_fd, "\r\n", 2, 0);
send(sock_fd, response->body, response->body_length, 0);
}
/**
* @brief HTTPのステータスコードに対応するエラーレスポンスを送信する
*
* @param sock_fd 送信するファイル記述子
* @param status_code HTTPステータスコード
*/
void send_http_error(int sock_fd, int status_code)
{
http_response_t *response = initialize_http_response();
response->status_code = status_code;
set_item(response->headers, "Server", "myserver/1.0");
set_item(response->headers, "Content-Type", "text/plain");
set_item(response->headers, "Connection", "close");
// レスポンスボディ用にステータステキストをフォーマットする
char err_buf[SERVER_BUFFER / 2];
sprintf(err_buf, "%s\r\n", get_status_text(status_code));
const int len = strlen(err_buf);
char len_buf[SERVER_BUFFER / 2];
sprintf(len_buf, "%d", len);
set_item(response->headers, "Content-Length", len_buf);
response->body_length = len;
response = realloc(response, sizeof(http_response_t) + sizeof(char) * len);
strcpy(response->body, err_buf);
send_http_response(sock_fd, response);
finalize_http_response(response);
}
/**
* @brief リクエストで要求されたファイルを送信する
*
* @param sock_fd 送信するファイル記述子
* @param request HTTPリクエスト
*/
void send_file(int sock_fd, const http_request_t *request)
{
char path[SERVER_BUFFER * 3];
char cwd[SERVER_BUFFER];
if (getcwd(cwd, SERVER_BUFFER) == NULL)
{
send_http_error(sock_fd, 500);
return;
}
char *host = get_value(request->headers, "Host");
if (host == NULL)
{
send_http_error(sock_fd, 404);
return;
}
char host_buf[SERVER_BUFFER];
for (int i = 0; i < strlen(host); i++)
{
if (host[i] == ':')
{
host_buf[i] = '\0';
break;
}
host_buf[i] = host[i];
}
sprintf(path, "%s/%s%s", cwd, host_buf, request->path);
struct stat st;
memset(&st, 0, sizeof(st));
if (stat(path, &st) < 0)
{
send_http_error(sock_fd, 404);
return;
}
http_response_t *response = initialize_http_response();
response->status_code = 200;
char length_buf[SERVER_BUFFER / 2];
sprintf(length_buf, "%ld", st.st_size);
set_item(response->headers, "Server", "myserver/1.0");
set_item(response->headers, "Content-Type", assume_file_type(path));
set_item(response->headers, "Content-Length", length_buf);
set_item(response->headers, "Connection", "close");
response->body_length = st.st_size;
response = realloc(response, sizeof(http_response_t) + sizeof(char) * st.st_size);
FILE *file = fopen(path, "r");
if (fread(response->body, st.st_size, 1, file) < 0)
{
send_http_error(sock_fd, 500);
return;
}
fclose(file);
send_http_response(sock_fd, response);
finalize_http_response(response);
}
/**
* @brief HTTPのヘッダを解析する
*
* @param sock_fd 受信するファイル記述子
* @param request 更新するHTTPリクエストオブジェクト
* @return int 成功ステータス。成功した場合のみ0が返る。
*/
int parse_request_header(int sock_fd, http_request_t *request)
{
char tmp_buffer[SERVER_BUFFER];
char header_buffer[SERVER_BUFFER];
int buffer_consumed = 0;
int is_cr = 0;
int recv_size;
while (1)
{
header:
{
buffer_consumed = -1;
is_cr = 0;
}
while (1)
{
key_loop:
if ((recv_size = recv(sock_fd, &tmp_buffer[++buffer_consumed], 1, MSG_WAITALL)) < 0)
{
perror("recv");
return -1;
}
char k = tmp_buffer[buffer_consumed];
switch (k)
{
case ':':
tmp_buffer[buffer_consumed] = '\0';
goto whitespaces;
case '\r':
is_cr = 1;
goto key_loop;
case '\n':
if (is_cr)
{
if (buffer_consumed != 1)
{
fprintf(stderr, "error: server returned a malformed response. %d\n", buffer_consumed);
return -2;
}
goto end_headers;
}
default:
is_cr = 0;
break;
}
}
whitespaces:
{
strncpy(header_buffer, tmp_buffer, buffer_consumed);
header_buffer[buffer_consumed] = '\0';
}
while (1)
{
if ((recv_size = recv(sock_fd, tmp_buffer, 1, MSG_WAITALL | MSG_PEEK)) < 0)
{
perror("recv");
return -1;
}
recv(sock_fd, tmp_buffer, 1, 0);
char k = tmp_buffer[0];
if (!isblank(k))
{
break;
}
}
buffer_consumed = 0;
while (1)
{
value_loop:
if ((recv_size = recv(sock_fd, &tmp_buffer[++buffer_consumed], 1, MSG_WAITALL)) < 0)
{
perror("recv");
return -1;
}
char k = tmp_buffer[buffer_consumed];
switch (k)
{
case '\r':
is_cr = 1;
buffer_consumed--;
goto value_loop;
case '\n':
if (is_cr)
{
tmp_buffer[buffer_consumed] = '\0';
set_item(request->headers, header_buffer, tmp_buffer);
goto header;
}
default:
is_cr = 0;
break;
}
}
}
end_headers:
{
return 0;
}
}
/**
* @brief HTTPのリクエストを解析する
*
* @param sock_fd 受信するファイル記述子
* @param status ステータスを返すためのint型ポインタ
* @return http_request_t* リクエストを解析した結果
*/
http_request_t *parse_http_request(int sock_fd, int *status)
{
http_request_t *request = initialize_http_request();
signed char tmp_buffer[2048];
int buffer_comsumed = 0;
// parse request line
while (1)
{
if (buffer_comsumed > SERVER_BUFFER)
{
*status = 400;
return NULL;
}
if (recv(sock_fd, tmp_buffer + buffer_comsumed, 1, 0) < 1)
{
*status = 400;
return NULL;
}
if (tmp_buffer[buffer_comsumed] == ' ')
{
goto request_path;
}
if ('a' > (*tmp_buffer | 32) || (*tmp_buffer | 32) > 'z')
{
*status = 400;
return NULL;
}
buffer_comsumed++;
}
request_path:
for (int i = 0; i < buffer_comsumed; i++)
{
request->method[i] = tmp_buffer[i];
}
request->method[buffer_comsumed] = '\0';
buffer_comsumed = 0;
while (1)
{
if (buffer_comsumed > SERVER_BUFFER)
{
*status = 414;
return NULL;
}
if (recv(sock_fd, tmp_buffer + buffer_comsumed, 1, 0) < 1)
{
*status = 400;
return NULL;
}
if (tmp_buffer[buffer_comsumed] == ' ')
{
goto request_version;
}
if (tmp_buffer[buffer_comsumed] == '\r' || tmp_buffer[buffer_comsumed] == '\n')
{
*status = 400;
return NULL;
}
buffer_comsumed++;
}
request_version:
for (int i = 0; i < buffer_comsumed; i++)
{
request->path[i] = tmp_buffer[i];
}
request->path[buffer_comsumed] = '\0';
buffer_comsumed = 0;
while (1)
{
if (recv(sock_fd, tmp_buffer + buffer_comsumed, 1, 0) < 1)
{
*status = 400;
return NULL;
}
if (tmp_buffer[buffer_comsumed] == '\r')
{
}
if (tmp_buffer[buffer_comsumed] == '\n')
{
if (buffer_comsumed < 2)
{
*status = 400;
return NULL;
}
if (tmp_buffer[buffer_comsumed - 1] == '\r')
{
buffer_comsumed -= 1;
}
else
{
buffer_comsumed -= 2;
}
goto headers;
}
buffer_comsumed++;
}
headers:
{
if (parse_request_header(sock_fd, request) < 0)
{
*status = 400;
return NULL;
}
}
const char *length = get_value(request->headers, "Content-Length");
if (length == NULL)
{
return request;
}
for (int i = 0; i < strlen(length); i++)
{
if (!isdigit(length[i]))
{
*status = 400;
return NULL;
}
}
int content_length = atoi(length);
request = realloc(request, sizeof(http_request_t) + sizeof(char) * content_length);
if (recv(sock_fd, request->body, content_length, MSG_WAITALL) < 0)
{
*status = 400;
return NULL;
}
return request;
}
/**
* @brief 指定されたポートにbindされたファイル記述子を返す
*
* @param port 受信待ちをしたいポート番号
* @return int ファイル記述子。0未満が返った場合、失敗していることを表す。
*/
int start_listening(int port)
{
struct sockaddr_in saddr;
int sockfd;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = INADDR_ANY;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket");
return -1;
}
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind");
return -1;
}
if (listen(sockfd, SOCK_BACKLOG) < 0)
{
perror("listen");
return -1;
}
return sockfd;
}
/**
* @brief 受信待ち状態のファイル記述子を閉じる
*
* @param sockfd 受信待ち状態のファイル記述子
* @return int 成功した場合 0 が返る。
*/
int finalize_connection(int sockfd)
{
close(sockfd);
return 0;
}
/**
* @brief リクエストを処理する
*
* @param sockfd 受信待ち状態のファイル記述子
* @return int 終了した場合は0が返るが、通常の場合この関数から復帰することはない
*/
int serve_forever(int sockfd)
{
int status = 0;
http_request_t *request = NULL;
socklen_t socklen;
struct sockaddr_in client_addr;
char addr_buffer[46];
memset(&client_addr, 0, sizeof(client_addr));
int client_sockfd;
int child_index;
while (1)
{
if ((client_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &socklen)) < 0)
{
perror("accept");
continue;
}
child_index = -1;
for (int i = 0; i < MAX_CLIENTS; i++)
{
if (waitpid(handlers[i], NULL, WNOHANG) > 0 || handlers[i] == 0)
{
child_index = i;
pid_t pid = fork();
if (pid != 0)
{
handlers[i] = pid;
break;
}
goto serve_client;
}
}
if (child_index == -1)
{
send_http_error(client_sockfd, 503);
if (close(client_sockfd) < 0)
{
perror("close");
}
}
}
serve_client:
if (inet_ntop(client_addr.sin_family, &client_addr.sin_addr, addr_buffer, sizeof(addr_buffer)) == NULL)
{
strcpy(addr_buffer, "<unknown>");
}
request = parse_http_request(client_sockfd, &status);
printf("%s %s %d -- %s\n", request->method, request->path, status, addr_buffer);
if (request == NULL)
{
send_http_error(client_sockfd, status);
}
else if (strcmp(request->method, "GET") == 0)
{
send_file(client_sockfd, request);
}
else
{
send_http_error(client_sockfd, 405);
}
close(client_sockfd);
finalize_http_request(request);
exit(0);
}
int main(void)
{
int sockfd;
void keyboard_interrupt_handler(int sig)
{
printf("waiting for children...\n");
for (int i = 0; i < MAX_CLIENTS; i++)
{
if (handlers[i] != 0)
{
waitpid(handlers[i], NULL, 0);
}
}
finalize_connection(sockfd);
printf("\n");
exit(0);
}
if (signal(SIGINT, keyboard_interrupt_handler) == SIG_ERR)
{
perror("signal");
return -1;
}
for (int i = 0; i < MAX_CLIENTS; i++)
{
handlers[i] = 0;
}
printf("Starting server...\n");
int port = 8008;
sockfd = start_listening(port);
if (sockfd < 0)
{
return -1;
}
printf("Server listening on port %d\n", port);
printf("Press Ctrl+C to stop\n");
serve_forever(sockfd);
// ここには到達しない
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment