]> git.sesse.net Git - cubemap/blob - config.cpp
fix potential ambiguity between ::bind() and std::bind()
[cubemap] / config.cpp
1 #include <arpa/inet.h>
2 #include <assert.h>
3 #include <ctype.h>
4 #include <stdint.h>
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <string.h>
8 #include <net/if.h>
9 #include <sys/socket.h>
10 #include <string>
11 #include <utility>
12 #include <unordered_map>
13 #include <vector>
14
15 #include "tlse.h"
16
17 #include "acceptor.h"
18 #include "config.h"
19 #include "input.h"
20 #include "log.h"
21 #include "parse.h"
22
23 using namespace std;
24
25 #define DEFAULT_BACKLOG_SIZE 10485760
26
27 struct ConfigLine {
28         string keyword;
29         vector<string> arguments;
30         unordered_map<string, string> parameters;
31 };
32
33 namespace {
34
35 bool parse_hostport(const string &hostport, sockaddr_in6 *addr)
36 {
37         memset(addr, 0, sizeof(*addr));
38         addr->sin6_family = AF_INET6;
39
40         string port_string;
41
42         // See if the argument if on the type [ipv6addr]:port.
43         if (!hostport.empty() && hostport[0] == '[') {
44                 size_t split = hostport.find("]:");
45                 if (split == string::npos) {
46                         log(ERROR, "address '%s' is malformed; must be either [ipv6addr]:port or ipv4addr:port");
47                         return false;
48                 }
49
50                 string host(hostport.begin() + 1, hostport.begin() + split);
51                 port_string = hostport.substr(split + 2);
52
53                 if (inet_pton(AF_INET6, host.c_str(), &addr->sin6_addr) != 1) {
54                         log(ERROR, "'%s' is not a valid IPv6 address");
55                         return false;
56                 }
57         } else {
58                 // OK, then it must be ipv4addr:port.
59                 size_t split = hostport.find(":");
60                 if (split == string::npos) {
61                         log(ERROR, "address '%s' is malformed; must be either [ipv6addr]:port or ipv4addr:port");
62                         return false;
63                 }
64
65                 string host(hostport.begin(), hostport.begin() + split);
66                 port_string = hostport.substr(split + 1);
67
68                 // Parse to an IPv4 address, then construct a mapped-v4 address from that.
69                 in_addr addr4;
70
71                 if (inet_pton(AF_INET, host.c_str(), &addr4) != 1) {
72                         log(ERROR, "'%s' is not a valid IPv4 address");
73                         return false;
74                 }
75
76                 addr->sin6_addr.s6_addr32[2] = htonl(0xffff);
77                 addr->sin6_addr.s6_addr32[3] = addr4.s_addr;
78         }
79
80         int port = stoi(port_string);
81         if (port < 1 || port >= 65536) {
82                 log(ERROR, "port %d is out of range (must be [1,65536>).", port);
83                 return false;
84         }
85         addr->sin6_port = ntohs(port);
86
87         return true;
88 }
89
90 bool read_config(const string &filename, vector<ConfigLine> *lines)
91 {
92         FILE *fp = fopen(filename.c_str(), "r");
93         if (fp == nullptr) {
94                 log_perror(filename.c_str());
95                 return false;
96         }
97
98         char buf[4096];
99         while (!feof(fp)) {
100                 if (fgets(buf, sizeof(buf), fp) == nullptr) {
101                         break;
102                 }
103
104                 // Chop off the string at the first #, \r or \n.
105                 buf[strcspn(buf, "#\r\n")] = 0;
106
107                 // Remove all whitespace from the end of the string.
108                 size_t len = strlen(buf);
109                 while (len > 0 && isspace(buf[len - 1])) {
110                         buf[--len] = 0;
111                 }
112
113                 // If the line is now all blank, ignore it.
114                 if (len == 0) {
115                         continue;
116                 }
117
118                 vector<string> tokens = split_tokens(buf);
119                 assert(!tokens.empty());
120                 
121                 ConfigLine line;
122                 line.keyword = tokens[0];
123
124                 for (size_t i = 1; i < tokens.size(); ++i) {
125                         // foo=bar is a parameter; anything else is an argument.
126                         size_t equals_pos = tokens[i].find_first_of('=');
127                         if (equals_pos == string::npos) {
128                                 line.arguments.push_back(tokens[i]);
129                         } else {
130                                 string key = tokens[i].substr(0, equals_pos);
131                                 string value = tokens[i].substr(equals_pos + 1, string::npos);
132                                 line.parameters.insert(make_pair(key, value));
133                         }
134                 }
135
136                 lines->push_back(line);
137         }
138
139         if (fclose(fp) == EOF) {
140                 log_perror(filename.c_str());
141                 return false;
142         }
143         return true;
144 }
145
146 bool fetch_config_string(const vector<ConfigLine> &config, const string &keyword, string *value)
147 {
148         for (const ConfigLine &line : config) {
149                 if (line.keyword != keyword) {
150                         continue;
151                 }
152                 if (line.parameters.size() > 0 ||
153                     line.arguments.size() != 1) {
154                         log(ERROR, "'%s' takes one argument and no parameters", keyword.c_str());
155                         return false;
156                 }
157                 *value = line.arguments[0];
158                 return true;
159         }
160         return false;
161 }
162
163 bool fetch_config_int(const vector<ConfigLine> &config, const string &keyword, int *value)
164 {
165         for (const ConfigLine &line : config) {
166                 if (line.keyword != keyword) {
167                         continue;
168                 }
169                 if (line.parameters.size() > 0 ||
170                     line.arguments.size() != 1) {
171                         log(ERROR, "'%s' takes one argument and no parameters", keyword.c_str());
172                         return false;
173                 }
174                 *value = stoi(line.arguments[0]);  // TODO: verify int validity.
175                 return true;
176         }
177         return false;
178 }
179
180 bool load_file_to_string(const string &filename, size_t max_size, string *contents)
181 {
182         contents->clear();
183
184         FILE *fp = fopen(filename.c_str(), "r");
185         if (fp == nullptr) {
186                 log_perror(filename.c_str());
187                 return false;
188         }
189
190         char buf[4096];
191         while (!feof(fp)) {
192                 size_t ret = fread(buf, 1, sizeof(buf), fp);
193                 if (ret > 0) {
194                         contents->append(buf, buf + ret);
195                 } else {
196                         if (ferror(fp)) {
197                                 log_perror(filename.c_str());
198                                 fclose(fp);
199                                 return false;
200                         }
201                         assert(feof(fp));
202                         break;
203                 }
204
205                 if (contents->size() > max_size) {
206                         log(ERROR, "%s was longer than the maximum allowed %zu bytes", filename.c_str(), max_size);
207                         fclose(fp);
208                         return false;
209                 }
210         }
211         fclose(fp);
212         return true;
213 }
214
215 bool parse_tls_parameters(const unordered_map<string, string> &parameters, AcceptorConfig *acceptor)
216 {
217         bool has_cert = false, has_key = false;
218
219         auto tls_cert_it = parameters.find("tls_cert");
220         if (tls_cert_it != parameters.end()) {
221                 if (!load_file_to_string(tls_cert_it->second, 1048576, &acceptor->certificate_chain)) {
222                         return false;
223                 }
224
225                 // Verify that the certificate is valid.
226                 bool is_server = true;
227                 TLSContext *server_context = tls_create_context(is_server, TLS_V12);
228                 int num_cert = tls_load_certificates(
229                         server_context,
230                         reinterpret_cast<const unsigned char *>(acceptor->certificate_chain.data()),
231                         acceptor->certificate_chain.size());
232                 if (num_cert < 0) {
233                         log_tls_error(tls_cert_it->second.c_str(), num_cert);
234                         tls_destroy_context(server_context);
235                         return false;
236                 } else if (num_cert == 0) {
237                         log(ERROR, "%s did not contain any certificates", tls_cert_it->second.c_str());
238                         return false;
239                 }
240                 tls_destroy_context(server_context);
241                 has_cert = true;
242         }
243
244         auto tls_key_it = parameters.find("tls_key");
245         if (tls_key_it != parameters.end()) {
246                 if (!load_file_to_string(tls_key_it->second, 1048576, &acceptor->private_key)) {
247                         return false;
248                 }
249
250                 // Verify that the key is valid.
251                 bool is_server = true;
252                 TLSContext *server_context = tls_create_context(is_server, TLS_V12);
253                 int num_keys = tls_load_private_key(
254                         server_context,
255                         reinterpret_cast<const unsigned char *>(acceptor->private_key.data()),
256                         acceptor->private_key.size());
257                 if (num_keys < 0) {
258                         log_tls_error(tls_key_it->second.c_str(), num_keys);
259                         tls_destroy_context(server_context);
260                         return false;
261                 } else if (num_keys == 0) {
262                         log(ERROR, "%s did not contain any private keys", tls_key_it->second.c_str());
263                         return false;
264                 }
265                 tls_destroy_context(server_context);
266                 has_key = true;
267         }
268
269         if (has_cert != has_key) {
270                 log(ERROR, "Only one of tls_cert= and tls_key= was given, needs zero or both");
271                 return false;
272         }
273
274         return true;
275 }
276
277
278 bool parse_port(const ConfigLine &line, Config *config)
279 {
280         if (line.arguments.size() != 1) {
281                 log(ERROR, "'port' takes exactly one argument");
282                 return false;
283         }
284
285         int port = stoi(line.arguments[0]);
286         if (port < 1 || port >= 65536) {
287                 log(ERROR, "port %d is out of range (must be [1,65536>).", port);
288                 return false;
289         }
290
291         AcceptorConfig acceptor;
292         acceptor.addr = create_any_address(port);
293
294         if (!parse_tls_parameters(line.parameters, &acceptor)) {
295                 return false;
296         }
297         config->acceptors.push_back(acceptor);
298         return true;
299 }
300
301 bool parse_listen(const ConfigLine &line, Config *config)
302 {
303         if (line.arguments.size() != 1) {
304                 log(ERROR, "'listen' takes exactly one argument");
305                 return false;
306         }
307
308         AcceptorConfig acceptor;
309         if (!parse_hostport(line.arguments[0], &acceptor.addr)) {
310                 return false;
311         }
312         if (!parse_tls_parameters(line.parameters, &acceptor)) {
313                 return false;
314         }
315         config->acceptors.push_back(acceptor);
316         return true;
317 }
318
319 bool parse_stream(const ConfigLine &line, Config *config)
320 {
321         if (line.arguments.size() != 1) {
322                 log(ERROR, "'stream' takes exactly one argument");
323                 return false;
324         }
325
326         StreamConfig stream;
327         stream.url = line.arguments[0];
328
329         const auto src_it = line.parameters.find("src");
330         bool input_is_udp = false;
331         if (src_it == line.parameters.end()) {
332                 log(WARNING, "stream '%s' has no src= attribute, clients will not get any data.",
333                         stream.url.c_str());
334         } else {
335                 stream.src = src_it->second;
336
337                 string protocol, user, host, port, path;
338                 if (!parse_url(stream.src, &protocol, &user, &host, &port, &path)) {
339                         log(ERROR, "could not parse URL '%s'", stream.src.c_str());
340                         return false;
341                 }
342                 if (protocol == "udp") {
343                         input_is_udp = true;
344                 }
345         }
346
347         const auto backlog_it = line.parameters.find("backlog_size");
348         if (backlog_it == line.parameters.end()) {
349                 stream.backlog_size = DEFAULT_BACKLOG_SIZE;
350         } else {
351                 stream.backlog_size = stoll(backlog_it->second);
352         }
353
354         const auto prebuffer_it = line.parameters.find("force_prebuffer");
355         if (prebuffer_it == line.parameters.end()) {
356                 stream.prebuffering_bytes = 0;
357         } else {
358                 stream.prebuffering_bytes = stoll(prebuffer_it->second);
359         }
360
361         // Parse output encoding.
362         const auto encoding_parm_it = line.parameters.find("encoding");
363         if (encoding_parm_it == line.parameters.end() ||
364             encoding_parm_it->second == "raw") {
365                 stream.encoding = StreamConfig::STREAM_ENCODING_RAW;
366         } else if (encoding_parm_it->second == "metacube") {
367                 stream.encoding = StreamConfig::STREAM_ENCODING_METACUBE;
368         } else {
369                 log(ERROR, "Parameter 'encoding' must be either 'raw' (default) or 'metacube'");
370                 return false;
371         }
372
373         // Parse input encoding.
374         const auto src_encoding_parm_it = line.parameters.find("src_encoding");
375         if (src_encoding_parm_it == line.parameters.end()) {
376                 stream.src_encoding = input_is_udp ? StreamConfig::STREAM_ENCODING_RAW : StreamConfig::STREAM_ENCODING_METACUBE;
377         } else if (src_encoding_parm_it->second == "metacube") {
378                 if (input_is_udp) {
379                         log(ERROR, "UDP streams cannot have Metacube input");
380                         return false;
381                 }
382                 stream.src_encoding = StreamConfig::STREAM_ENCODING_METACUBE;
383         } else if (src_encoding_parm_it->second == "raw") {
384                 stream.src_encoding = StreamConfig::STREAM_ENCODING_RAW;
385         } else {
386                 log(ERROR, "Parameter 'src_encoding' must be either 'raw' (default for UDP) or 'metacube' (default for HTTP)");
387                 return false;
388         }
389
390         // Parse the pacing rate, converting from kilobits to bytes as needed.
391         const auto pacing_rate_it = line.parameters.find("pacing_rate_kbit");
392         if (pacing_rate_it == line.parameters.end()) {
393                 stream.pacing_rate = ~0U;
394         } else {
395                 stream.pacing_rate = stoll(pacing_rate_it->second.c_str()) * 1024 / 8;
396         }
397
398         // Parse the HLS URL, if any.
399         const auto hls_url_it = line.parameters.find("hls_playlist");
400         if (hls_url_it != line.parameters.end()) {
401                 stream.hls_url = hls_url_it->second;
402                 if (stream.hls_url.empty()) {
403                         log(ERROR, "Parameter 'hls_playlist' was given but empty");
404                         return false;
405                 }
406                 if (stream.encoding == StreamConfig::STREAM_ENCODING_METACUBE) {
407                         log(ERROR, "HLS cannot be used with Metacube output");
408                         return false;
409                 }
410         }
411
412         // Parse the HLS fragment duration, if any.
413         const auto hls_frag_duration_it = line.parameters.find("hls_frag_duration");
414         if (hls_frag_duration_it != line.parameters.end()) {
415                 if (stream.hls_url.empty()) {
416                         log(ERROR, "Parameter 'hls_frag_duration' given, but no 'hls_playlist' given");
417                         return false;
418                 }
419                 stream.hls_frag_duration = stoi(hls_frag_duration_it->second);
420                 if (stream.hls_frag_duration <= 0) {
421                         log(ERROR, "'hls_frag_duration' must be a strictly positive integer");
422                         return false;
423                 }
424         }
425
426         // Parse the HLS backlog margin, if any.
427         const auto hls_backlog_margin_it = line.parameters.find("hls_backlog_margin");
428         if (hls_backlog_margin_it != line.parameters.end()) {
429                 if (stream.hls_url.empty()) {
430                         log(ERROR, "Parameter 'hls_backlog_margin' given, but no 'hls_playlist' given");
431                         return false;
432                 }
433                 stream.hls_backlog_margin = stoi(hls_backlog_margin_it->second);
434                 if (stream.hls_backlog_margin >= stream.backlog_size) {
435                         log(ERROR, "'hls_backlog_margin' must be nonnegative, but less than the backlog size");
436                         return false;
437                 }
438         }
439
440         // Parse the CORS origin, if it exists.
441         const auto allow_origin_it = line.parameters.find("allow_origin");
442         if (allow_origin_it != line.parameters.end()) {
443                 stream.allow_origin = allow_origin_it->second;
444         }
445
446         config->streams.push_back(stream);
447         return true;
448 }
449
450 bool parse_udpstream(const ConfigLine &line, Config *config)
451 {
452         if (line.arguments.size() != 1) {
453                 log(ERROR, "'udpstream' takes exactly one argument");
454                 return false;
455         }
456
457         UDPStreamConfig udpstream;
458
459         string hostport = line.arguments[0];
460         if (!parse_hostport(hostport, &udpstream.dst)) {
461                 return false;
462         }
463
464         const auto src_it = line.parameters.find("src");
465         if (src_it == line.parameters.end()) {
466                 // This is pretty meaningless, but OK, consistency is good.
467                 log(WARNING, "udpstream to %s has no src= attribute, clients will not get any data.",
468                         hostport.c_str());
469         } else {
470                 udpstream.src = src_it->second;
471                 // TODO: Verify that the URL is parseable?
472         }
473
474         // Parse the pacing rate, converting from kilobits to bytes as needed.
475         const auto pacing_rate_it = line.parameters.find("pacing_rate_kbit");
476         if (pacing_rate_it == line.parameters.end()) {
477                 udpstream.pacing_rate = ~0U;
478         } else {
479                 udpstream.pacing_rate = stoi(pacing_rate_it->second) * 1024 / 8;
480         }
481
482         // Parse the TTL. The same value is used for unicast and multicast.
483         const auto ttl_it = line.parameters.find("ttl");
484         if (ttl_it == line.parameters.end()) {
485                 udpstream.ttl = -1;
486         } else {
487                 udpstream.ttl = stoi(ttl_it->second);
488         }
489
490         // Parse the multicast interface index.
491         const auto multicast_iface_it = line.parameters.find("multicast_output_interface");
492         if (multicast_iface_it == line.parameters.end()) {
493                 udpstream.multicast_iface_index = -1;
494         } else {
495                 udpstream.multicast_iface_index = if_nametoindex(multicast_iface_it->second.c_str());
496                 if (udpstream.multicast_iface_index == 0) {
497                         log(ERROR, "Interface '%s' does not exist", multicast_iface_it->second.c_str());
498                         return false;
499                 }
500         }
501
502         config->udpstreams.push_back(udpstream);
503         return true;
504 }
505
506 bool parse_gen204(const ConfigLine &line, Config *config)
507 {
508         if (line.arguments.size() != 1) {
509                 log(ERROR, "'gen204' takes exactly one argument");
510                 return false;
511         }
512
513         Gen204Config gen204;
514         gen204.url = line.arguments[0];
515
516         // Parse the CORS origin, if it exists.
517         const auto allow_origin_it = line.parameters.find("allow_origin");
518         if (allow_origin_it != line.parameters.end()) {
519                 gen204.allow_origin = allow_origin_it->second;
520         }
521
522         config->pings.push_back(gen204);
523         return true;
524 }
525
526 bool parse_error_log(const ConfigLine &line, Config *config)
527 {
528         if (line.arguments.size() != 0) {
529                 log(ERROR, "'error_log' takes no arguments (only parameters type= and filename=)");
530                 return false;
531         }
532
533         LogConfig log_config;
534         const auto type_it = line.parameters.find("type");
535         if (type_it == line.parameters.end()) {
536                 log(ERROR, "'error_log' has no type= parameter");
537                 return false; 
538         }
539
540         string type = type_it->second;
541         if (type == "file") {
542                 log_config.type = LogConfig::LOG_TYPE_FILE;
543         } else if (type == "syslog") {
544                 log_config.type = LogConfig::LOG_TYPE_SYSLOG;
545         } else if (type == "console") {
546                 log_config.type = LogConfig::LOG_TYPE_CONSOLE;
547         } else {
548                 log(ERROR, "Unknown log type '%s'", type.c_str());
549                 return false; 
550         }
551
552         if (log_config.type == LogConfig::LOG_TYPE_FILE) {
553                 const auto filename_it = line.parameters.find("filename");
554                 if (filename_it == line.parameters.end()) {
555                         log(ERROR, "error_log type 'file' with no filename= parameter");
556                         return false; 
557                 }
558                 log_config.filename = filename_it->second;
559         }
560
561         config->log_destinations.push_back(log_config);
562         return true;
563 }
564
565 }  // namespace
566
567 bool parse_config(const string &filename, Config *config)
568 {
569         vector<ConfigLine> lines;
570         if (!read_config(filename, &lines)) {
571                 return false;
572         }
573
574         config->daemonize = false;
575
576         if (!fetch_config_int(lines, "num_servers", &config->num_servers)) {
577                 log(ERROR, "Missing 'num_servers' statement in config file.");
578                 return false;
579         }
580         if (config->num_servers < 1 || config->num_servers >= 20000) {  // Insanely high max limit.
581                 log(ERROR, "'num_servers' is %d, needs to be in [1, 20000>.", config->num_servers);
582                 return false;
583         }
584
585         // See if the user wants stats.
586         config->stats_interval = 60;
587         bool has_stats_file = fetch_config_string(lines, "stats_file", &config->stats_file);
588         bool has_stats_interval = fetch_config_int(lines, "stats_interval", &config->stats_interval);
589         if (has_stats_interval && !has_stats_file) {
590                 log(WARNING, "'stats_interval' given, but no 'stats_file'. No client statistics will be written.");
591         }
592
593         config->input_stats_interval = 60;
594         bool has_input_stats_file = fetch_config_string(lines, "input_stats_file", &config->input_stats_file);
595         bool has_input_stats_interval = fetch_config_int(lines, "input_stats_interval", &config->input_stats_interval);
596         if (has_input_stats_interval && !has_input_stats_file) {
597                 log(WARNING, "'input_stats_interval' given, but no 'input_stats_file'. No input statistics will be written.");
598         }
599         
600         fetch_config_string(lines, "access_log", &config->access_log_file);
601
602         for (const ConfigLine &line : lines) {
603                 if (line.keyword == "num_servers" ||
604                     line.keyword == "stats_file" ||
605                     line.keyword == "stats_interval" ||
606                     line.keyword == "input_stats_file" ||
607                     line.keyword == "input_stats_interval" ||
608                     line.keyword == "access_log") {
609                         // Already taken care of, above.
610                 } else if (line.keyword == "port") {
611                         if (!parse_port(line, config)) {
612                                 return false;
613                         }
614                 } else if (line.keyword == "listen") {
615                         if (!parse_listen(line, config)) {
616                                 return false;
617                         }
618                 } else if (line.keyword == "stream") {
619                         if (!parse_stream(line, config)) {
620                                 return false;
621                         }
622                 } else if (line.keyword == "udpstream") {
623                         if (!parse_udpstream(line, config)) {
624                                 return false;
625                         }
626                 } else if (line.keyword == "gen204") {
627                         if (!parse_gen204(line, config)) {
628                                 return false;
629                         }
630                 } else if (line.keyword == "error_log") {
631                         if (!parse_error_log(line, config)) {
632                                 return false;
633                         }
634                 } else if (line.keyword == "daemonize") {
635                         config->daemonize = true;
636                 } else {
637                         log(ERROR, "Unknown configuration keyword '%s'.",
638                                 line.keyword.c_str());
639                         return false;
640                 }
641         }
642
643         return true;
644 }