diff --git a/src/guacd/Makefile.am b/src/guacd/Makefile.am index 2c2078e4..8ce296b0 100644 --- a/src/guacd/Makefile.am +++ b/src/guacd/Makefile.am @@ -27,10 +27,21 @@ AM_CFLAGS = -Werror -Wall -pedantic @LIBGUAC_INCLUDE@ @COMMON_INCLUDE@ sbin_PROGRAMS = guacd man_MANS = man/guacd.8 -noinst_HEADERS = client.h client-map.h log.h -guacd_SOURCES = daemon.c client.c client-map.c log.c -guacd_LDADD = @LIBGUAC_LTLIB@ @COMMON_LTLIB@ -guacd_LDFLAGS = @PTHREAD_LIBS@ @SSL_LIBS@ +noinst_HEADERS = \ + client.h \ + client-map.h \ + conf-file.h \ + log.h + +guacd_SOURCES = \ + daemon.c \ + client.c \ + client-map.c \ + conf-file.c \ + log.c + +guacd_LDADD = @LIBGUAC_LTLIB@ @COMMON_LTLIB@ +guacd_LDFLAGS = @PTHREAD_LIBS@ @SSL_LIBS@ EXTRA_DIST = init.d/guacd.in man/guacd.8 CLEANFILES = $(init_SCRIPTS) diff --git a/src/guacd/conf-file.c b/src/guacd/conf-file.c new file mode 100644 index 00000000..d9c531f4 --- /dev/null +++ b/src/guacd/conf-file.c @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "config.h" + +#include "conf-file.h" + +#include +#include + +/* + * Simple recursive descent parser for an INI-like conf file grammar. The + * grammar is, roughly: + * + * ::= + * ::= + * ::= | | "" + * ::= "[" "]" + * ::= "=" + * + * Where: + * is any number of tabs or spaces. + * is a # character followed by any length of text without an EOL. + * is an alpha-numeric string consisting of: [A-Za-z0-9_-]. + * is any length of text without an EOL or # character, or a double-quoted string (backslash escapes legal). + * is a carriage return or line feed character. + */ + +/** + * The current section. Note that this means the parser is NOT threadsafe. + */ +char __guacd_current_section[GUACD_CONF_MAX_NAME_LENGTH + 1] = ""; + +char* guacd_conf_parse_error = NULL; + +char* guacd_conf_parse_error_location = NULL; + +/** + * Reads through all whitespace at the beginning of the buffer, returning the + * number of characters read. This is in the grammar above. As + * the whitespace is zero or more whitespace characters, this function cannot + * fail, but it may read zero chars overall. + */ +static int guacd_parse_whitespace(char* buffer, int length) { + + int chars_read = 0; + + /* Read through all whitespace */ + while (chars_read < length) { + + /* Read character */ + char c = *buffer; + + /* Stop at non-whitespace */ + if (c != ' ' && c != '\t') + break; + + chars_read++; + buffer++; + + } + + return chars_read; + +} + +/** + * Parses the name of a parameter, section, etc. A section/parameter name can + * consist only of alphanumeric characters and underscores. The resulting name + * will be stored in the name string, which must have at least 256 bytes + * available. + */ +static int guacd_parse_name(char* buffer, int length, char* name) { + + char* name_start = buffer; + int chars_read = 0; + + /* Read through all valid name chars */ + while (chars_read < length) { + + /* Read character */ + char c = *buffer; + + /* Stop at non-name characters */ + if (!isalnum(c) && c != '_') + break; + + chars_read++; + buffer++; + + /* Ensure name does not exceed maximum length */ + if (chars_read > GUACD_CONF_MAX_NAME_LENGTH) { + guacd_conf_parse_error = "Names can be no more than 255 characters long"; + guacd_conf_parse_error_location = buffer; + return -1; + } + + } + + /* Names must contain at least one character */ + if (chars_read == 0) + return 0; + + /* Copy name from buffer */ + memcpy(name, name_start, chars_read); + name[chars_read] = '\0'; + + return chars_read; + +} + +/** + * Parses the value of a parameter. A value can consist of any character except + * '#', whitespace, or EOL. The resulting value will be stored in the value + * string, which must have at least 256 bytes available. + */ +static int guacd_parse_value(char* buffer, int length, char* value) { + + char* value_start = buffer; + int chars_read = 0; + + /* Read through all valid value chars */ + while (chars_read < length) { + + /* Read character */ + char c = *buffer; + + /* Stop at invalid character */ + if (c == '#' || c == '"' || c == '\r' || c == '\n' || c == ' ' || c == '\t') + break; + + chars_read++; + buffer++; + + /* Ensure value does not exceed maximum length */ + if (chars_read > GUACD_CONF_MAX_VALUE_LENGTH) { + guacd_conf_parse_error = "Values can be no more than 8191 characters long"; + guacd_conf_parse_error_location = buffer; + return -1; + } + + } + + /* Values must contain at least one character */ + if (chars_read == 0) { + guacd_conf_parse_error = "Unquoted values must contain at least one character"; + guacd_conf_parse_error_location = buffer; + return -1; + } + + /* Copy value from buffer */ + memcpy(value, value_start, chars_read); + value[chars_read] = '\0'; + + return chars_read; + +} + +/** + * Parses the quoted value of a parameter. Quoted values may contain any + * character except double quotes or backslashes, which must be + * backslash-escaped. + */ +static int guacd_parse_quoted_value(char* buffer, int length, char* value) { + + int escaped = 0; + + /* Assert first character is '"' */ + if (length == 0 || *buffer != '"') + return 0; + + int chars_read = 1; + buffer++; + length--; + + /* Read until end of quoted value */ + while (chars_read < length) { + + /* Read character */ + char c = *buffer; + + /* Handle special characters if not escaped */ + if (!escaped) { + + /* Stop at quote or invalid character */ + if (c == '"' || c == '\r' || c == '\n') + break; + + /* Backslash escaping */ + else if (c == '\\') + escaped = 1; + + else + *(value++) = c; + + } + + /* Reset escape flag */ + else { + escaped = 0; + *(value++) = c; + } + + chars_read++; + buffer++; + + /* Ensure value does not exceed maximum length */ + if (chars_read > GUACD_CONF_MAX_VALUE_LENGTH) { + guacd_conf_parse_error = "Values can be no more than 8191 characters long"; + guacd_conf_parse_error_location = buffer; + return -1; + } + + } + + /* Assert value ends with '"' */ + if (length == 0 || *buffer != '"') { + guacd_conf_parse_error = "'\"' expected"; + guacd_conf_parse_error_location = buffer; + return -1; + } + + chars_read++; + + /* Terminate read value */ + *value = '\0'; + + return chars_read; + +} + +/** + * Reads a parameter/value pair, separated by an '=' character. If the + * parameter/value pair is invalid for any reason, a negative value is + * returned. + */ +static int guacd_parse_parameter(guacd_param_callback* callback, char* buffer, int length) { + + char param_name[GUACD_CONF_MAX_NAME_LENGTH + 1]; + char param_value[GUACD_CONF_MAX_VALUE_LENGTH + 1]; + + int retval; + int chars_read = 0; + + retval = guacd_parse_name(buffer, length, param_name); + if (retval < 0) + return -1; + + /* If no name found, no parameter/value pair */ + if (retval == 0) + return 0; + + /* Validate presence of section header */ + if (__guacd_current_section[0] == '\0') { + guacd_conf_parse_error = "Parameters must have a corresponding section"; + guacd_conf_parse_error_location = buffer; + return -1; + } + + chars_read += retval; + buffer += retval; + length -= retval; + + /* Optional whitespace before '=' */ + retval = guacd_parse_whitespace(buffer, length); + chars_read += retval; + buffer += retval; + length -= retval; + + /* Required '=' */ + if (length == 0 || *buffer != '=') { + guacd_conf_parse_error = "'=' expected"; + guacd_conf_parse_error_location = buffer; + return -1; + } + + chars_read++; + buffer++; + length--; + + /* Optional whitespace before value */ + retval = guacd_parse_whitespace(buffer, length); + chars_read += retval; + buffer += retval; + length -= retval; + + /* Quoted parameter value */ + retval = guacd_parse_quoted_value(buffer, length, param_value); + if (retval < 0) + return -1; + + /* Non-quoted parameter value (required if no quoted value given) */ + if (retval == 0) retval = guacd_parse_value(buffer, length, param_value); + if (retval < 0) + return -1; + + chars_read += retval; + + /* Call callback, handling error code */ + if (callback(__guacd_current_section, param_name, param_value)) + return -1; + + return chars_read; + +} + +/** + * Reads a section name from the beginning of the given buffer. This section + * name must conform to the grammar definition. If the section name does not + * match, a negative value is returned. + */ +static int guacd_parse_section(char* buffer, int length) { + + int retval; + + /* Assert first character is '[' */ + if (length == 0 || *buffer != '[') + return 0; + + int chars_read = 1; + buffer++; + length--; + + retval = guacd_parse_name(buffer, length, __guacd_current_section); + if (retval < 0) + return -1; + + /* If no name found, invalid section */ + if (retval == 0) { + guacd_conf_parse_error = "Section names must contain at least one character"; + guacd_conf_parse_error_location = buffer; + return -1; + } + + chars_read += retval; + buffer += retval; + length -= retval; + + /* Name must end with ']' */ + if (length == 0 || *buffer != ']') { + guacd_conf_parse_error = "']' expected"; + guacd_conf_parse_error_location = buffer; + return -1; + } + + chars_read++; + + return chars_read; + +} + +/** + * Parses a declaration, which may be either a section name or a + * parameter/value pair. The empty string is acceptable, as well, as a + * "null declaration". + */ +static int guacd_parse_declaration(guacd_param_callback* callback, char* buffer, int length) { + + int retval; + + /* Look for section name */ + retval = guacd_parse_section(buffer, length); + if (retval != 0) + return retval; + + /* Lacking a section name, read parameter/value pair */ + retval = guacd_parse_parameter(callback, buffer, length); + if (retval != 0) + return retval; + + /* Null declaration (default) */ + return 0; + +} + +/** + * Parses a comment, which must start with a '#' character, and terminate with + * an end-of-line character. If no EOL is found, or the first character is not + * a '#', a negative value is returned. Otherwise, the number of characters + * parsed is returned. If no comment is present, zero is returned. + */ +static int guacd_parse_comment(char* buffer, int length) { + + /* Need at least one character */ + if (length == 0) + return 0; + + /* Assert first character is '#' */ + if (*(buffer++) != '#') + return 0; + + int chars_read = 1; + + /* Advance to first non-space character */ + while (chars_read < length) { + + /* Read character */ + char c = *buffer; + + /* End of comment found at end of line */ + if (c == '\n' || c == '\r') + return chars_read; + + chars_read++; + buffer++; + + } + + /* No end of line in comment */ + guacd_conf_parse_error = "expected end-of-line"; + guacd_conf_parse_error_location = buffer; + return -1; + +} + +/** + * Parses the end of a line, which may contain a comment. If a parse error + * occurs, a negative value is returned. Otherwise, the number of characters + * parsed is returned. + */ +static int guacd_parse_line_end(char* buffer, int length) { + + int chars_read = 0; + int retval; + + /* Initial optional whitespace */ + retval = guacd_parse_whitespace(buffer, length); + chars_read += retval; + buffer += retval; + length -= retval; + + /* Optional comment */ + retval = guacd_parse_comment(buffer, length); + if (retval < 0) + return -1; + + chars_read += retval; + buffer += retval; + length -= retval; + + /* Assert EOL */ + if (length == 0 || (*buffer != '\r' && *buffer != '\n')) { + guacd_conf_parse_error = "expected end-of-line"; + guacd_conf_parse_error_location = buffer; + return -1; + } + + chars_read++; + + /* Line is valid */ + return chars_read; + +} + +/** + * Parses an entire line - declaration, comment, and all. If a parse error + * occurs, a negative value is returned. Otherwise, the number of characters + * parsed is returned. + */ +static int guacd_parse_line(guacd_param_callback* callback, char* buffer, int length) { + + int chars_read = 0; + int retval; + + /* Initial optional whitespace */ + retval = guacd_parse_whitespace(buffer, length); + chars_read += retval; + buffer += retval; + length -= retval; + + /* Declaration (which may be the empty string) */ + retval = guacd_parse_declaration(callback, buffer, length); + if (retval < 0) + return retval; + + chars_read += retval; + buffer += retval; + length -= retval; + + /* End of line */ + retval = guacd_parse_line_end(buffer, length); + if (retval < 0) + return retval; + + chars_read += retval; + + return chars_read; + +} + +int guacd_parse_conf(guacd_param_callback* callback, char* buffer, int length) { + return guacd_parse_line(callback, buffer, length); +} + diff --git a/src/guacd/conf-file.h b/src/guacd/conf-file.h new file mode 100644 index 00000000..28082b7f --- /dev/null +++ b/src/guacd/conf-file.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef _GUACD_CONF_FILE_H +#define _GUACD_CONF_FILE_H + +/** + * The maximum length of a name, in characters. + */ +#define GUACD_CONF_MAX_NAME_LENGTH 255 + +/** + * The maximum length of a value, in characters. + */ +#define GUACD_CONF_MAX_VALUE_LENGTH 8191 + +/** + * A callback function which is provided to guacd_parse_conf() and is called + * for each parameter/value pair set. The current section is always given. This + * function will not be called for parameters outside of sections, which are + * illegal. + */ +typedef int guacd_param_callback(const char* section, const char* param, const char* value); + +/** + * Parses an arbitrary buffer of configuration file data, calling the given + * callback for each valid parameter/value pair. Upon success, the number of + * characters parsed is returned. On failure, a negative value is returned, and + * guacd_conf_parse_error and guacd_conf_parse_error_location are set. + */ +int guacd_parse_conf(guacd_param_callback* callback, char* buffer, int length); + +/** + * Human-readable description of the current error, if any. + */ +extern char* guacd_conf_parse_error; + +/** + * The location of the most recent parse error. This will be a pointer to the + * location of the error within the buffer passed to guacd_parse_conf(). + */ +extern char* guacd_conf_parse_error_location; + +#endif +