Zero-copy FTP/HTTP Daemon compatible with all POSIX systems
| 1 | /* |
| 2 | MIT License |
| 3 | |
| 4 | Copyright (c) 2026 Seregon |
| 5 | |
| 6 | Permission is hereby granted, free of charge, to any person obtaining a copy |
| 7 | of this software and associated documentation files (the "Software"), to deal |
| 8 | in the Software without restriction, including without limitation the rights |
| 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 10 | copies of the Software, and to permit persons to whom the Software is |
| 11 | furnished to do so, subject to the following conditions: |
| 12 | |
| 13 | The above copyright notice and this permission notice shall be included in all |
| 14 | copies or substantial portions of the Software. |
| 15 | |
| 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 22 | SOFTWARE. |
| 23 | */ |
| 24 | /** |
| 25 | * @file http_response.c |
| 26 | * @brief HTTP response builder implementation |
| 27 | */ |
| 28 | |
| 29 | #include "http_response.h" |
| 30 | #include "http_config.h" |
| 31 | #include <dirent.h> |
| 32 | #include <stdio.h> |
| 33 | #include <stdlib.h> |
| 34 | #include <string.h> |
| 35 | #include <unistd.h> |
| 36 | |
| 37 | static http_response_t g_response_pool[HTTP_MAX_CONNECTIONS]; |
| 38 | static unsigned char g_response_in_use[HTTP_MAX_CONNECTIONS]; |
| 39 | |
| 40 | /*===========================================================================* |
| 41 | * STATUS CODE → TEXT MAPPING |
| 42 | *===========================================================================*/ |
| 43 | |
| 44 | static const char *status_text(http_status_t status) { |
| 45 | switch (status) { |
| 46 | /* 2xx */ |
| 47 | case HTTP_STATUS_200_OK: |
| 48 | return "OK"; |
| 49 | case HTTP_STATUS_201_CREATED: |
| 50 | return "Created"; |
| 51 | case HTTP_STATUS_204_NO_CONTENT: |
| 52 | return "No Content"; |
| 53 | /* 3xx */ |
| 54 | case HTTP_STATUS_301_MOVED: |
| 55 | return "Moved Permanently"; |
| 56 | case HTTP_STATUS_304_NOT_MODIFIED: |
| 57 | return "Not Modified"; |
| 58 | /* 4xx */ |
| 59 | case HTTP_STATUS_400_BAD_REQUEST: |
| 60 | return "Bad Request"; |
| 61 | case HTTP_STATUS_403_FORBIDDEN: |
| 62 | return "Forbidden"; |
| 63 | case HTTP_STATUS_404_NOT_FOUND: |
| 64 | return "Not Found"; |
| 65 | case HTTP_STATUS_405_METHOD_NOT_ALLOWED: |
| 66 | return "Method Not Allowed"; |
| 67 | case HTTP_STATUS_409_CONFLICT: |
| 68 | return "Conflict"; |
| 69 | /* 5xx */ |
| 70 | case HTTP_STATUS_500_INTERNAL_ERROR: |
| 71 | return "Internal Server Error"; |
| 72 | default: |
| 73 | return "Unknown"; |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | /*===========================================================================* |
| 78 | * CREATE / DESTROY |
| 79 | *===========================================================================*/ |
| 80 | |
| 81 | http_response_t *http_response_create(http_status_t status) { |
| 82 | http_response_t *resp = NULL; |
| 83 | for (size_t i = 0; i < (size_t)HTTP_MAX_CONNECTIONS; i++) { |
| 84 | if (g_response_in_use[i] == 0U) { |
| 85 | g_response_in_use[i] = 1U; |
| 86 | resp = &g_response_pool[i]; |
| 87 | break; |
| 88 | } |
| 89 | } |
| 90 | if (resp == NULL) { |
| 91 | return NULL; |
| 92 | } |
| 93 | |
| 94 | memset(resp, 0, sizeof(*resp)); |
| 95 | resp->sendfile_fd = -1; |
| 96 | resp->stream_dir = NULL; |
| 97 | resp->mem_body = NULL; |
| 98 | resp->mem_length = 0U; |
| 99 | resp->mem_sent = 0U; |
| 100 | resp->mem_seg_count = 0U; |
| 101 | resp->mem_seg_index = 0U; |
| 102 | resp->mem_seg_sent = 0U; |
| 103 | |
| 104 | resp->used = (size_t)snprintf(resp->data, sizeof(resp->data), |
| 105 | "HTTP/1.1 %d %s\r\n", (int)status, |
| 106 | status_text(status)); |
| 107 | if (resp->used >= sizeof(resp->data)) { |
| 108 | http_response_destroy(resp); |
| 109 | return NULL; |
| 110 | } |
| 111 | |
| 112 | if (http_response_add_header(resp, "X-Content-Type-Options", "nosniff") != |
| 113 | 0 || |
| 114 | http_response_add_header(resp, "X-Frame-Options", "DENY") != 0 || |
| 115 | http_response_add_header(resp, "Referrer-Policy", "no-referrer") != 0 || |
| 116 | http_response_add_header(resp, "Cache-Control", "no-store") != 0 || |
| 117 | http_response_add_header(resp, "Content-Security-Policy", |
| 118 | "default-src 'self'; " |
| 119 | "connect-src *; " |
| 120 | "img-src 'self' data: blob:; " |
| 121 | "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " |
| 122 | "style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com; " |
| 123 | "font-src 'self' data: https://fonts.gstatic.com; " |
| 124 | "script-src 'self' 'unsafe-inline' blob:; " |
| 125 | "script-src-elem 'self' 'unsafe-inline' blob:; " |
| 126 | "object-src 'none'; base-uri 'none'; frame-ancestors 'none'") != |
| 127 | 0) { |
| 128 | http_response_destroy(resp); |
| 129 | return NULL; |
| 130 | } |
| 131 | |
| 132 | return resp; |
| 133 | } |
| 134 | |
| 135 | void http_response_destroy(http_response_t *resp) { |
| 136 | if (resp == NULL) { |
| 137 | return; |
| 138 | } |
| 139 | |
| 140 | if (resp->sendfile_fd >= 0) { |
| 141 | close(resp->sendfile_fd); |
| 142 | resp->sendfile_fd = -1; |
| 143 | } |
| 144 | if (resp->stream_dir != NULL) { |
| 145 | closedir((DIR *)resp->stream_dir); |
| 146 | resp->stream_dir = NULL; |
| 147 | } |
| 148 | if (resp->mem_body_owned && resp->mem_body != NULL) { |
| 149 | void *tmp; |
| 150 | memcpy(&tmp, &resp->mem_body, sizeof(tmp)); |
| 151 | free(tmp); |
| 152 | resp->mem_body = NULL; |
| 153 | resp->mem_body_owned = 0; |
| 154 | } |
| 155 | |
| 156 | if ((resp >= &g_response_pool[0]) && |
| 157 | (resp < &g_response_pool[HTTP_MAX_CONNECTIONS])) { |
| 158 | size_t idx = (size_t)(resp - &g_response_pool[0]); |
| 159 | g_response_in_use[idx] = 0U; |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | /*===========================================================================* |
| 164 | * HEADERS |
| 165 | *===========================================================================*/ |
| 166 | |
| 167 | /** |
| 168 | * Grow the response buffer so that `extra` more bytes fit. |
| 169 | */ |
| 170 | static int ensure_space(http_response_t *resp, size_t extra) { |
| 171 | if (resp == NULL) { |
| 172 | return -1; |
| 173 | } |
| 174 | if (extra > (sizeof(resp->data) - resp->used)) { |
| 175 | return -1; |
| 176 | } |
| 177 | return 0; |
| 178 | } |
| 179 | |
| 180 | int http_response_add_header(http_response_t *resp, const char *name, |
| 181 | const char *value) { |
| 182 | if (resp == NULL || name == NULL || value == NULL) { |
| 183 | return -1; |
| 184 | } |
| 185 | |
| 186 | int needed = snprintf(NULL, 0, "%s: %s\r\n", name, value); |
| 187 | if (needed < 0) { |
| 188 | return -1; |
| 189 | } |
| 190 | if (ensure_space(resp, (size_t)needed + 1U) < 0) { |
| 191 | return -1; |
| 192 | } |
| 193 | |
| 194 | resp->used += |
| 195 | (size_t)snprintf(resp->data + resp->used, |
| 196 | sizeof(resp->data) - resp->used, |
| 197 | "%s: %s\r\n", name, value); |
| 198 | |
| 199 | #if HTTP_DEBUG_LOG_HEADERS |
| 200 | printf("DEBUG: Added header %s: %s\n", name, value); |
| 201 | #endif |
| 202 | return 0; |
| 203 | } |
| 204 | |
| 205 | /*===========================================================================* |
| 206 | * BODY |
| 207 | *===========================================================================*/ |
| 208 | |
| 209 | int http_response_set_body(http_response_t *resp, const void *body, |
| 210 | size_t length) { |
| 211 | if (resp == NULL || body == NULL) { |
| 212 | return -1; |
| 213 | } |
| 214 | |
| 215 | char len_str[32]; |
| 216 | int len_n = snprintf(len_str, sizeof(len_str), "%zu", length); |
| 217 | if (len_n < 0) { |
| 218 | return -1; |
| 219 | } |
| 220 | if ((size_t)len_n >= sizeof(len_str)) { |
| 221 | return -1; |
| 222 | } |
| 223 | |
| 224 | int hdr_needed = snprintf(NULL, 0, "Content-Length: %s\r\n", len_str); |
| 225 | if (hdr_needed < 0) { |
| 226 | return -1; |
| 227 | } |
| 228 | |
| 229 | size_t needed = (size_t)hdr_needed + 2U + length; |
| 230 | if (ensure_space(resp, needed) < 0) { |
| 231 | return -1; |
| 232 | } |
| 233 | |
| 234 | if (http_response_add_header(resp, "Content-Length", len_str) != 0) { |
| 235 | return -1; |
| 236 | } |
| 237 | |
| 238 | memcpy(resp->data + resp->used, "\r\n", 2); |
| 239 | resp->used += 2; |
| 240 | |
| 241 | memcpy(resp->data + resp->used, body, length); |
| 242 | resp->used += length; |
| 243 | |
| 244 | return 0; |
| 245 | } |
| 246 | |
| 247 | int http_response_set_body_owned(http_response_t *resp, void *body, |
| 248 | size_t length) { |
| 249 | if (resp == NULL || body == NULL || length == 0U) { |
| 250 | return -1; |
| 251 | } |
| 252 | if (resp->mem_body != NULL) { |
| 253 | return -1; |
| 254 | } |
| 255 | |
| 256 | char len_str[32]; |
| 257 | int len_n = snprintf(len_str, sizeof(len_str), "%zu", length); |
| 258 | if (len_n < 0 || (size_t)len_n >= sizeof(len_str)) { |
| 259 | return -1; |
| 260 | } |
| 261 | |
| 262 | int hdr_needed = snprintf(NULL, 0, "Content-Length: %s\r\n", len_str); |
| 263 | if (hdr_needed < 0) { |
| 264 | return -1; |
| 265 | } |
| 266 | if (ensure_space(resp, (size_t)hdr_needed + 2U) < 0) { |
| 267 | return -1; |
| 268 | } |
| 269 | if (http_response_add_header(resp, "Content-Length", len_str) != 0) { |
| 270 | return -1; |
| 271 | } |
| 272 | /* Blank line terminating headers */ |
| 273 | resp->data[resp->used++] = '\r'; |
| 274 | resp->data[resp->used++] = '\n'; |
| 275 | |
| 276 | resp->mem_body = body; |
| 277 | resp->mem_length = length; |
| 278 | resp->mem_sent = 0U; |
| 279 | resp->mem_body_owned = 1; |
| 280 | return 0; |
| 281 | } |
| 282 | |
| 283 | int http_response_set_body_ref(http_response_t *resp, const void *body, |
| 284 | size_t length) { |
| 285 | if (resp == NULL || body == NULL) { |
| 286 | return -1; |
| 287 | } |
| 288 | if (resp->mem_body != NULL) { |
| 289 | return -1; |
| 290 | } |
| 291 | if (length == 0U) { |
| 292 | return -1; |
| 293 | } |
| 294 | |
| 295 | char len_str[32]; |
| 296 | int len_n = snprintf(len_str, sizeof(len_str), "%zu", length); |
| 297 | if (len_n < 0) { |
| 298 | return -1; |
| 299 | } |
| 300 | if ((size_t)len_n >= sizeof(len_str)) { |
| 301 | return -1; |
| 302 | } |
| 303 | |
| 304 | int hdr_needed = snprintf(NULL, 0, "Content-Length: %s\r\n", len_str); |
| 305 | if (hdr_needed < 0) { |
| 306 | return -1; |
| 307 | } |
| 308 | |
| 309 | if (ensure_space(resp, (size_t)hdr_needed + 2U) < 0) { |
| 310 | return -1; |
| 311 | } |
| 312 | if (http_response_add_header(resp, "Content-Length", len_str) != 0) { |
| 313 | return -1; |
| 314 | } |
| 315 | if (http_response_finalize(resp) != 0) { |
| 316 | return -1; |
| 317 | } |
| 318 | |
| 319 | resp->mem_body = body; |
| 320 | resp->mem_length = length; |
| 321 | resp->mem_sent = 0U; |
| 322 | return 0; |
| 323 | } |
| 324 | |
| 325 | static int size_add_checked(size_t a, size_t b, size_t *out) { |
| 326 | if (out == NULL) { |
| 327 | return -1; |
| 328 | } |
| 329 | if (a > (SIZE_MAX - b)) { |
| 330 | return -1; |
| 331 | } |
| 332 | *out = a + b; |
| 333 | return 0; |
| 334 | } |
| 335 | |
| 336 | int http_response_set_body_splice(http_response_t *resp, const void *prefix, |
| 337 | size_t prefix_len, const void *insert, |
| 338 | size_t insert_len, const void *suffix, |
| 339 | size_t suffix_len) { |
| 340 | if ((resp == NULL) || (prefix == NULL) || (suffix == NULL)) { |
| 341 | return -1; |
| 342 | } |
| 343 | if (resp->mem_seg_count != 0U) { |
| 344 | return -1; |
| 345 | } |
| 346 | if ((insert_len > 0U) && (insert == NULL)) { |
| 347 | return -1; |
| 348 | } |
| 349 | if (insert_len >= sizeof(resp->mem_inline)) { |
| 350 | return -1; |
| 351 | } |
| 352 | |
| 353 | size_t total = 0U; |
| 354 | if (size_add_checked(prefix_len, insert_len, &total) != 0) { |
| 355 | return -1; |
| 356 | } |
| 357 | if (size_add_checked(total, suffix_len, &total) != 0) { |
| 358 | return -1; |
| 359 | } |
| 360 | if (total == 0U) { |
| 361 | return -1; |
| 362 | } |
| 363 | |
| 364 | if (insert_len > 0U) { |
| 365 | memcpy(resp->mem_inline, insert, insert_len); |
| 366 | } |
| 367 | resp->mem_inline[insert_len] = '\0'; |
| 368 | |
| 369 | char len_str[32]; |
| 370 | int len_n = snprintf(len_str, sizeof(len_str), "%zu", total); |
| 371 | if (len_n < 0) { |
| 372 | return -1; |
| 373 | } |
| 374 | if ((size_t)len_n >= sizeof(len_str)) { |
| 375 | return -1; |
| 376 | } |
| 377 | |
| 378 | int hdr_needed = snprintf(NULL, 0, "Content-Length: %s\r\n", len_str); |
| 379 | if (hdr_needed < 0) { |
| 380 | return -1; |
| 381 | } |
| 382 | |
| 383 | if (ensure_space(resp, (size_t)hdr_needed + 2U) < 0) { |
| 384 | return -1; |
| 385 | } |
| 386 | if (http_response_add_header(resp, "Content-Length", len_str) != 0) { |
| 387 | return -1; |
| 388 | } |
| 389 | if (http_response_finalize(resp) != 0) { |
| 390 | return -1; |
| 391 | } |
| 392 | |
| 393 | resp->mem_segs[0] = prefix; |
| 394 | resp->mem_lens[0] = prefix_len; |
| 395 | resp->mem_segs[1] = (insert_len > 0U) ? (const void *)resp->mem_inline : NULL; |
| 396 | resp->mem_lens[1] = insert_len; |
| 397 | resp->mem_segs[2] = suffix; |
| 398 | resp->mem_lens[2] = suffix_len; |
| 399 | resp->mem_seg_count = 3U; |
| 400 | resp->mem_seg_index = 0U; |
| 401 | resp->mem_seg_sent = 0U; |
| 402 | return 0; |
| 403 | } |
| 404 | |
| 405 | int http_response_append_raw(http_response_t *resp, const void *data, |
| 406 | size_t length) { |
| 407 | if (resp == NULL || data == NULL) |
| 408 | return -1; |
| 409 | if (ensure_space(resp, length) < 0) |
| 410 | return -1; |
| 411 | memcpy(resp->data + resp->used, data, length); |
| 412 | resp->used += length; |
| 413 | return 0; |
| 414 | } |
| 415 | |
| 416 | /*===========================================================================* |
| 417 | * FINALIZE (when body is already appended or there is no body) |
| 418 | *===========================================================================*/ |
| 419 | |
| 420 | int http_response_finalize(http_response_t *resp) { |
| 421 | if (resp == NULL) { |
| 422 | return -1; |
| 423 | } |
| 424 | |
| 425 | /* Append the blank line that terminates headers */ |
| 426 | if (ensure_space(resp, 2) < 0) { |
| 427 | return -1; |
| 428 | } |
| 429 | |
| 430 | memcpy(resp->data + resp->used, "\r\n", 2); |
| 431 | resp->used += 2; |
| 432 | |
| 433 | return 0; |
| 434 | } |
| 435 |