Compare commits
5 Commits
8fb6fa87f1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c6729eebc | |||
| c183477686 | |||
| d400722563 | |||
| cff0d58de4 | |||
| 695122ce13 |
@@ -22,12 +22,6 @@ meson compile -C build
|
|||||||
2. Run `./build/cws`
|
2. Run `./build/cws`
|
||||||
3. Open `http://localhost:3030` in your browser
|
3. Open `http://localhost:3030` in your browser
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
- [ ] Virtual hosts support
|
|
||||||
- [ ] Minimal templating engine
|
|
||||||
- [ ] IPv6 compatibility
|
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
Tested with [goku](https://github.com/jcaromiq/goku) (`-c 400 -d 30`):
|
Tested with [goku](https://github.com/jcaromiq/goku) (`-c 400 -d 30`):
|
||||||
|
|||||||
@@ -29,4 +29,6 @@ cws_config_s *cws_config_init(void);
|
|||||||
|
|
||||||
void cws_config_free(cws_config_s *config);
|
void cws_config_free(cws_config_s *config);
|
||||||
|
|
||||||
|
cws_vhost_s *config_get_vhost(cws_config_s *config, char *host);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+14
-11
@@ -5,10 +5,6 @@ project(
|
|||||||
default_options: ['c_std=gnu23', 'warning_level=3'],
|
default_options: ['c_std=gnu23', 'warning_level=3'],
|
||||||
)
|
)
|
||||||
|
|
||||||
add_global_arguments('-Wno-pedantic', language: 'c')
|
|
||||||
|
|
||||||
cc = meson.get_compiler('c')
|
|
||||||
|
|
||||||
incdir = include_directories('include')
|
incdir = include_directories('include')
|
||||||
srcdir = include_directories('src')
|
srcdir = include_directories('src')
|
||||||
|
|
||||||
@@ -17,7 +13,7 @@ include_dirs = [incdir, srcdir]
|
|||||||
subdir('src')
|
subdir('src')
|
||||||
|
|
||||||
libtomlc17 = dependency('libtomlc17', required: true)
|
libtomlc17 = dependency('libtomlc17', required: true)
|
||||||
libmyclib = cc.find_library('myclib', required: true)
|
libmyclib = dependency('myclib', required: true)
|
||||||
|
|
||||||
deps = [libtomlc17, libmyclib]
|
deps = [libtomlc17, libmyclib]
|
||||||
|
|
||||||
@@ -26,11 +22,18 @@ add_global_arguments('-DEVELOPER', language: 'c')
|
|||||||
add_global_arguments('-D_POSIX_C_SOURCE=200809L', language: 'c')
|
add_global_arguments('-D_POSIX_C_SOURCE=200809L', language: 'c')
|
||||||
|
|
||||||
exe = executable('cws', server, include_directories: include_dirs, dependencies: deps)
|
exe = executable('cws', server, include_directories: include_dirs, dependencies: deps)
|
||||||
|
subdir('test')
|
||||||
|
|
||||||
# Test
|
# Commands
|
||||||
test_src = files('test/server.c')
|
|
||||||
test_curl_dep = dependency('libcurl', required: false)
|
|
||||||
test_deps = [test_curl_dep]
|
|
||||||
test_exec = executable('test_http', test_src, dependencies: test_deps)
|
|
||||||
|
|
||||||
test('index get', test_exec, args: [exe.full_path()])
|
clangformat = find_program('clang-format', required: false)
|
||||||
|
if clangformat.found()
|
||||||
|
run_target(
|
||||||
|
'format',
|
||||||
|
command: [
|
||||||
|
'ninja',
|
||||||
|
'-C', join_paths(meson.current_build_dir()),
|
||||||
|
'clang-format',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
endif
|
||||||
@@ -173,3 +173,15 @@ void cws_config_free(cws_config_s *config) {
|
|||||||
free(config);
|
free(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cws_vhost_s *config_get_vhost(cws_config_s *config, char *host) {
|
||||||
|
for (unsigned i = 0; i < config->virtual_hosts_count; ++i) {
|
||||||
|
cws_vhost_s *vh = config->virtual_hosts;
|
||||||
|
if (!strcmp(vh[i].domain, host)) {
|
||||||
|
return &vh[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return default domain */
|
||||||
|
return config->default_vh;
|
||||||
|
}
|
||||||
|
|||||||
+35
-12
@@ -1,6 +1,7 @@
|
|||||||
#include "core/server.h"
|
#include "core/server.h"
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <netdb.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/epoll.h>
|
#include <sys/epoll.h>
|
||||||
@@ -45,56 +46,74 @@ cws_return cws_server_setup(cws_server_s *server, cws_config_s *config) {
|
|||||||
return CWS_CONFIG_ERROR;
|
return CWS_CONFIG_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
memset(server, 0, sizeof *server);
|
cws_return returncode = CWS_OK;
|
||||||
|
|
||||||
struct addrinfo hints;
|
struct addrinfo hints = {0};
|
||||||
struct addrinfo *res;
|
struct addrinfo *res = {0};
|
||||||
cws_server_setup_hints(&hints, config->host);
|
cws_server_setup_hints(&hints, config->host);
|
||||||
|
|
||||||
int status = getaddrinfo(config->host, config->port, &hints, &res);
|
int status = getaddrinfo(config->host, config->port, &hints, &res);
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
cws_log_error("getaddrinfo() error: %s", gai_strerror(status));
|
cws_log_error("getaddrinfo() error: %s", gai_strerror(status));
|
||||||
return CWS_GETADDRINFO_ERROR;
|
returncode = CWS_GETADDRINFO_ERROR;
|
||||||
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
server->sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
server->sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
||||||
if (server->sockfd < 0) {
|
if (server->sockfd < 0) {
|
||||||
cws_log_error("socket(): %s", strerror(errno));
|
cws_log_error("socket(): %s", strerror(errno));
|
||||||
return CWS_SOCKET_ERROR;
|
returncode = CWS_SOCKET_ERROR;
|
||||||
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int opt = 1;
|
const int opt = 1;
|
||||||
status = setsockopt(server->sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
|
status = setsockopt(server->sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
cws_log_error("setsockopt(): %s", strerror(errno));
|
cws_log_error("setsockopt(): %s", strerror(errno));
|
||||||
return CWS_SETSOCKOPT_ERROR;
|
returncode = CWS_SETSOCKOPT_ERROR;
|
||||||
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
status = bind(server->sockfd, res->ai_addr, res->ai_addrlen);
|
status = bind(server->sockfd, res->ai_addr, res->ai_addrlen);
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
cws_log_error("bind(): %s", strerror(errno));
|
cws_log_error("bind(): %s", strerror(errno));
|
||||||
return CWS_BIND_ERROR;
|
returncode = CWS_BIND_ERROR;
|
||||||
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
status = listen(server->sockfd, CWS_SERVER_BACKLOG);
|
status = listen(server->sockfd, CWS_SERVER_BACKLOG);
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
cws_log_error("listen(): %s", strerror(errno));
|
cws_log_error("listen(): %s", strerror(errno));
|
||||||
return CWS_LISTEN_ERROR;
|
returncode = CWS_LISTEN_ERROR;
|
||||||
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
freeaddrinfo(res);
|
freeaddrinfo(res);
|
||||||
|
|
||||||
cws_return ret = cws_server_setup_epoll(server->sockfd, &server->epfd);
|
cws_return ret = cws_server_setup_epoll(server->sockfd, &server->epfd);
|
||||||
if (ret != CWS_OK) {
|
if (ret != CWS_OK) {
|
||||||
return ret;
|
returncode = ret;
|
||||||
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
server->workers = cws_worker_new(config->workers, config);
|
server->workers = cws_worker_new(config->workers, config);
|
||||||
if (server->workers == NULL) {
|
if (server->workers == NULL) {
|
||||||
return CWS_WORKER_ERROR;
|
returncode = CWS_WORKER_ERROR;
|
||||||
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CWS_OK;
|
return CWS_OK;
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
if (res) {
|
||||||
|
freeaddrinfo(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server->sockfd >= 0) {
|
||||||
|
close(server->sockfd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returncode;
|
||||||
}
|
}
|
||||||
|
|
||||||
cws_return cws_server_start(cws_server_s *server) {
|
cws_return cws_server_start(cws_server_s *server) {
|
||||||
@@ -115,6 +134,10 @@ cws_return cws_server_start(cws_server_s *server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < nfds; ++i) {
|
for (int i = 0; i < nfds; ++i) {
|
||||||
|
if (events[i].data.fd != server->sockfd) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
int client_fd = cws_server_handle_new_client(server->sockfd);
|
int client_fd = cws_server_handle_new_client(server->sockfd);
|
||||||
if (client_fd < 0) {
|
if (client_fd < 0) {
|
||||||
continue;
|
continue;
|
||||||
@@ -163,11 +186,11 @@ void cws_server_shutdown(cws_server_s *server) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server->sockfd > 0) {
|
if (server->sockfd >= 0) {
|
||||||
close(server->sockfd);
|
close(server->sockfd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server->epfd > 0) {
|
if (server->epfd >= 0) {
|
||||||
close(server->epfd);
|
close(server->epfd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-13
@@ -28,18 +28,6 @@ static void worker_close_client(int epfd, int client_fd) {
|
|||||||
close(client_fd);
|
close(client_fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
static cws_vhost_s *get_vhost(cws_config_s *config, char *host) {
|
|
||||||
for (unsigned i = 0; i < config->virtual_hosts_count; ++i) {
|
|
||||||
cws_vhost_s *vh = config->virtual_hosts;
|
|
||||||
if (!strcmp(vh[i].domain, host)) {
|
|
||||||
return &vh[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Return default domain */
|
|
||||||
return config->default_vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
static cws_return worker_handle_client_data(int epfd, int client_fd, cws_config_s *config) {
|
static cws_return worker_handle_client_data(int epfd, int client_fd, cws_config_s *config) {
|
||||||
string_s *data = string_new("", 4096);
|
string_s *data = string_new("", 4096);
|
||||||
|
|
||||||
@@ -78,7 +66,7 @@ static cws_return worker_handle_client_data(int epfd, int client_fd, cws_config_
|
|||||||
|
|
||||||
/* Configure handler */
|
/* Configure handler */
|
||||||
char *host = cws_request_get_header(request, "host");
|
char *host = cws_request_get_header(request, "host");
|
||||||
cws_vhost_s *vh = get_vhost(config, host);
|
cws_vhost_s *vh = config_get_vhost(config, host);
|
||||||
cws_handler_config_s conf = {
|
cws_handler_config_s conf = {
|
||||||
.domain = vh->domain,
|
.domain = vh->domain,
|
||||||
.root = vh->root,
|
.root = vh->root,
|
||||||
@@ -151,6 +139,7 @@ cws_worker_s **cws_worker_new(size_t workers_num, cws_config_s *config) {
|
|||||||
/* Create per-worker epoll instance */
|
/* Create per-worker epoll instance */
|
||||||
if (worker_setup_epoll(workers[i]) != CWS_OK) {
|
if (worker_setup_epoll(workers[i]) != CWS_OK) {
|
||||||
for (size_t j = 0; j < i; ++j) {
|
for (size_t j = 0; j < i; ++j) {
|
||||||
|
close(workers[j]->epfd);
|
||||||
free(workers[j]);
|
free(workers[j]);
|
||||||
}
|
}
|
||||||
free(workers);
|
free(workers);
|
||||||
@@ -174,6 +163,7 @@ void cws_worker_free(cws_worker_s **workers, size_t workers_num) {
|
|||||||
|
|
||||||
for (size_t i = 0; i < workers_num; ++i) {
|
for (size_t i = 0; i < workers_num; ++i) {
|
||||||
pthread_join(workers[i]->thread, NULL);
|
pthread_join(workers[i]->thread, NULL);
|
||||||
|
close(workers[i]->epfd);
|
||||||
free(workers[i]);
|
free(workers[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-2
@@ -1,21 +1,33 @@
|
|||||||
#include "http/handler.h"
|
#include "http/handler.h"
|
||||||
|
#include "config/config.h"
|
||||||
#include "utils/debug.h"
|
#include "utils/debug.h"
|
||||||
#include <myclib/mystring.h>
|
#include <myclib/mystring.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
/* Sanitize and resolve file path */
|
/* Sanitize and resolve file path */
|
||||||
/* TODO: fix path traversal */
|
|
||||||
static string_s *resolve_file_path(const char *url_path, cws_handler_config_s *config) {
|
static string_s *resolve_file_path(const char *url_path, cws_handler_config_s *config) {
|
||||||
string_s *full_path = string_new(config->root, 256);
|
string_s *full_path = string_new(config->root, 256);
|
||||||
|
if (!full_path) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(url_path, "/") == 0) {
|
if (strcmp(url_path, "/") == 0) {
|
||||||
string_append(full_path, "/");
|
string_append(full_path, "/");
|
||||||
/* Use vhost index file */
|
/* @TODO: Use vhost index file */
|
||||||
string_append(full_path, "index.html");
|
string_append(full_path, "index.html");
|
||||||
return full_path;
|
return full_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string_s *url_path_string = string_new(url_path, 0);
|
||||||
|
if (!url_path_string) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string_find(url_path_string, "..")) {
|
||||||
|
return full_path;
|
||||||
|
}
|
||||||
|
|
||||||
string_append(full_path, url_path);
|
string_append(full_path, url_path);
|
||||||
|
|
||||||
return full_path;
|
return full_path;
|
||||||
@@ -43,6 +55,8 @@ cws_response_s *cws_handler_static_file(cws_request_s *request, cws_handler_conf
|
|||||||
return cws_handler_not_implemented();
|
return cws_handler_not_implemented();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* @TODO: use config_get_vhost */
|
||||||
|
// cws_vhost_s *vhost = config_get_vhost(, request->host);
|
||||||
string_s *filepath = resolve_file_path(string_cstr(request->path), config);
|
string_s *filepath = resolve_file_path(string_cstr(request->path), config);
|
||||||
const char *path = string_cstr(filepath);
|
const char *path = string_cstr(filepath);
|
||||||
|
|
||||||
|
|||||||
+28
-5
@@ -3,6 +3,7 @@
|
|||||||
#include "utils/debug.h"
|
#include "utils/debug.h"
|
||||||
#include "utils/hash.h"
|
#include "utils/hash.h"
|
||||||
#include <core/socket.h>
|
#include <core/socket.h>
|
||||||
|
#include <myclib/myhashmap.h>
|
||||||
#include <myclib/mystring.h>
|
#include <myclib/mystring.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -16,6 +17,26 @@ static hashmap_s *response_headers_new(void) {
|
|||||||
sizeof(char) * HEADER_VALUE_MAX);
|
sizeof(char) * HEADER_VALUE_MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int response_get_headers(hashmap_s *headers, char *out_headers, size_t len) {
|
||||||
|
size_t keys_len = 0;
|
||||||
|
char **keys = (char **)hm_get_keys(headers, &keys_len);
|
||||||
|
if (!keys) {
|
||||||
|
cws_log_debug("%s", "no headers??");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t offset = 0;
|
||||||
|
for (size_t i = 0; i < keys_len; ++i) {
|
||||||
|
bucket_s *bucket = hm_get(headers, keys[i]);
|
||||||
|
offset += snprintf(out_headers, *out_headers + offset, "%s: %s", keys[i], (char *)bucket->value);
|
||||||
|
if ((size_t)(headers + offset) >= len) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
cws_response_s *cws_response_new(cws_http_status_e status) {
|
cws_response_s *cws_response_new(cws_http_status_e status) {
|
||||||
cws_response_s *resp = malloc(sizeof(*resp));
|
cws_response_s *resp = malloc(sizeof(*resp));
|
||||||
if (!resp) {
|
if (!resp) {
|
||||||
@@ -137,16 +158,18 @@ int cws_response_send(int sockfd, cws_response_s *response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
char headers[HEADERS_BUFFER_SIZE];
|
char headers[HEADERS_BUFFER_SIZE];
|
||||||
|
|
||||||
|
response_get_headers(response->headers, headers, sizeof headers);
|
||||||
int offset = snprintf(headers, sizeof(headers), "HTTP/1.1 %s\r\n", cws_http_status_string(response->status));
|
int offset = snprintf(headers, sizeof(headers), "HTTP/1.1 %s\r\n", cws_http_status_string(response->status));
|
||||||
|
|
||||||
char content_length_str[32];
|
char content_length_str[32];
|
||||||
snprintf(content_length_str, sizeof(content_length_str), "%zu", response->content_length);
|
snprintf(content_length_str, sizeof(content_length_str), "%zu", response->content_length);
|
||||||
cws_response_set_header(response, "Content-Length", content_length_str);
|
|
||||||
|
|
||||||
offset += snprintf(headers + offset, sizeof(headers) - offset,
|
/* @TODO: I can do this in the response_get_header() but I need to check
|
||||||
"Content-Length: %zu\r\n"
|
* if I have space left for \r\n
|
||||||
"Connection: close\r\n",
|
*/
|
||||||
response->content_length);
|
cws_response_set_header(response, "Content-Length", content_length_str);
|
||||||
|
offset += snprintf(headers + offset, sizeof(headers) - offset, "Content-Length: %zu\r\n", response->content_length);
|
||||||
|
|
||||||
offset += snprintf(headers + offset, sizeof(headers) - offset, "\r\n");
|
offset += snprintf(headers + offset, sizeof(headers) - offset, "\r\n");
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -16,23 +16,23 @@ int main(void) {
|
|||||||
cws_log_init();
|
cws_log_init();
|
||||||
|
|
||||||
if (signal(SIGINT, cws_signal_handler) == SIG_ERR) {
|
if (signal(SIGINT, cws_signal_handler) == SIG_ERR) {
|
||||||
cws_log_error("signal()");
|
cws_log_error("%s", "signal()");
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signal(SIGTERM, cws_signal_handler) == SIG_ERR) {
|
if (signal(SIGTERM, cws_signal_handler) == SIG_ERR) {
|
||||||
cws_log_error("signal()");
|
cws_log_error("%s", "signal()");
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
cws_config_s *config = cws_config_init();
|
cws_config_s *config = cws_config_init();
|
||||||
if (!config) {
|
if (!config) {
|
||||||
cws_log_error("Unable to parse config");
|
cws_log_error("%s", "Unable to parse config");
|
||||||
cws_log_shutdown();
|
cws_log_shutdown();
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
cws_server_s server;
|
cws_server_s server = {0};
|
||||||
cws_return ret;
|
cws_return ret;
|
||||||
|
|
||||||
ret = cws_server_setup(&server, config);
|
ret = cws_server_setup(&server, config);
|
||||||
@@ -51,7 +51,7 @@ int main(void) {
|
|||||||
cws_log_error("Unable to start web server: %s", cws_error_str(ret));
|
cws_log_error("Unable to start web server: %s", cws_error_str(ret));
|
||||||
}
|
}
|
||||||
|
|
||||||
cws_log_info("Shutting down cws");
|
cws_log_info("%s", "Shutting down cws");
|
||||||
cws_server_shutdown(&server);
|
cws_server_shutdown(&server);
|
||||||
cws_config_free(config);
|
cws_config_free(config);
|
||||||
cws_log_shutdown();
|
cws_log_shutdown();
|
||||||
|
|||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
#include "common.h"
|
||||||
|
#include <errno.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
bool start_server(const char *server_path, pid_t *pid_out) {
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
execl(server_path, "cws", NULL);
|
||||||
|
fprintf(stderr, "execl failed: %s\n", strerror(errno));
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid < 0) {
|
||||||
|
fprintf(stderr, "fork failed: %s\n", strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pid_out = pid;
|
||||||
|
fprintf(stdout, "[INFO] server pid: %d\n", pid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop_server(pid_t pid) {
|
||||||
|
if (pid <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stdout, "[INFO] server stopped (pid=%d)\n", pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool perform_request(const char *url, const char *method, http_result_s *out) {
|
||||||
|
CURL *curl = curl_easy_init();
|
||||||
|
if (!curl) {
|
||||||
|
fprintf(stderr, "[ERROR] curl_easy_init failed\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
out->content_length = -1;
|
||||||
|
out->error[0] = '\0';
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_NOBODY, 0L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, out->error);
|
||||||
|
|
||||||
|
out->curl_code = curl_easy_perform(curl);
|
||||||
|
if (out->curl_code == CURLE_OK) {
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &out->code);
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &out->content_length);
|
||||||
|
|
||||||
|
char *ct = NULL;
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct);
|
||||||
|
if (ct) {
|
||||||
|
strncpy(out->content_type, ct, sizeof(out->content_type) - 1);
|
||||||
|
out->content_type[sizeof(out->content_type) - 1] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
return out->curl_code == CURLE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wait_until_ready(void) {
|
||||||
|
char url[256];
|
||||||
|
snprintf(url, sizeof(url), "%s/", BASE_URL);
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_RETRIES; i++) {
|
||||||
|
http_result_s result;
|
||||||
|
if (perform_request(url, "GET", &result)) {
|
||||||
|
fprintf(stdout, "[INFO] server ready after %d attempt(s), code=%ld\n", i + 1, result.code);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stdout, "[INFO] server not ready (%d/%d): %s\n", i + 1, MAX_RETRIES,
|
||||||
|
result.error[0] ? result.error : "unknown error");
|
||||||
|
sleep(RETRY_SLEEP_SEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int assert_status(const char *name, const char *url, const char *method, long expected) {
|
||||||
|
http_result_s result;
|
||||||
|
if (!perform_request(url, method, &result)) {
|
||||||
|
fprintf(stderr, "[FAIL] %s: request failed (%s)\n", name,
|
||||||
|
result.error[0] ? result.error : "unknown curl error");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stdout, "[TEST] %s -> %s %s => %ld (expected %ld)\n", name, method, url, result.code, expected);
|
||||||
|
|
||||||
|
if (result.code != expected) {
|
||||||
|
fprintf(stderr, "[FAIL] %s: expected HTTP %ld, got %ld\n", name, expected, result.code);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#ifndef CWS_TEST_COMMON_H
|
||||||
|
#define CWS_TEST_COMMON_H
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#define BASE_URL "http://localhost:3030"
|
||||||
|
#define MAX_RETRIES 15
|
||||||
|
#define RETRY_SLEEP_SEC 1
|
||||||
|
|
||||||
|
typedef struct http_result {
|
||||||
|
long code;
|
||||||
|
long content_length;
|
||||||
|
char content_type[256];
|
||||||
|
char error[CURL_ERROR_SIZE];
|
||||||
|
CURLcode curl_code;
|
||||||
|
} http_result_s;
|
||||||
|
|
||||||
|
bool start_server(const char *server_path, pid_t *pid_out);
|
||||||
|
void stop_server(pid_t pid);
|
||||||
|
bool wait_until_ready(void);
|
||||||
|
bool perform_request(const char *url, const char *method, http_result_s *out);
|
||||||
|
int assert_status(const char *name, const char *url, const char *method, long expected);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
test_common_src = files('common.c')
|
||||||
|
test_curl_dep = dependency('libcurl', required: false)
|
||||||
|
test_deps = [test_curl_dep]
|
||||||
|
|
||||||
|
test_get_index = executable(
|
||||||
|
'test_get_index',
|
||||||
|
test_common_src,
|
||||||
|
files('test_get_index.c'),
|
||||||
|
dependencies: test_deps,
|
||||||
|
)
|
||||||
|
test('index get', test_get_index, args: [exe.full_path()])
|
||||||
|
|
||||||
|
test_get_missing = executable(
|
||||||
|
'test_get_missing',
|
||||||
|
test_common_src,
|
||||||
|
files('test_get_missing.c'),
|
||||||
|
dependencies: test_deps,
|
||||||
|
)
|
||||||
|
test('missing 404', test_get_missing, args: [exe.full_path()])
|
||||||
|
|
||||||
|
test_post = executable(
|
||||||
|
'test_post',
|
||||||
|
test_common_src,
|
||||||
|
files('test_post.c'),
|
||||||
|
dependencies: test_deps,
|
||||||
|
)
|
||||||
|
test('post not implemented', test_post, args: [exe.full_path()])
|
||||||
|
|
||||||
|
test_traversal = executable(
|
||||||
|
'test_traversal',
|
||||||
|
test_common_src,
|
||||||
|
files('test_traversal.c'),
|
||||||
|
dependencies: test_deps,
|
||||||
|
)
|
||||||
|
test('traversal blocked', test_traversal, args: [exe.full_path()])
|
||||||
|
|
||||||
|
test_content_length = executable(
|
||||||
|
'test_content_length',
|
||||||
|
test_common_src,
|
||||||
|
files('test_content_length.c'),
|
||||||
|
dependencies: test_deps,
|
||||||
|
)
|
||||||
|
test('content length present', test_content_length, args: [exe.full_path()])
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#include <curl/curl.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#define URL "http://localhost:3030"
|
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
|
||||||
if (argc < 2) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *server_path = argv[1];
|
|
||||||
fprintf(stdout, "server path: %s\n", server_path);
|
|
||||||
|
|
||||||
/* Run the server as child proc */
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid == 0) {
|
|
||||||
execl(server_path, "cws", NULL);
|
|
||||||
fprintf(stderr, "execl failed: %s\n", strerror(errno));
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pid == -1) {
|
|
||||||
fprintf(stderr, "fork failed: %s\n", strerror(errno));
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf(stdout, "pid: %d\n", pid);
|
|
||||||
|
|
||||||
CURL *curl = curl_easy_init();
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, URL);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 1L);
|
|
||||||
|
|
||||||
long http_code = 0;
|
|
||||||
int max_retries = 10;
|
|
||||||
for (int i = 0; i < max_retries; i++) {
|
|
||||||
CURLcode res = curl_easy_perform(curl);
|
|
||||||
if (res == CURLE_OK) {
|
|
||||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
fprintf(stdout, "Server not ready, retry %d/%d...\n", i + 1, max_retries);
|
|
||||||
sleep(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
|
|
||||||
fprintf(stdout, "http_code: %ld\n", http_code);
|
|
||||||
|
|
||||||
/* Kill the server */
|
|
||||||
kill(pid, SIGTERM);
|
|
||||||
waitpid(pid, NULL, 0);
|
|
||||||
|
|
||||||
return http_code == 200 ? 0 : 1;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#include "common.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static int test_content_length_present_on_index(void) {
|
||||||
|
char url[256];
|
||||||
|
snprintf(url, sizeof(url), "%s/", BASE_URL);
|
||||||
|
|
||||||
|
http_result_s result;
|
||||||
|
if (!perform_request(url, "GET", &result)) {
|
||||||
|
fprintf(stderr, "[FAIL] content-length on index: request failed (%s)\n",
|
||||||
|
result.error[0] ? result.error : "unknown curl error");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stdout, "[TEST] content-length on index -> code=%ld, content-length=%ld\n", result.code,
|
||||||
|
result.content_length);
|
||||||
|
|
||||||
|
if (result.code != 200) {
|
||||||
|
fprintf(stderr, "[FAIL] content-length on index: expected 200, got %ld\n", result.code);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.content_length < 0) {
|
||||||
|
fprintf(stderr, "[FAIL] content-length on index: missing/invalid content length\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "Usage: %s <server_binary_path>\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||||
|
|
||||||
|
const char *server_path = argv[1];
|
||||||
|
fprintf(stdout, "[INFO] server path: %s\n", server_path);
|
||||||
|
|
||||||
|
pid_t server_pid = -1;
|
||||||
|
if (!start_server(server_path, &server_pid)) {
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wait_until_ready()) {
|
||||||
|
fprintf(stderr, "[FAIL] server did not become ready in time\n");
|
||||||
|
stop_server(server_pid);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int failures = test_content_length_present_on_index();
|
||||||
|
|
||||||
|
stop_server(server_pid);
|
||||||
|
curl_global_cleanup();
|
||||||
|
|
||||||
|
if (failures == 0) {
|
||||||
|
fprintf(stdout, "[PASS] test_content_length passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "[FAIL] test_content_length failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#include "common.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static int test_get_index_ok(void) {
|
||||||
|
char url[256];
|
||||||
|
snprintf(url, sizeof(url), "%s/", BASE_URL);
|
||||||
|
return assert_status("GET / returns 200", url, "GET", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "Usage: %s <server_binary_path>\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||||
|
|
||||||
|
const char *server_path = argv[1];
|
||||||
|
fprintf(stdout, "[INFO] server path: %s\n", server_path);
|
||||||
|
|
||||||
|
pid_t server_pid = -1;
|
||||||
|
if (!start_server(server_path, &server_pid)) {
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wait_until_ready()) {
|
||||||
|
fprintf(stderr, "[FAIL] server did not become ready in time\n");
|
||||||
|
stop_server(server_pid);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int failures = test_get_index_ok();
|
||||||
|
|
||||||
|
stop_server(server_pid);
|
||||||
|
curl_global_cleanup();
|
||||||
|
|
||||||
|
if (failures == 0) {
|
||||||
|
fprintf(stdout, "[PASS] test_get_index passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "[FAIL] test_get_index failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#include "common.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static int test_get_missing_404(void) {
|
||||||
|
char url[256];
|
||||||
|
snprintf(url, sizeof(url), "%s/does-not-exist-xyz", BASE_URL);
|
||||||
|
return assert_status("GET /missing returns 404", url, "GET", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "Usage: %s <server_binary_path>\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||||
|
|
||||||
|
const char *server_path = argv[1];
|
||||||
|
fprintf(stdout, "[INFO] server path: %s\n", server_path);
|
||||||
|
|
||||||
|
pid_t server_pid = -1;
|
||||||
|
if (!start_server(server_path, &server_pid)) {
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wait_until_ready()) {
|
||||||
|
fprintf(stderr, "[FAIL] server did not become ready in time\n");
|
||||||
|
stop_server(server_pid);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int failures = test_get_missing_404();
|
||||||
|
|
||||||
|
stop_server(server_pid);
|
||||||
|
curl_global_cleanup();
|
||||||
|
|
||||||
|
if (failures == 0) {
|
||||||
|
fprintf(stdout, "[PASS] test_get_missing passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "[FAIL] test_get_missing failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#include "common.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static int test_post_not_implemented(void) {
|
||||||
|
char url[256];
|
||||||
|
snprintf(url, sizeof(url), "%s/", BASE_URL);
|
||||||
|
return assert_status("POST / returns 501", url, "POST", 501);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "Usage: %s <server_binary_path>\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||||
|
|
||||||
|
const char *server_path = argv[1];
|
||||||
|
fprintf(stdout, "[INFO] server path: %s\n", server_path);
|
||||||
|
|
||||||
|
pid_t server_pid = -1;
|
||||||
|
if (!start_server(server_path, &server_pid)) {
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wait_until_ready()) {
|
||||||
|
fprintf(stderr, "[FAIL] server did not become ready in time\n");
|
||||||
|
stop_server(server_pid);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int failures = test_post_not_implemented();
|
||||||
|
|
||||||
|
stop_server(server_pid);
|
||||||
|
curl_global_cleanup();
|
||||||
|
|
||||||
|
if (failures == 0) {
|
||||||
|
fprintf(stdout, "[PASS] test_post passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "[FAIL] test_post failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#include "common.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static int test_traversal_blocked(void) {
|
||||||
|
char url[256];
|
||||||
|
snprintf(url, sizeof(url), "%s/../config.toml", BASE_URL);
|
||||||
|
|
||||||
|
http_result_s result;
|
||||||
|
if (!perform_request(url, "GET", &result)) {
|
||||||
|
fprintf(stderr, "[FAIL] traversal blocked: request failed (%s)\n",
|
||||||
|
result.error[0] ? result.error : "unknown curl error");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stdout, "[TEST] traversal blocked -> GET %s => %ld (expected 404 or 500)\n", url, result.code);
|
||||||
|
|
||||||
|
/* Accept either 404 (ideal) or 500 (still blocked but internal error path). */
|
||||||
|
if (result.code != 404 && result.code != 500) {
|
||||||
|
fprintf(stderr, "[FAIL] traversal blocked: expected 404/500, got %ld\n", result.code);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "Usage: %s <server_binary_path>\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||||
|
|
||||||
|
const char *server_path = argv[1];
|
||||||
|
fprintf(stdout, "[INFO] server path: %s\n", server_path);
|
||||||
|
|
||||||
|
pid_t server_pid = -1;
|
||||||
|
if (!start_server(server_path, &server_pid)) {
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wait_until_ready()) {
|
||||||
|
fprintf(stderr, "[FAIL] server did not become ready in time\n");
|
||||||
|
stop_server(server_pid);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int failures = test_traversal_blocked();
|
||||||
|
|
||||||
|
stop_server(server_pid);
|
||||||
|
curl_global_cleanup();
|
||||||
|
|
||||||
|
if (failures == 0) {
|
||||||
|
fprintf(stdout, "[PASS] test_traversal passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "[FAIL] test_traversal failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user