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