@velox0/cerver 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,344 @@
1
+ /*
2
+ * server.c — Socket setup and event loop for the cerver runtime.
3
+ *
4
+ * Uses kqueue on macOS, epoll on Linux, with a select() fallback.
5
+ */
6
+
7
+ #include "cerver.h"
8
+
9
+ #include <stdio.h>
10
+ #include <stdlib.h>
11
+ #include <string.h>
12
+ #include <unistd.h>
13
+ #include <errno.h>
14
+ #include <signal.h>
15
+ #include <fcntl.h>
16
+ #include <sys/socket.h>
17
+ #include <sys/types.h>
18
+ #include <netinet/in.h>
19
+ #include <arpa/inet.h>
20
+
21
+ /* Platform-specific event API */
22
+ #if defined(__APPLE__) || defined(__FreeBSD__)
23
+ #define CERVER_USE_KQUEUE 1
24
+ #include <sys/event.h>
25
+ #elif defined(__linux__)
26
+ #define CERVER_USE_EPOLL 1
27
+ #include <sys/epoll.h>
28
+ #else
29
+ #define CERVER_USE_SELECT 1
30
+ #include <sys/select.h>
31
+ #endif
32
+
33
+ #define MAX_EVENTS 64
34
+
35
+ /* Global server pointer for signal handler */
36
+ static cerver_server_t *g_srv = NULL;
37
+
38
+ static void signal_handler(int sig) {
39
+ (void)sig;
40
+ if (g_srv) {
41
+ g_srv->running = 0;
42
+ }
43
+ }
44
+
45
+ static int set_nonblocking(int fd) {
46
+ int flags = fcntl(fd, F_GETFL, 0);
47
+ if (flags < 0) return -1;
48
+ return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
49
+ }
50
+
51
+ /* ------------------------------------------------------------------ */
52
+ /* Handle a single connection: read, parse, dispatch, respond */
53
+ /* ------------------------------------------------------------------ */
54
+
55
+ static int set_blocking(int fd) {
56
+ int flags = fcntl(fd, F_GETFL, 0);
57
+ if (flags < 0) return -1;
58
+ return fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
59
+ }
60
+
61
+ static void handle_connection(cerver_server_t *srv, int client_fd) {
62
+ /* Client sockets may inherit non-blocking mode from listener.
63
+ Set to blocking so read() waits for data. */
64
+ set_blocking(client_fd);
65
+
66
+ char buf[CERVER_READ_BUF];
67
+ ssize_t n = read(client_fd, buf, sizeof(buf) - 1);
68
+
69
+ if (n <= 0) {
70
+ close(client_fd);
71
+ return;
72
+ }
73
+ buf[n] = '\0';
74
+
75
+ /* Parse HTTP request */
76
+ cerver_request_t req;
77
+ memset(&req, 0, sizeof(req));
78
+
79
+ if (cerver_parse_request(buf, (size_t)n, &req) < 0) {
80
+ /* Bad request — send 400 */
81
+ const char *resp = "HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\nConnection: close\r\n\r\nBad Request";
82
+ write(client_fd, resp, strlen(resp));
83
+ close(client_fd);
84
+ return;
85
+ }
86
+
87
+ /* Prepare response */
88
+ cerver_response_t res;
89
+ memset(&res, 0, sizeof(res));
90
+
91
+ /* Try to dispatch to a route handler */
92
+ cerver_handler_fn handler = cerver_dispatch(srv, &req);
93
+
94
+ if (handler) {
95
+ handler(&req, &res);
96
+ } else {
97
+ /* Try static assets */
98
+ if (cerver_serve_static(srv, &req, &res) < 0) {
99
+ /* 404 */
100
+ cerver_res_text(&res, 404, "Not Found");
101
+ }
102
+ }
103
+
104
+ /* Write response */
105
+ cerver_write_response(client_fd, &res);
106
+
107
+ /* Cleanup */
108
+ if (res._body_owned && res.body) {
109
+ free((void *)res.body);
110
+ }
111
+ if (req._raw_buf) {
112
+ free(req._raw_buf);
113
+ }
114
+
115
+ close(client_fd);
116
+ }
117
+
118
+ /* ------------------------------------------------------------------ */
119
+ /* Server init */
120
+ /* ------------------------------------------------------------------ */
121
+
122
+ int cerver_init(cerver_server_t *srv, int port) {
123
+ memset(srv, 0, sizeof(*srv));
124
+ srv->port = port;
125
+ srv->sock_fd = -1;
126
+ srv->running = 0;
127
+ srv->public_dir = NULL;
128
+ return 0;
129
+ }
130
+
131
+ int cerver_add_routes(cerver_server_t *srv, cerver_route_t *routes, int count) {
132
+ srv->routes = routes;
133
+ srv->route_count = count;
134
+ return 0;
135
+ }
136
+
137
+ int cerver_set_assets(cerver_server_t *srv, cerver_asset_t *assets, int count) {
138
+ srv->assets = assets;
139
+ srv->asset_count = count;
140
+ return 0;
141
+ }
142
+
143
+ void cerver_set_public_dir(cerver_server_t *srv, const char *dir) {
144
+ srv->public_dir = dir;
145
+ }
146
+
147
+ /* ------------------------------------------------------------------ */
148
+ /* Server listen — event loop */
149
+ /* ------------------------------------------------------------------ */
150
+
151
+ int cerver_listen(cerver_server_t *srv) {
152
+ /* Create socket */
153
+ srv->sock_fd = socket(AF_INET, SOCK_STREAM, 0);
154
+ if (srv->sock_fd < 0) {
155
+ perror("cerver: socket");
156
+ return -1;
157
+ }
158
+
159
+ /* SO_REUSEADDR so we can restart quickly */
160
+ int opt = 1;
161
+ setsockopt(srv->sock_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
162
+
163
+ /* Bind */
164
+ struct sockaddr_in addr;
165
+ memset(&addr, 0, sizeof(addr));
166
+ addr.sin_family = AF_INET;
167
+ addr.sin_addr.s_addr = INADDR_ANY;
168
+ addr.sin_port = htons((uint16_t)srv->port);
169
+
170
+ if (bind(srv->sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
171
+ perror("cerver: bind");
172
+ close(srv->sock_fd);
173
+ return -1;
174
+ }
175
+
176
+ /* Listen */
177
+ if (listen(srv->sock_fd, 128) < 0) {
178
+ perror("cerver: listen");
179
+ close(srv->sock_fd);
180
+ return -1;
181
+ }
182
+
183
+ set_nonblocking(srv->sock_fd);
184
+
185
+ /* Install signal handlers */
186
+ g_srv = srv;
187
+ signal(SIGINT, signal_handler);
188
+ signal(SIGTERM, signal_handler);
189
+ signal(SIGPIPE, SIG_IGN);
190
+
191
+ srv->running = 1;
192
+
193
+ printf("cerver: listening on http://0.0.0.0:%d\n", srv->port);
194
+
195
+ /* ================================================================== */
196
+ /* kqueue event loop (macOS / FreeBSD) */
197
+ /* ================================================================== */
198
+ #if CERVER_USE_KQUEUE
199
+
200
+ int kq = kqueue();
201
+ if (kq < 0) {
202
+ perror("cerver: kqueue");
203
+ close(srv->sock_fd);
204
+ return -1;
205
+ }
206
+
207
+ struct kevent change;
208
+ EV_SET(&change, srv->sock_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
209
+ kevent(kq, &change, 1, NULL, 0, NULL);
210
+
211
+ struct kevent events[MAX_EVENTS];
212
+
213
+ while (srv->running) {
214
+ struct timespec timeout = { 1, 0 }; /* 1 second */
215
+ int nev = kevent(kq, NULL, 0, events, MAX_EVENTS, &timeout);
216
+
217
+ if (nev < 0) {
218
+ if (errno == EINTR) continue;
219
+ perror("cerver: kevent");
220
+ break;
221
+ }
222
+
223
+ for (int i = 0; i < nev; i++) {
224
+ int fd = (int)events[i].ident;
225
+
226
+ if (fd == srv->sock_fd) {
227
+ /* Accept new connections */
228
+ while (1) {
229
+ struct sockaddr_in client_addr;
230
+ socklen_t client_len = sizeof(client_addr);
231
+ int client_fd = accept(srv->sock_fd,
232
+ (struct sockaddr *)&client_addr,
233
+ &client_len);
234
+ if (client_fd < 0) {
235
+ if (errno == EAGAIN || errno == EWOULDBLOCK) break;
236
+ perror("cerver: accept");
237
+ break;
238
+ }
239
+ handle_connection(srv, client_fd);
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ close(kq);
246
+
247
+ /* ================================================================== */
248
+ /* epoll event loop (Linux) */
249
+ /* ================================================================== */
250
+ #elif CERVER_USE_EPOLL
251
+
252
+ int ep = epoll_create1(0);
253
+ if (ep < 0) {
254
+ perror("cerver: epoll_create1");
255
+ close(srv->sock_fd);
256
+ return -1;
257
+ }
258
+
259
+ struct epoll_event ev;
260
+ ev.events = EPOLLIN;
261
+ ev.data.fd = srv->sock_fd;
262
+ epoll_ctl(ep, EPOLL_CTL_ADD, srv->sock_fd, &ev);
263
+
264
+ struct epoll_event events[MAX_EVENTS];
265
+
266
+ while (srv->running) {
267
+ int nev = epoll_wait(ep, events, MAX_EVENTS, 1000);
268
+
269
+ if (nev < 0) {
270
+ if (errno == EINTR) continue;
271
+ perror("cerver: epoll_wait");
272
+ break;
273
+ }
274
+
275
+ for (int i = 0; i < nev; i++) {
276
+ if (events[i].data.fd == srv->sock_fd) {
277
+ while (1) {
278
+ struct sockaddr_in client_addr;
279
+ socklen_t client_len = sizeof(client_addr);
280
+ int client_fd = accept(srv->sock_fd,
281
+ (struct sockaddr *)&client_addr,
282
+ &client_len);
283
+ if (client_fd < 0) {
284
+ if (errno == EAGAIN || errno == EWOULDBLOCK) break;
285
+ perror("cerver: accept");
286
+ break;
287
+ }
288
+ handle_connection(srv, client_fd);
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ close(ep);
295
+
296
+ /* ================================================================== */
297
+ /* select() fallback */
298
+ /* ================================================================== */
299
+ #elif CERVER_USE_SELECT
300
+
301
+ while (srv->running) {
302
+ fd_set readfds;
303
+ FD_ZERO(&readfds);
304
+ FD_SET(srv->sock_fd, &readfds);
305
+
306
+ struct timeval timeout = { 1, 0 };
307
+ int ret = select(srv->sock_fd + 1, &readfds, NULL, NULL, &timeout);
308
+
309
+ if (ret < 0) {
310
+ if (errno == EINTR) continue;
311
+ perror("cerver: select");
312
+ break;
313
+ }
314
+
315
+ if (ret > 0 && FD_ISSET(srv->sock_fd, &readfds)) {
316
+ struct sockaddr_in client_addr;
317
+ socklen_t client_len = sizeof(client_addr);
318
+ int client_fd = accept(srv->sock_fd,
319
+ (struct sockaddr *)&client_addr,
320
+ &client_len);
321
+ if (client_fd >= 0) {
322
+ handle_connection(srv, client_fd);
323
+ }
324
+ }
325
+ }
326
+
327
+ #endif
328
+
329
+ cerver_shutdown(srv);
330
+ return 0;
331
+ }
332
+
333
+ /* ------------------------------------------------------------------ */
334
+ /* Shutdown */
335
+ /* ------------------------------------------------------------------ */
336
+
337
+ void cerver_shutdown(cerver_server_t *srv) {
338
+ if (srv->sock_fd >= 0) {
339
+ close(srv->sock_fd);
340
+ srv->sock_fd = -1;
341
+ }
342
+ srv->running = 0;
343
+ printf("\ncerver: server stopped\n");
344
+ }
@@ -0,0 +1,145 @@
1
+ /*
2
+ * static.c — Static file serving for the cerver runtime.
3
+ *
4
+ * In embedded mode, serves from the compiled-in asset array.
5
+ * In external mode, serves from the filesystem (public/ directory).
6
+ */
7
+
8
+ #include "cerver.h"
9
+
10
+ #include <stdio.h>
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+ #include <sys/stat.h>
14
+
15
+ /* ------------------------------------------------------------------ */
16
+ /* Path safety: prevent directory traversal */
17
+ /* ------------------------------------------------------------------ */
18
+
19
+ static int path_is_safe(const char *path) {
20
+ /* Reject paths with ".." */
21
+ if (strstr(path, "..")) return 0;
22
+
23
+ /* Reject paths with null bytes */
24
+ if (memchr(path, '\0', strlen(path))) return 0;
25
+
26
+ /* Must start with "/" */
27
+ if (path[0] != '/') return 0;
28
+
29
+ return 1;
30
+ }
31
+
32
+ /* ------------------------------------------------------------------ */
33
+ /* Serve from embedded assets */
34
+ /* ------------------------------------------------------------------ */
35
+
36
+ static int serve_embedded(cerver_server_t *srv, cerver_request_t *req,
37
+ cerver_response_t *res) {
38
+ if (!srv->assets || srv->asset_count == 0) return -1;
39
+
40
+ const char *path = req->path;
41
+
42
+ /* Try exact match first */
43
+ for (int i = 0; i < srv->asset_count; i++) {
44
+ if (strcmp(srv->assets[i].path, path) == 0) {
45
+ cerver_res_file(res, 200, srv->assets[i].mime_type,
46
+ srv->assets[i].data, srv->assets[i].data_len);
47
+ return 0;
48
+ }
49
+ }
50
+
51
+ /* Try with /index.html appended (for directory-like paths) */
52
+ char index_path[CERVER_MAX_PATH];
53
+ if (path[strlen(path) - 1] == '/') {
54
+ snprintf(index_path, sizeof(index_path), "%sindex.html", path);
55
+ } else {
56
+ snprintf(index_path, sizeof(index_path), "%s/index.html", path);
57
+ }
58
+
59
+ for (int i = 0; i < srv->asset_count; i++) {
60
+ if (strcmp(srv->assets[i].path, index_path) == 0) {
61
+ cerver_res_file(res, 200, srv->assets[i].mime_type,
62
+ srv->assets[i].data, srv->assets[i].data_len);
63
+ return 0;
64
+ }
65
+ }
66
+
67
+ return -1;
68
+ }
69
+
70
+ /* ------------------------------------------------------------------ */
71
+ /* Serve from filesystem */
72
+ /* ------------------------------------------------------------------ */
73
+
74
+ static int serve_filesystem(cerver_server_t *srv, cerver_request_t *req,
75
+ cerver_response_t *res) {
76
+ if (!srv->public_dir) return -1;
77
+
78
+ const char *path = req->path;
79
+ if (!path_is_safe(path)) return -1;
80
+
81
+ /* Build the full filesystem path */
82
+ char full_path[CERVER_MAX_PATH * 2];
83
+ snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
84
+
85
+ /* Check if it's a directory — try index.html */
86
+ struct stat st;
87
+ if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
88
+ snprintf(full_path, sizeof(full_path), "%s%s/index.html",
89
+ srv->public_dir, path);
90
+ if (stat(full_path, &st) != 0) return -1;
91
+ }
92
+
93
+ /* Must be a regular file */
94
+ if (stat(full_path, &st) != 0 || !S_ISREG(st.st_mode)) {
95
+ return -1;
96
+ }
97
+
98
+ /* Read the file */
99
+ FILE *fp = fopen(full_path, "rb");
100
+ if (!fp) return -1;
101
+
102
+ size_t file_size = (size_t)st.st_size;
103
+ char *file_data = malloc(file_size);
104
+ if (!file_data) {
105
+ fclose(fp);
106
+ return -1;
107
+ }
108
+
109
+ size_t bytes_read = fread(file_data, 1, file_size, fp);
110
+ fclose(fp);
111
+
112
+ if (bytes_read != file_size) {
113
+ free(file_data);
114
+ return -1;
115
+ }
116
+
117
+ /* Determine MIME type */
118
+ const char *mime = cerver_mime_from_path(full_path);
119
+
120
+ res->status = 200;
121
+ res->content_type = mime;
122
+ res->body = file_data;
123
+ res->body_len = file_size;
124
+ res->_body_owned = 1; /* We malloc'd this */
125
+
126
+ return 0;
127
+ }
128
+
129
+ /* ------------------------------------------------------------------ */
130
+ /* Main static serving entry point */
131
+ /* ------------------------------------------------------------------ */
132
+
133
+ int cerver_serve_static(cerver_server_t *srv, cerver_request_t *req,
134
+ cerver_response_t *res) {
135
+ /* Only serve GET requests for static files */
136
+ if (strcmp(req->method, "GET") != 0) return -1;
137
+
138
+ /* Try embedded assets first */
139
+ if (serve_embedded(srv, req, res) == 0) return 0;
140
+
141
+ /* Fall back to filesystem */
142
+ if (serve_filesystem(srv, req, res) == 0) return 0;
143
+
144
+ return -1;
145
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ port: 8080,
3
+ embed: true,
4
+ minify: true,
5
+ compression: "none"
6
+ }
@@ -0,0 +1,3 @@
1
+ export function GET(req, res) {
2
+ return res.text(200, "Hello from cerver!");
3
+ }