Condy v1.5.0
C++ Asynchronous System Call Layer for Linux
Loading...
Searching...
No Matches
file-server.cpp
Go to the documentation of this file.
1
5
6#include "condy/buffers.hpp"
7#include <arpa/inet.h>
8#include <condy.hpp>
9#include <cstdint>
10#include <cstdio>
11#include <cstring>
12#include <format>
13#include <iostream>
14#include <linux/stat.h>
15#include <netinet/in.h>
16#include <sys/socket.h>
17#include <unistd.h>
18
19static std::string bind_address = "0.0.0.0";
20static std::string serve_directory = ".";
21static uint16_t port = 8080;
22
23static const char HTTP_200_TEMPLATE[] = "HTTP/1.1 200 OK\r\n"
24 "Content-Length: %d\r\n"
25 "\r\n";
26
27static const char HTTP_400[] = "HTTP/1.1 400 Bad Request\r\n"
28 "Content-Length: 0\r\n"
29 "\r\n";
30
31static const char HTTP_404[] = "HTTP/1.1 404 Not Found\r\n"
32 "Content-Length: 0\r\n"
33 "\r\n";
34
35static const char HTTP_500[] = "HTTP/1.1 500 Internal Server Error\r\n"
36 "Content-Length: 0\r\n"
37 "\r\n";
38
39constexpr size_t BACKLOG = 128;
40
41condy::Coro<void> sendfile(int out_fd, int in_fd) {
42 constexpr size_t CHUNK_SIZE = 8192;
43
44 int pipe_fds[2];
45 if (pipe(pipe_fds) != 0) {
46 std::perror("Failed to create pipe");
47 exit(1);
48 }
49
50 while (true) {
51 int n = co_await condy::async_splice(in_fd, -1, pipe_fds[1], -1,
52 CHUNK_SIZE, 0);
53 if (n == 0) { // End of file
54 co_return;
55 }
56 if (n < 0) {
57 std::cerr << std::format("Error during splice operation: {}\n", n);
58 exit(1);
59 }
60
61 n = co_await condy::async_splice(pipe_fds[0], -1, out_fd, -1,
62 CHUNK_SIZE, 0);
63 if (n < 0) {
64 std::cerr << std::format("Error during splice operation: {}\n", n);
65 exit(1);
66 }
67 }
68
69 co_await condy::async_close(pipe_fds[0]);
70 co_await condy::async_close(pipe_fds[1]);
71}
72
73std::string parse_request(const std::string &request) {
74 size_t pos = request.find("\r\n");
75 if (pos == std::string::npos) {
76 return "";
77 }
78
79 std::string request_line = request.substr(0, pos);
80 size_t method_end = request_line.find(' ');
81 if (method_end == std::string::npos) {
82 return "";
83 }
84
85 size_t path_start = method_end + 1;
86 size_t path_end = request_line.find(' ', path_start);
87 if (path_end == std::string::npos) {
88 return "";
89 }
90
91 return request_line.substr(path_start, path_end - path_start);
92}
93
94condy::Coro<void> session(int client_fd) {
95 char buffer[1024];
96 int n = co_await condy::async_recv(client_fd, condy::buffer(buffer), 0);
97 if (n < 0) {
98 std::cerr << std::format("Read error: {}\n", n);
99 co_await condy::async_close(client_fd);
100 co_return;
101 }
102
103 std::string request(buffer, n);
104
105 // Just for simplicity, we only partially parse the HTTP request here.
106 std::string path = parse_request(request);
107 if (path.empty()) {
108 co_await condy::async_send(
109 client_fd, condy::buffer(HTTP_400, sizeof(HTTP_400) - 1), 0);
110 co_await condy::async_close(client_fd);
111 co_return;
112 }
113 if (path == "/") {
114 path = "/index.html";
115 }
116 std::string file_path = serve_directory + path;
117 int file_fd = co_await condy::async_open(file_path.c_str(), O_RDONLY, 0);
118 if (file_fd < 0) {
119 co_await condy::async_send(
120 client_fd, condy::buffer(HTTP_404, sizeof(HTTP_404) - 1), 0);
121 co_await condy::async_close(client_fd);
122 co_return;
123 }
124
125 struct statx statx_buf;
126 int r_stat = co_await condy::async_statx(
127 file_fd, "", AT_EMPTY_PATH | AT_STATX_SYNC_AS_STAT,
128 STATX_SIZE | STATX_MODE, &statx_buf);
129 if (r_stat < 0) {
130 std::cerr << std::format("Failed to statx file: {}\n", r_stat);
131 co_await condy::async_send(
132 client_fd, condy::buffer(HTTP_500, sizeof(HTTP_500) - 1), 0);
133 co_await condy::async_close(file_fd);
134 co_await condy::async_close(client_fd);
135 co_return;
136 }
137 if (!S_ISREG(statx_buf.stx_mode)) {
138 co_await condy::async_send(
139 client_fd, condy::buffer(HTTP_404, sizeof(HTTP_404) - 1), 0);
140 co_await condy::async_close(file_fd);
141 co_await condy::async_close(client_fd);
142 co_return;
143 }
144
145 char header_buffer[128];
146 int header_length =
147 std::snprintf(header_buffer, sizeof(header_buffer), HTTP_200_TEMPLATE,
148 static_cast<int>(statx_buf.stx_size));
149 co_await condy::async_send(client_fd,
150 condy::buffer(header_buffer, header_length), 0);
151 co_await sendfile(client_fd, file_fd);
152 co_await condy::async_close(file_fd);
153 co_await condy::async_close(client_fd);
154}
155
156condy::Coro<int> co_main(int server_fd) {
157 while (true) {
158 sockaddr_in client_addr;
159 socklen_t client_len = sizeof(client_addr);
160 int client_fd = co_await condy::async_accept(
161 server_fd, (struct sockaddr *)&client_addr, &client_len, 0);
162 if (client_fd < 0) {
163 std::cerr << std::format("Failed to accept connection: {}\n",
164 client_fd);
165 co_return 1;
166 }
167
168 condy::co_spawn(session(client_fd)).detach();
169 }
170}
171
172void prepare_address(const std::string &host, uint16_t port,
173 sockaddr_in &addr) {
174 memset(&addr, 0, sizeof(addr));
175 addr.sin_family = AF_INET;
176 addr.sin_port = htons(port);
177 inet_pton(AF_INET, host.c_str(), &addr.sin_addr);
178}
179
180void usage(const char *prog_name) {
181 std::cerr << std::format(
182 "Usage: {} [-h] [-b <address>] [-d <directory>] [-p <port>]\n"
183 " -h Show this help message\n"
184 " -b <address> Bind to the specified address (default: 0.0.0.0)\n"
185 " -d <directory> Serve directory (default: current directory)\n"
186 " -p <port> Port number to listen on (default: 8080)\n",
187 prog_name);
188}
189
190int main(int argc, char **argv) noexcept(false) {
191 int opt;
192 while ((opt = getopt(argc, argv, "hb:d:p:")) != -1) {
193 switch (opt) {
194 case 'h':
195 usage(argv[0]);
196 return 0;
197 case 'b':
198 bind_address = optarg;
199 break;
200 case 'd':
201 serve_directory = optarg;
202 break;
203 case 'p':
204 port = static_cast<uint16_t>(std::stoi(optarg));
205 break;
206 default:
207 usage(argv[0]);
208 return 1;
209 }
210 }
211
212 sockaddr_in server_addr;
213 prepare_address(bind_address, port, server_addr);
214
215 int server_fd = socket(AF_INET, SOCK_STREAM, 0);
216 if (server_fd < 0) {
217 std::perror("Failed to create socket");
218 return 1;
219 }
220
221 int optval = 1;
222 if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &optval,
223 sizeof(optval)) < 0) {
224 std::perror("Failed to set socket options");
225 close(server_fd);
226 return 1;
227 }
228
229 if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) <
230 0) {
231 std::perror("Failed to bind socket");
232 close(server_fd);
233 return 1;
234 }
235
236 if (listen(server_fd, BACKLOG) < 0) {
237 std::perror("Failed to listen on socket");
238 close(server_fd);
239 return 1;
240 }
241
242 std::cout << std::format("Serving HTTP on port {} (http://{}:{}/) ...\n",
243 port, bind_address, port);
244
245 return condy::sync_wait(co_main(server_fd));
246}
Basic buffer types and conversion utilities.
Coroutine type used to define a coroutine function.
Definition coro.hpp:26
Main include file for the Condy library.
auto async_splice(Fd1 fd_in, int64_t off_in, Fd2 fd_out, int64_t off_out, unsigned int nbytes, unsigned int splice_flags)
See io_uring_prep_splice.
T sync_wait(Runtime &runtime, Coro< T, Allocator > coro)
Synchronously wait for a coroutine to complete in the given runtime.
Definition sync_wait.hpp:24
auto async_statx(int dfd, const char *path, int flags, unsigned mask, struct statx *statxbuf)
See io_uring_prep_statx.
auto async_recv(Fd sockfd, Buffer &&buf, int flags)
See io_uring_prep_recv.
MutableBuffer buffer(void *data, size_t size)
Create a buffer object from various data sources.
Definition buffers.hpp:85
auto async_close(int fd)
See io_uring_prep_close.
auto async_open(const char *path, int flags, mode_t mode)
See io_uring_prep_openat.
auto async_send(Fd sockfd, Buffer &&buf, int flags)
See io_uring_prep_send.
Task< T, Allocator > co_spawn(Runtime &runtime, Coro< T, Allocator > coro)
Spawn a coroutine as a task in the given runtime.
Definition task.hpp:266
auto async_accept(Fd fd, struct sockaddr *addr, socklen_t *addrlen, int flags)
See io_uring_prep_accept.