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