From 79386b5db780fee3d1469941e640c2cbe2598a5c Mon Sep 17 00:00:00 2001 From: nbd Date: Mon, 26 Feb 2007 20:04:04 +0000 Subject: Integrate basic UCI config file validation support Needs more testing and validation is not enforced yet Code contributed by Fraunhofer Fokus git-svn-id: svn://svn.openwrt.org/openwrt/trunk@6391 3c298f89-4303-0410-b956-a3cf2f4a3e73 --- package/base-files/files/lib/config/parse_spec.awk | 255 +++++++++++++++++++++ .../base-files/files/lib/config/specs/network.spec | 7 + package/base-files/files/lib/config/uci.sh | 15 +- package/base-files/files/lib/config/validate.sh | 74 ++++++ .../files/lib/config/validate_config.awk | 105 +++++++++ .../base-files/files/lib/config/validate_spec.awk | 171 ++++++++++++++ 6 files changed, 620 insertions(+), 7 deletions(-) create mode 100644 package/base-files/files/lib/config/parse_spec.awk create mode 100644 package/base-files/files/lib/config/specs/network.spec mode change 100755 => 100644 package/base-files/files/lib/config/uci.sh create mode 100644 package/base-files/files/lib/config/validate.sh create mode 100644 package/base-files/files/lib/config/validate_config.awk create mode 100644 package/base-files/files/lib/config/validate_spec.awk (limited to 'package/base-files/files/lib') diff --git a/package/base-files/files/lib/config/parse_spec.awk b/package/base-files/files/lib/config/parse_spec.awk new file mode 100644 index 000000000..5eabc5ac9 --- /dev/null +++ b/package/base-files/files/lib/config/parse_spec.awk @@ -0,0 +1,255 @@ +# AWK file for parsing uci specification files +# +# Copyright (C) 2006 by Fokus Fraunhofer +# +# 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 of the License, or +# (at your option) any later version. +# +# This program 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 program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# +# general: unfortunately, the development was done using gawk providing +# a different match() functions than e.g. mawk on debian systems +# - therefore, the script was changed to run on most awk's +# - even things like [:space:] are not used +# +# - script parses the config section definition contained in one +# specification file +# global variables: +# * section - contains the current config section name +# * var - contains the name of the current config option +# * type - contains the type of the current config option +# * required - contains the requirements of the current config option +# * optional - contains the optional scope of the current config option +# * vars[] - array, contains the name of all config options valid within +# a certain config section, format: csv +# +# XXX todo: more than one config option with the same in different section +# will clash for the following tables +# * types[] - contains the type of a config option +# * reqs[] - contains the requirements of a config option +# * opts[] - contains the optional scope of a config option +# +BEGIN { + section_count=1 + section = "" + simple_types = "int|ip|netmask|string|wep|hostname|mac|port|ports|wpapsk" +} + +# function print_specification +# - prints all information about the created tables containing the +# specification +function print_specification() { + for (section in vars) { + printf("%s\n",section); + split(vars[section],arr,",") + for (idx in arr) { + printf("\t%s[%s]",arr[idx],types[section "_" arr[idx]]); + if (length(reqs[section "_" arr[idx]])) { + if (reqs[section "_" arr[idx]]==1) { + printf(",req"); + }else{ + printf(", req(%s)", reqs[section "_" arr[idx]]); + } + } + if (length(opts[section "_" arr[idx]])) { + printf(", opt(%s)", opts[section "_" arr[idx]]); + } + printf("\n"); + } + } +} + + +function reset_option() { + # just set global variables parsed on one line back to defaults + var = "" + type = "" + required = "" + optional = "" + found = 0 +} + +function store_option() { + # save all information about a config option parsed from the spec file + # to the relevant tables for future use + + # first check minimum requirements for storing information + if (!length(section)) { + print STDERR "line " NR ": section definition missing" + exit 1 + } + if (!length(var)) { + print STDERR "line " NR ": invalid config option name" + exit 1 + } + if (!length(type)) { + print STDERR "line " NR ": invalid config option type" + exit 1 + } + + # add config options to the names of options available for this + # section + if (exists[section]!=1) { + section_names[section_count] = section + section_count++ + exists[section] = 1 + vars[section] = var + } else { + vars[section] = vars[section] "," var + } + + # save the type, the requirements and the optional scope of the + # config option + types[section "_" var] = type + reqs[section "_" var] = required + opts[section "_" var] = optional +} + +/^declare -x|^export/ { + sub(/^declare -x /,"") + sub(/^export /,"") + split($0,arr,"=") + val=substr(arr[2],2,length(arr[2])-2) + ENVIRON[arr[1]] = val + next +} + +# main parsing function +# this is done in one function block to allow multiple semicolon separated +# definitions on one line +{ + # replace leading/trailing white space + gsub("^[ \t\n]+",""); + gsub("[ \t\n]+$",""); + + # comments are removed + # XXX todo: check for quoted comments?? + if (match($0,/[^#]*/)) { + rest=substr($0,RSTART,RLENGTH) + } else { + rest=$0 + } + + # match the config section "
{" + if (match(rest,/^[^ \t\n{]+[ \t\n]*\{/)) { + match(rest,/^[^ \t\n{]+/) + section = substr(rest,RSTART,RLENGTH) + rest=substr($0,RSTART+RLENGTH); + match(rest,/[ \t\n]*\{/) + rest=substr(rest,RSTART+RLENGTH) + # check for array indication + if (match(section,/\[[ \t\n]*\]/)) { + section=substr(section,1,RSTART-1) + multiple[section] = 1 + } else { + multiple[section] = 0 + } + } + + reset_option() + + # parse the remaing line as long as there is something to parse + while (rest ~ "[^ \t\n}]+") { + found = 0 + + # get option name and option type + # first, check for "simple" datatype definitions + if (match(rest,"[^: \t\n]+[ \t\n]*:[ \t\n]*(" \ + simple_types ")")){ + match(rest,"[^: \t\n]+") + var=substr(rest,RSTART,RLENGTH) + rest=substr(rest,RSTART+RLENGTH) + match(rest,"[ \t\n]*:[ \t\n]*") + rest=substr(rest,RSTART+RLENGTH) + match(rest,"(" simple_types ")") + type=substr(rest,RSTART,RLENGTH) + rest = substr(rest,RSTART+RLENGTH) + found = 1 + # next, check for enum definitions + } else if (match(rest,/[^: \t\n]+[ \t\n]*:[ \t\n]*enum\([^\)]+\)/ )) { + match(rest,"[^: \t\n]+") + var=substr(rest,RSTART,RLENGTH) + rest=substr(rest,RSTART+RLENGTH) + match(rest,/[ \t\n]*:[ \t\n]*enum\(/) + rest=substr(rest,RSTART+RLENGTH) + match(rest,/[^\)]+/) + type="enum," substr(rest,RSTART,RLENGTH) + rest = substr(rest,RSTART+RLENGTH+1) + found=1 + } + + # after the name and the type, + # get the option requirements/scope + if (match(rest,/[^,]*,[ \t\n]*required\[[^]]+\]/)) { + match(rest,"[^,]*") + save=substr(rest,RSTART,RLENGTH) + rest=substr(rest,RSTART+RLENGTH) + match(rest,/,[ \t\n]*required\[/); + rest=substr(rest,RSTART+RLENGTH) + match(rest,/[^]]+\]/) + required=substr(rest,RSTART,RLENGTH-1) + save=save substr(rest,RSTART+RLENGTH) + rest=save + found=1 + } else if (match(rest,/[^,]*,[ \t\n]*required/)) { + match(rest,"[^,]*") + save=substr(rest,RSTART,RLENGTH) + rest=substr(rest,RSTART+RLENGTH) + match(rest,",[ \t\n]*required"); + rest=substr(rest,RSTART+RLENGTH) + required=1 + save=save substr(rest,RSTART+RLENGTH) + rest=save + found=1 + } + if (match(rest,/[^,]*,[ \t\n]*optional\[[^]]+\]/)) { + match(rest,"[^,]*") + save=substr(rest,RSTART,RLENGTH) + rest=substr(rest,RSTART+RLENGTH) + match(rest,/,[ \t\n]*optional\[/); + rest=substr(rest,RSTART+RLENGTH) + match(rest,/[^]]+\]/) + optional=substr(rest,RSTART,RLENGTH-1) + save=save substr(rest,RSTART+RLENGTH) + rest=save + found=1 + } + + # if the remaining line contains a semicolon, complete the + # specification of the config options + if (match(rest, "^[ \t\n]*;(.*)")) { + match(rest,"^[ \t\n]*;") + rest=substr(rest,RSTART+RLENGTH) + if (found==1) { + store_option() + } + reset_option() + + # if nothing matched on this line, clear the rest + } else if (!found) { + rest = "" + } + } + + # after the line is pared, store the configuration option in the + # table if any has been defined + if (length(var)) { + store_option() + reset_option() + } + # close the section if the line contained a closing section bracket, + # XXX todo: check if this has to be done more intelligent + if ($0 ~ /\}/) { + section="" + } +} diff --git a/package/base-files/files/lib/config/specs/network.spec b/package/base-files/files/lib/config/specs/network.spec new file mode 100644 index 000000000..3d19b5cf4 --- /dev/null +++ b/package/base-files/files/lib/config/specs/network.spec @@ -0,0 +1,7 @@ +interface[] { + proto: string, required; + ipaddr: ip, required[proto=static]; + netmask: ip, required[proto=static]; + gateway: ip; + dns: ip; +} diff --git a/package/base-files/files/lib/config/uci.sh b/package/base-files/files/lib/config/uci.sh old mode 100755 new mode 100644 index 12795d813..43bb981cc --- a/package/base-files/files/lib/config/uci.sh +++ b/package/base-files/files/lib/config/uci.sh @@ -1,8 +1,8 @@ #!/bin/sh # Shell script defining macros for manipulating config files # -# Copyright (C) 2006 by Fokus Fraunhofer -# Copyright (C) 2006 by Felix Fietkau +# Copyright (C) 2006 Fokus Fraunhofer +# Copyright (C) 2006,2007 Felix Fietkau # # 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 @@ -31,7 +31,7 @@ uci_load() { uci_do_update() { local FILENAME="$1" local UPDATE="$2" - awk -f /lib/config/uci-update.awk -f - < "/tmp/.uci/$PACKAGE_BASE.new" && { mv -f "/tmp/.uci/$PACKAGE_BASE.new" "$CONFIG_FILENAME" && \ rm -f "/tmp/.uci/$PACKAGE_BASE" } ) - lock -u "/tmp/.uci/$PACKAGE_BASE.lock" + $LOCK -u "/tmp/.uci/$PACKAGE_BASE.lock" } diff --git a/package/base-files/files/lib/config/validate.sh b/package/base-files/files/lib/config/validate.sh new file mode 100644 index 000000000..e16319dad --- /dev/null +++ b/package/base-files/files/lib/config/validate.sh @@ -0,0 +1,74 @@ +# Shell script defining validating configuration macros +# +# Copyright (C) 2006 by Fokus Fraunhofer +# Copyright (C) 2007 by Felix Fietkau +# +# 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 of the License, or +# (at your option) any later version. +# +# This program 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 program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +validate_spec() { + export | grep 'CONFIG_' | cat - "$@" | awk \ + -f $UCI_ROOT/lib/config/validate_config.awk \ + -f $UCI_ROOT/lib/config/parse_spec.awk \ + -f $UCI_ROOT/lib/config/validate_spec.awk +} + +validate_config_cb () { + local TYPE + local res= + + [ -n "${CONFIG_SECTION}" ] || return 0 + + config_get TYPE ${CONFIG_SECTION} TYPE + [ -n "$TYPE" ] || return 0 + + if type validate_${PACKAGE}_${TYPE} >/dev/null 2>&1; then + validate_${PACKAGE}_${TYPE} + res="$?" + else + if [ -f $UCI_ROOT/lib/config/specs/${PACKAGE}.spec ]; then + # no special defined, use default one + validate_spec $UCI_ROOT/lib/config/specs/${PACKAGE}.spec + res="$?" + fi + fi + + VALIDATE_RES="${VALIDATE_RES:-$res}" +} + +uci_validate() {( + PACKAGE="$1" + FILE="$2" + VALIDATE_RES= + + [ -z "${PACKAGE}" ] && { + echo "Error: no package defined" + return 1 + } + + reset_cb + config_cb() { + validate_config_cb "$@" + } + unset NO_EXPORT + if [ -n "$FILE" ]; then + . "$FILE" + config + else + config_load "$1" + fi + + return ${VALIDATE_RES:-0} +)} diff --git a/package/base-files/files/lib/config/validate_config.awk b/package/base-files/files/lib/config/validate_config.awk new file mode 100644 index 000000000..053694efd --- /dev/null +++ b/package/base-files/files/lib/config/validate_config.awk @@ -0,0 +1,105 @@ +# AWK file for validating uci specification files +# +# Copyright (C) 2006 by Fokus Fraunhofer +# +# 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 of the License, or +# (at your option) any later version. +# +# This program 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 program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +function is_int(value) { + valid = 1 + if (value !~ /^[0-9]*$/) { valid = 0 } + return valid +} + +function is_netmask(value) { + return is_ip(value) +} + +function is_ip(value) { + valid = 1 + if ((value != "") && (value !~ /^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$/)) valid = 0 + else { + split(value, ipaddr, "\\.") + for (i = 1; i <= 4; i++) { + if ((ipaddr[i] < 0) || (ipaddr[i] > 255)) valid = 0 + } + } + return valid +} + +function is_wep(value) { + valid = 1 + if (value !~ /^[0-9A-Fa-f]*$/) { + valid = 0 + } else if ((length(value) != 0) && (length(value) != 10) && (length(value) != 26)) { + valid = 0 + } else if (value ~ /0$/) { + valid = 0 + } + return valid +} + +function is_hostname(value) { + valid = 1 + if (value !~ /^[0-9a-zA-z\.\-]*$/) { + valid = 0 + } + return valid; +} + +function is_string(value) { + return 1; +} + +function is_mac(value) { + valid = 1 + if ((value != "") && (value !~ /^[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]$/)) { + valid = 0 + } + return valid +} + +function is_port(value) { + valid = 1 + if (value !~ /^[0-9]*$/) { + valid = 0 + } + return valid +} + +function is_ports(value) { + valid = 1 + n = split(value ",", ports, ",") + for (i = 1; i <= n; i++) { + if ((ports[i] !~ /^[0-9]*$/) && (ports[i] !~ /^[0-9][0-9]*-[0-9][0-9]*$/)) { + valid = 0 + } + } + return valid +} + +function is_wpapsk(value) { + valid = 1 + if (length(value) > 64) { + valid = 0 + } + if ((length(value) != 0) && (length(value) < 8)) { + valid = 0 + } + if ((length(value) == 64) && (value ~ /[^0-9a-fA-F]/)) { + valid = 0 + } + return valid +} + diff --git a/package/base-files/files/lib/config/validate_spec.awk b/package/base-files/files/lib/config/validate_spec.awk new file mode 100644 index 000000000..0816c0829 --- /dev/null +++ b/package/base-files/files/lib/config/validate_spec.awk @@ -0,0 +1,171 @@ +# AWK file for validating uci specification files +# +# Copyright (C) 2006 by Fokus Fraunhofer +# Copyright (C) 2007 by Felix Fietkau +# +# 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 of the License, or +# (at your option) any later version. +# +# This program 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 program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# +# general: unfortunately, the development was done using gawk providing +# a different match() functions than e.g. mawk on debian systems +# - therefore, the script was changed to run on most awk's +# - even things like [:space:] are not used +# +# - script parses the config section definition contained in one +# specification file +# global variables: +# * section - contains the current config section name +# * var - contains the name of the current config option +# * type - contains the type of the current config option +# * required - contains the requirements of the current config option +# * optional - contains the optional scope of the current config option +# * vars[] - array, contains the name of all config options valid within +# a certain config section, format: csv +# +# XXX todo: more than one config option with the same in different section +# will clash for the following tables +# * types[] - contains the type of a config option +# * reqs[] - contains the requirements of a config option +# * opts[] - contains the optional scope of a config option +# + +# - check requirement validates, if the config option is required in +# the config section type and if so, if it is defined +# - the functions exits with error in case of non-conforming +# behaviour +# XXX todo: use return instead of exit +# +function check_requirements(vsec,var) { + # check, if config option is required in all cases + if (reqs[vsec "_" var] == 1) { + # option is always required, is it defined? + if (!length(ENVIRON["CONFIG_" vsec "_" var])) { + print STDERR "Error: missing config option " var " in " vsec + exit 1 + } + + # check, if config option is required only when other options + # have certain values + } else if (length(reqs[vsec "_" var])) { + # - check all requirements, e.g. proto=static,proto=pptp + # - note, that the required flag is tiggered if at least one + # of the conditions is met + split(reqs[vsec "_" var],arr,","); + for (idx in arr) { + # parse the condition space tolerant + if (!match(arr[idx],"^[ \t\n]*[^ \t\n=]+"\ + "[ \t\n]*=.+")) { + print STDERR "Error: invalid requirement "\ + "in spec file for " var " : " arr[idx] + exit 1 + } + # get the name of the variable + match(arr[idx],"[^ \t\n=]+"); + name=substr(arr[idx],RSTART,RLENGTH) + mrest=substr(arr[idx],RSTART+RLENGTH) + # get the spaces + match(mrest,"[ \t\n]*=[ \t\n]*") + val=substr(mrest,RSTART+RLENGTH) + # check the condition + if (ENVIRON["CONFIG_" vsec "_" name] == val) { + # condition is met, check requirement + if (!length(ENVIRON["CONFIG_" vsec "_" var])) { + print STDERR "Error: missing config " \ + "option " var " in " vsec + exit 1 + } + } + } + } +} + +# is_valid just returns true(1)/false(0) if the +# given value is conform with the type definition +# NOTE: this function needs the type validating function from +# validate_config.awk +# +function is_valid(type,value) { + + # the enum type contains a definition of all allowed values as csv + # e.g. enum,alpha,beta,gamma + if (type ~ "enum" ) { + split(type,tarr,",") + for (num in tarr) { + if (num > 0) { + gsub("^[ \t\n]*","",tarr[num]); + gsub("[ \t\n]*$","",tarr[num]); + if (tarr[num] == value) { + return 1 + } + } + } + return 0; + } + + # all other types are checked as defined in the former validate.awk + if (type ~ "int") return is_int(value) + if (type ~ "ip" ) return is_ip(value) + if (type ~ "netmask" ) return is_netmask(value) + if (type ~ "string" ) return is_string(value) + if (type ~ "wep" ) return is_wep(value) + if (type ~ "hostname" ) return is_hostname(value) + if (type ~ "mac" ) return is_mac(value) + if (type ~ "port" ) return is_port(value) + if (type ~ "ports" ) return is_ports(value) + if (type ~ "wpapsk" ) return is_wpapsk(value) +} + +# validate_config compares the specification as parsed from the spec file +# with the environment variables +# CONFIG_SECTION contains the relevant config section name, e.g. wan +# CONFIG_
_TYPE contains the type of the config, e.g. interface +# CONFIG_
_ contains the value of the config option +# +function validate_config() { + # get the config section name + vname=ENVIRON["CONFIG_SECTION"] + if (!length(vname)) { + print STDERR "Error: no current configuration" + exit 1 + } + # get the config section type + vsec=ENVIRON["CONFIG_" vname "_TYPE"] + if (!length(vsec)) { + print STDERR "Error: section " vsec " not found" + exit 1 + } + + # loop through all config options specified for this section type + split(vars[vsec],options,",") + for (oidx in options) { + # first, look for all required attributes + var=options[oidx] + check_requirements(vname,var) + + # next look at each option and validate it + val=ENVIRON["CONFIG_" vname "_" var] + if (length(val)) { + if (!is_valid(types[vsec "_" var],val)) { + print "Error: type validation error for '" var "' in section '" vname "'" + exit 1 + } + } + } +} + + +END { + validate_config() +} -- cgit v1.2.3