Skip to content

Commit cd91658

Browse files
committed
Add secure reloading gzip file server example
1 parent d437169 commit cd91658

File tree

2 files changed

+217
-2
lines changed

2 files changed

+217
-2
lines changed

build.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ int main(int argc, char **argv) {
99
char *CXX = strncpy(calloc(1024, 1), or_else(getenv("CXX"), "g++"), 1024);
1010
char *EXEC_SUFFIX = strncpy(calloc(1024, 1), maybe(getenv("EXEC_SUFFIX")), 1024);
1111

12-
char *EXAMPLE_FILES[] = {"Precompress", "EchoBody", "HelloWorldThreaded", "Http3Server", "Broadcast", "HelloWorld", "Crc32", "ServerName",
12+
char *EXAMPLE_FILES[] = {"SecureGzipFileServer", "Precompress", "EchoBody", "HelloWorldThreaded", "Http3Server", "Broadcast", "HelloWorld", "Crc32", "ServerName",
1313
"EchoServer", "BroadcastingEchoServer", "UpgradeSync", "UpgradeAsync", "ParameterRoutes"};
1414

15-
strcat(CXXFLAGS, " -march=native -O3 -Wpedantic -Wall -Wextra -Wsign-conversion -Wconversion -std=c++20 -Isrc -IuSockets/src");
15+
strcat(CXXFLAGS, " -march=native -O3 -Wpedantic -Wall -Wextra -Wsign-conversion -Wconversion -std=c++23 -Isrc -IuSockets/src");
1616
strcat(LDFLAGS, " uSockets/*.o");
1717

1818
// We can use libdeflate as a fast path to zlib (you need to build it first)

examples/SecureGzipFileServer.cpp

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
#include <iostream>
2+
#include <map>
3+
#include <unordered_map>
4+
#include <mutex>
5+
#include <thread>
6+
#include <chrono>
7+
#include <filesystem>
8+
#include <fstream>
9+
#include <set>
10+
#include <sys/inotify.h>
11+
#include <unistd.h>
12+
#include <cstring>
13+
#include "App.h" // Ensure uWS is installed and this header is available
14+
15+
int cooldown = 0; // how much to sleep after reloading files, seconds
16+
17+
// Custom hasher for both std::string and std::string_view
18+
struct StringViewHasher {
19+
using is_transparent = void; // Enable transparent lookup
20+
21+
std::size_t operator()(const std::string& s) const {
22+
return std::hash<std::string_view>{}(s);
23+
}
24+
25+
std::size_t operator()(std::string_view s) const {
26+
return std::hash<std::string_view>{}(s);
27+
}
28+
};
29+
30+
// Custom equality comparator for transparent lookup
31+
struct StringViewEqual {
32+
using is_transparent = void; // Enable transparent lookup
33+
34+
// Single operator() that handles all combinations
35+
bool operator()(std::string_view lhs, std::string_view rhs) const {
36+
return lhs == rhs;
37+
}
38+
};
39+
40+
41+
// Global variables for the file map, mutex, and inotify file descriptor
42+
std::unordered_map<std::string, std::pair<std::string, bool>, StringViewHasher, StringViewEqual> file_map;
43+
std::mutex map_mutex;
44+
int inotify_fd;
45+
unsigned long fileSizes = 0;
46+
47+
#include <zlib.h>
48+
#include <stdexcept>
49+
50+
51+
std::pair<std::string, bool> load_file_content(const std::filesystem::path& path) {
52+
// Load file content
53+
std::ifstream file(path, std::ios::binary);
54+
if (!file) {
55+
std::cerr << "Failed to open file: " << path << std::endl;
56+
return {"", false};
57+
}
58+
file.seekg(0, std::ios::end);
59+
size_t size = file.tellg();
60+
file.seekg(0, std::ios::beg);
61+
std::string content(size, 0);
62+
file.read(&content[0], size);
63+
//std::cout << "Loaded file: " << path << " (" << size << " bytes)" << std::endl;
64+
65+
// Compress in memory using zlib
66+
z_stream zs = {};
67+
zs.zalloc = Z_NULL;
68+
zs.zfree = Z_NULL;
69+
zs.opaque = Z_NULL;
70+
if (deflateInit2(&zs, Z_BEST_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
71+
std::cerr << "Failed to initialize zlib" << std::endl;
72+
return {"", false};
73+
}
74+
75+
zs.next_in = reinterpret_cast<Bytef*>(content.data());
76+
zs.avail_in = content.size();
77+
78+
// Calculate output buffer size (zlib guarantees max output size)
79+
size_t bound = deflateBound(&zs, content.size());
80+
std::string compressed(bound, 0);
81+
zs.next_out = reinterpret_cast<Bytef*>(&compressed[0]);
82+
zs.avail_out = bound;
83+
84+
int ret = deflate(&zs, Z_FINISH);
85+
if (ret != Z_STREAM_END) {
86+
deflateEnd(&zs);
87+
std::cerr << "Failed to compress data" << std::endl;
88+
return {"", false};
89+
}
90+
91+
size_t compressed_size = zs.total_out;
92+
deflateEnd(&zs);
93+
94+
// Resize to actual compressed size
95+
compressed.resize(compressed_size);
96+
97+
if (compressed_size > size) {
98+
fileSizes += size;
99+
return {content, false};
100+
}
101+
102+
//std::cout << "Compressed to: " << compressed_size << " bytes" << std::endl;
103+
fileSizes += compressed_size;
104+
return {compressed, true};
105+
}
106+
107+
// Function to load all files from the root folder into the map and add inotify watches
108+
void load_files(const std::string& root, int inotify_fd) {
109+
fileSizes = 0;
110+
std::unordered_map<std::string, std::pair<std::string, bool>, StringViewHasher, StringViewEqual> new_map;
111+
for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) {
112+
if (entry.is_regular_file() && !entry.path().filename().string().starts_with(".")) {
113+
std::string relative_path = "/" + std::filesystem::relative(entry.path(), root).generic_string();
114+
auto [content, compressed] = load_file_content(entry.path());
115+
new_map[relative_path] = {content, compressed};
116+
// Add watch to file for IN_MODIFY
117+
inotify_add_watch(inotify_fd, entry.path().c_str(), IN_MODIFY);
118+
} else if (entry.is_directory()) {
119+
// Add watch to directory for IN_CREATE | IN_DELETE | IN_MOVE
120+
inotify_add_watch(inotify_fd, entry.path().c_str(), IN_CREATE | IN_DELETE | IN_MOVE);
121+
}
122+
}
123+
{
124+
std::lock_guard<std::mutex> lock(map_mutex);
125+
file_map = std::move(new_map);
126+
std::cout << "Reloaded " << (fileSizes / 1024 / 1024) << " MB of files into RAM" << std::endl;
127+
}
128+
}
129+
130+
// Background thread function to monitor inotify events and reload files on changes
131+
void inotify_reloader_function(const std::string& root, int inotify_fd) {
132+
load_files(root, inotify_fd);
133+
char buffer[4096];
134+
while (true) {
135+
int length = read(inotify_fd, buffer, sizeof(buffer));
136+
if (length < 0) {
137+
std::cerr << "Error reading inotify events: " << strerror(errno) << std::endl;
138+
break;
139+
}
140+
load_files(root, inotify_fd);
141+
if (cooldown) {
142+
std::cout << "Sleeping for " << cooldown << " seconds after reload" << std::endl;
143+
std::this_thread::sleep_for(std::chrono::seconds(cooldown));
144+
}
145+
}
146+
}
147+
148+
int main(int argc, char** argv) {
149+
if (argc != 3) {
150+
std::cerr << "Usage: " << argv[0] << " <root_folder> <cooldown>" << std::endl;
151+
return 1;
152+
}
153+
std::string root = argv[1];
154+
cooldown = std::stoi(argv[2]);
155+
if (cooldown < 0) {
156+
std::cerr << "Cooldown must be a positive integer" << std::endl;
157+
return 1;
158+
}
159+
160+
inotify_fd = inotify_init();
161+
if (inotify_fd < 0) {
162+
std::cerr << "Failed to initialize inotify: " << strerror(errno) << std::endl;
163+
return 1;
164+
}
165+
166+
std::thread inotify_reloader(inotify_reloader_function, root, inotify_fd);
167+
168+
uWS::App app;
169+
170+
// We only need to lock once per event loop iteration, not on every request
171+
uWS::Loop::get()->addPostHandler(&inotify_reloader, [](uWS::Loop */*loop*/) {
172+
std::lock_guard<std::mutex> lock(map_mutex);
173+
});
174+
175+
uWS::Loop::get()->addPreHandler(&inotify_reloader, [](uWS::Loop */*loop*/) {
176+
std::lock_guard<std::mutex> lock(map_mutex);
177+
});
178+
179+
app.get("/", [](auto* res, auto* req) {
180+
auto it = file_map.find(std::string_view("/index.html", 11));
181+
if (it != file_map.end()) {
182+
if (it->second.second) {
183+
res->writeHeader("Content-Encoding", "gzip");
184+
}
185+
res->end(it->second.first);
186+
} else {
187+
res->writeStatus("404 Not Found");
188+
res->end("Not Found");
189+
}
190+
});
191+
app.get("/*", [](auto* res, auto* req) {
192+
auto it = file_map.find(req->getUrl());
193+
if (it != file_map.end()) {
194+
if (it->second.second) {
195+
res->writeHeader("Content-Encoding", "gzip");
196+
}
197+
res->end(it->second.first);
198+
} else {
199+
res->writeStatus("404 Not Found");
200+
res->end("Not Found");
201+
}
202+
});
203+
204+
app.listen(8000, [](auto* token) {
205+
if (token) {
206+
std::cout << "Listening on port 8000" << std::endl;
207+
}
208+
});
209+
210+
app.run();
211+
212+
inotify_reloader.join();
213+
close(inotify_fd);
214+
return 0;
215+
}

0 commit comments

Comments
 (0)