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 { 22 bool operator()(const char* a, const char* b) const 23 { 24 return std::strcmp(a, b) < 0; 25 } 26 }; 27 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 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