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