0

I have an application where I want the user to be able to enter ip addresses that are saved to a conf file. The addresses need to be checked to ensure they are valid ip addresses (xxx.xxx.xxx.xxx) Given that this is a user set persistent value running on a user application (ie. not root), the conf file must reside in a user folder. I have chosen the user home directory (Raspbian).

The conf file test sample looks like this:

interface=eth0
ip_address=172.30.21.40
routers=172.30.21.1
domain_name_server_1=199.85.126.30
damaim_name_server_2=8.8.8.8

If the user saves a valid ip_address, I want to read and store this in a variable . If the user saves an invalid ip_address, then I want to read and discard the ip address and return an empty string.

I have looked at range of options to do this.
I looked at using source, but I found this requires the conf to be executable. That would add the risk of a user injecting executable code into the conf file.
I think I should be able to read, check and store the ip_address value in a one line sed command, but I just can't get it to work.

The test script is:

!/bin/bash
conf_file='/home/user/ip.conf'
 v1="$(sed -n 's/\b(?:ip_address=)(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\b/\1/p' $conf_file)"
echo "The ip address is : $v1"
exit

To break this down into parts:

\b(?:ip_address=)   # match the string "ip_address=" starting with a word separator \b

(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))
^                                                                                  ^
# This section checks the format and number range of the ip address.  This is made up of three
# groups that are all contained with a set of brackets (marked with ^) to create a group 1 with
# the whole ip address.  This is what I want to capture.  This ends with a word separator \b

/\1/p         # This is the substitution section where I specify group 1 and print to save to $v1.

When I run this command I get the error

sed: -e expression #1, char 110: invalid reference \1 on `s' command's RHS

When I enter:

