httpp.h (15235B)
1 /* 2 * Tiny header only http parser library for 3 * http 1/1 version (enums don't respect your namespace much, sorry) 4 * Pipelined requests are not supported because nobody realy cares about them 5 * (https://en.wikipedia.org/wiki/HTTP_pipelining#Implementation_status) 6 * 7 * TODO Chunked transfer is not really supported 8 * TODO Folded headers aren't handled. 9 */ 10 11 #include <stddef.h> 12 #include <stdio.h> 13 #include <stdlib.h> 14 #include <string.h> 15 16 #define HTTPP_INITIAL_HEADERS_ARR_CAP 20 17 #define HTTPP_HEADERS_ARR_LIMIT -1 18 19 #define HTTPP_MAX_METHOD_LENGTH (10 + 1) 20 #define HTTPP_VERSION_BUFSIZE (10 + 1) 21 #define HTTPP_SUPPORTED_VERSION "HTTP/1.1" 22 23 #define _HTTP_DELIMITER "\r\n" 24 #define _HTTP_DELIMITER_SIZE 2 25 #define _HTTP_MAX_STATUS_CODE_SIZE 3 26 27 #define httpp_string_to_method(s) (strcmp(s, "GET") == 0 ? 0 : \ 28 strcmp(s, "POST") == 0 ? 1 : \ 29 strcmp(s, "DELETE") == 0 ? 2 : -1) 30 31 typedef enum { 32 GET, 33 POST, 34 DELETE, 35 UNKNOWN = -1 36 } httpp_method_t; 37 38 typedef enum { 39 Continue = 100, 40 Switching_Protocols, 41 42 Ok = 200, 43 Created, 44 Accepted, 45 46 Multiple_Choices = 300, 47 Moved_Permanently, 48 Found, 49 See_Other, 50 Not_Modified, 51 Use_Proxy, 52 Temporary_Redirect = 307, 53 Permanent_Redirect, 54 55 Bad_Request = 400, 56 Unauthorized, 57 Payment_Required, // Ahahaha 58 Forbidden, 59 Not_Found, 60 61 Internal_Server_Error = 500, 62 Not_Implemented, 63 Bad_Gateway, 64 Service_Unavailable, 65 Gateway_Timeout, 66 HTTP_Version_Not_Supported, 67 68 Unspecified = -1 69 } httpp_status_t; 70 71 typedef struct { 72 char* name; 73 char* value; 74 } httpp_header_t; 75 76 typedef struct { 77 httpp_header_t* arr; 78 size_t capacity; 79 size_t length; 80 } httpp_headers_arr_t; 81 82 typedef struct { 83 httpp_method_t method; 84 httpp_headers_arr_t* headers; 85 char* route; 86 char* body; 87 } httpp_req_t; 88 89 typedef struct { 90 httpp_headers_arr_t* headers; 91 httpp_status_t code; 92 char* body; 93 } httpp_res_t; 94 95 const char* httpp_method_to_string(httpp_method_t m); 96 const char* httpp_status_to_string(httpp_status_t s); 97 98 httpp_req_t* httpp_req_new(); 99 void httpp_req_free(httpp_req_t* req); 100 101 httpp_req_t* httpp_parse_request(char* raw); 102 httpp_header_t* httpp_parse_header(httpp_headers_arr_t* hs, char* line, size_t content_len); 103 104 httpp_headers_arr_t* httpp_headers_arr_new(); 105 httpp_header_t* httpp_headers_arr_append(httpp_headers_arr_t* hs, httpp_header_t header); 106 httpp_header_t* httpp_headers_arr_add(httpp_headers_arr_t* hs, char* name, char* value); // stdrup and httpp_headers_arr_append 107 httpp_header_t* httpp_headers_arr_find(httpp_headers_arr_t* hs, char* name); 108 void httpp_headers_arr_free(httpp_headers_arr_t* hs); 109 110 httpp_res_t* httpp_res_new(); 111 int httpp_res_set_body(httpp_res_t* res, char* body); // strdup body and set it 112 char* httpp_res_to_raw(httpp_res_t* res); 113 void httpp_res_free(httpp_res_t* res); 114 115 #define httpp_add_header(req_or_res, name, value) \ 116 (httpp_headers_arr_add((req_or_res)->headers, name, value)) 117 118 #define httpp_find_header(req_or_res, name) \ 119 (httpp_headers_arr_find((req_or_res)->headers, name)) 120 121 #ifdef HTTPP_IMPLEMENTATION 122 123 #define ltrim(str) do { \ 124 while ((*(str) && *(str) == ' ') || (*(str) == '\r') || (*(str) == '\n')) { \ 125 (str)++; \ 126 } \ 127 } while (0) 128 129 130 #define ltrim_buf(str, len) do { \ 131 while ((*(str) && *(str) == ' ') || (*(str) == '\r') || (*(str) == '\n')) { \ 132 (str)++; \ 133 (len)--; \ 134 } \ 135 } while (0) 136 137 static int chop_until(char c, char** src, char* dest, size_t n) 138 { 139 char* pos = strchr(*src, c); 140 if (!pos) 141 return 1; 142 143 size_t chopped_size = pos - *src; 144 if (chopped_size >= n) 145 return 1; 146 147 memcpy(dest, *src, chopped_size); 148 dest[chopped_size] = '\0'; 149 150 *src = pos + 1; 151 return 0; 152 } 153 154 static char* dchop_until(char c, char** src) 155 { 156 char* pos = strchr(*src, c); 157 if (!pos) 158 return NULL; 159 160 size_t chopped_size = pos - *src; 161 char* out = (char*) malloc(chopped_size + 1); 162 if (!out) 163 return NULL; 164 165 memcpy(out, *src, chopped_size); 166 out[chopped_size] = '\0'; 167 168 *src = pos + 1; 169 return out; 170 } 171 172 const char* httpp_method_to_string(httpp_method_t m) 173 { 174 switch (m) { 175 case GET: return "GET"; 176 case POST: return "POST"; 177 case DELETE: return "DELETE"; 178 default: return "UNKNOWN"; 179 } 180 } 181 182 const char* httpp_status_to_string(httpp_status_t s) 183 { 184 switch (s) { 185 case Continue: return "Continue"; 186 case Switching_Protocols: return "Switching Protocols"; 187 188 case Ok: return "OK"; 189 case Created: return "Created"; 190 case Accepted: return "Accepted"; 191 192 case Multiple_Choices: return "Multiple Choices"; 193 case Moved_Permanently: return "Moved Permanently"; 194 case Found: return "Found"; 195 case See_Other: return "See Other"; 196 case Not_Modified: return "Not Modified"; 197 case Use_Proxy: return "Use Proxy"; 198 case Temporary_Redirect: return "Temporary Redirect"; 199 case Permanent_Redirect: return "Permanent Redirect"; 200 201 case Bad_Request: return "Bad Request"; 202 case Unauthorized: return "Unauthorized"; 203 case Payment_Required: return "Payment Required"; 204 case Forbidden: return "Forbidden"; 205 case Not_Found: return "Not Found"; 206 207 case Internal_Server_Error: return "Internal Server Error"; 208 case Not_Implemented: return "Not Implemented"; 209 case Bad_Gateway: return "Bad Gateway"; 210 case Service_Unavailable: return "Service Unavailable"; 211 case Gateway_Timeout: return "Gateway Timeout"; 212 case HTTP_Version_Not_Supported: return "HTTP Version Not Supported"; 213 214 case Unspecified: 215 default: return "Unspecified"; 216 } 217 } 218 219 httpp_headers_arr_t* httpp_headers_arr_new() 220 { 221 httpp_headers_arr_t* out = (httpp_headers_arr_t*) malloc(sizeof(httpp_headers_arr_t)); 222 223 out->arr = (httpp_header_t*) malloc(sizeof(httpp_header_t) * HTTPP_INITIAL_HEADERS_ARR_CAP); 224 if (!out->arr) { 225 free(out); 226 return NULL; 227 } 228 229 out->capacity = HTTPP_INITIAL_HEADERS_ARR_CAP; 230 out->length = 0; 231 return out; 232 } 233 234 httpp_req_t* httpp_req_new() 235 { 236 httpp_req_t* out = (httpp_req_t*) malloc(sizeof(httpp_req_t)); 237 if (!out) 238 return NULL; 239 240 out->headers = httpp_headers_arr_new(); 241 if (!out->headers) { 242 free(out); 243 return NULL; 244 } 245 246 out->route = NULL; 247 out->body = NULL; 248 return out; 249 } 250 251 void httpp_headers_arr_free(httpp_headers_arr_t* hs) 252 { 253 if (!hs) 254 return; 255 256 for (size_t i = 0; i < hs->length; i++) { 257 free(hs->arr[i].name); 258 free(hs->arr[i].value); 259 } 260 261 free(hs->arr); 262 free(hs); 263 } 264 265 void httpp_req_free(httpp_req_t* req) 266 { 267 if (!req) 268 return; 269 270 httpp_headers_arr_free(req->headers); 271 free(req->route); 272 free(req->body); 273 free(req); 274 } 275 276 httpp_header_t* httpp_headers_arr_append(httpp_headers_arr_t* hs, httpp_header_t header) 277 { 278 if (hs->length >= hs->capacity) { 279 size_t new_cap = hs->capacity * 2; 280 281 if (new_cap <= hs->capacity) { 282 free(hs->arr); 283 return NULL; 284 } 285 286 hs->arr = (httpp_header_t*) realloc(hs->arr, new_cap * sizeof(httpp_header_t)); 287 if (!hs->arr) { 288 free(hs->arr); 289 return NULL; 290 } 291 292 hs->capacity = new_cap; 293 } 294 295 if (!header.name || !header.value) 296 return NULL; 297 298 hs->arr[hs->length++] = header; 299 return &hs->arr[hs->length - 1]; 300 } 301 302 httpp_header_t* httpp_headers_arr_find(httpp_headers_arr_t* hs, char* name) 303 { 304 // For the sake of simplicity and minimalism, it's just a for loop. No hash table here. 305 for (size_t i = 0; i < hs->length; i++) { 306 if (strcmp(hs->arr[i].name, name) == 0) 307 return &hs->arr[i]; 308 } 309 310 return NULL; 311 } 312 313 314 httpp_header_t* httpp_headers_arr_add(httpp_headers_arr_t* hs, char* name, char* value) 315 { 316 if (!name || !value) 317 return NULL; 318 319 httpp_header_t h = { 320 strdup(name), 321 strdup(value) 322 }; 323 324 httpp_header_t* out = httpp_headers_arr_append(hs, h); 325 326 if (out == NULL) { 327 free(h.name); 328 free(h.value); 329 } 330 331 return out; 332 } 333 334 httpp_header_t* httpp_parse_header(httpp_headers_arr_t* hs, char* line, size_t content_len) 335 { 336 // RFC says that header starting with whitespace or any other non printable ascii should be rejected. 337 if (*line == ' ' || *line == '\r' || *line == '\n') 338 return NULL; 339 340 char* colon = (char*) memchr(line, ':', content_len); 341 if (!colon) 342 return NULL; 343 344 size_t name_len = colon - line; 345 346 char* name = (char*) malloc(name_len + 1); 347 if (!name) 348 return NULL; 349 350 memcpy(name, line, name_len); 351 name[name_len] = '\0'; 352 353 char* value_start = colon + 1; 354 size_t value_len = content_len - name_len - 1; 355 356 ltrim_buf(value_start, value_len); 357 358 char* value = (char*) malloc(value_len + 1); 359 if (!value) { 360 free(name); 361 return NULL; 362 } 363 364 memcpy(value, value_start, value_len); 365 value[value_len] = '\0'; 366 367 return httpp_headers_arr_append(hs, (httpp_header_t){name, value}); 368 } 369 370 static int parse_start_line(char** itr, httpp_req_t* dest) 371 { 372 char method_buf[HTTPP_MAX_METHOD_LENGTH]; 373 char version_buf[HTTPP_VERSION_BUFSIZE]; 374 char* route; 375 376 if (chop_until(' ', itr, method_buf, HTTPP_MAX_METHOD_LENGTH) != 0) 377 return 1; 378 379 if ((route = dchop_until(' ', itr)) == NULL) 380 return 1; 381 382 if (chop_until('\r', itr, version_buf, HTTPP_VERSION_BUFSIZE) != 0) { 383 free(route); 384 return 1; 385 } 386 387 if (strcmp(version_buf, HTTPP_SUPPORTED_VERSION) != 0) { 388 free(route); 389 return 1; 390 } 391 392 dest->method = (httpp_method_t) httpp_string_to_method(method_buf); 393 dest->route = route; 394 395 ltrim(*itr); 396 return 0; 397 } 398 399 httpp_req_t* httpp_parse_request(char* raw) 400 { 401 httpp_req_t* out = httpp_req_new(); 402 403 char* itr = raw; 404 char* end = raw + strlen(raw); 405 406 if (parse_start_line(&itr, out) != 0) { 407 httpp_req_free(out); 408 return NULL; 409 } 410 411 while (itr < end) { 412 char* del_pos = strstr(itr, _HTTP_DELIMITER); 413 if (!del_pos) 414 break; 415 416 size_t line_size = del_pos - itr; 417 if (line_size == 0) { 418 itr = del_pos + _HTTP_DELIMITER_SIZE; 419 break; 420 } 421 422 if (httpp_parse_header(out->headers, itr, line_size) == NULL) { 423 httpp_req_free(out); 424 return NULL; 425 } 426 427 if (HTTPP_HEADERS_ARR_LIMIT != -1 && out->headers->length >= HTTPP_HEADERS_ARR_LIMIT) { 428 httpp_req_free(out); // Limit reached 429 return NULL; 430 } 431 432 itr = del_pos + _HTTP_DELIMITER_SIZE; 433 } 434 435 out->body = strdup(itr); 436 if (!out->body) { 437 httpp_req_free(out); 438 return NULL; 439 } 440 441 return out; 442 } 443 444 // --- Responses part --- 445 httpp_res_t* httpp_res_new() 446 { 447 httpp_res_t* out = (httpp_res_t*) malloc(sizeof(httpp_res_t)); 448 if (!out) 449 return NULL; 450 451 out->headers = httpp_headers_arr_new(); 452 if (!out->headers) 453 return NULL; 454 455 out->code = Unspecified; 456 return out; 457 } 458 459 int httpp_res_set_body(httpp_res_t* res, char* body) 460 { 461 char* content = strdup(body); 462 if (!content) 463 return 1; 464 465 res->body = content; 466 return 0; 467 } 468 469 void httpp_res_free(httpp_res_t* res) 470 { 471 if (!res) 472 return; 473 474 httpp_headers_arr_free(res->headers); 475 free(res->body); 476 } 477 478 char* httpp_res_to_raw(httpp_res_t* res) 479 { 480 if (res == NULL || res->headers == NULL) 481 return NULL; 482 483 if (res->code == -1 || (int) res->code > 999) 484 return NULL; 485 486 const char* status_msg = httpp_status_to_string(res->code); 487 size_t out_size = 488 strlen(HTTPP_SUPPORTED_VERSION) 489 + 1 // Space 490 + _HTTP_MAX_STATUS_CODE_SIZE 491 + 1 // Space 492 + strlen(status_msg) 493 + _HTTP_DELIMITER_SIZE; 494 495 for (size_t i = 0; i < res->headers->length; i++) { 496 httpp_header_t header = res->headers->arr[i]; 497 if (!header.name || !header.value) 498 continue; 499 500 out_size += strlen(header.name) + 2 // ": " 501 + strlen(header.value) 502 + _HTTP_DELIMITER_SIZE; 503 } 504 505 out_size += _HTTP_DELIMITER_SIZE; 506 if (res->body) 507 out_size += strlen(res->body); 508 509 out_size += 1; // '\0' 510 char* out = (char*) malloc(out_size); 511 if (!out) 512 return NULL; 513 514 int written = snprintf(out, out_size, "%s %d %s\r\n", HTTPP_SUPPORTED_VERSION, res->code, status_msg); 515 if (written < 0 || (size_t) written >= out_size) { 516 free(out); 517 return NULL; 518 } 519 520 size_t offset = written; 521 522 for (size_t i = 0; i < res->headers->length; i++) { 523 httpp_header_t header = res->headers->arr[i]; 524 if (!header.name || !header.value) 525 continue; 526 527 int n = snprintf(out + offset, out_size - offset, 528 "%s: %s\r\n", header.name, header.value); 529 530 if (n < 0 || (size_t) n >= out_size - offset) { 531 free(out); 532 return NULL; 533 } 534 535 offset += n; 536 } 537 538 strcat(out, _HTTP_DELIMITER); 539 if (res->body) 540 strcat(out, res->body); 541 542 return out; 543 } 544 545 #endif // HTTPP_IMPLEMENTATION 546 547 /* 548 * MIT License 549 * 550 * Copyright (c) 2025 Mint 551 * 552 * Permission is hereby granted, free of charge, to any person obtaining a copy 553 * of this software and associated documentation files (the "Software"), to deal 554 * in the Software without restriction, including without limitation the rights 555 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 556 * copies of the Software, and to permit persons to whom the Software is 557 * furnished to do so, subject to the following conditions: 558 * 559 * The above copyright notice and this permission notice shall be included in all 560 * copies or substantial portions of the Software. 561 * 562 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 563 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 564 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 565 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 566 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 567 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 568 * SOFTWARE. 569 */