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