Haio 0.2.X
Homebrew Asset IO/CDN
Loading...
Searching...
No Matches
haio_cdn.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "haio.hpp"
4#include "haio_cache.hpp"
5
6#include <boost/asio/awaitable.hpp>
7
8#include <chrono>
9#include <filesystem>
10#include <map>
11#include <set>
12#include <string_view>
13
14/**
15 * @defgroup cdn CDN
16 * serving images over http.
17 *
18 * one toml file, named on the command line or handed over whole in HAIO_CDN_TOML.
19 * every setting lives in a table; a key outside one is refused.
20 *
21 * @code{.toml}
22 * [cdn]
23 * host = "0.0.0.0" # address to listen on
24 * port = 8080
25 * @endcode
26 */
27
28/**
29 * @defgroup cdn_bucket Bucket
30 * @ingroup cdn
31 * where the bytes come from. the scheme of the url picks the reader, so there is no
32 * type to set. `url` is the only key a bucket takes.
33 *
34 * ## Bucket File
35 *
36 * @code{.toml}
37 * [bucket.assets]
38 * url = "file://assets" # relative to the working directory
39 * url = "file:///srv/img" # absolute
40 * @endcode
41 *
42 * ".." and a segment starting with "~" are refused.
43 *
44 * ## Bucket Zip
45 *
46 * a path may name a file inside an archive, as in `/cdn/assets/pack.zip/logo.png`,
47 * when `allow_unzip` is on. the archive is read once and kept in memory, so asking
48 * for a second picture out of it costs no fetch.
49 *
50 * an entry larger than `max_unzip`, or one that expands more than two hundred times,
51 * is refused: real image data barely compresses, so anything near that is an attempt
52 * to spend memory rather than an archive.
53 *
54 * ## Bucket Http
55 *
56 * @code{.toml}
57 * [bucket.upstream]
58 * url = "https://host/prefix" # everything under one prefix
59 * url = "s3://s3.eu-west-1.amazonaws.com/bucket" # signed, region from the host
60 * url = "s3://minio.local:9000/bucket?region=us-east-1" # or said outright
61 * url = "https://*" # open: the request names the host
62 * @endcode
63 *
64 * redirects are followed, downgrades included. an open bucket fetches whatever host
65 * the caller names, warns at startup, and is meant for testing.
66 *
67 * ## Bucket S3
68 *
69 * an `s3://` bucket is https with a signature. the region is settled while the config
70 * loads, from `?region=` if it says so, else from an amazon host, else from
71 * `AWS_DEFAULT_REGION`. a bucket whose region nobody can work out is refused at
72 * startup rather than at the first request.
73 *
74 * @code{.toml}
75 * [bucket.orders]
76 * url = "s3://bucket.s3.us-east-1.amazonaws.com/prefix"
77 * access_key = "AKIA..."
78 * secret_key = "..."
79 * session_token = "..." # only for temporary credentials
80 * @endcode
81 *
82 * the keys may be left out, and then AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and
83 * AWS_SESSION_TOKEN are read instead. with neither the request goes out unsigned,
84 * which is what a public bucket wants.
85 *
86 * a config holding a secret is a file worth guarding, and haio says so at startup if
87 * anybody but its owner can read it.
88 *
89 * an s3 bucket cannot be open: a signature is made for one host, and there is nothing
90 * to sign for a host that arrives with the request.
91 */
92
93/**
94 * @defgroup cdn_cache Cache
95 * @ingroup cdn
96 * keeping the answer, so the fetch and the conversion behind it happen once.
97 *
98 * @code{.toml}
99 * [cache]
100 * ttl = 5000 # in seconds, since the entry was last wanted
101 * max_usage = 50 # in mb, wherever the entries are kept
102 * url = "redis://127.0.0.1:6379" # optional; without it, kept in this process
103 * @endcode
104 *
105 * ## Cache Key
106 *
107 * the md5 of the bucket, the path and the query, in the order written. the query is
108 * not sorted: it will spell a pipeline once filters chain, and a sorted key would
109 * serve one picture when another was asked for. the method is left out, so a HEAD
110 * finds what a GET stored.
111 *
112 * ## Cache Where
113 *
114 * | url | kept in | bounded by |
115 * | ---------------- | ---------------- | ------------------- |
116 * | absent | this process | what it holds |
117 * | `file://dir` | one file per key | what the files take |
118 * | `redis://host` | a shared redis | an index haio keeps |
119 *
120 * a redis that cannot be reached reads as a miss: losing the cache costs latency and
121 * nothing else.
122 *
123 * ## Cache Flow
124 *
125 * deduplication is not part of caching. with no `[cache]` nothing is kept, and a
126 * burst on one key still reaches the producer once.
127 *
128 * @startuml
129 * start
130 * if (cache on?) then (yes)
131 * :look in the store;
132 * if (hit?) then (yes)
133 * :renew the ttl;
134 * stop
135 * endif
136 * endif
137 * if (already being produced?) then (yes)
138 * :wait for the leader;
139 * :take their answer;
140 * stop
141 * else (no)
142 * :become the leader;
143 * endif
144 * :fetch and convert;
145 * if (cache on and within quota?) then (yes)
146 * :store;
147 * endif
148 * :wake the waiters;
149 * stop
150 * @enduml
151 */
152
153/**
154 * @defgroup cdn_security Security
155 * @ingroup cdn
156 * every limit in one table, so reviewing what protects a deployment is one page.
157 *
158 * @code{.toml}
159 * [security]
160 * timeout = 10 # in seconds, one conversion
161 * max_size_pixel = 4096 # in pixels, what a resize may ask for outright
162 * max_size_percent = 500 # in percent, what a resize may ask for as a share
163 * max_cache_entries_by_ip = 30 # entries one address may create per ttl window
164 * max_requests_by_ip = 10 # per second, as a leaky bucket
165 * allow_unzip = false # whether a path may reach inside a zip
166 * max_unzip = 64 # in mb, one extracted file and the archives kept
167 * @endcode
168 *
169 * ## Security Defaults
170 *
171 * each has a default and the server starts without them. the two counted per address
172 * default to no limit on purpose: a number picked blind shuts out an office behind
173 * one NAT. either way the server says at startup which ones it chose for itself.
174 *
175 * ## Security Why
176 *
177 * | key | answers |
178 * | ------------------------- | ------------------------------------------------ |
179 * | `max_size_pixel` | a url asking for an allocation |
180 * | `max_size_percent` | the same, asked for as a share of the picture |
181 * | `timeout` | a url asking for work |
182 * | `max_cache_entries_by_ip` | one caller evicting everyone else's entries |
183 * | `max_requests_by_ip` | a flood |
184 * | `allow_unzip` | work on behalf of whoever wrote the file |
185 * | `max_unzip` | a small zip that expands into a large one |
186 *
187 * over the cache quota the answer is still produced and still correct, it is simply
188 * not stored: the limit is on eviction, not on access.
189 */
190
191namespace Haio::Cdn {
192
193/**
194 * a bucket is described entirely by the scheme of its url:
195 *
196 * file://relative/dir file:///absolute/dir
197 * http://host/prefix https://host/prefix s3://host/bucket
198 * https://\* open, the request names the host
199 * //\* open, the request names the scheme and the host
200 */
202 std::string name;
203 std::string url;
204
205 // derived from the url while the config loads, so a bad one fails at startup
206 std::string scheme;
207 bool open = false;
208 std::filesystem::path root;
209
210 /** s3 only: from ?region=, then the host, then AWS_DEFAULT_REGION */
211 std::string region;
212
213 /**
214 * s3 only, and the reason a config file deserves careful permissions. empty
215 * leaves the request unsigned, which is what a public bucket wants.
216 */
217 std::string accessKey;
218 std::string secretKey;
219 std::string sessionToken;
220};
221
222/**
223 * every limit the server has, in one table, so that reviewing what protects this
224 * deployment is reading one page of the config rather than hunting for it.
225 *
226 * each one is safe when left out, and the server says at startup which ones it chose
227 * for itself; see @ref cdn_security.
228 */
230 /** seconds one conversion may run before it is given up on */
231 std::chrono::seconds timeout{10};
232
233 /** the largest width or height a resize may ask for outright */
234 int maxSizePixel = 4096;
235
236 /**
237 * the largest share a resize may ask for, as a percentage.
238 *
239 * it is a separate number because it bounds a different thing: a picture scaled
240 * by a share is bounded by what it was, so five hundred percent of a thumbnail is
241 * still small, while five hundred percent of something large is not.
242 */
243 int maxSizePercent = 500;
244
245 /** entries one address may create per cache window; zero is no limit */
247
248 /** requests one address may make per second; zero is no limit */
249 size_t maxRequestsByIp = 0;
250
251 /** whether a path may reach inside a zip; off unless asked for */
252 bool allowUnzip = false;
253
254 /**
255 * bytes one file out of a zip may take, and what the archives kept in memory may
256 * take altogether. megabytes in the config.
257 */
258 size_t maxUnzip = 64u << 20;
259};
260
261struct Config {
262 std::string host = "0.0.0.0";
263 unsigned short port = 8080;
264
267 std::map<std::string, BucketConfig> buckets;
268
269 /**
270 * the keys the file actually spelled out, as "max_size" or "cache.max_ip".
271 *
272 * the safety limits all have a default, so leaving one out is safe rather than
273 * open; this is what lets the server say which ones it chose for itself instead
274 * of letting an operator believe they had configured something.
275 */
276 std::set<std::string> declared;
277};
278
279Config parseConfig(std::string_view text);
280Config loadConfig(const std::filesystem::path& path);
281namespace Bucket { class ZipArchives; }
282
283boost::asio::awaitable<Result<Blob>> fetchBucket(const Config& config, Bucket::ZipArchives& archives, std::string bucket, std::string path);
284boost::asio::awaitable<void> runServer(Config config);
285
286}
SecurityConfig security
Definition haio_cdn.hpp:265
CacheConfig cache
Definition haio_cdn.hpp:266
boost::asio::awaitable< void > runServer(Config config)
Definition server.cpp:431
std::map< std::string, BucketConfig > buckets
Definition haio_cdn.hpp:267
std::string host
Definition haio_cdn.hpp:262
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
boost::asio::awaitable< Result< Blob > > fetchBucket(const Config &config, Bucket::ZipArchives &archives, std::string bucket, std::string path)
Definition bucket.cpp:15
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