diff --git a/src/config.c b/src/config.c index 071d64c..9af41b6 100644 --- a/src/config.c +++ b/src/config.c @@ -1,280 +1,285 @@ /* * Copyright (C) 2011 Jiaju Zhang * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include "booth.h" #include "config.h" #include "log.h" static int ticket_size = 0; static int ticket_realloc(void) { void *p; booth_conf = realloc(booth_conf, sizeof(struct booth_config) + (ticket_size + TICKET_ALLOC) * sizeof(struct ticket_config)); if (!booth_conf) { log_error("can't alloc more booth config"); return -ENOMEM; } p = booth_conf + sizeof(struct booth_config) + ticket_size * sizeof(struct ticket_config); memset(p, 0, TICKET_ALLOC * sizeof(struct ticket_config)); ticket_size += TICKET_ALLOC; return 0; } int read_config(const char *path) { char line[1024]; FILE *fp; char *s, *key, *val, *expiry, *weight, *c; int in_quotes, got_equals, got_quotes, i; int lineno = 0; int got_transport = 0; fp = fopen(path, "r"); if (!fp) { log_error("failed to open %s: %s", path, strerror(errno)); return -1; } booth_conf = malloc(sizeof(struct booth_config) + TICKET_ALLOC * sizeof(struct ticket_config)); if (!booth_conf) { log_error("failed to alloc memory for booth config"); return -ENOMEM; } memset(booth_conf, 0, sizeof(struct booth_config) + TICKET_ALLOC * sizeof(struct ticket_config)); ticket_size = TICKET_ALLOC; while (fgets(line, sizeof(line), fp)) { lineno++; s = line; while (*s == ' ') s++; if (*s == '#' || *s == '\n') continue; if (*s == '-' || *s == '.' || *s =='/' || *s == '+' || *s == '(' || *s == ')' || *s == ':' || *s == ',' || *s == '@' || *s == '=' || *s == '"') { log_error("invalid key name in config file " "('%c', line %d char %ld)", *s, lineno, s - line); goto out; } key = s; /* will point to the key on the left hand side */ val = NULL; /* will point to the value on the right hand side */ in_quotes = 0; /* true iff we're inside a double-quoted string */ got_equals = 0; /* true iff we're on the RHS of the = assignment */ got_quotes = 0; /* true iff the RHS is quoted */ while (*s != '\n' && *s != '\0') { if (!(*s >='a' && *s <= 'z') && !(*s >= 'A' && *s <= 'Z') && !(*s >= '0' && *s <= '9') && !(*s == '_') && !(*s == '-') && !(*s == '.') && !(*s == '/') && !(*s == ' ') && !(*s == '+') && !(*s == '(') && !(*s == ')') && !(*s == ':') && !(*s == ';') && !(*s == ',') && !(*s == '@') && !(*s == '=') && !(*s == '"')) { log_error("invalid character ('%c', line %d char %ld)" " in config file", *s, lineno, s - line); goto out; } if (*s == '=' && !got_equals) { got_equals = 1; *s = '\0'; val = s + 1; } else if ((*s == '=' || *s == '_' || *s == '-' || *s == '.') && got_equals && !in_quotes) { log_error("invalid config file format: unquoted '%c' " "(line %d char %ld)", *s, lineno, s - line); goto out; - } else if ((*s == '/' || *s == ' ' || *s == '+' + } else if ((*s == '/' || *s == '+' || *s == '(' || *s == ')' || *s == ':' || *s == ',' || *s == '@') && !in_quotes) { log_error("invalid config file format: unquoted '%c' " "(line %d char %ld)", *s, lineno, s - line); goto out; + } else if ((*s == ' ') + && !in_quotes && !got_quotes) { + log_error("invalid config file format: unquoted whitespace " + "(line %d char %ld)", lineno, s - line); + goto out; } else if (*s == '"' && !got_equals) { log_error("invalid config file format: unexpected quotes " "(line %d char %ld)", lineno, s - line); goto out; } else if (*s == '"' && !in_quotes) { in_quotes = 1; if (val) { val++; got_quotes = 1; } } else if (*s == '"' && in_quotes) { in_quotes = 0; *s = '\0'; } s++; } if (!got_equals) { log_error("invalid config file format: missing '=' (lineno %d)", lineno); goto out; } if (in_quotes) { log_error("invalid config file format: unterminated quotes (lineno %d)", lineno); goto out; } if (!got_quotes) *s = '\0'; if (strlen(key) > BOOTH_NAME_LEN || strlen(val) > BOOTH_NAME_LEN) { log_error("key/value too long"); goto out; } if (!strcmp(key, "transport")) { if (!strcmp(val, "UDP")) booth_conf->proto = UDP; else if (!strcmp(val, "SCTP")) booth_conf->proto = SCTP; else { log_error("invalid transport protocol"); goto out; } got_transport = 1; } if (!strcmp(key, "port")) booth_conf->port = atoi(val); if (!strcmp(key, "site")) { if (booth_conf->node_count == MAX_NODES) { log_error("too many nodes"); goto out; } booth_conf->node[booth_conf->node_count].family = BOOTH_PROTO_FAMILY; booth_conf->node[booth_conf->node_count].type = SITE; booth_conf->node[booth_conf->node_count].nodeid = booth_conf->node_count; strcpy(booth_conf->node[booth_conf->node_count++].addr, val); } if (!strcmp(key, "arbitrator")) { if (booth_conf->node_count == MAX_NODES) { log_error("too many nodes"); goto out; } booth_conf->node[booth_conf->node_count].family = BOOTH_PROTO_FAMILY; booth_conf->node[booth_conf->node_count].type = ARBITRATOR; booth_conf->node[booth_conf->node_count].nodeid = booth_conf->node_count; strcpy(booth_conf->node[booth_conf->node_count++].addr, val); } if (!strcmp(key, "ticket")) { int count = booth_conf->ticket_count; if (booth_conf->ticket_count == ticket_size) { if (ticket_realloc() < 0) goto out; } expiry = index(val, ';'); weight = rindex(val, ';'); if (!expiry) strcpy(booth_conf->ticket[count].name, val); else if (expiry && expiry == weight) { *expiry++ = '\0'; while (*expiry == ' ') expiry++; strcpy(booth_conf->ticket[count].name, val); booth_conf->ticket[count].expiry = atoi(expiry); } else { *expiry++ = '\0'; *weight++ = '\0'; while (*expiry == ' ') expiry++; while (*weight == ' ') weight++; strcpy(booth_conf->ticket[count].name, val); booth_conf->ticket[count].expiry = atoi(expiry); i = 0; while ((c = index(weight, ','))) { *c++ = '\0'; booth_conf->ticket[count].weight[i++] = atoi(weight); while (*c == ' ') c++; weight = c; if (i == MAX_NODES) { log_error("too many weights"); break; } } } booth_conf->ticket_count++; } } if (!got_transport) { log_error("config file was missing transport line"); goto out; } return 0; out: free(booth_conf); return -1; } int check_config(int type) { // int i; if (!booth_conf) return -1; /* for (i = 0; i < booth_conf->node_count; i++) { if (booth_conf->node[i].local && booth_conf->node[i].type == type) return 0; } return -1;*/ return 0; } diff --git a/test/servertests.py b/test/servertests.py index aee83dd..9b8b55b 100755 --- a/test/servertests.py +++ b/test/servertests.py @@ -1,104 +1,129 @@ #!/usr/bin/python import copy from pprint import pprint, pformat import re import string from serverenv import ServerTestEnvironment class ServerTests(ServerTestEnvironment): # We don't know enough about the build/test system to rely on the # existence, permissions, contents of the default config file. So # we can't predict (and hence test) how booth will behave when -c # is not specified. # # def test_no_args(self): # # If do_server() called lockfile() first then this would be # # the appropriate test: # #self.assertLockFileError(lock_file=False) # # # If do_server() called setup() first, and the default # # config file was readable non-root, then this would be the # # appropriate test: # self.configFileMissingMyIP(lock_file=False) # # def test_custom_lock_file(self): # (pid, ret, stdout, stderr, runner) = self.run_booth(expected_exitcode=1) # self.assertRegexpMatches( # stderr, # 'failed to open %s: ' % runner.config_file_used(), # 'should fail to read default config file' # ) def test_example_config(self): self.configFileMissingMyIP(config_file=self.example_config_path) def test_config_file_buffer_overflow(self): # https://bugzilla.novell.com/show_bug.cgi?id=750256 longfile = (string.lowercase * 5)[:127] expected_error = "'%s' exceeds maximum config file length" % longfile self._test_buffer_overflow(expected_error, config_file=longfile) def test_lock_file_buffer_overflow(self): # https://bugzilla.novell.com/show_bug.cgi?id=750256 longfile = (string.lowercase * 5)[:127] expected_error = "'%s' exceeds maximum lock file length" % longfile self._test_buffer_overflow(expected_error, lock_file=longfile) def test_working_config(self): (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=self.working_config) def test_missing_quotes(self): orig_lines = self.working_config.split("\n") for i in xrange(len(orig_lines)): new_lines = copy.copy(orig_lines) new_lines[i] = new_lines[i].replace('"', '') new_config = "\n".join(new_lines) line_contains_IP = re.search('=.+\.', orig_lines[i]) if line_contains_IP: # IP addresses need to be surrounded by quotes expected_exitcode = 1 else: expected_exitcode = 0 (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=new_config, expected_exitcode=expected_exitcode) if line_contains_IP: self.assertRegexpMatches( self.read_log(), "ERROR: invalid config file format: unquoted '.'", 'IP addresses need to be quoted' ) def test_debug_mode(self): (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=self.working_config, debug=True, expected_exitcode=None) def test_missing_transport(self): config = re.sub('transport=.+\n', '', self.typical_config) (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=config, expected_exitcode=1) self.assertRegexpMatches( self.read_log(), 'config file was missing transport line' ) def test_invalid_transport_protocol(self): config = re.sub('transport=.+', 'transport=SNEAKERNET', self.typical_config) (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=config, expected_exitcode=1) self.assertRegexpMatches( self.read_log(), 'invalid transport protocol' ) def test_missing_final_newline(self): config = re.sub('\n$', '', self.working_config) (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=config) + + def test_a_few_trailing_whitespaces(self): + for ws in (' ', ' '): + new_config = self.working_config.replace("\n", ws + "\n", 3) + (pid, ret, stdout, stderr, runner) = \ + self.run_booth(config_text=new_config, + expected_exitcode=0) + + def test_trailing_space_everywhere(self): + for ws in (' ', ' '): + new_config = self.working_config.replace("\n", ws + "\n") + (pid, ret, stdout, stderr, runner) = \ + self.run_booth(config_text=new_config, + expected_exitcode=0) + + def test_unquoted_space(self): + for ticket in ('unquoted space', 'unquoted space man'): + new_config = re.sub('ticket=.+', 'ticket=' + ticket, + self.working_config, 1) + (pid, ret, stdout, stderr, runner) = \ + self.run_booth(config_text=new_config, expected_exitcode=1) + self.assertRegexpMatches( + self.read_log(), + 'invalid config file format: unquoted whitespace' + )