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