Skip to content

Commit 7583283

Browse files
committed
new file server
1 parent cfe7e81 commit 7583283

17 files changed

Lines changed: 1974 additions & 17 deletions

File tree

examples/static_server/main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ using namespace Lambda;
66

77
int main(int argc, char const *argv[]) {
88

9-
StaticServer sserver("examples/static_server/sample_dist");
9+
FileServer sserver("examples/static_server/sample_dist");
1010

1111
auto handler = [&](const Request& req, const Context& context) {
1212
return sserver.serve(req);

extra/staticserver/staticserver.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ std::vector<uint8_t> readBinaryFileSync(const std::string& filepath) {
1818
return std::vector<uint8_t>(std::istreambuf_iterator<char>(readablefile), {});
1919
}
2020

21-
std::string StaticServer::flattenPathName(std::string urlpath) const noexcept {
21+
std::string FileServer::flattenPathName(std::string urlpath) const noexcept {
2222

2323
auto segmentPos = std::string::npos;
2424
while ((segmentPos = urlpath.find("\\")) != std::string::npos) {
@@ -53,7 +53,7 @@ std::string StaticServer::flattenPathName(std::string urlpath) const noexcept {
5353
return urlpath;
5454
}
5555

56-
StaticServer::StaticServer(const std::string& rootDir) {
56+
FileServer::FileServer(const std::string& rootDir) {
5757

5858
this->m_root = rootDir;
5959

@@ -70,7 +70,7 @@ StaticServer::StaticServer(const std::string& rootDir) {
7070
}
7171
}
7272

73-
std::optional<std::string> StaticServer::resolvePath(const std::string& pathname) const noexcept {
73+
std::optional<std::string> FileServer::resolvePath(const std::string& pathname) const noexcept {
7474

7575
auto basePath = this->m_root + (pathname.starts_with('/') ? pathname : ('/' + pathname));
7676

@@ -109,11 +109,11 @@ std::optional<std::string> StaticServer::resolvePath(const std::string& pathname
109109
return std::nullopt;
110110
}
111111

112-
HTTP::Response StaticServer::serve(const HTTP::Request& request) const noexcept {
112+
HTTP::Response FileServer::serve(const HTTP::Request& request) const noexcept {
113113
return serve(request.url.pathname);
114114
}
115115

116-
HTTP::Response StaticServer::serve(const std::string& pathname) const noexcept {
116+
HTTP::Response FileServer::serve(const std::string& pathname) const noexcept {
117117

118118
auto normalizedPathname = flattenPathName(pathname);
119119
auto resolvedFile = this->resolvePath(normalizedPathname);

extra/staticserver/staticserver.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99

1010
namespace Lambda {
1111

12-
class StaticServer {
12+
class FileServer {
1313
private:
1414
std::string m_root;
1515

1616
public:
17-
StaticServer(const std::string& rootDir);
17+
FileServer(const std::string& rootDir);
1818

1919
HTTP::Response serve(const HTTP::Request& request) const noexcept;
2020
HTTP::Response serve(const std::string& pathname) const noexcept;

v3/fs/fs.hpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#ifndef __LAMBDA_FILESERVER__
2+
#define __LAMBDA_FILESERVER__
3+
4+
#include <string>
5+
#include <optional>
6+
#include <vector>
7+
#include <memory>
8+
#include <fstream>
9+
#include <filesystem>
10+
11+
#include "../http/http.hpp"
12+
13+
namespace Lambda {
14+
15+
class ServedFile {
16+
public:
17+
18+
enum struct Type {
19+
File,
20+
Directory
21+
};
22+
23+
virtual std::string name() const = 0;
24+
virtual Type type() const noexcept = 0;
25+
virtual time_t modified() = 0;
26+
virtual size_t size() = 0;
27+
28+
virtual std::vector<uint8_t> content() = 0;
29+
virtual std::vector<uint8_t> content(size_t begin, size_t end) = 0;
30+
};
31+
32+
class FileServerReader {
33+
public:
34+
virtual std::unique_ptr<ServedFile> open(const std::string& filename) = 0;
35+
};
36+
37+
class FsDirectoryServe : public FileServerReader {
38+
private:
39+
std::filesystem::path m_root;
40+
41+
public:
42+
FsDirectoryServe(const std::string& root_dir);
43+
std::unique_ptr<ServedFile> open(const std::string& filename);
44+
};
45+
46+
class FileServer {
47+
private:
48+
std::unique_ptr<FileServerReader> m_reader;
49+
void handle_request(Request& req, ResponseWriter& wrt);
50+
51+
public:
52+
bool html_error_pages = true;
53+
bool debug = false;
54+
55+
FileServer(FileServerReader* reader);
56+
57+
// todo: fix lifetime
58+
HandlerFn handler() noexcept;
59+
};
60+
61+
namespace Fs {
62+
const std::string& infer_mimetype(const std::string& filename);
63+
};
64+
};
65+
66+
#endif

v3/fs/fs_handler.cpp

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#include "./fs.hpp"
2+
#include "../server/server.hpp"
3+
4+
#include <filesystem>
5+
6+
using namespace Lambda;
7+
8+
std::string flatten_path(const std::string& path);
9+
void log_access(const Request& req, Status status);
10+
11+
// generated fn
12+
std::string render_404_page(const std::string& requeted_url);
13+
14+
FileServer::FileServer(FileServerReader* reader) {
15+
16+
if (!reader) {
17+
std::logic_error("FileServerReader reader is null");
18+
}
19+
20+
this->m_reader = std::unique_ptr<FileServerReader>(reader);
21+
}
22+
23+
void FileServer::handle_request(Request& req, ResponseWriter& wrt) {
24+
25+
switch (req.method) {
26+
27+
case Method::HEAD: break;
28+
case Method::GET: break;
29+
30+
case Method::OPTIONS: {
31+
32+
wrt.header().set("allow", "OPTIONS, GET, HEAD");
33+
wrt.write_header(Status::NoContent);
34+
35+
if (this->debug) {
36+
log_access(req, Status::NoContent);
37+
}
38+
39+
} return;
40+
41+
default: {
42+
43+
wrt.write_header(Status::MethodNotAllowed);
44+
45+
if (this->debug) {
46+
log_access(req, Status::MethodNotAllowed);
47+
}
48+
49+
} return;
50+
}
51+
52+
// flatten request path (remove segments like "./", "/../")
53+
auto flattened = flatten_path(req.url.path);
54+
if (flattened != req.url.path) {
55+
56+
wrt.header().set("location", flattened);
57+
wrt.write_header(Status::PermanentRedirect);
58+
59+
if (this->debug) {
60+
log_access(req, Status::PermanentRedirect);
61+
}
62+
63+
return;
64+
}
65+
66+
// apply directory redirect
67+
auto fs_file_path = req.url.path;
68+
if (fs_file_path.ends_with('/')) {
69+
fs_file_path.append("index.html");
70+
}
71+
72+
auto file_hit = this->m_reader->open(fs_file_path);
73+
if (!file_hit) {
74+
75+
auto response_body = this->html_error_pages ? render_404_page(req.url.path) : ("path '" + req.url.path + "' not found");
76+
77+
if (req.method != Method::HEAD) {
78+
wrt.header().set("content-type", this->html_error_pages ? "text/html" : "text/plain");
79+
wrt.header().set("content-length", std::to_string(response_body.size()));
80+
}
81+
82+
wrt.write_header(Status::NotFound);
83+
84+
if (req.method != Method::HEAD) {
85+
wrt.write(response_body);
86+
}
87+
88+
if (this->debug) {
89+
log_access(req, Status::NotFound);
90+
}
91+
92+
return;
93+
}
94+
95+
// return redirect for directories
96+
if (file_hit->type() == ServedFile::Type::Directory) {
97+
98+
if (!req.url.path.ends_with('/')) {
99+
req.url.path.push_back('/');
100+
}
101+
102+
wrt.header().set("location", req.url.to_string());
103+
wrt.write_header(Status::Found);
104+
105+
if (this->debug) {
106+
log_access(req, Status::Found);
107+
}
108+
109+
return;
110+
}
111+
112+
// todo: add caching/etag support
113+
114+
wrt.header().set("content-type", Fs::infer_mimetype(file_hit->name()));
115+
wrt.header().set("last-modified", Date(file_hit->modified()).to_utc_string());
116+
wrt.header().set("content-length", std::to_string(file_hit->size()));
117+
wrt.write_header(Status::OK);
118+
119+
if (this->debug) {
120+
log_access(req, Status::OK);
121+
}
122+
123+
if (req.method == Method::HEAD || !file_hit->size()) {
124+
return;
125+
}
126+
127+
// todo: support chunked reads
128+
129+
wrt.write(file_hit->content());
130+
}
131+
132+
HandlerFn FileServer::handler() noexcept {
133+
return [&](Request& req, ResponseWriter& wrt) -> void {
134+
this->handle_request(req, wrt);
135+
};
136+
}
137+
138+
std::string flatten_path(const std::string& path) {
139+
140+
auto normalized = std::filesystem::path(path).lexically_normal();
141+
if (normalized.preferred_separator == '/') {
142+
return normalized.string();
143+
}
144+
145+
auto normalized_string = normalized.string();
146+
147+
for (auto& rune : normalized_string) {
148+
if (rune == normalized.preferred_separator) {
149+
rune = '/';
150+
}
151+
}
152+
153+
return normalized_string;
154+
}
155+
156+
void log_access(const Request& req, Status status) {
157+
158+
auto unwrap_method = [](Method method) {
159+
switch (method) {
160+
case Method::GET: return "GET";
161+
case Method::POST: return "POST";
162+
case Method::PATCH: return "PATCH";
163+
case Method::PUT: return "PUT";
164+
case Method::DEL: return "DEL";
165+
case Method::OPTIONS: return "OPTIONS";
166+
case Method::HEAD: return "HEAD";
167+
case Method::TRACE: return "CONNECT";
168+
case Method::CONNECT: return "CONNECT";
169+
default: return "NULL";
170+
}
171+
};
172+
173+
fprintf(stdout, "%s DEBUG Lambda::Fileserver %s %s %s -> %i\n",
174+
Date().to_log_string().c_str(),
175+
req.remote_addr.hostname.c_str(),
176+
unwrap_method(req.method),
177+
req.url.path.c_str(),
178+
static_cast<std::underlying_type_t<Status>>(status)
179+
);
180+
}
181+
182+
// generated fn
183+
std::string render_404_page(const std::string& requeted_url) {
184+
return (
185+
"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>404 | Not found</title><style> * { box-sizing: border-box; margin: 0; padding: 0; } body { display: flex; flex-direction: column; height: 100vh; align-items: center; justify-content: center; font-family: sans-serif; color: black; background-color: white; } @media (prefers-color-scheme: dark) { body { color: whitesmoke; background-color: #171717; } } .message { display: flex; flex-direction: row; gap: 1.25rem; align-items: center; justify-content: flex-start; flex-shrink: 0; } .status-code { font-weight: 400; font-size: 6rem; } .content { display: flex; flex-direction: column; gap: 0.625rem; align-items: flex-start; justify-content: flex-start; flex-shrink: 0; max-width: 20rem; } .message-title { font-weight: 400; font-size: 2.25rem; } .message-content { font-weight: 400; font-size: 1rem; } </style></head><body><div class=\"message\"><div class=\"status-code\">404</div><div class=\"content\"><div class=\"message-title\">Not found</div><div class=\"message-content\">The requested URL '" + requeted_url + "' doesn't point to any valid resources</div></div></div></body></html>"
186+
);
187+
}

0 commit comments

Comments
 (0)