1 #pragma once
2
3 #include "app.hpp"
4 #include "http_request.hpp"
5 #include "http_response.hpp"
6 #include "routing.hpp"
7 #include "webroutes.hpp"
8
9 #include <boost/container/flat_set.hpp>
10
11 #include <filesystem>
12 #include <fstream>
13 #include <string>
14
15 namespace crow
16 {
17 namespace webassets
18 {
19
20 struct CmpStr
21 {
operator ()crow::webassets::CmpStr22 bool operator()(const char* a, const char* b) const
23 {
24 return std::strcmp(a, b) < 0;
25 }
26 };
27
getStaticEtag(const std::filesystem::path & webpath)28 inline std::string getStaticEtag(const std::filesystem::path& webpath)
29 {
30 // webpack outputs production chunks in the form:
31 // <filename>.<hash>.<extension>
32 // For example app.63e2c453.css
33 // Try to detect this, so we can use the hash as the ETAG
34 std::vector<std::string> split;
35 bmcweb::split(split, webpath.filename().string(), '.');
36 BMCWEB_LOG_DEBUG("Checking {} split.size() {}", webpath.filename().string(),
37 split.size());
38 if (split.size() < 3)
39 {
40 return "";
41 }
42
43 // get the second to last element
44 std::string hash = split.rbegin()[1];
45
46 // Webpack hashes are 8 characters long
47 if (hash.size() != 8)
48 {
49 return "";
50 }
51 // Webpack hashes only include hex printable characters
52 if (hash.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos)
53 {
54 return "";
55 }
56 return std::format("\"{}\"", hash);
57 }
58
requestRoutes(App & app)59 inline void requestRoutes(App& app)
60 {
61 constexpr static std::array<std::pair<const char*, const char*>, 17>
62 contentTypes{
63 {{".css", "text/css;charset=UTF-8"},
64 {".html", "text/html;charset=UTF-8"},
65 {".js", "application/javascript;charset=UTF-8"},
66 {".png", "image/png;charset=UTF-8"},
67 {".woff", "application/x-font-woff"},
68 {".woff2", "application/x-font-woff2"},
69 {".gif", "image/gif"},
70 {".ico", "image/x-icon"},
71 {".ttf", "application/x-font-ttf"},
72 {".svg", "image/svg+xml"},
73 {".eot", "application/vnd.ms-fontobject"},
74 {".xml", "application/xml"},
75 {".json", "application/json"},
76 {".jpg", "image/jpeg"},
77 {".jpeg", "image/jpeg"},
78 // dev tools don't care about map type, setting to json causes
79 // browser to show as text
80 // https://stackoverflow.com/questions/19911929/what-mime-type-should-i-use-for-javascript-source-map-files
81 {".map", "application/json"}}};
82
83 std::filesystem::path rootpath{"/usr/share/www/"};
84
85 std::error_code ec;
86
87 std::filesystem::recursive_directory_iterator dirIter(rootpath, ec);
88 if (ec)
89 {
90 BMCWEB_LOG_ERROR(
91 "Unable to find or open {} static file hosting disabled",
92 rootpath.string());
93 return;
94 }
95
96 // In certain cases, we might have both a gzipped version of the file AND a
97 // non-gzipped version. To avoid duplicated routes, we need to make sure we
98 // get the gzipped version first. Because the gzipped path should be longer
99 // than the non gzipped path, if we sort in descending order, we should be
100 // guaranteed to get the gzip version first.
101 std::vector<std::filesystem::directory_entry> paths(
102 std::filesystem::begin(dirIter), std::filesystem::end(dirIter));
103 std::sort(paths.rbegin(), paths.rend());
104
105 for (const std::filesystem::directory_entry& dir : paths)
106 {
107 const std::filesystem::path& absolutePath = dir.path();
108 std::filesystem::path relativePath{
109 absolutePath.string().substr(rootpath.string().size() - 1)};
110 if (std::filesystem::is_directory(dir))
111 {
112 // don't recurse into hidden directories or symlinks
113 if (dir.path().filename().string().starts_with(".") ||
114 std::filesystem::is_symlink(dir))
115 {
116 dirIter.disable_recursion_pending();
117 }
118 }
119 else if (std::filesystem::is_regular_file(dir))
120 {
121 std::string extension = relativePath.extension();
122 std::filesystem::path webpath = relativePath;
123 const char* contentEncoding = nullptr;
124
125 if (extension == ".gz")
126 {
127 webpath = webpath.replace_extension("");
128 // Use the non-gzip version for determining content type
129 extension = webpath.extension().string();
130 contentEncoding = "gzip";
131 }
132
133 std::string etag = getStaticEtag(webpath);
134
135 bool renamed = false;
136 if (webpath.filename().string().starts_with("index."))
137 {
138 webpath = webpath.parent_path();
139 if (webpath.string().empty() || webpath.string().back() != '/')
140 {
141 // insert the non-directory version of this path
142 webroutes::routes.insert(webpath);
143 webpath += "/";
144 renamed = true;
145 }
146 }
147
148 std::pair<boost::container::flat_set<std::string>::iterator, bool>
149 inserted = webroutes::routes.insert(webpath);
150
151 if (!inserted.second)
152 {
153 // Got a duplicated path. This is expected in certain
154 // situations
155 BMCWEB_LOG_DEBUG("Got duplicated path {}", webpath.string());
156 continue;
157 }
158 const char* contentType = nullptr;
159
160 for (const std::pair<const char*, const char*>& ext : contentTypes)
161 {
162 if (ext.first == nullptr || ext.second == nullptr)
163 {
164 continue;
165 }
166 if (extension == ext.first)
167 {
168 contentType = ext.second;
169 }
170 }
171
172 if (contentType == nullptr)
173 {
174 BMCWEB_LOG_ERROR(
175 "Cannot determine content-type for {} with extension {}",
176 absolutePath.string(), extension);
177 }
178
179 if (webpath == "/")
180 {
181 forward_unauthorized::hasWebuiRoute = true;
182 }
183
184 app.routeDynamic(webpath)(
185 [absolutePath, contentType, contentEncoding, etag,
186 renamed](const crow::Request& req,
187 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
188 if (contentType != nullptr)
189 {
190 asyncResp->res.addHeader(
191 boost::beast::http::field::content_type, contentType);
192 }
193
194 if (contentEncoding != nullptr)
195 {
196 asyncResp->res.addHeader(
197 boost::beast::http::field::content_encoding,
198 contentEncoding);
199 }
200
201 if (!etag.empty())
202 {
203 asyncResp->res.addHeader(boost::beast::http::field::etag,
204 etag);
205 // Don't cache paths that don't have the etag in them, like
206 // index, which gets transformed to /
207 if (!renamed)
208 {
209 // Anything with a hash can be cached forever and is
210 // immutable
211 asyncResp->res.addHeader(
212 boost::beast::http::field::cache_control,
213 "max-age=31556926, immutable");
214 }
215
216 std::string_view cachedEtag = req.getHeaderValue(
217 boost::beast::http::field::if_none_match);
218 if (cachedEtag == etag)
219 {
220 asyncResp->res.result(
221 boost::beast::http::status::not_modified);
222 return;
223 }
224 }
225
226 // res.set_header("Cache-Control", "public, max-age=86400");
227 if (!asyncResp->res.openFile(absolutePath))
228 {
229 BMCWEB_LOG_DEBUG("failed to read file");
230 asyncResp->res.result(
231 boost::beast::http::status::internal_server_error);
232 return;
233 }
234 });
235 }
236 }
237 }
238 } // namespace webassets
239 } // namespace crow
240