Cozis

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
4bool is_digit(char c)
5{
6 return c >= '0' && c <= '9';
7}
8
9char to_lower(char c)
10{
11 if (c >= 'A' && c <= 'Z')
12 return c - 'A' + 'a';
13 return c;
14}
15
16bool 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
26int 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

1int 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
20typedef struct { char *data; int size; } string;
21#define NULLSTR ((string) {NULL, 0})
22#define S(x) ((string) {(x), (int) sizeof(x)-1})
23
24bool is_digit(char c)
25{
26 return c >= '0' && c <= '9';
27}
28
29char to_lower(char c)
30{
31 if (c >= 'A' && c <= 'Z')
32 return c - 'A' + 'a';
33 return c;
34}
35
36bool 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
48bool parse_request(string src, HTTPRequest *dst)
49{
50 // ...
51}
52
53int get_content_length(HTTPRequest *request)
54{
55 // ...
56}
57
58int send_all(SOCKET_TYPE sock, void *src, size_t num)
59{
60 // ...
61}
62
63int recv_all(SOCKET_TYPE sock, char *dst, int num)
64{
65 // ...
66}
67
68int recv_request_head(SOCKET_TYPE sock, char *dst, int max, int *head_len)
69{
70 // ...
71}
72
73int 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.