shttp

A small http server written in C

git clone git://git.janzachar.dev/shttp.git


0#include "super.h"
1#include <string.h>
2#include <ctype.h>
3#include <stdio.h>
4
5
6#define RULE(name) int (name)(struct HTTP_request *rq, char **buff)
7#define rule(r) (r)(rq, buff)
8#define try_rule(r) { \
9	int o = rule(r); \
10	if (o) return o; \
11}
12#define if_rule(r) if (rule(r))
13#define match(str) (**buff != '\0' && strchr((str), **buff))
14
15#define CH_RULE(name, chr)			\
16inline RULE(name) {				\
17	if (**buff != (chr)) return 1;		\
18	(*buff)++;				\
19	return 0;				\
20}
21
22#define CHS_RULE(name, str)			\
23RULE(name) {					\
24	if (!match(str)) return 1;		\
25	(*buff)++;				\
26	return 0;				\
27}						
28
29// ----------------------------------------------------------------------------
30// Primitives
31// ----------------------------------------------------------------------------
32
33CH_RULE(SP, ' ');
34CH_RULE(HTAB, '\t');
35CH_RULE(CR, '\n');
36
37RULE(OWS)
38{
39	while (!rule(SP) || !rule(HTAB)) {}
40	return 0;
41}
42RULE(RWS)
43{
44	if (rule(SP) && rule(HTAB)) return 1;
45	return rule(OWS);
46}
47
48RULE(CRLF)
49{
50	// Recognise '\n' as '\r\n'
51	// rfc9112 Section 2.2-3
52	if (!rule(CR)) return 0;
53
54	if (**buff != '\r') return 1;
55
56	// Replace '\r' with ' '
57	// rfc9112 Section 2.2-4 
58	if (*(*buff+1) != '\n') {
59		**buff = ' ';
60		return 1;
61	}
62
63	(*buff) += 2;
64	return 0;
65}
66
67#define ishex(c)	(isdigit(c) || ('A' <= (c) && (c) <= 'F') || ('a' <= (c) && (c) <= 'f')) 
68RULE(pct_encoded)
69{
70	if (**buff != '%') return 1;
71	if (!ishex(*(*buff+1)) || !ishex(*(*buff+2))) return 1;
72	
73	(*buff) += 3;
74	return 0;
75}
76
77RULE(pchar)
78{
79	if (isalnum(**buff) || match("-._~!$&'()*,;=:@")) {
80		(*buff)++;
81		return 0;
82	}
83	if (!rule(pct_encoded)) return 0;
84	return 1;
85}
86
87
88// ----------------------------------------------------------------------------
89// Request line
90// ----------------------------------------------------------------------------
91
92RULE(method)
93{
94	if (!strncmp("GET", *buff, 3)) {
95		rq->method = "GET";
96		(*buff) += 3;
97		return 0;
98	}
99
100	if (!strncmp("HEAD", *buff, 4)) {
101		rq->method = "HEAD";
102		(*buff) += 4;
103		return 0;
104	}
105	
106	return 501;
107}
108
109
110#define to_int(chr) (					\
111	'0' <= (chr) && (chr) <= '9' ? (chr)-'0' :	\
112	'a' <= (chr) && (chr) <= 'z' ? (chr)-'a'+10 :	\
113	'A' <= (chr) && (chr) <= 'Z' ? (chr)-'A'+10 : -1\
114)
115int url_decode(char *url)
116{
117	char *ptr = url;
118	for (; *url; ptr++) {
119		if (*url != '%') {
120			*ptr = *(url++);
121			continue;
122		}
123
124		if (*(ptr+1) == '%') {
125			*ptr = '%';
126			url += 2;
127			continue;
128		}
129
130		*ptr = to_int(*(url+1))*16 + to_int(*(url+2));
131		if (*ptr == '/' || *ptr == '\0') return 1;
132		url += 3;
133	}
134	*ptr = '\0';
135	return 0;
136}
137
138RULE(absolute_path)
139{
140	if (**buff != '/') return 400;
141	rq->path = *buff;
142
143	while (1) {
144		if (**buff != '/') break;
145		(*buff)++;
146
147		while (!rule(pchar)) {}
148	}
149	if (**buff == '\0') return 414;
150
151
152	if (rule(SP)) return 400;
153	*(*buff-1) = '\0';
154
155	if (url_decode(rq->path)) return 400;
156	return 0;
157}
158
159RULE(query)
160{
161	while (1) {
162		if (!rule(pchar)) continue;
163		if (**buff == '/' || **buff == '?') {
164			(*buff)++;
165			continue;
166		}
167		break;
168	}
169	return 0;
170}
171
172RULE (origin_form)
173{
174	try_rule(absolute_path);
175	if (**buff == '?')
176		try_rule(query);
177
178	return 0;
179}
180
181RULE(HTTP_version)
182{
183	if (strncmp("HTTP", *buff, 4))
184		return 400;
185	
186	(*buff) += 4;
187
188	if (**buff != '/') return 400;
189	(*buff)++;
190
191
192	if (!isdigit(**buff) || *(*buff+1) != '.' || !isdigit(*(*buff+2)))
193		return 400;
194
195	// Only support HTTP/1.1
196	if (strncmp("1.1", *buff, 3))
197		return 505;
198	(*buff) += 3;
199
200
201	rq->version = "HTTP/1.1";
202	return 0;
203}
204
205RULE(request_line)
206{
207	try_rule(method);
208	try_rule(SP);
209	try_rule(origin_form);
210	try_rule(HTTP_version);
211	return 0;
212}
213
214
215// ----------------------------------------------------------------------------
216// Headers
217// ----------------------------------------------------------------------------
218
219RULE(token)
220{
221	int ret = 1;
222	while (isalnum(**buff) || match("!#$%&'*+-.^_`|~")) {
223		(*buff)++;
224		ret = 0;
225	}
226	return ret;
227}
228
229RULE(field_vchar)
230{
231	if (**buff < 32 || **buff == 127) return 1;
232	(*buff)++;
233	return 0;
234}
235
236RULE(field_value)
237{
238	try_rule(field_vchar);
239
240	// field_value may include SP or HTAB, but should start and end with `field_vchar`
241	int space = 0;
242	while (1) {
243		if (!rule(RWS)) space = 1;
244		else if (!rule(field_vchar)) space = 0;
245		else break;
246	}
247
248	return space;
249}
250
251
252RULE(headers)
253{
254	// check for whitespaced lines
255	// Section 2.2-8
256	if (match("\t\v\f \xa0")) return 400;
257
258	while (rule(CRLF)) {
259		char *name = *buff;
260		if (rule(token)) return 400;
261
262		if (**buff != ':') return 400;
263		**buff = '\0'; 
264		(*buff)++;
265
266		rule(OWS);
267
268		char *value = *buff;
269		if (rule(field_value)) return 400;
270		char *end = *buff;
271
272		rule(OWS);
273		rule(CRLF);
274
275		*end = '\0';
276
277		header_push(&rq->headers, name, value);
278	}
279	return 0;
280}
281
282
283// ----------------------------------------------------------------------------
284// Starting point
285// ----------------------------------------------------------------------------
286RULE(request)
287{
288	// Ignore empty lines preceeding the request
289	// rfc9112 Section 2.2-7
290	while (!rule(CRLF)) {}
291
292
293	try_rule(request_line);
294	try_rule(CRLF);
295
296	try_rule(headers);
297	return 0;
298}
299
300