\b(?:ip_address=)(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\b

into the online regex tester it works without error. It identifies the full ip address as group 1.

The sed command doesn't seem to recognise the back reference \1 and I can't figure out what I am doing wrong.

Edit

I tried a simple command:

 v1="$(sed -n -E  's/^\s*(interface=)(.*)\b/\2/p' $conf_file)"

This only worked correctly with the -E option added. This is based on an answer found here. I can't find any documentation on -E but it appears to enable extended regular expressions.

dazz
  • 119
  • 2
  • 14
  • Are you able to get a back reference `\1` working for a simple sed command? – jas May 15 '20 at 09:59
  • 1
    This may help: https://stackoverflow.com/questions/13777387/check-for-ip-validity – cmosig May 15 '20 at 11:23
  • @jas I tried this command ` v1="$(sed -n 's/^\s*(?:ip_address=)(\d\d\d\.)\b/\1/p' $conf_file)"` This should return `123.` but I get the same error. I also tried removing `?:` which should have returned `ip_address` but same error. The error message indicates that the reference command recognised as a reference command, but has an error. – dazz May 15 '20 at 21:36
  • @cmosig I looked at a wide range of answers to the problem of reading an IP address from a config file. sed allows for a 1 line command that does a number of tasks (open file, searches for a command, ignores comments on the same line, returns a selected group, does full range/format checking and saves to a variable.) The only thing it doesn't do is work. If it would work, I think it would be useful to others with the same problem. It could easily be modified to return other conf options. – dazz May 15 '20 at 21:46
  • I tried this command ` v1="$(sed -n --debug 's/^\s*(?:interface=)(.*)\b/\1/p' $conf_file)" ` which should have returned `eth0` but gave the error `sed: -e expression #1, char 31: invalid reference \1 on `s' command's RHS` Turning on --debug had no effect on the error message. – dazz May 15 '20 at 21:54
  • `(?:` - no, `sed` doesn't support advanced regex features like lookarounds. No, `(?:ip_address=)` matches the string `(?:ip_address=)` literally in basic regex. `I can't find any documentation on -E` [sed manual](https://www.gnu.org/software/sed/manual/sed.html) – KamilCuk May 15 '20 at 22:16
  • @KamilCuk Yes, testing shows the `?:` is the problem. – dazz May 15 '20 at 22:36

2 Answers2

1

Too much. Big problems are sum of small problems - just take one little problem one at a time.

# Filter lines with ip_address. Allow leading spaces.
if ! ip_address=$(grep '^[[:space:]]*ip_address=' "$conf_file")l then
    echo "ERROR: no line ip_Address found in config file" >&2
    exit 2
fi

# dissallow two ip_address= lines
if [[ "$(printf "%s\n" "$ip_address" | wc -l)" -gt 1 ]]; then
    echo "ERROR: There are two lines with ip_address in config file!" >&2
    exit 2
fi

# remove the string before `=`
ip_address=${ip_address##*=}

# check if it's a valid address
re='^(0*(1?[0-9]{1,2}|2([0-4][0-9]|5[0-5]))\.){3}'
re+='0*(1?[0-9]{1,2}|2([‌​0-4][0-9]|5[0-5]))$'
if [[ ! $ip_address =~ $re ]]; then
    echo "ERROR: ip_Address option is not valid ip address" >&2
    exit 2
fi
echo "found ip_Address=$ip_address"

But sure, you can do it all in GNU sed, including error handling:

if ! ip_address=$(sed -n '
       # if its the last line i should have ip_address in hold space
       ${
          x
          # if I dont, that means error
          /^$/{
             s/.*/ERROR: no ip_Address found in the file/
             p
             q 1
          }
          # print the ip address
          p
       }
       # remove lines that are not ip_Addresses
       /^[[:space:]]*ip_address=/!{d;b}
       # remove ip_address= strnig
       s///

       # if there is something in hold space, means we already found ip_address
       x
       /^$/!{
         s/.*/ERROR: two lines with ipaddress found/
         p
         q 1
       }
       x

       # check if the rest is a valid ip addresss
       /^\(0*\(1\?[0-9]\{1,2\}\|2\([0-4][0-9]\|5[0-5]\)\)\.\)\{3\}0*\(1\?[0-9]\{1,2\}\|2\([0-4][0-9]\|5[0-5]\)\)$/!{
          s/.*/ERROR: Invalid ip address: &/
          p
          q 1
       }

       # hold the valid ip_address in hold space
       h

' "$conf_file"); then
       echo "$ip_address" >&2
       exit 2
fi
echo "Found ip_address=$ip_address"

I believe your idea was to do it just like:

sed -n -E 's/^ip_address=(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/\1/p' "$conf_file"

which would be "good enough", but will be silent if user makes a mistake.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
0

Thanks to assistance in the comments, the problem was found to be the ?: term in the regex. sed couldn't process that. Here is a demo script of the solution I was looking for:

#!/bin/bash
conf_file='/home/user/ip.conf'

v1=$(sed -n -E  's/^\s*(ip_address=)(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\s(.*)$/\2/p' $conf_file)
echo "The ip address is : $v1"

v2="$(sed -n -E  's/^\s*(interface=)(.*)\s(.*)/\2/p' $conf_file)"
echo "The interface  is : $v2"

v3=$(sed -n -E  's/^\s*(routers=)(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\b(.*)/\2/p' $conf_file)
echo "The router ip  is : $v3"

exit 0

The test config file, which intentionally includes errors, is:

    interface=eth0  #comment
ip_address=172.30.21.40   # comment
ip_address=123.30.21.40   comment
ip_address=1234.123.30.21.40
ip_address=ab3.dd30.21.40

routers=172.30.21.1  172.123.456.234
domain_name_server_1=199.85.126.30
damaim_name_server_2=8.8.8.8

The output is:

The ip address is : 172.30.21.40
123.30.21.40
The interface  is : eth0
The router ip  is : 172.30.21.1

The error detection isn't perfect (it doesn't gracefully handle duplicate lines), and for my application it doesn't need to be. It is good enough. In my application, users will not have direct access to the command line or their home directory so this script is intended to be a second line of defense against bad input. Your requirements might vary.

This one line of code completes a number of tasks.

  1. open file,

  2. searches for a option name,

  3. reads the option value,

  4. checks the option value format/content against the regex filter,

  5. ignores other text and comments on the same line,

  6. returns the option value if valid, or "" if not and,

  7. saves to a variable.

If the entry is missing or invalid, the return value will be "". In that case, a default value will be used (code not shown).

Getting it to work took way too much time but I learnt a lot about regex and sed. Now that it works, it is easy to adapt to read other than ip addresses, as shown in the test files.

dazz
  • 119
  • 2
  • 14