]> git.sesse.net Git - cubemap/blob - config.cpp
Add latency statistics to the Munin plugin.
[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         config->udpstreams.push_back(udpstream);
334         return true;
335 }
336
337 bool parse_gen204(const ConfigLine &line, Config *config)
338 {
339         if (line.arguments.size() != 1) {
340                 log(ERROR, "'gen204' takes exactly one argument");
341                 return false;
342         }
343
344         Gen204Config gen204;
345         gen204.url = line.arguments[0];
346
347         // Parse the CORS origin, if it exists.
348         map<string, string>::const_iterator allow_origin_it = line.parameters.find("allow_origin");
349         if (allow_origin_it != line.parameters.end()) {
350                 gen204.allow_origin = allow_origin_it->second;
351         }
352
353         config->pings.push_back(gen204);
354         return true;
355 }
356
357 bool parse_error_log(const ConfigLine &line, Config *config)
358 {
359         if (line.arguments.size() != 0) {
360                 log(ERROR, "'error_log' takes no arguments (only parameters type= and filename=)");
361                 return false;
362         }
363
364         LogConfig log_config;
365         map<string, string>::const_iterator type_it = line.parameters.find("type");
366         if (type_it == line.parameters.end()) {
367                 log(ERROR, "'error_log' has no type= parameter");
368                 return false; 
369         }
370
371         string type = type_it->second;
372         if (type == "file") {
373                 log_config.type = LogConfig::LOG_TYPE_FILE;
374         } else if (type == "syslog") {
375                 log_config.type = LogConfig::LOG_TYPE_SYSLOG;
376         } else if (type == "console") {
377                 log_config.type = LogConfig::LOG_TYPE_CONSOLE;
378         } else {
379                 log(ERROR, "Unknown log type '%s'", type.c_str());
380                 return false; 
381         }
382
383         if (log_config.type == LogConfig::LOG_TYPE_FILE) {
384                 map<string, string>::const_iterator filename_it = line.parameters.find("filename");
385                 if (filename_it == line.parameters.end()) {
386                         log(ERROR, "error_log type 'file' with no filename= parameter");
387                         return false; 
388                 }
389                 log_config.filename = filename_it->second;
390         }
391
392         config->log_destinations.push_back(log_config);
393         return true;
394 }
395
396 }  // namespace
397
398 bool parse_config(const string &filename, Config *config)
399 {
400         vector<ConfigLine> lines;
401         if (!read_config(filename, &lines)) {
402                 return false;
403         }
404
405         config->daemonize = false;
406
407         if (!fetch_config_int(lines, "num_servers", &config->num_servers)) {
408                 log(ERROR, "Missing 'num_servers' statement in config file.");
409                 return false;
410         }
411         if (config->num_servers < 1 || config->num_servers >= 20000) {  // Insanely high max limit.
412                 log(ERROR, "'num_servers' is %d, needs to be in [1, 20000>.", config->num_servers);
413                 return false;
414         }
415
416         // See if the user wants stats.
417         config->stats_interval = 60;
418         bool has_stats_file = fetch_config_string(lines, "stats_file", &config->stats_file);
419         bool has_stats_interval = fetch_config_int(lines, "stats_interval", &config->stats_interval);
420         if (has_stats_interval && !has_stats_file) {
421                 log(WARNING, "'stats_interval' given, but no 'stats_file'. No client statistics will be written.");
422         }
423
424         config->input_stats_interval = 60;
425         bool has_input_stats_file = fetch_config_string(lines, "input_stats_file", &config->input_stats_file);
426         bool has_input_stats_interval = fetch_config_int(lines, "input_stats_interval", &config->input_stats_interval);
427         if (has_input_stats_interval && !has_input_stats_file) {
428                 log(WARNING, "'input_stats_interval' given, but no 'input_stats_file'. No input statistics will be written.");
429         }
430         
431         fetch_config_string(lines, "access_log", &config->access_log_file);
432
433         for (size_t i = 0; i < lines.size(); ++i) {
434                 const ConfigLine &line = lines[i];
435                 if (line.keyword == "num_servers" ||
436                     line.keyword == "stats_file" ||
437                     line.keyword == "stats_interval" ||
438                     line.keyword == "input_stats_file" ||
439                     line.keyword == "input_stats_interval" ||
440                     line.keyword == "access_log") {
441                         // Already taken care of, above.
442                 } else if (line.keyword == "port") {
443                         if (!parse_port(line, config)) {
444                                 return false;
445                         }
446                 } else if (line.keyword == "listen") {
447                         if (!parse_listen(line, config)) {
448                                 return false;
449                         }
450                 } else if (line.keyword == "stream") {
451                         if (!parse_stream(line, config)) {
452                                 return false;
453                         }
454                 } else if (line.keyword == "udpstream") {
455                         if (!parse_udpstream(line, config)) {
456                                 return false;
457                         }
458                 } else if (line.keyword == "gen204") {
459                         if (!parse_gen204(line, config)) {
460                                 return false;
461                         }
462                 } else if (line.keyword == "error_log") {
463                         if (!parse_error_log(line, config)) {
464                                 return false;
465                         }
466                 } else if (line.keyword == "daemonize") {
467                         config->daemonize = true;
468                 } else {
469                         log(ERROR, "Unknown configuration keyword '%s'.",
470                                 line.keyword.c_str());
471                         return false;
472                 }
473         }
474
475         return true;
476 }