Line data Source code
1 : #ifndef HEADER_fd_src_ballet_http_fd_http_server_h
2 : #define HEADER_fd_src_ballet_http_fd_http_server_h
3 :
4 : /* An fd_http_server is a WebSocket capable HTTP server designed to
5 : stream output messages quickly to many connected clients, where each
6 : output message can go to many (and in some cases all) clients.
7 :
8 : The primary use case is for serving ongoing RPC data to RPC
9 : subscribers, but it also serves a WebSocket stream for browser
10 : clients to show the GUI.
11 :
12 : The server does not allocate and has a built in allocation strategy
13 : and memory region for outgoing messages which the caller should use.
14 : HTTP repsonse bodies and WebSocket frames are placed into an outgoing
15 : ring buffer, wrapping around when reaching the end, and the server
16 : will automatically evict slow clients that do not read their messages
17 : in time and would be overwriten when the buffer has wrapped fully
18 : around.
19 :
20 : Using the outgoing ring has two steps,
21 :
22 : (1) Stage data into the ring with fd_http_server_printf and
23 : fd_http_server_memcpy functions.
24 : (2) Send the staged data to clients with fd_http_server_send and
25 : fd_http_server_oring_broadcast.
26 :
27 : The server is designed to be used in a single threaded event loop and
28 : run within a tile. The caller should call fd_http_server_poll as
29 : frequently as possible to service connections and make forward
30 : progress. */
31 :
32 : #include "../../util/fd_util_base.h"
33 :
34 30 : #define FD_HTTP_SERVER_ALIGN (128UL)
35 :
36 0 : #define FD_HTTP_SERVER_METHOD_GET (0)
37 0 : #define FD_HTTP_SERVER_METHOD_POST (1)
38 0 : #define FD_HTTP_SERVER_METHOD_OPTIONS (2)
39 0 : #define FD_HTTP_SERVER_METHOD_PUT (3)
40 :
41 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_OK ( -1)
42 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_EVICTED ( -2)
43 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_TOO_SLOW ( -3)
44 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_EXPECTED_EOF ( -4)
45 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_PEER_RESET ( -5)
46 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_LARGE_REQUEST ( -6)
47 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_BAD_REQUEST ( -7)
48 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_MISSING_CONENT_LENGTH_HEADER ( -8)
49 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_UNKNOWN_METHOD ( -9)
50 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_PATH_TOO_LONG (-10)
51 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_BAD_KEY (-11)
52 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_UNEXPECTED_VERSION (-12)
53 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_MISSING_KEY_HEADER (-13)
54 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_MISSING_VERSION_HEADER (-14)
55 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_BAD_MASK (-15)
56 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_UNKNOWN_OPCODE (-16)
57 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_OVERSIZE_FRAME (-17)
58 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_CLIENT_TOO_SLOW (-18)
59 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_MISSING_UPGRADE (-19)
60 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_EXPECTED_CONT_OPCODE (-20)
61 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_EXPECTED_TEXT_OPCODE (-21)
62 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_CONTROL_FRAME_TOO_LARGE (-22)
63 0 : #define FD_HTTP_SERVER_CONNECTION_CLOSE_WS_CHANGED_OPCODE (-23)
64 :
65 : /* Given a FD_HTTP_SERVER_CONNECTION_CLOSE_* reason code, a reason that
66 : a HTTP connection a client was closed, produce a human readable
67 : string describing the reason. */
68 :
69 : FD_FN_CONST char const *
70 : fd_http_server_connection_close_reason_str( int reason );
71 :
72 : /* Given a FD_HTTP_SERVER_METHOD_* code, produce the string for that
73 : method. */
74 :
75 : FD_FN_CONST char const *
76 : fd_http_server_method_str( uchar method );
77 :
78 : /* Parameters needed for constructing an HTTP server. */
79 :
80 : struct fd_http_server_params {
81 : ulong max_connection_cnt; /* Maximum number of concurrent HTTP/1.1 connections open. Connections are not persistent and will be closed after one request is served */
82 : ulong max_ws_connection_cnt; /* Maximum number of concurrent websocket connections open */
83 : ulong max_request_len; /* Maximum total length of an HTTP request, including the terminating \r\n\r\n and any body in the case of a POST */
84 : ulong max_ws_recv_frame_len; /* Maximum size of an incoming websocket frame from the client. Must be >= max_request_len */
85 : ulong max_ws_send_frame_cnt; /* Maximum number of outgoing websocket frames that can be queued before the client is disconnected */
86 : ulong outgoing_buffer_sz; /* Size of the outgoing data ring, which is used to stage outgoing HTTP response bodies and WebSocket frames */
87 : int compress_websocket; /* True if large websocket messages are compressed and sent as binary websocket frames */
88 : };
89 :
90 : typedef struct fd_http_server_params fd_http_server_params_t;
91 :
92 : struct fd_http_server_request {
93 : ulong connection_id; /* Unique identifier for the connection. In [0, max_connection_cnt). The connection ID is a unique identifier for the lifetime of the connection, and will be
94 : provided to close to indicate that the connection is closed. After a connection is closed the ID may be recycled */
95 :
96 : uchar method; /* One of FD_HTTP_SERVER_METHOD_* indicating what the method of the request is */
97 : char const * path; /* The NUL termoinated path component of the request. Not sanitized and may contain arbitrary content. Path is the full HTTP path of the request, for example
98 : "/img/monkeys/gorilla.jpg" */
99 :
100 : void * ctx; /* The user provided context pointer passed when constructing the HTTP server */
101 :
102 : struct {
103 : char const * content_type; /* The NUL terminated value of the Content-Type header of the request. Not sanitized and may contain arbitrary content. May be NULL if the header was not present */
104 : char const * accept_encoding; /* The NUL terminated value of the Accept-Encoding header of the request. Not sanitized and may contain arbitrary content. May be NULL if the header was not present */
105 : int compress_websocket; /* True if the client has provided an `Sec-WebSocket-Protocol: compress-zstd` header indicating that the responder can choose to compress WebSocket frames with ZStandard. Only large (>200 bytes) Server -> Client messages are compressed */
106 : int upgrade_websocket; /* True if the client has provided an `Upgrade: websocket` header, valid `Sec-WebSocket-Key` and supported `Sec-Websocket-Version`, indicating that the
107 : responder should upgrade the connection to a WebSocket by setting `upgrade_websocket` to 1 in the response */
108 : } headers;
109 :
110 : union {
111 : struct {
112 : uchar const * body; /* The body of the HTTP request. The body is byte data, might have internal NUL characters, and may not be NUL terminated */
113 : ulong body_len; /* The length of the body of the HTTP request */
114 : } post;
115 : };
116 : };
117 :
118 : typedef struct fd_http_server_request fd_http_server_request_t;
119 :
120 : /* A response issued by the server handler function to an HTTP request.
121 :
122 : The handler should typically create response bodies via. the HTTP
123 : server functions like fd_http_server_printf. This allows the server
124 : to manage buffer lifetimes and ensure high performance. If using
125 : the server buffers, the handler should not set a static_body or
126 : static_body_len, and should instead use fd_http_server_stage_body
127 : to snap off the staging buffer contents into the body.
128 :
129 : In certain cases, it is desirable to send static content where the
130 : lifetime of the buffer is known to outlive the HTTP server. In
131 : that case, you can set body_static to non-NULL and body_len_static
132 : to the length of the body payload, and the server will send this
133 : data instead of the staged data instead.
134 :
135 : status is an HTTP status code. If status is not 200, the response
136 : body is ignored and the server will send an empty response.
137 :
138 : If upgrade_websocket is true, the connection will be upgraded to a
139 : websocket, after which the handler will begin receiving websocket
140 : frames. */
141 :
142 : struct fd_http_server_response {
143 : ulong status; /* Status code of the HTTP response */
144 : int upgrade_websocket; /* 1 if we should send a websocket upgrade response */
145 : int compress_websocket; /* 1 if we should add a `Sec-WebSocket-Protocol: compress-zstd` header to the response */
146 :
147 : char const * content_type; /* Content-Type to set in the HTTP response */
148 : char const * cache_control; /* Cache-Control to set in the HTTP response */
149 : char const * content_encoding; /* Content-Encoding to set in the HTTP response */
150 :
151 : char const * access_control_allow_origin;
152 : char const * access_control_allow_methods;
153 : char const * access_control_allow_headers;
154 : ulong access_control_max_age;
155 :
156 : uchar const * static_body; /* Response body to send. Lifetime of response data must outlive the entire HTTP server. */
157 : ulong static_body_len; /* Length of the response body */
158 :
159 : ulong _body_off; /* Internal use only. Offset into the outgoing buffer where the body starts */
160 : ulong _body_len; /* Internal use only. Length of the body in the outgoing buffer */
161 : };
162 :
163 : typedef struct fd_http_server_response fd_http_server_response_t;
164 :
165 : struct fd_http_server_callbacks {
166 : /* Handle an incoming HTTP request. The callback must be provided
167 : and is assumed to be non-NULL. request is a representation of
168 : the incoming HTTP request. The callback should return a response
169 : which will be sent to the client. */
170 :
171 : fd_http_server_response_t ( * request )( fd_http_server_request_t const * request );
172 :
173 : /* Called when a regular HTTP connection is established. Called
174 : immediately after the connection is accepted. sockfd is the file
175 : descriptor of the socket. ctx is the user provided context pointer
176 : provided when constructing the HTTP server. The open callback can
177 : be NULL in which case the callback will not be invoked. */
178 :
179 : void ( * open )( ulong conn_id, int sockfd, void * ctx );
180 :
181 : /* Close an HTTP request. This is called back once all the data has
182 : been sent to the HTTP client, or an error condition occurs, or the
183 : caller force closes the connection by calling close. If a
184 : connection is upgraded to a WebSocket connection, a close event is
185 : first sent once the HTTP upgrade response is sent, before a ws_open
186 : event is sent. Close is not called when a WebSocket connection is
187 : closed, instead ws_close is called. reason is one of
188 : FD_HTTP_SERVER_CONNECTION_CLOSE_* indicating why the connection is
189 : being closed. ctx is the user provided context pointer provided
190 : when constructing the HTTP server. The close callback can be NULL
191 : in which case the callback will not be invoked. */
192 :
193 : void ( * close )( ulong conn_id, int reason, void * ctx );
194 :
195 : /* Called when a WebSocket is opened. ws_conn_id in [0,
196 : max_ws_connection_cnt). Connection IDs are recycled as WebSocket
197 : connections are closed. Connection IDs overlap with regular
198 : (non-WebSocket) connection IDs, but are in a distinct namespace,
199 : and WebSocket connection 0 is different from regular connection 0.
200 : ctx is the user provided context pointer provided when constructing
201 : the HTTP server. */
202 :
203 : void ( * ws_open )( ulong ws_conn_id, void * ctx );
204 :
205 : /* Called when a WebSocket message is received on the connection.
206 : data is the message data, and data_len is the length of the message
207 : data. ctx is the user provided context pointer provided when
208 : constructing the HTTP server. The data provided is valid only
209 : until the callback returns, and the buffer will be recycled again
210 : immediately. data_len is in [0, max_ws_recv_frame_len). */
211 :
212 : void ( * ws_message )( ulong ws_conn_id, uchar const * data, ulong data_len, void * ctx );
213 :
214 : /* Called when a WebSocket connection is closed. reason is one of
215 : FD_HTTP_SERVER_CONNECTION_CLOSE_* indicating why the connection was
216 : closed. ctx is the user provided context pointer provided when
217 : constructing the HTTP server. Typical reasons for closing the
218 : WebSocket include the peer disconnecting or timing out, or being
219 : evicted to make space for a new incoming connection. Also called
220 : back when the user of the API forcibly closes a connection by
221 : calling ws_close. */
222 :
223 : void ( * ws_close )( ulong ws_conn_id, int reason, void * ctx );
224 : };
225 :
226 : typedef struct fd_http_server_callbacks fd_http_server_callbacks_t;
227 :
228 : struct fd_http_server_private;
229 : typedef struct fd_http_server_private fd_http_server_t;
230 :
231 : FD_PROTOTYPES_BEGIN
232 :
233 : /* fd_http_server_{align,footprint} give the needed alignment and
234 : footprint of a memory region suitable to hold an http server.
235 :
236 : fd_http_server_new formats memory region with suitable alignment and
237 : footprint suitable for holding a http server state. Assumes shmem
238 : points on the caller to the first byte of the memory region owned by
239 : the caller to use. Returns shmem on success and NULL on failure
240 : (logs details). The memory region will be owned by the state on
241 : successful return. The caller is not joined on return.
242 :
243 : fd_http_server_join joins the caller to a http server state. Assumes
244 : shhttp points to the first byte of the memory region holding the
245 : state. Returns a local handle to the join on success (this is
246 : not necessarily a simple cast of the address) and NULL on failure
247 : (logs details).
248 :
249 : fd_http_server_leave leaves the caller's current local join to a http
250 : server state. Returns a pointer to the memory region holding the
251 : state on success (this is not necessarily a simple cast of the
252 : address) and NULL on failure (logs details). The caller is not
253 : joined on successful return.
254 :
255 : fd_http_server_delete unformats a memory region that holds a http
256 : server state. Assumes shhttp points on the caller to the first
257 : byte of the memory region holding the state and that nobody is
258 : joined. Returns a pointer to the memory region on success and NULL
259 : on failure (logs details). The caller has ownership of the memory
260 : region on successful return. */
261 :
262 : FD_FN_CONST ulong
263 : fd_http_server_align( void );
264 :
265 : FD_FN_CONST ulong
266 : fd_http_server_footprint( fd_http_server_params_t params );
267 :
268 : void *
269 : fd_http_server_new( void * shmem,
270 : fd_http_server_params_t params,
271 : fd_http_server_callbacks_t callbacks,
272 : void * callback_ctx );
273 :
274 : fd_http_server_t *
275 : fd_http_server_join( void * shhttp );
276 :
277 : void *
278 : fd_http_server_leave( fd_http_server_t * http );
279 :
280 : void *
281 : fd_http_server_delete( void * shhttp );
282 :
283 : /* fd_http_server_fd returns the file descriptor of the server. The
284 : file descriptor is used to poll for incoming connections and data
285 : on the server. */
286 :
287 : int
288 : fd_http_server_fd( fd_http_server_t * http );
289 :
290 : fd_http_server_t *
291 : fd_http_server_listen( fd_http_server_t * http,
292 : uint address,
293 : ushort port );
294 :
295 : /* Close an active connection. The connection ID must be an open
296 : open connection in [0, max_connection_cnt). The connection will
297 : be forcibly (ungracefully) terminated. The connection ID is released
298 : and should not be used again, as it may be recycled for a future
299 : connection. If a close callback has been provided to the http
300 : server, it will be invoked with the reason provided. */
301 :
302 : void
303 : fd_http_server_close( fd_http_server_t * http,
304 : ulong conn_id,
305 : int reason );
306 :
307 : /* Close an active WebSocket conection. The connection ID must be an
308 : open WebSocket connection ID in [0, max_ws_connection_cnt). The
309 : connection will be forcibly (ungracefully) terminated. The
310 : connection ID is released and should not be used again, as it may be
311 : recycled for a future WebSocket connection. If a ws_close callback
312 : has been provided to the http server, it will be invoked with the
313 : reason provided. */
314 :
315 : void
316 : fd_http_server_ws_close( fd_http_server_t * http,
317 : ulong ws_conn_id,
318 : int reason );
319 :
320 : /* fd_http_server_buffer_trunc truncates the pending message to the given length. */
321 :
322 : void
323 : fd_http_server_stage_trunc( fd_http_server_t * http,
324 : ulong len );
325 :
326 : /* fd_http_server_buffer_len returns the length of the pending message. */
327 :
328 : ulong
329 : fd_http_server_stage_len( fd_http_server_t * http );
330 :
331 : /* fd_http_server_printf appends the rendered format string fmt into the
332 : staging area of the outgoing ring buffer. Assumes http is a current
333 : local join.
334 :
335 : If appending to the ring buffer causes it to wrap around and
336 : overwrite existing data from a prior message, any connections which
337 : are still using data from the prior message will be evicted, as they
338 : cannot keep up.
339 :
340 : Once the full message has been appended into the outgoing ring buffer,
341 : the staged contents can be sent to all connected WebSocket clients of
342 : the HTTP server using fd_http_server_broadcast. This will end the
343 : current staged message so future prints go into a new message.
344 :
345 : Printing is not error-free, it is assumed that the format string is
346 : valid but the entire outgoing buffer may not be large enough to hold
347 : the printed string. In that case, the staging buffer is marked as
348 : being in an error state internally. The next call to send or
349 : broadcast will fail, returning the error, and the error state will be
350 : cleared. */
351 :
352 : void
353 : fd_http_server_printf( fd_http_server_t * http,
354 : char const * fmt,
355 : ... ) __attribute__((format (printf, 2, 3)));
356 :
357 : /* fd_http_server_memcpy appends the data provided to the end of the
358 : staging area of the outgoing ring buffer. Assumes http is a current
359 : local join.
360 :
361 : If appending to the ring buffer causes it to wrap around and
362 : overwrite existing data from a prior message, any connections which
363 : are still using data from the prior message will be evicted, as they
364 : cannot keep up.
365 :
366 : Once the full message has been appended into the outgoing ring buffer,
367 : the staged contents can be sent to all connected WebSocket clients of
368 : the HTTP server using fd_http_server_broadcast. This will end the
369 : current staged message so future prints go into a new message.
370 :
371 : Printing is not error-free, it is assumed that the format string is
372 : valid but the entire outgoing buffer may not be large enough to hold
373 : the printed string. In that case, the staging buffer is marked as
374 : being in an error state internally. The next call to send or
375 : broadcast will fail, returning the error, and the error state will be
376 : cleared. */
377 :
378 : void
379 : fd_http_server_memcpy( fd_http_server_t * http,
380 : uchar const * data,
381 : ulong data_len );
382 :
383 : /* fd_http_server_unstage unstages any data written into the staging
384 : buffer, clearing its contents. It does not advance the ring buffer
385 : usage, and no clients will be evicted. */
386 :
387 : void
388 : fd_http_server_unstage( fd_http_server_t * http );
389 :
390 : /* fd_http_server_stage_body marks the current contents of the staging
391 : buffer as the body of the response. The response is then ready to be
392 : sent to the client. Returns 0 on success and -1 on failure if the
393 : ring buffer is in an error state, and then clears the error state. */
394 :
395 : int
396 : fd_http_server_stage_body( fd_http_server_t * http,
397 : fd_http_server_response_t * response );
398 :
399 : /* Send the contents of the staging buffer as a a WebSocket message to a
400 : single client. The staging buffer is then cleared. Returns -1 on
401 : failure if the ring buffer is an error state, and then clears the
402 : error state.
403 :
404 : The contents are marked as needing to be sent to the client, but this
405 : does not block or wait for them to send, which happens async as the
406 : client is able to read. If the client reads too slow, and the
407 : staging buffer wraps around and is eventually overwritten by another
408 : printer, this client will be force disconnected as being too slow. */
409 :
410 : int
411 : fd_http_server_ws_send( fd_http_server_t * http,
412 : ulong ws_conn_id ); /* An existing, open connection. In [0, max_ws_connection_cnt) */
413 :
414 : /* Broadcast the contents of the staging buffer as a WebSocket message
415 : to all connected clients. The staging buffer is then cleared.
416 : Returns -1 on failure if the ring buffer is an error state, and then
417 : clears the error state.
418 :
419 : The contents are marked as needing to be sent to the client, but this
420 : does not block or wait for them to send, which happens async as the
421 : client is able to read. If the client reads too slow, and the
422 : staging buffer wraps around and is eventually overwritten by another
423 : printer, this client will be force disconnected as being too slow. */
424 :
425 : int
426 : fd_http_server_ws_broadcast( fd_http_server_t * http );
427 :
428 : /* fd_http_server_poll needs to be continuously called in a spin loop to
429 : drive the HTTP server forward. Setting poll_timeout==0 makes it a
430 : non-blocking socket ppoll(2). Returns 1 if there was any work to do
431 : on the HTTP server, or 0 otherwise. */
432 :
433 : int
434 : fd_http_server_poll( fd_http_server_t * http,
435 : int poll_timeout );
436 :
437 : FD_PROTOTYPES_END
438 :
439 : #endif /* HEADER_fd_src_ballet_http_fd_http_server_h */
|