Seregon/zftpd

Zero-copy FTP/HTTP Daemon compatible with all POSIX systems

C/11.0 KB/No license
src/http_response.c
zftpd / src / http_response.c
1/*
2MIT License
3 
4Copyright (c) 2026 Seregon
5 
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12 
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15 
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
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 
37static http_response_t g_response_pool[HTTP_MAX_CONNECTIONS];
38static unsigned char g_response_in_use[HTTP_MAX_CONNECTIONS];
39 
40/*===========================================================================*
41 * STATUS CODE → TEXT MAPPING
42 *===========================================================================*/
43 
44static 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 
81http_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 
135void 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 */
170static 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 
180int 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 
209int 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 
247int 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 
283int 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 
325static 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 
336int 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 
405int 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 
420int 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