3#include <boost/url/parse.hpp>
4#include <boost/url/url.hpp>
13#include <initializer_list>
16namespace urls = boost::urls;
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());
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);
35unsigned short parsePort(
const std::string& value) {
37 const auto number = std::stoi(value);
38 if (number > 0 && number <= 65535)
return static_cast<unsigned short>(number);
39 }
catch (
const std::exception&) {
41 throw std::runtime_error(
"port must be a number between 1 and 65535, got: " + value);
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());
50 if (path.empty() || path ==
"/") {
51 throw std::runtime_error(
"file bucket \"" + name +
"\" has no directory in its url");
53 return std::filesystem::path(path);
55 return std::filesystem::path(host + path);
58std::string hostRegion(
const std::string& host);
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;
73 if (
const auto found = hostRegion(std::string(url.host())); !found.empty())
return found;
75 const char* fromEnv = std::getenv(
"AWS_DEFAULT_REGION");
76 return fromEnv ? fromEnv : std::string{};
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}
85 if (from == std::string::npos)
return {};
87 const auto rest = host.substr(from);
88 const auto dot = rest.find(
'.');
89 if (dot == std::string::npos)
return {};
91 const auto candidate = rest.substr(0, dot);
92 return candidate ==
"amazonaws" ? std::string{} : candidate;
95void applyUrl(Haio::Cdn::BucketConfig& bucket) {
96 if (bucket.
url.empty()) {
97 throw std::runtime_error(
"bucket \"" + bucket.
name +
"\" has no url");
100 const auto parsed = urls::parse_uri_reference(bucket.
url);
102 throw std::runtime_error(
"bucket \"" + bucket.
name +
"\" has an invalid url: " + bucket.
url);
105 bucket.
scheme = std::string(parsed->scheme());
106 bucket.
open = parsed->host() ==
"*";
108 if (bucket.
scheme ==
"file") {
110 throw std::runtime_error(
"a file bucket cannot be open: " + bucket.
url);
112 bucket.
root = fileRoot(*parsed, bucket.
name);
116 if (bucket.
scheme ==
"s3") {
119 if (bucket.
open)
throw std::runtime_error(
"an s3 bucket cannot be open: " + bucket.
url);
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");
129 if (bucket.
scheme ==
"http" || bucket.
scheme ==
"https")
return;
132 if (bucket.
scheme.empty()) {
134 throw std::runtime_error(
"bucket \"" + bucket.
name +
"\" needs a scheme in its url: " + bucket.
url);
139 throw std::runtime_error(
"unsupported url scheme \"" + bucket.
scheme +
"\" in bucket \"" + bucket.
name +
"\"");
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);
153 return std::runtime_error(message);
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);
163size_t parseCount(
const std::string& key,
const std::string& value) {
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);
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);
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");
184enum class Scope { Root, Cdn, Security, Cache, Bucket, Other };
192 std::istringstream in{std::string(text)};
194 Scope scope = Scope::Root;
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;
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") {
209 }
else if (section ==
"security") {
211 scope = Scope::Security;
212 }
else if (section ==
"cache") {
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];
220 scope = Scope::Bucket;
223 scope = Scope::Other;
228 const auto eq = line.find(
'=');
229 if (eq == std::string::npos)
continue;
231 const auto key = trim(line.substr(0, eq));
232 const auto value = unquote(line.substr(eq + 1));
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"});
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));
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"});
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"});
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>]");
273 if (scope != Scope::Bucket || !current)
continue;
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"});
283 for (
auto& [name, bucket] : config.
buckets) applyUrl(bucket);
288 std::ifstream in(path);
289 if (!in)
throw std::runtime_error(
"cannot open config: " + path.string());
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";
305 std::ostringstream text;
size_t maxCacheEntriesByIp
std::map< std::string, BucketConfig > buckets
Config parseConfig(std::string_view text)
std::set< std::string > declared
std::chrono::seconds timeout
std::filesystem::path root
Config loadConfig(const std::filesystem::path &path)