Haio 0.2.X
Homebrew Asset IO/CDN
Loading...
Searching...
No Matches
config.cpp
Go to the documentation of this file.
1#include <haio_cdn.hpp>
2
3#include <boost/url/parse.hpp>
4#include <boost/url/url.hpp>
5
6#include <algorithm>
7#include <cctype>
8#include <fstream>
9#include <iostream>
10#include <sstream>
11#include <chrono>
12#include <cstdlib>
13#include <initializer_list>
14#include <stdexcept>
15
16namespace urls = boost::urls;
17
18namespace {
19
20std::string trim(std::string value) {
21 auto is_space = [](unsigned char c) { return std::isspace(c); };
22 value.erase(value.begin(), std::find_if_not(value.begin(), value.end(), is_space));
23 value.erase(std::find_if_not(value.rbegin(), value.rend(), is_space).base(), value.end());
24 return value;
25}
26
27std::string unquote(std::string value) {
28 value = trim(std::move(value));
29 if (value.size() >= 2 && ((value.front() == '"' && value.back() == '"') || (value.front() == '\'' && value.back() == '\''))) {
30 return value.substr(1, value.size() - 2);
31 }
32 return value;
33}
34
35unsigned short parsePort(const std::string& value) {
36 try {
37 const auto number = std::stoi(value);
38 if (number > 0 && number <= 65535) return static_cast<unsigned short>(number);
39 } catch (const std::exception&) {
40 }
41 throw std::runtime_error("port must be a number between 1 and 65535, got: " + value);
42}
43
44/** file://relative keeps the authority as the first path segment, file:///absolute has none */
45std::filesystem::path fileRoot(const urls::url_view_base& url, const std::string& name) {
46 const auto host = std::string(url.host());
47 const auto path = std::string(url.path());
48
49 if (host.empty()) {
50 if (path.empty() || path == "/") {
51 throw std::runtime_error("file bucket \"" + name + "\" has no directory in its url");
52 }
53 return std::filesystem::path(path);
54 }
55 return std::filesystem::path(host + path);
56}
57
58std::string hostRegion(const std::string& host);
59
60/**
61 * most explicit first: what the url says outright, then what an amazon host implies,
62 * then the environment every other aws tool reads.
63 *
64 * it is settled once, while the config loads, so a bucket whose region nobody can
65 * work out fails at startup. guessing it wrong would instead sign a request that
66 * comes back as a plain 403, which says nothing about the region being the problem.
67 */
68std::string regionOf(const urls::url_view_base& url) {
69 for (const auto param : url.params()) {
70 if (param.key == "region" && !param.value.empty()) return param.value;
71 }
72
73 if (const auto found = hostRegion(std::string(url.host())); !found.empty()) return found;
74
75 const char* fromEnv = std::getenv("AWS_DEFAULT_REGION");
76 return fromEnv ? fromEnv : std::string{};
77}
78
79/** s3.eu-west-1.amazonaws.com, and bucket.s3.eu-west-1.amazonaws.com, both name it */
80std::string hostRegion(const std::string& host) {
81 const auto marker = host.find(".s3.");
82 const auto from = marker != std::string::npos ? marker + 4
83 : host.starts_with("s3.") ? size_t{3}
84 : std::string::npos;
85 if (from == std::string::npos) return {};
86
87 const auto rest = host.substr(from);
88 const auto dot = rest.find('.');
89 if (dot == std::string::npos) return {};
90
91 const auto candidate = rest.substr(0, dot);
92 return candidate == "amazonaws" ? std::string{} : candidate;
93}
94
95void applyUrl(Haio::Cdn::BucketConfig& bucket) {
96 if (bucket.url.empty()) {
97 throw std::runtime_error("bucket \"" + bucket.name + "\" has no url");
98 }
99
100 const auto parsed = urls::parse_uri_reference(bucket.url);
101 if (!parsed) {
102 throw std::runtime_error("bucket \"" + bucket.name + "\" has an invalid url: " + bucket.url);
103 }
104
105 bucket.scheme = std::string(parsed->scheme());
106 bucket.open = parsed->host() == "*";
107
108 if (bucket.scheme == "file") {
109 if (bucket.open) {
110 throw std::runtime_error("a file bucket cannot be open: " + bucket.url);
111 }
112 bucket.root = fileRoot(*parsed, bucket.name);
113 return;
114 }
115
116 if (bucket.scheme == "s3") {
117 // a signature is made for one host, so there is nothing sensible to sign for
118 // a bucket whose host arrives with the request
119 if (bucket.open) throw std::runtime_error("an s3 bucket cannot be open: " + bucket.url);
120
121 bucket.region = regionOf(*parsed);
122 if (bucket.region.empty()) {
123 throw std::runtime_error("cannot tell the region of s3 bucket \"" + bucket.name
124 + "\"; name it in the host, as in s3.eu-west-1.amazonaws.com, "
125 "or add ?region=<name> to its url, or set AWS_DEFAULT_REGION");
126 }
127 return;
128 }
129 if (bucket.scheme == "http" || bucket.scheme == "https") return;
130
131 // "//\*" carries no scheme on purpose: the request supplies it
132 if (bucket.scheme.empty()) {
133 if (!bucket.open) {
134 throw std::runtime_error("bucket \"" + bucket.name + "\" needs a scheme in its url: " + bucket.url);
135 }
136 return;
137 }
138
139 throw std::runtime_error("unsupported url scheme \"" + bucket.scheme + "\" in bucket \"" + bucket.name + "\"");
140}
141
142/**
143 * a key nobody reads is almost always a typo, and silently ignoring it is how a cache
144 * ends up switched off without anybody noticing. so an unknown key stops the server
145 * at startup and says what it could have meant.
146 */
147std::runtime_error unknownKey(const std::string& where, const std::string& key, std::initializer_list<std::string_view> known) {
148 std::string message = "unknown key \"" + key + "\" in " + where + "; it takes ";
149 for (auto it = known.begin(); it != known.end(); ++it) {
150 if (it != known.begin()) message += it + 1 == known.end() ? " and " : ", ";
151 message += std::string(*it);
152 }
153 return std::runtime_error(message);
154}
155
156bool parseFlag(const std::string& key, const std::string& value) {
157 if (value == "true" || value == "yes" || value == "1") return true;
158 if (value == "false" || value == "no" || value == "0") return false;
159 throw std::runtime_error(key + " must be true or false, got: " + value);
160}
161
162/** a count, a number of seconds, a number of megabytes: no units to misspell */
163size_t parseCount(const std::string& key, const std::string& value) {
164 try {
165 const auto number = std::stoll(value);
166 if (number < 0) throw std::runtime_error("negative");
167 return static_cast<size_t>(number);
168 } catch (const std::exception&) {
169 throw std::runtime_error(key + " must be a whole number, got: " + value);
170 }
171}
172
173/** file://dir, redis://host, or nothing at all, which keeps entries in this process */
174void checkCacheUrl(const std::string& value) {
175 const auto parsed = urls::parse_uri_reference(value);
176 if (!parsed) throw std::runtime_error("cache url is not a url: " + value);
177
178 const auto scheme = std::string(parsed->scheme());
179 if (scheme != "file" && scheme != "redis" && scheme != "rediss") {
180 throw std::runtime_error("unsupported cache scheme \"" + scheme + "\"; it takes file, redis and rediss");
181 }
182}
183
184enum class Scope { Root, Cdn, Security, Cache, Bucket, Other };
185
186}
187
188namespace Haio::Cdn {
189
190Config parseConfig(std::string_view text) {
191 Config config;
192 std::istringstream in{std::string(text)};
193
194 Scope scope = Scope::Root;
195 BucketConfig* current = nullptr;
196 std::string line;
197
198 while (std::getline(in, line)) {
199 if (const auto comment = line.find('#'); comment != std::string::npos) line.resize(comment);
200 line = trim(std::move(line));
201 if (line.empty()) continue;
202
203 if (line.front() == '[' && line.back() == ']') {
204 const auto section = line.substr(1, line.size() - 2);
205 constexpr std::string_view prefix = "bucket.";
206 if (section == "cdn") {
207 current = nullptr;
208 scope = Scope::Cdn;
209 } else if (section == "security") {
210 current = nullptr;
211 scope = Scope::Security;
212 } else if (section == "cache") {
213 current = nullptr;
214 scope = Scope::Cache;
215 } else if (section.starts_with(prefix)) {
216 const auto name = section.substr(prefix.size());
217 auto& bucket = config.buckets[name];
218 bucket.name = name;
219 current = &bucket;
220 scope = Scope::Bucket;
221 } else {
222 current = nullptr;
223 scope = Scope::Other;
224 }
225 continue;
226 }
227
228 const auto eq = line.find('=');
229 if (eq == std::string::npos) continue;
230
231 const auto key = trim(line.substr(0, eq));
232 const auto value = unquote(line.substr(eq + 1));
233
234 if (scope == Scope::Cdn) {
235 config.declared.insert("cdn." + key);
236 if (key == "host") config.host = value;
237 else if (key == "port") config.port = parsePort(value);
238 else throw unknownKey("cdn", key, {"host", "port"});
239 continue;
240 }
241
242 if (scope == Scope::Security) {
243 config.declared.insert("security." + key);
244 if (key == "timeout") config.security.timeout = std::chrono::seconds(parseCount(key, value));
245 else if (key == "max_size_pixel") config.security.maxSizePixel = static_cast<int>(parseCount(key, value));
246 else if (key == "max_size_percent") config.security.maxSizePercent = static_cast<int>(parseCount(key, value));
247 else if (key == "max_cache_entries_by_ip") config.security.maxCacheEntriesByIp = parseCount(key, value);
248 else if (key == "max_requests_by_ip") config.security.maxRequestsByIp = parseCount(key, value);
249 else if (key == "allow_unzip") config.security.allowUnzip = parseFlag(key, value);
250 else if (key == "max_unzip") config.security.maxUnzip = parseCount(key, value) * 1024 * 1024;
251 else throw unknownKey("security", key, {"timeout", "max_size_pixel", "max_size_percent",
252 "max_cache_entries_by_ip", "max_requests_by_ip",
253 "allow_unzip", "max_unzip"});
254 continue;
255 }
256
257 if (scope == Scope::Cache) {
258 config.declared.insert("cache." + key);
259 if (key == "ttl") config.cache.ttl = std::chrono::seconds(parseCount(key, value));
260 else if (key == "max_usage") config.cache.maxUsage = parseCount(key, value) * 1024 * 1024;
261 else if (key == "url") { checkCacheUrl(value); config.cache.url = value; }
262 else throw unknownKey("cache", key, {"ttl", "max_usage", "url"});
263 continue;
264 }
265
266 // everything lives in a table now, so a key before the first one is a config
267 // written against an older haio rather than something to guess at
268 if (scope == Scope::Root) {
269 throw std::runtime_error("\"" + key + "\" is outside any table; settings live under "
270 "[cdn], [security], [cache] or [bucket.<name>]");
271 }
272
273 if (scope != Scope::Bucket || !current) continue;
274
275 if (key == "url") current->url = value;
276 else if (key == "access_key") current->accessKey = value;
277 else if (key == "secret_key") current->secretKey = value;
278 else if (key == "session_token") current->sessionToken = value;
279 else throw unknownKey("bucket \"" + current->name + "\"", key,
280 {"url", "access_key", "secret_key", "session_token"});
281 }
282
283 for (auto& [name, bucket] : config.buckets) applyUrl(bucket);
284 return config;
285}
286
287Config loadConfig(const std::filesystem::path& path) {
288 std::ifstream in(path);
289 if (!in) throw std::runtime_error("cannot open config: " + path.string());
290
291 /**
292 * the file may hold an s3 secret, so anybody who can read it can sign as this
293 * server. the check is a warning rather than a refusal: whose machine this is, and
294 * who else is on it, is not haio's call to make.
295 */
296 if (std::error_code ec; true) {
297 using std::filesystem::perms;
298 const auto mode = std::filesystem::status(path, ec).permissions();
299 if (!ec && (mode & (perms::group_read | perms::others_read)) != perms::none) {
300 std::cerr << "warning: " << path.string() << " can be read by more than its owner, "
301 "and a bucket's secret_key would be readable with it\n";
302 }
303 }
304
305 std::ostringstream text;
306 text << in.rdbuf();
307 return parseConfig(text.str());
308}
309
310}
SecurityConfig security
Definition haio_cdn.hpp:265
CacheConfig cache
Definition haio_cdn.hpp:266
std::map< std::string, BucketConfig > buckets
Definition haio_cdn.hpp:267
std::string host
Definition haio_cdn.hpp:262
std::chrono::seconds ttl
Config parseConfig(std::string_view text)
Definition config.cpp:190
unsigned short port
Definition haio_cdn.hpp:263
std::set< std::string > declared
Definition haio_cdn.hpp:276
std::chrono::seconds timeout
Definition haio_cdn.hpp:231
std::filesystem::path root
Definition haio_cdn.hpp:208
Config loadConfig(const std::filesystem::path &path)
Definition config.cpp:287