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