]> git.sesse.net Git - cubemap/blob - config.cpp
Release Cubemap 1.1.1.
[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         // Parse encoding.
239         map<string, string>::const_iterator encoding_parm_it = line.parameters.find("encoding");
240         if (encoding_parm_it == line.parameters.end() ||
241             encoding_parm_it->second == "raw") {
242                 stream.encoding = StreamConfig::STREAM_ENCODING_RAW;
243         } else if (encoding_parm_it->second == "metacube") {
244                 stream.encoding = StreamConfig::STREAM_ENCODING_METACUBE;
245         } else {
246                 log(ERROR, "Parameter 'encoding' must be either 'raw' (default) or 'metacube'");
247                 return false;
248         }
249
250         // Parse the pacing rate, converting from kilobits to bytes as needed.
251         map<string, string>::const_iterator pacing_rate_it = line.parameters.find("pacing_rate_kbit");
252         if (pacing_rate_it == line.parameters.end()) {
253                 stream.pacing_rate = ~0U;
254         } else {
255                 stream.pacing_rate = atoi(pacing_rate_it->second.c_str()) * 1024 / 8;
256         }
257
258         config->streams.push_back(stream);
259         return true;
260 }
261
262 bool parse_udpstream(const ConfigLine &line, Config *config)
263 {
264         if (line.arguments.size() != 1) {
265                 log(ERROR, "'udpstream' takes exactly one argument");
266                 return false;
267         }
268
269         UDPStreamConfig udpstream;
270
271         string hostport = line.arguments[0];
272         if (!parse_hostport(hostport, &udpstream.dst)) {
273                 return false;
274         }
275
276         map<string, string>::const_iterator src_it = line.parameters.find("src");
277         if (src_it == line.parameters.end()) {
278                 // This is pretty meaningless, but OK, consistency is good.
279                 log(WARNING, "udpstream to %s has no src= attribute, clients will not get any data.",
280                         hostport.c_str());
281         } else {
282                 udpstream.src = src_it->second;
283                 // TODO: Verify that the URL is parseable?
284         }
285
286         // Parse the pacing rate, converting from kilobits to bytes as needed.
287         map<string, string>::const_iterator pacing_rate_it = line.parameters.find("pacing_rate_kbit");
288         if (pacing_rate_it == line.parameters.end()) {
289                 udpstream.pacing_rate = ~0U;
290         } else {
291                 udpstream.pacing_rate = atoi(pacing_rate_it->second.c_str()) * 1024 / 8;
292         }
293
294         // Parse the TTL. The same value is used for unicast and multicast.
295         map<string, string>::const_iterator ttl_it = line.parameters.find("ttl");
296         if (ttl_it == line.parameters.end()) {
297                 udpstream.ttl = -1;
298         } else {
299                 udpstream.ttl = atoi(ttl_it->second.c_str());
300         }
301
302         // Parse the multicast interface index.
303         map<string, string>::const_iterator multicast_iface_it = line.parameters.find("multicast_output_interface");
304         if (multicast_iface_it == line.parameters.end()) {
305                 udpstream.multicast_iface_index = -1;
306         } else {
307                 udpstream.multicast_iface_index = if_nametoindex(multicast_iface_it->second.c_str());
308                 if (udpstream.multicast_iface_index == 0) {
309                         log(ERROR, "Interface '%s' does not exist", multicast_iface_it->second.c_str());
310                         return false;
311                 }
312         }
313
314         config->udpstreams.push_back(udpstream);
315         return true;
316 }
317
318 bool parse_error_log(const ConfigLine &line, Config *config)
319 {
320         if (line.arguments.size() != 0) {
321                 log(ERROR, "'error_log' takes no arguments (only parameters type= and filename=)");
322                 return false;
323         }
324
325         LogConfig log_config;
326         map<string, string>::const_iterator type_it = line.parameters.find("type");
327         if (type_it == line.parameters.end()) {
328                 log(ERROR, "'error_log' has no type= parameter");
329                 return false; 
330         }
331
332         string type = type_it->second;
333         if (type == "file") {
334                 log_config.type = LogConfig::LOG_TYPE_FILE;
335         } else if (type == "syslog") {
336                 log_config.type = LogConfig::LOG_TYPE_SYSLOG;
337         } else if (type == "console") {
338                 log_config.type = LogConfig::LOG_TYPE_CONSOLE;
339         } else {
340                 log(ERROR, "Unknown log type '%s'", type.c_str());
341                 return false; 
342         }
343
344         if (log_config.type == LogConfig::LOG_TYPE_FILE) {
345                 map<string, string>::const_iterator filename_it = line.parameters.find("filename");
346                 if (filename_it == line.parameters.end()) {
347                         log(ERROR, "error_log type 'file' with no filename= parameter");
348                         return false; 
349                 }
350                 log_config.filename = filename_it->second;
351         }
352
353         config->log_destinations.push_back(log_config);
354         return true;
355 }
356
357 }  // namespace
358
359 bool parse_config(const string &filename, Config *config)
360 {
361         vector<ConfigLine> lines;
362         if (!read_config(filename, &lines)) {
363                 return false;
364         }
365
366         config->daemonize = false;
367
368         if (!fetch_config_int(lines, "num_servers", &config->num_servers)) {
369                 log(ERROR, "Missing 'num_servers' statement in config file.");
370                 return false;
371         }
372         if (config->num_servers < 1 || config->num_servers >= 20000) {  // Insanely high max limit.
373                 log(ERROR, "'num_servers' is %d, needs to be in [1, 20000>.", config->num_servers);
374                 return false;
375         }
376
377         // See if the user wants stats.
378         config->stats_interval = 60;
379         bool has_stats_file = fetch_config_string(lines, "stats_file", &config->stats_file);
380         bool has_stats_interval = fetch_config_int(lines, "stats_interval", &config->stats_interval);
381         if (has_stats_interval && !has_stats_file) {
382                 log(WARNING, "'stats_interval' given, but no 'stats_file'. No client statistics will be written.");
383         }
384
385         config->input_stats_interval = 60;
386         bool has_input_stats_file = fetch_config_string(lines, "input_stats_file", &config->input_stats_file);
387         bool has_input_stats_interval = fetch_config_int(lines, "input_stats_interval", &config->input_stats_interval);
388         if (has_input_stats_interval && !has_input_stats_file) {
389                 log(WARNING, "'input_stats_interval' given, but no 'input_stats_file'. No input statistics will be written.");
390         }
391         
392         fetch_config_string(lines, "access_log", &config->access_log_file);
393
394         for (size_t i = 0; i < lines.size(); ++i) {
395                 const ConfigLine &line = lines[i];
396                 if (line.keyword == "num_servers" ||
397                     line.keyword == "stats_file" ||
398                     line.keyword == "stats_interval" ||
399                     line.keyword == "input_stats_file" ||
400                     line.keyword == "input_stats_interval" ||
401                     line.keyword == "access_log") {
402                         // Already taken care of, above.
403                 } else if (line.keyword == "port") {
404                         if (!parse_port(line, config)) {
405                                 return false;
406                         }
407                 } else if (line.keyword == "listen") {
408                         if (!parse_listen(line, config)) {
409                                 return false;
410                         }
411                 } else if (line.keyword == "stream") {
412                         if (!parse_stream(line, config)) {
413                                 return false;
414                         }
415                 } else if (line.keyword == "udpstream") {
416                         if (!parse_udpstream(line, config)) {
417                                 return false;
418                         }
419                 } else if (line.keyword == "error_log") {
420                         if (!parse_error_log(line, config)) {
421                                 return false;
422                         }
423                 } else if (line.keyword == "daemonize") {
424                         config->daemonize = true;
425                 } else {
426                         log(ERROR, "Unknown configuration keyword '%s'.",
427                                 line.keyword.c_str());
428                         return false;
429                 }
430         }
431
432         return true;
433 }