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