Compare commits

...

14 Commits

27 changed files with 910 additions and 516 deletions
-6
View File
@@ -22,12 +22,6 @@ meson compile -C build
2. Run `./build/cws`
3. Open `http://localhost:3030` in your browser
## Roadmap
- [ ] Virtual hosts support
- [ ] Minimal templating engine
- [ ] IPv6 compatibility
## Performance
Tested with [goku](https://github.com/jcaromiq/goku) (`-c 400 -d 30`):
+3
View File
@@ -22,10 +22,13 @@ typedef struct cws_config {
int workers;
cws_vhost_s *virtual_hosts;
unsigned virtual_hosts_count;
cws_vhost_s *default_vh;
} cws_config_s;
cws_config_s *cws_config_init(void);
void cws_config_free(cws_config_s *config);
cws_vhost_s *config_get_vhost(cws_config_s *config, char *host);
#endif
+2 -2
View File
@@ -4,8 +4,8 @@
#include <myclib/mystring.h>
#include <sys/types.h>
ssize_t cws_socket_read(int sockfd, string_s *str);
int cws_socket_read(int sockfd, string_s *str);
ssize_t cws_socket_send(int sockfd, const char *buffer, size_t len, int flags);
int cws_socket_send(int sockfd, const char *buffer, size_t len, int flags);
#endif
+3 -7
View File
@@ -6,10 +6,6 @@
#include <myclib/mystring.h>
#include <stddef.h>
#define CWS_HTTP_CONTENT_TYPE 64
#define CWS_HTTP_HEADER_MAX 512
#define CWS_HTTP_HEADER_CONTENT_MAX 1024
typedef struct cws_request {
cws_http_method_e method;
string_s *host;
@@ -20,10 +16,10 @@ typedef struct cws_request {
string_s *body;
} cws_request_s;
cws_request_s *cws_http_parse(string_s *request_str);
cws_request_s *cws_request_parse(string_s *request_str);
char *cws_http_get_host(cws_request_s *request);
char *cws_request_get_header(cws_request_s *request, const char *header);
void cws_http_free(cws_request_s *request);
void cws_request_free(cws_request_s *request);
#endif
+25 -12
View File
@@ -1,26 +1,39 @@
project(
'cws',
'c',
version: '0.1.0',
default_options: ['c_std=gnu23', 'warning_level=3'],
'cws',
'c',
version: '0.1.1',
default_options: ['c_std=gnu23', 'warning_level=3'],
)
add_global_arguments('-Wno-pedantic', language: 'c')
incdir = include_directories('include')
srcdir = include_directories('src')
cc = meson.get_compiler('c')
include_dirs = [incdir, srcdir]
subdir('src')
incdir = include_directories('include')
libtomlc17 = dependency('libtomlc17', required: true)
libmath = cc.find_library('m', required: true)
libmyclib = cc.find_library('myclib', required: true)
libmyclib = dependency('myclib', required: true)
deps = [libtomlc17, libmath, libmyclib]
deps = [libtomlc17, libmyclib]
add_global_arguments('-DUSE_COLORS', language: 'c')
add_global_arguments('-DEVELOPER', language: 'c')
add_global_arguments('-D_POSIX_C_SOURCE=200809L', language: 'c')
executable('cws', server, include_directories: incdir, dependencies: deps)
exe = executable('cws', server, include_directories: include_dirs, dependencies: deps)
subdir('test')
# Commands
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
+40 -16
View File
@@ -2,28 +2,48 @@
#include <stdlib.h>
#include <string.h>
#include <string.h>
#include <tomlc17.h>
#include "utils/debug.h"
static bool is_default(const char *domain) {
if (!strcmp(domain, "default")) {
return true;
}
return false;
}
static bool parse_vhosts(cws_config_s *config, toml_result_t result) {
toml_datum_t vhosts = toml_seek(result.toptab, "virtual_hosts");
/* Retrieve virtual hosts counter */
config->virtual_hosts_count = vhosts.u.arr.size;
/* Allocate virtual hosts array */
config->virtual_hosts = malloc(sizeof *config->virtual_hosts * config->virtual_hosts_count);
if (!config->virtual_hosts) {
return false;
}
/* Iterate for each virtual host */
for (int i = 0; i < vhosts.u.arr.size; ++i) {
cws_vhost_s *vh = &config->virtual_hosts[i];
toml_datum_t elem = vhosts.u.arr.elem[i];
/* Retrieve vh's domain */
toml_datum_t domain = toml_seek(elem, "domain");
vh->domain = strdup(domain.u.str.ptr);
if (!vh->domain) {
return false;
}
/* Check if vh->domain is the default domain */
if (is_default(vh->domain)) {
config->default_vh = vh;
}
/* Retrieve vh's root folder */
toml_datum_t root = toml_seek(elem, "root");
vh->root = strdup(root.u.str.ptr);
if (!vh->root) {
@@ -33,13 +53,17 @@ static bool parse_vhosts(cws_config_s *config, toml_result_t result) {
/* Pages */
toml_datum_t pages = toml_seek(elem, "pages");
vh->error_pages_count = pages.u.arr.size;
/* Allocate error pages array */
vh->error_pages = malloc(sizeof *vh->error_pages * vh->error_pages_count);
if (!vh->error_pages) {
return false;
}
/* Iterate for each page */
for (int j = 0; j < pages.u.arr.size; ++j) {
toml_datum_t page = pages.u.arr.elem[i];
toml_datum_t status = toml_seek(page, "status");
vh->error_pages[j].status = strdup(status.u.str.ptr);
if (!vh->error_pages[i].status) {
@@ -57,17 +81,6 @@ static bool parse_vhosts(cws_config_s *config, toml_result_t result) {
return true;
}
static bool find_default(cws_config_s *config) {
for (unsigned i = 0; i < config->virtual_hosts_count; ++i) {
cws_vhost_s *vh = config->virtual_hosts;
if (!strcmp(vh[i].domain, "default")) {
return true;
}
}
return false;
}
static bool parse_toml(cws_config_s *config) {
const char *path = "config.toml";
@@ -99,15 +112,14 @@ static bool parse_toml(cws_config_s *config) {
toml_datum_t workers = toml_seek(result.toptab, "server.workers");
config->workers = workers.u.int64;
parse_vhosts(config, result);
bool ret = parse_vhosts(config, result);
toml_free(result);
return find_default(config);
return ret;
}
cws_config_s *cws_config_init(void) {
cws_config_s *config = malloc(sizeof *config);
cws_config_s *config = calloc(1, sizeof *config);
if (!config) {
return NULL;
}
@@ -161,3 +173,15 @@ void cws_config_free(cws_config_s *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
View File
@@ -1,6 +1,7 @@
#include "core/server.h"
#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <string.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;
}
memset(server, 0, sizeof *server);
cws_return returncode = CWS_OK;
struct addrinfo hints;
struct addrinfo *res;
struct addrinfo hints = {0};
struct addrinfo *res = {0};
cws_server_setup_hints(&hints, config->host);
int status = getaddrinfo(config->host, config->port, &hints, &res);
if (status != 0) {
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);
if (server->sockfd < 0) {
cws_log_error("socket(): %s", strerror(errno));
return CWS_SOCKET_ERROR;
returncode = CWS_SOCKET_ERROR;
goto cleanup;
}
const int opt = 1;
status = setsockopt(server->sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
if (status != 0) {
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);
if (status != 0) {
cws_log_error("bind(): %s", strerror(errno));
return CWS_BIND_ERROR;
returncode = CWS_BIND_ERROR;
goto cleanup;
}
status = listen(server->sockfd, CWS_SERVER_BACKLOG);
if (status != 0) {
cws_log_error("listen(): %s", strerror(errno));
return CWS_LISTEN_ERROR;
returncode = CWS_LISTEN_ERROR;
goto cleanup;
}
freeaddrinfo(res);
cws_return ret = cws_server_setup_epoll(server->sockfd, &server->epfd);
if (ret != CWS_OK) {
return ret;
returncode = ret;
goto cleanup;
}
server->workers = cws_worker_new(config->workers, config);
if (server->workers == NULL) {
return CWS_WORKER_ERROR;
returncode = CWS_WORKER_ERROR;
goto cleanup;
}
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) {
@@ -115,6 +134,10 @@ cws_return cws_server_start(cws_server_s *server) {
}
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);
if (client_fd < 0) {
continue;
@@ -163,11 +186,11 @@ void cws_server_shutdown(cws_server_s *server) {
return;
}
if (server->sockfd > 0) {
if (server->sockfd >= 0) {
close(server->sockfd);
}
if (server->epfd > 0) {
if (server->epfd >= 0) {
close(server->epfd);
}
+40 -22
View File
@@ -3,46 +3,64 @@
#include <errno.h>
#include <sys/socket.h>
ssize_t cws_socket_read(int sockfd, string_s *str) {
int cws_socket_read(int sockfd, string_s *str) {
char tmp[4096] = {0};
ssize_t n = recv(sockfd, tmp, sizeof tmp, 0);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
for (;;) {
ssize_t n = recv(sockfd, tmp, sizeof tmp, 0);
/* We have some data */
if (n > 0) {
tmp[n] = '\0';
string_append(str, tmp);
return n;
}
/* Client closed */
if (n == 0) {
return 0;
}
if (errno == EINTR) {
continue;
}
/* No data now */
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return -2;
}
/* Something happened */
return -1;
}
if (n == 0) {
return -1;
}
tmp[n] = '\0';
string_append(str, tmp);
return n;
}
ssize_t cws_socket_send(int sockfd, const char *buffer, size_t len, int flags) {
int cws_socket_send(int sockfd, const char *buffer, size_t len, int flags) {
size_t total_sent = 0;
ssize_t n;
while (total_sent < len) {
n = send(sockfd, buffer + total_sent, len - total_sent, flags);
ssize_t n = send(sockfd, buffer + total_sent, len - total_sent, flags);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
return -1;
if (n > 0) {
total_sent += (size_t)n;
continue;
}
if (n == 0) {
break;
}
total_sent += n;
if (errno == EINTR) {
continue;
}
/* Partial write */
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
return -1;
}
return total_sent;
return (ssize_t)total_sent;
}
+26 -23
View File
@@ -10,6 +10,7 @@
#include "http/handler.h"
#include "http/request.h"
#include "http/response.h"
#include "utils/debug.h"
#include "utils/error.h"
/* Create epoll instance for a worker */
@@ -27,39 +28,36 @@ static void worker_close_client(int epfd, int 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];
}
}
/* This should not happen */
return NULL;
}
static cws_return worker_handle_client_data(int epfd, int client_fd, cws_config_s *config) {
string_s *data = string_new("", 4096);
/* Read data from socket */
ssize_t total_bytes = cws_socket_read(client_fd, data);
int total_bytes = cws_socket_read(client_fd, data);
if (total_bytes == 0) {
/* Partial request; wait for more data */
/* Partial request, wait for more data */
if (total_bytes == -2) {
string_free(data);
return CWS_OK;
}
if (total_bytes < 0) {
/* Connection closed */
if (total_bytes == 0) {
cws_log_info("Client (fd: %d) disconnected", client_fd);
worker_close_client(epfd, client_fd);
string_free(data);
return CWS_CLIENT_DISCONNECTED_ERROR;
}
/* Client error */
if (total_bytes == -1) {
worker_close_client(epfd, client_fd);
string_free(data);
return CWS_CLIENT_DISCONNECTED_ERROR;
}
/* Parse HTTP request */
cws_request_s *request = cws_http_parse(data);
cws_request_s *request = cws_request_parse(data);
string_free(data);
if (request == NULL) {
worker_close_client(epfd, client_fd);
@@ -67,8 +65,8 @@ static cws_return worker_handle_client_data(int epfd, int client_fd, cws_config_
}
/* Configure handler */
char *host = cws_http_get_host(request);
cws_vhost_s *vh = get_vhost(config, host);
char *host = cws_request_get_header(request, "host");
cws_vhost_s *vh = config_get_vhost(config, host);
cws_handler_config_s conf = {
.domain = vh->domain,
.root = vh->root,
@@ -83,11 +81,14 @@ static cws_return worker_handle_client_data(int epfd, int client_fd, cws_config_
cws_response_free(response);
}
/* Cleanup */
cws_http_free(request);
/* Close connection if requested */
/* keep-alive by default (http/1.1) */
if (!strcmp(cws_request_get_header(request, "Connection"), "close")) {
worker_close_client(epfd, client_fd);
}
/* TODO: check Connection: keep-alive */
worker_close_client(epfd, client_fd);
/* Cleanup */
cws_request_free(request);
return CWS_OK;
}
@@ -138,6 +139,7 @@ cws_worker_s **cws_worker_new(size_t workers_num, cws_config_s *config) {
/* Create per-worker epoll instance */
if (worker_setup_epoll(workers[i]) != CWS_OK) {
for (size_t j = 0; j < i; ++j) {
close(workers[j]->epfd);
free(workers[j]);
}
free(workers);
@@ -161,6 +163,7 @@ void cws_worker_free(cws_worker_s **workers, size_t workers_num) {
for (size_t i = 0; i < workers_num; ++i) {
pthread_join(workers[i]->thread, NULL);
close(workers[i]->epfd);
free(workers[i]);
}
+22 -1
View File
@@ -1,4 +1,5 @@
#include "http/handler.h"
#include "config/config.h"
#include "utils/debug.h"
#include <myclib/mystring.h>
#include <string.h>
@@ -7,14 +8,26 @@
/* Sanitize and resolve file path */
static string_s *resolve_file_path(const char *url_path, cws_handler_config_s *config) {
string_s *full_path = string_new(config->root, 256);
if (!full_path) {
return NULL;
}
if (strcmp(url_path, "/") == 0) {
string_append(full_path, "/");
/* Use vhost index file */
/* @TODO: Use vhost index file */
string_append(full_path, "index.html");
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);
return full_path;
@@ -42,6 +55,8 @@ cws_response_s *cws_handler_static_file(cws_request_s *request, cws_handler_conf
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);
const char *path = string_cstr(filepath);
@@ -50,12 +65,18 @@ cws_response_s *cws_handler_static_file(cws_request_s *request, cws_handler_conf
return cws_handler_not_found();
}
/* Allocate a response object */
/* @TODO: do not use http 200 ok as default */
cws_response_s *response = cws_response_new(HTTP_OK);
if (!response) {
string_free(filepath);
return cws_response_error(HTTP_INTERNAL_ERROR, "Failed to create response");
}
/* Retrieve Connection header and set it in the response */
const char *conn = cws_request_get_header(request, "Connection");
cws_response_set_header(response, "Connection", conn);
cws_response_set_body_file(response, path);
cws_log_debug("Serving file: %s (%zu bytes)", path, response->content_length);
string_free(filepath);
+4 -3
View File
@@ -3,9 +3,10 @@
#include <stdio.h>
#include <string.h>
#include "http/request.h"
#include "utils/error.h"
#include "internal/common.h"
static mimetype mimetypes[] = {{"html", "text/html"}, {"css", "text/css"}, {"js", "application/javascript"},
{"jpg", "image/jpeg"}, {"png", "image/png"}, {"ico", "image/x-icon"}};
@@ -18,13 +19,13 @@ int cws_mime_get_ct(const char *location_path, char *content_type) {
for (size_t i = 0; i < ARR_SIZE(mimetypes); ++i) {
if (!strcmp(ptr, mimetypes[i].ext)) {
snprintf(content_type, CWS_HTTP_CONTENT_TYPE - 1, "%s", mimetypes[i].type);
snprintf(content_type, CONTENT_TYPE_MAX - 1, "%s", mimetypes[i].type);
return CWS_OK;
}
}
snprintf(content_type, CWS_HTTP_CONTENT_TYPE - 1, "%s", "Content-Type not supported");
snprintf(content_type, CONTENT_TYPE_MAX - 1, "%s", "Content-Type not supported");
return CWS_OK;
}
+21 -15
View File
@@ -9,6 +9,8 @@
#include "utils/debug.h"
#include "utils/hash.h"
#include "internal/common.h"
static cws_request_s *http_request_new(void) {
cws_request_s *request = malloc(sizeof(*request));
if (!request) {
@@ -91,7 +93,7 @@ static bool parse_version(cws_request_s *req, char **cursor) {
static bool parse_headers(cws_request_s *req, char **cursor) {
req->headers = hm_new(my_str_hash_fn, my_str_equal_fn, my_str_free_fn, my_str_free_fn,
sizeof(char) * CWS_HTTP_HEADER_MAX, sizeof(char) * CWS_HTTP_HEADER_CONTENT_MAX);
sizeof(char) * HEADER_KEY_MAX, sizeof(char) * HEADER_VALUE_MAX);
char *s = *cursor + strspn(*cursor, "\r\n");
while (*s != '\0' && *s != '\r') {
@@ -113,8 +115,8 @@ static bool parse_headers(cws_request_s *req, char **cursor) {
char *header_value = colon + 1;
header_value += strspn(header_value, " \t");
char hk[CWS_HTTP_HEADER_MAX];
char hv[CWS_HTTP_HEADER_CONTENT_MAX];
char hk[HEADER_KEY_MAX];
char hv[HEADER_VALUE_MAX];
strncpy(hk, header_key, sizeof(hk) - 1);
hk[sizeof(hk) - 1] = '\0';
@@ -133,7 +135,7 @@ static bool parse_headers(cws_request_s *req, char **cursor) {
return true;
}
cws_request_s *cws_http_parse(string_s *request_str) {
cws_request_s *cws_request_parse(string_s *request_str) {
if (!request_str || !string_cstr(request_str)) {
return NULL;
}
@@ -145,7 +147,7 @@ cws_request_s *cws_http_parse(string_s *request_str) {
char *str = string_copy(request_str);
if (!str) {
cws_http_free(request);
cws_request_free(request);
return NULL;
}
char *orig = str;
@@ -153,28 +155,28 @@ cws_request_s *cws_http_parse(string_s *request_str) {
/* Parse HTTP method */
if (!parse_method(request, &str)) {
free(orig);
cws_http_free(request);
cws_request_free(request);
return NULL;
}
/* Parse location (URL path) */
if (!parse_location(request, &str)) {
free(orig);
cws_http_free(request);
cws_request_free(request);
return NULL;
}
/* Parse HTTP version */
if (!parse_version(request, &str)) {
free(orig);
cws_http_free(request);
cws_request_free(request);
return NULL;
}
/* Parse headers */
if (!parse_headers(request, &str)) {
free(orig);
cws_http_free(request);
cws_request_free(request);
return NULL;
}
@@ -183,16 +185,20 @@ cws_request_s *cws_http_parse(string_s *request_str) {
return request;
}
char *cws_http_get_host(cws_request_s *request) {
bucket_s *host = hm_get(request->headers, "Host");
if (!host) {
return "default";
char *cws_request_get_header(cws_request_s *request, const char *header) {
if (!request || !header || !request->headers) {
return "";
}
return (char *)host->value;
bucket_s *bucket = hm_get(request->headers, (void *)header);
if (!bucket) {
return "";
}
return (char *)bucket->value;
}
void cws_http_free(cws_request_s *request) {
void cws_request_free(cws_request_s *request) {
if (!request) {
return;
}
+33 -14
View File
@@ -3,18 +3,38 @@
#include "utils/debug.h"
#include "utils/hash.h"
#include <core/socket.h>
#include <myclib/myhashmap.h>
#include <myclib/mystring.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#define CHUNK_SIZE 8192
#define HEADERS_BUFFER_SIZE 2048
#include "internal/common.h"
static hashmap_s *response_headers_new(void) {
return hm_new(my_str_hash_fn, my_str_equal_fn, my_str_free_fn, my_str_free_fn, sizeof(char) * 256,
sizeof(char) * 512);
return hm_new(my_str_hash_fn, my_str_equal_fn, my_str_free_fn, my_str_free_fn, sizeof(char) * HEADER_KEY_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) {
@@ -30,9 +50,6 @@ cws_response_s *cws_response_new(cws_http_status_e status) {
resp->body_file = NULL;
resp->content_length = 0;
/* TODO: get the value from connection */
cws_response_set_header(resp, "Connection", "close");
return resp;
}
@@ -61,7 +78,7 @@ void cws_response_set_header(cws_response_s *response, const char *key, const ch
return;
}
char k[256], v[512];
char k[HEADER_KEY_MAX], v[HEADER_VALUE_MAX];
strncpy(k, key, sizeof(k) - 1);
k[sizeof(k) - 1] = '\0';
strncpy(v, value, sizeof(v) - 1);
@@ -101,7 +118,7 @@ void cws_response_set_body_file(cws_response_s *response, const char *filepath)
return;
}
char content_type[64];
char content_type[CONTENT_TYPE_MAX];
cws_mime_get_ct(filepath, content_type);
cws_response_set_header(response, "Content-Type", content_type);
@@ -141,16 +158,18 @@ int cws_response_send(int sockfd, cws_response_s *response) {
}
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));
char content_length_str[32];
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,
"Content-Length: %zu\r\n"
"Connection: close\r\n",
response->content_length);
/* @TODO: I can do this in the response_get_header() but I need to check
* if I have space left for \r\n
*/
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");
+10
View File
@@ -0,0 +1,10 @@
#ifndef CWS_INTERNALS_COMMON_H
#define CWS_INTERNALS_COMMON_H
#define CONTENT_TYPE_MAX 64
#define HEADER_KEY_MAX 256
#define HEADER_VALUE_MAX 1024
#define CHUNK_SIZE 8192
#define HEADERS_BUFFER_SIZE 2048
#endif
+7 -5
View File
@@ -16,23 +16,23 @@ int main(void) {
cws_log_init();
if (signal(SIGINT, cws_signal_handler) == SIG_ERR) {
cws_log_error("signal()");
cws_log_error("%s", "signal()");
return EXIT_FAILURE;
}
if (signal(SIGTERM, cws_signal_handler) == SIG_ERR) {
cws_log_error("signal()");
cws_log_error("%s", "signal()");
return EXIT_FAILURE;
}
cws_config_s *config = cws_config_init();
if (!config) {
cws_log_error("Unable to parse config");
cws_log_error("%s", "Unable to parse config");
cws_log_shutdown();
return EXIT_FAILURE;
}
cws_server_s server;
cws_server_s server = {0};
cws_return ret;
ret = cws_server_setup(&server, config);
@@ -43,13 +43,15 @@ int main(void) {
return EXIT_FAILURE;
}
server.config = config;
cws_log_info("Running cws on http://%s:%s", config->host, config->port);
ret = cws_server_start(&server);
if (ret != CWS_OK) {
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_config_free(config);
cws_log_shutdown();
+107
View File
@@ -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;
}
+26
View File
@@ -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
+43
View File
@@ -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()])
+67
View File
@@ -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;
}
+46
View File
@@ -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;
}
+46
View File
@@ -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;
}
+46
View File
@@ -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;
}
+62
View File
@@ -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;
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

+107 -155
View File
@@ -4,199 +4,151 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>cws • home</title>
<link rel="icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;800&display=swap"
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400&family=DM+Sans:wght@300;400&display=swap"
rel="stylesheet"
/>
<style>
:root {
--bg: #f3f4f6;
--accent: #3b82f6;
--accent-soft: rgba(59, 130, 246, 0.08);
--text: #0f172a;
--muted: #6b7280;
}
* {
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background:
radial-gradient(
circle at top left,
#dbeafe 0,
transparent 50%
),
radial-gradient(
circle at bottom right,
#e5e7eb 0,
transparent 55%
),
var(--bg);
font-family:
"Montserrat",
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
color: var(--text);
font-size: 18px; /* increased base text size */
:root {
--bg: #0a0a0a;
--fg: #f0ede8;
--muted: #4a4a4a;
--line: #1e1e1e;
--accent: #e8e0d0;
}
.wrapper {
body {
background: var(--bg);
color: var(--fg);
font-family: "DM Sans", sans-serif;
font-weight: 300;
min-height: 100vh;
display: grid;
place-items: center;
padding: 40px 24px;
overflow: hidden;
}
/* subtle grid */
body::before {
content: "";
position: fixed;
inset: 0;
background-image:
linear-gradient(var(--line) 1px, transparent 1px),
linear-gradient(90deg, var(--line) 1px, transparent 1px);
background-size: 60px 60px;
pointer-events: none;
opacity: 0.5;
}
.container {
position: relative;
max-width: 600px;
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
padding: 24px;
width: 100%;
text-align: left;
opacity: 0;
transform: translateY(16px);
animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
}
main {
max-width: 560px;
width: 100%;
@keyframes rise {
to {
opacity: 1;
transform: translateY(0);
}
}
.badge {
.status {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 7px 14px;
border-radius: 999px;
background: var(--accent-soft);
color: var(--accent);
font-size: 13px; /* slightly larger */
gap: 8px;
font-family: "DM Mono", monospace;
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
letter-spacing: 0.11em;
margin-bottom: 20px;
font-weight: 600;
color: #888;
margin-bottom: 40px;
}
.badge-dot {
width: 9px;
height: 9px;
border-radius: 999px;
background: var(--accent);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.22);
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #4ade80;
box-shadow: 0 0 8px #4ade8099;
animation: pulse 2.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
h1 {
font-size: 36px; /* increased */
line-height: 1.2;
margin-bottom: 8px;
font-size: clamp(42px, 8vw, 64px);
font-weight: 300;
line-height: 1.05;
letter-spacing: -0.03em;
margin-bottom: 28px;
color: var(--fg);
white-space: nowrap;
}
.highlight {
background: linear-gradient(115deg, #38bdf8, #3b82f6, #818cf8);
-webkit-background-clip: text;
color: transparent;
h1 em {
font-style: normal;
font-family: "DM Mono", monospace;
font-weight: 300;
color: var(--muted);
}
.divider {
width: 32px;
height: 1px;
background: var(--muted);
margin-bottom: 28px;
opacity: 0;
animation: rise 0.5s ease 0.5s forwards;
}
p {
font-size: 18px; /* increased */
color: var(--muted);
margin-bottom: 16px;
line-height: 1.7;
}
.lead {
font-size: 19px; /* increased */
margin-bottom: 10px;
}
.bg-orbit {
position: absolute;
width: 240px;
height: 240px;
border-radius: 50%;
border: 1px dashed rgba(148, 163, 184, 0.5);
top: -100px;
right: -60px;
opacity: 0.7;
pointer-events: none;
}
.bg-orbit-dot {
position: absolute;
width: 9px;
height: 9px;
border-radius: 50%;
background: #38bdf8;
box-shadow: 0 0 12px rgba(56, 189, 248, 0.9);
top: 32%;
left: 12%;
}
footer.icon-credit {
font-size: 13px; /* increased */
color: var(--muted);
text-align: center;
opacity: 0.9;
}
footer.icon-credit a {
color: var(--muted);
font-weight: 500;
text-decoration: none;
}
footer.icon-credit a:hover {
color: var(--accent);
text-decoration: underline;
}
@media (max-width: 480px) {
h1 {
font-size: 30px;
}
.lead {
font-size: 17px;
}
font-size: 15px;
line-height: 1.8;
color: #999;
}
</style>
</head>
<body>
<div class="bg-orbit">
<div class="bg-orbit-dot"></div>
</div>
<div class="container">
<div class="status">
<span class="dot"></span>
running
</div>
<div class="wrapper">
<main>
<div class="badge">
<span class="badge-dot"></span>
<span>cws is running</span>
</div>
<h1>Hello from <em>cws</em></h1>
<h1>Hello from <span class="highlight">cws</span>!</h1>
<p>
Youre seeing the default homepage served by
<strong>cws</strong>. Replace this file or point your
virtual host to a different root directory to serve your own
content.
</p>
</main>
<div class="divider"></div>
<footer class="icon-credit">
<a target="_blank" href="https://icons8.com/icon/38796/linux"
>Linux</a
>
icon by
<a target="_blank" href="https://icons8.com">Icons8</a>
</footer>
<p>
Default homepage. Replace this file or point your virtual host
to a different root to serve your own content.
</p>
</div>
</body>
</html>
+89 -107
View File
@@ -2,146 +2,128 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>404 • Page not found</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>404 • Not Found</title>
<link
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400&family=DM+Sans:wght@300;400&display=swap"
rel="stylesheet"
/>
<style>
:root {
--bg: #f9fafb;
--accent: #f97316;
--accent-soft: rgba(249, 115, 22, 0.14);
--text: #0f172a;
--muted: #6b7280;
}
* {
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background:
radial-gradient(circle at top, #ffedd5 0, transparent 55%),
var(--bg);
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
color: var(--text);
padding: 16px;
:root {
--bg: #0a0a0a;
--fg: #f0ede8;
--muted: #4a4a4a;
--line: #1e1e1e;
}
.wrap {
max-width: 520px;
body {
background: var(--bg);
color: var(--fg);
font-family: "DM Sans", sans-serif;
font-weight: 300;
min-height: 100vh;
display: grid;
place-items: center;
padding: 40px 24px;
overflow: hidden;
}
body::before {
content: "";
position: fixed;
inset: 0;
background-image:
linear-gradient(var(--line) 1px, transparent 1px),
linear-gradient(90deg, var(--line) 1px, transparent 1px);
background-size: 60px 60px;
pointer-events: none;
opacity: 0.5;
}
.container {
position: relative;
max-width: 600px;
width: 100%;
text-align: center;
}
.badge {
display: inline-flex;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: var(--accent-soft);
color: var(--accent);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.12em;
margin-bottom: 20px;
opacity: 0;
transform: translateY(16px);
animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
}
.badge-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 4px rgba(249, 115, 22, 0.35);
@keyframes rise {
to {
opacity: 1;
transform: translateY(0);
}
}
.code {
font-size: 60px;
font-weight: 900;
letter-spacing: 0.18em;
margin-bottom: 10px;
text-transform: uppercase;
background: linear-gradient(120deg, #fbbf24, #f97316, #ec4899);
-webkit-background-clip: text;
color: transparent;
font-family: "DM Mono", monospace;
font-size: clamp(80px, 18vw, 140px);
font-weight: 300;
letter-spacing: -0.04em;
line-height: 1;
color: #1e1e1e;
margin-bottom: 32px;
user-select: none;
}
h1 {
font-size: 28px;
margin-bottom: 10px;
font-size: clamp(22px, 5vw, 32px);
font-weight: 300;
letter-spacing: -0.02em;
color: var(--fg);
white-space: nowrap;
margin-bottom: 16px;
}
.divider {
width: 32px;
height: 1px;
background: var(--muted);
margin-bottom: 16px;
}
p {
font-size: 15px;
color: var(--muted);
margin-bottom: 24px;
line-height: 1.6;
line-height: 1.8;
color: #999;
}
.actions {
display: flex;
justify-content: center;
}
.btn-primary {
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 999px;
padding: 10px 18px;
font-size: 13px;
font-weight: 600;
.back {
margin-top: 40px;
font-family: "DM Mono", monospace;
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #555;
text-decoration: none;
border: none;
cursor: pointer;
background: linear-gradient(135deg, #f97316, #ec4899);
color: #fff7ed;
box-shadow: 0 10px 22px rgba(236, 72, 153, 0.3);
transition:
transform 120ms ease,
box-shadow 120ms ease,
filter 120ms ease;
transition: color 0.2s;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 14px 30px rgba(236, 72, 153, 0.4);
filter: brightness(1.03);
}
@media (max-width: 480px) {
.code {
font-size: 50px;
}
h1 {
font-size: 24px;
}
p {
font-size: 14px;
}
.back:hover {
color: var(--fg);
}
</style>
</head>
<body>
<div class="wrap">
<div class="badge">
<span class="badge-dot"></span>
<span>Page not found</span>
</div>
<div class="container">
<div class="code">404</div>
<h1>Youve reached an empty corner of the internet.</h1>
<p>The page youre looking for doesnt exist.</p>
<h1>Page not found</h1>
<div class="divider"></div>
<p>The page you're looking for doesn't exist.</p>
<a class="back" href="/">← back to home</a>
</div>
</body>
</html>
-116
View File
@@ -1,116 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>500 • Internal server error</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
:root {
--bg: #f9fafb;
--accent: #38bdf8;
--accent-soft: rgba(56, 189, 248, 0.14);
--text: #0f172a;
--muted: #6b7280;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background:
radial-gradient(circle at top, #ffedd5 0, transparent 55%),
var(--bg);
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
color: var(--text);
padding: 16px;
}
.wrap {
max-width: 520px;
width: 100%;
text-align: center;
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: var(--accent-soft);
color: var(--accent);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.12em;
margin-bottom: 20px;
}
.badge-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.35);
}
.code {
font-size: 60px;
font-weight: 900;
letter-spacing: 0.18em;
margin-bottom: 10px;
text-transform: uppercase;
background: linear-gradient(120deg, #bae6fd, #38bdf8, #818cf8);
-webkit-background-clip: text;
color: transparent;
}
h1 {
font-size: 28px;
margin-bottom: 10px;
}
p {
font-size: 15px;
color: var(--muted);
margin-bottom: 24px;
line-height: 1.6;
}
@media (max-width: 480px) {
.code {
font-size: 50px;
}
h1 {
font-size: 24px;
}
p {
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="wrap">
<div class="badge">
<span class="badge-dot"></span>
<span>Internal server error</span>
</div>
<div class="code">500</div>
<h1>Something went wrong on our side.</h1>
<p>Try again in a moment or head back home while we fix things.</p>
</div>
</body>
</html>