Building web apps from scratch - Handling request payload - Part 3
NOTE: This post is still a work in progress!!
Our server can now receive requests and inspect their data, then send a response based on that data. That's pretty much all we need to get a simple website going, even with dynamic content and everything! There is one thing missing though. At the moment we are ignoring the request payload. This is necessary when the user want to send us information such as login data or a post's content.
At the moment, our server receives the request in a 4Kb buffer, hoping it is large enough to hold its head. We continue reading bytes into the buffer until we find the \r\n\r\n
token which signifies the end of the request head and the start of the body, if present. It is also possible that our buffer reads over the request head limit and into the body.
Once we receive the head (and some bytes of the body, potentially), we can parse it and read the Content-Length
header, which will tell us how many bytes we still need to read after the head.
Lets make a function that detects the payload length given the parsed request:
1 | #define NULLSTR ((string) {NULL, 0}) |
2 | #define S(x) ((string) {(x), (int) sizeof(x)-1}) |
3 | |
4 | bool is_digit(char c) |
5 | { |
6 | return c >= '0' && c <= '9'; |
7 | } |
8 | |
9 | char to_lower(char c) |
10 | { |
11 | if (c >= 'A' && c <= 'Z') |
12 | return c - 'A' + 'a'; |
13 | return c; |
14 | } |
15 | |
16 | bool streq_case_insensitive(string s1, string s2) |
17 | { |
18 | if (s1.size != s2.size) |
19 | return false; |
20 | for (int i = 0; i < s1.size; i++) |
21 | if (to_lower(s1.data[i]) != to_lower(s2.data[i])) |
22 | return false; |
23 | return true; |
24 | } |
25 | |
26 | int get_content_length(HTTPRequest *request) |
27 | { |
28 | string content_length_value = NULLSTR; |
29 | for (int i = 0; i < request->num_headers; i++) { |
30 | if (streq_case_insensitive(S("Content-Length"), request->headers[i].name)) { |
31 | content_length_value = request->headers[i].value; |
32 | break; |
33 | } |
34 | } |
35 | // If we didn't find the header, then the value is set to the empty string, which works too |
36 | |
37 | // The number may be preceded by spaces |
38 | int cur = 0; |
39 | |
40 | // Move cursor over the initial spaces |
41 | while (cur < content_length_value.size && content_length_value.data[cur] == ' ') |
42 | cur++; |
43 | |
44 | // Check that the cursor now points to a number |
45 | if (cur == content_length_value.size) |
46 | return 0; // Empty or missing Content-Length |
47 | if (!is_digit(content_length_value.data[cur])) |
48 | return -1; // Invalid char |
49 | |
50 | // Parse the number for text to integer form |
51 | int content_length = 0; |
52 | do { |
53 | int n = content_length_value.data[cur++] - '0'; |
54 | if (content_length > (INT_MAX - n) / 10) |
55 | return -1; // The parsed integer can't be represented by an "int" |
56 | content_length = content_length * 10 + n; |
57 | } while (cur < content_length_value.size && is_digit(content_length_value.data[cur])); |
58 | |
59 | // We ignore anything that comes after the integer |
60 | |
61 | return content_length; |
62 | } |
63 |
This function returns the payload length or -1 if anything bad happened.
After parsing the head, we read the content length and then call recv
until we get that many bytes. We also need to handle partial reads, so we implement a recv_all
analogous to the send_all
we used some time ago
1 | int recv_all(SOCKET_TYPE sock, char *dst, int num) |
2 | { |
3 | int received = 0; |
4 | while (received < num) { |
5 | int just_received = recv(sock, dst + received, num - received, 0); |
6 | if (just_received < 0) |
7 | return -1; |
8 | received += just_received; |
9 | } |
10 | return received; |
11 | } |
12 |
this function won't return until exactly num bytes are read, or an error occurres. This is how the program looks now:
1 | #include <stdio.h> // printf |
2 | #include <stdbool.h> // bool, true, false |
3 | #include <stdlib.h> // malloc, free |
4 | #include <string.h> // memcpy |
5 | #include <limits.h> // INT_MAX |
6 | |
7 | #ifdef _WIN32 |
8 | #include <winsock2.h> |
9 | #define SOCKET_TYPE SOCKET |
10 | #define INVALID_SOCKET_VALUE INVALID_SOCKET |
11 | #define CLOSE_SOCKET closesocket |
12 | #else |
13 | #include <unistd.h> // close |
14 | #include <arpa/inet.h> // socket, htons, inet_addr, sockaddr_in, bind, listen, accept, recv, send |
15 | #define SOCKET_TYPE int |
16 | #define INVALID_SOCKET_VALUE -1 |
17 | #define CLOSE_SOCKET close |
18 | #endif |
19 | |
20 | typedef struct { char *data; int size; } string; |
21 | #define NULLSTR ((string) {NULL, 0}) |
22 | #define S(x) ((string) {(x), (int) sizeof(x)-1}) |
23 | |
24 | bool is_digit(char c) |
25 | { |
26 | return c >= '0' && c <= '9'; |
27 | } |
28 | |
29 | char to_lower(char c) |
30 | { |
31 | if (c >= 'A' && c <= 'Z') |
32 | return c - 'A' + 'a'; |
33 | return c; |
34 | } |
35 | |
36 | bool streq_case_insensitive(string s1, string s2) |
37 | { |
38 | if (s1.size != s2.size) |
39 | return false; |
40 | for (int i = 0; i < s1.size; i++) |
41 | if (to_lower(s1.data[i]) != to_lower(s2.data[i])) |
42 | return false; |
43 | return true; |
44 | } |
45 | |
46 | // ... definitions for HTTPRequest, HTTPVersion, HTTPHeader, HTTPMethod ... |
47 | |
48 | bool parse_request(string src, HTTPRequest *dst) |
49 | { |
50 | // ... |
51 | } |
52 | |
53 | int get_content_length(HTTPRequest *request) |
54 | { |
55 | // ... |
56 | } |
57 | |
58 | int send_all(SOCKET_TYPE sock, void *src, size_t num) |
59 | { |
60 | // ... |
61 | } |
62 | |
63 | int recv_all(SOCKET_TYPE sock, char *dst, int num) |
64 | { |
65 | // ... |
66 | } |
67 | |
68 | int recv_request_head(SOCKET_TYPE sock, char *dst, int max, int *head_len) |
69 | { |
70 | // ... |
71 | } |
72 | |
73 | int main() |
74 | { |
75 | // .. socket, bind, listen ... |
76 | |
77 | while (1) { |
78 | SOCKET_TYPE client_socket = accept(listen_socket, NULL, NULL); |
79 | if (client_socket == INVALID_SOCKET_VALUE) { |
80 | printf("accept failed\n"); |
81 | continue; |
82 | } |
83 | |
84 | char request_buffer[1024]; |
85 | int received_total, head_len; |
86 | received_total = recv_request_head(client_socket, request_buffer, sizeof(request_buffer), &head_len); |
87 | if (received_total < 0) { |
88 | printf("recv_request_head failed\n"); |
89 | CLOSE_SOCKET(client_socket); |
90 | continue; |
91 | } |
92 | string request_head = {request_buffer, head_len}; |
93 | |
94 | HTTPRequest parsed_request; |
95 | if (!parse_request(request_head, &parsed_request)) { |
96 | // Parsing failed |
97 | char response_buffer[] = |
98 | "HTTP/1.0 400 Bad Request\r\n" |
99 | "Content-Length: 0\r\n" |
100 | "\r\n"; |
101 | send_all(client_socket, response_buffer, sizeof(response_buffer)); |
102 | CLOSE_SOCKET(client_socket); |
103 | continue; |
104 | } |
105 | |
106 | int content_length = get_content_length(&parsed_request); |
107 | if (content_length < 0) { |
108 | char response_buffer[] = |
109 | "HTTP/1.0 400 Bad Request\r\n" |
110 | "Content-Length: 0\r\n" |
111 | "\r\n"; |
112 | send_all(client_socket, response_buffer, sizeof(response_buffer)); |
113 | CLOSE_SOCKET(client_socket); |
114 | continue; |
115 | } |
116 | |
117 | string content = NULLSTR; |
118 | if (content_length > 0) { |
119 | content.data = malloc(content_length+1); |
120 | content.size = content_length; |
121 | |
122 | int received_body = received_total - head_len; |
123 | if (received_body > content_length) |
124 | received_body = content_length; |
125 | |
126 | memcpy(content.data, request_buffer + head_len, received_body); |
127 | |
128 | if (received_body < content_length) { |
129 | int result = recv_all(client_socket, content.data + received_body, content.size - received_body); |
130 | if (result < 0) { |
131 | CLOSE_SOCKET(client_socket); |
132 | free(content.data); |
133 | continue; |
134 | } |
135 | } |
136 | content.data[content.size] = '\0'; |
137 | } |
138 | |
139 | // The request payload is in the "content" string |
140 | |
141 | char response_buffer[] = |
142 | "HTTP/1.0 200 OK\r\n" |
143 | "Content-Length: 13\r\n" |
144 | "Content-Type: text/plain\r\n" |
145 | "\r\n" |
146 | "Hello, world!"; |
147 | send_all(client_socket, response_buffer, sizeof(response_buffer)); |
148 | |
149 | // Free the dynamic buffer |
150 | free(content.data); |
151 | |
152 | CLOSE_SOCKET(client_socket); |
153 | } |
154 | // This point will never be reached |
155 | } |
156 |
Once the content length is known, we allocate a buffer of the appropriate size with malloc
. We then receive the body into this buffer with recv_all
. We ignore the request head and body for now since we always return the usual “Hello, world!” message. We also need to return the memory allocated with malloc
to the system by calling free
at the end.