#!/usr/bin/python
# Shorewall per-ip rules generator
#
##############################################################################
#
# Copyright (c) 2007 Nexedi SARL and Contributors. All Rights Reserved.
#                     Julien Gormotte <julien@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
##############################################################################

import sys, os

# Specific error classes

class FileError(Exception):
  def __init__(self, value):
    self.value = 'FileError: '+value
  def __str__(self):
    return self.value
  FILE_DONT_EXIST ='Specified file doesn\'t exists'
  CANT_CREATE_FILE = 'File doesn\'t exists and cannot create it'
  CONFIG_FILE_NOT_READABLE = 'Cannot read given configuration file.'
  OUTPUT_FILE_NOT_WRITABLE = 'Output file cannot be written.'

class ConfigurationError(Exception):
  def __init__(self, error, error_info=None):
    self.error_info = error_info
    self.error = 'ConfigurationError: '+error % error_info
  def __str__(self):
    return self.error
  SECTION_NOT_PRESENT = 'A section is missing in configuration file%s.'
  SYNTAX_ERROR = 'Syntax error on line %s of given configuration file'
  USER_DONT_EXIST = 'User %s doesn\'t exist'
  GROUP_DONT_EXIST = 'Group %s doesn\'t exist'

def msgHelp():
  print '''genrules.py - a tool to generate per-IP Shorewall rules
Usage : genrules.py [-o FILE -i FILE]
  -i, --input-file FILE         Use FILE as configuration file
  -o, --output-file FILE        Write rules in FILE
  -z, --zone ZONE               Write rules for specified ZONE

If no file is specified, stdin and/or stdout are used by default.'''
  sys.exit(2)

def parseFile(line_list): # Cleans up the line list for better processing

  configuration_list = []
  buffer = ''

  for line in line_list:
    if line == '\n':
      continue
    elif '#' in line:
      line = line[:line.find('#')]
      if line <> '':
        configuration_list.append(line.strip('\n'))
    elif buffer <> '':
      if line.strip().endswith('\\'):
        buffer = buffer + line.strip('\n').strip('\\').strip() + ' '
      else:
        
        buffer = buffer + line.strip('\n').strip('\\').strip() + ' '
        configuration_list.append(buffer.strip())
        buffer = ''
    else:    
      if line.strip().endswith('\\'):
        buffer = line.strip('\n').strip('\\').strip() + ' '
      else:
        configuration_list.append(line.strip('\n'))

  return configuration_list

def getRuleSet(configuration_list, shorewall_zone):

  user_start_position = configuration_list.index('[users]')
  group_start_position = configuration_list.index('[groups]')
  rule_start_position = configuration_list.index('[rules]')

  # Create a dictionary for users
  user_dict = {}
  for line in configuration_list[user_start_position+1:group_start_position]:
    user_dict[line.split('=')[0].strip()] = [ ip.strip() for ip in line.split('=')[1].split(',') ]

  # Create a dictionary for groups
  group_dict = {}
  for line in configuration_list[group_start_position+1:rule_start_position]:
    group_buffer = line.split('=')
    for user in group_buffer[1].split():
      if not user in user_dict.keys():
        raise ConfigurationError,(ConfigurationError.USER_DONT_EXIST,user)
    group_dict[group_buffer[0].strip()] = group_buffer[1].split()

  # Create a set of rules
  rule_list = configuration_list[rule_start_position+1:]
  rule_set = set()

  for rule in rule_list:

    option_dict = {}

    # Check if there are options, and if so, make a list
    # If ports were defined, but no protocol, tcp is assumed
    # If sport or dport has been omitted, set it to '-'
    if '(' in rule:
      option_string = rule[rule.find('(')+1:rule.find(')')]
      for option in option_string.split():
        option_dict[option.split('=')[0]] = option.split('=')[1]
      if (option_dict.has_key('dport') or option_dict.has_key('sport')) and \
          not option_dict.has_key('proto'):
        option_dict['proto'] = 'tcp'
      rule = rule[:rule.find('(')]

    if not option_dict.has_key('dport'):
      option_dict['dport'] = '-'
    if not option_dict.has_key('sport'):
      option_dict['sport'] = '-'
    if not option_dict.has_key('proto'):
      option_dict['proto'] = '-'

    # List all groups listed before and after the direction indicator
    if '->' in rule:
      direction = 0
      source_group_list = rule[:rule.find('-')].split()
      source_ip_list = groupListToIpSet(source_group_list, group_dict,
          user_dict)
      destination_group_list = rule[rule.find('>')+1:].split()
      destination_ip_list = groupListToIpSet(destination_group_list,
          group_dict, user_dict)
    elif '<-' in rule:
      destination_group_list = rule[:rule.find('<')].split()
      destination_ip_list = groupListToIpSet(destination_group_list, group_dict,
          user_dict)
      source_group_list = rule[rule.find('-')+1:].split()
      source_ip_list = groupListToIpSet(source_group_list,
          group_dict, user_dict)

    # Generate shorewall configuration line
    for source_ip in source_ip_list:
      for destination_ip in destination_ip_list:
        rule_set.add('ACCEPT\t%s:%s\t%s:%s\t%s\t%s\t%s\n' % (shorewall_zone, \
            source_ip,shorewall_zone,destination_ip,option_dict['proto'],\
            option_dict['dport'],option_dict['sport']))
	    
  return rule_set

def groupListToIpSet(group_list, group_dict, user_dict): 
  # Returns a set of IPs from a list of groups
  ip_set = set()                                        
  for group in group_list:
    if group_dict.has_key(group):
      for user in group_dict[group]:
        for ip in user_dict[user]:
          ip_set.add(ip)
    else:
      raise ConfigurationError, (ConfigurationError.GROUP_DONT_EXIST, group)
  return ip_set

def checkSyntax(line_list): # Checks the syntax, using regexps
  import re
  user_syntax = re.compile(\
      '[a-zA-Z0-9_-]+ *=  *[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'\
      '(,[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})* *$')
  group_syntax = re.compile(\
      '[a-zA-Z0-9_-]+ *= *[a-zA-Z0-9]*')
  rule_syntax = re.compile(\
      '[a-zA-Z0-9_-]+ *(->|<-) *[a-zA-Z0-9]+ *(\((dport=|sport=|proto=).+ *\))?')
  section = None
  line_count = 0
  if (not '[users]\n' in line_list or not '[groups]\n' in line_list or not \
      '[rules]\n' in line_list):
    raise ConfigurationError, (ConfigurationError.SECTION_NOT_PRESENT, '')
  for line in line_list:
    line_count = line_count+1
    line = line.strip('\n').split('#')[0]
    if line == '':
      continue
    if line == '[users]':
      section = 'Users'
      continue
    if line == '[groups]':
      section = 'Groups'
      continue
    if line == '[rules]':
      section = 'Rules'
      continue
    if section == 'Users':
      if re.match(user_syntax, line) == None:
        raise ConfigurationError, (ConfigurationError.SYNTAX_ERROR, line_count)
    if section == 'Groups':
      if re.match(group_syntax, line) == None:
        raise ConfigurationError, (ConfigurationError.SYNTAX_ERROR, line_count)
    if section == 'Rules':
      if re.match(rule_syntax, line) == None:
        raise ConfigurationError, (ConfigurationError.SYNTAX_ERROR, line_count)

def readFile(input_file_path): # Used to load informations from a file
  
  if not os.access(input_file_path,os.F_OK):
    raise FileError, FileError.CONFIG_FILE_NOT_PRESENT
  if not os.access(input_file_path,os.R_OK):
    raise FileError, FileError.CONFIG_FILE_NOT_READABLE

  line_list = open(input_file_path,'r').readlines()
  temp_line_list = line_list
  for line in line_list:
    if line.strip().endswith('\\'):
      line_list.index(line)
  return line_list

def writeFile(output_file_path,rule_set): # Used to write files
  if os.access(output_file_path, os.F_OK):
    if os.access(output_file_path, os.W_OK):
      output_file = open(output_file_path, 'w')
      for rule in rule_set:
        output_file.write(rule)
      print 'File \'',output_file_path,'\' written'
    else:
      raise FileError, FileError.OUTPUT_FILE_NOT_WRITABLE
  else:
    try:
      output_file = open(output_file_path, 'w')
      for rule in rule_set:
        output_file.write(rule)
      print 'File \'',output_file_path,'\' written'
    except:
      raise FileError, FileError.CANT_CREATE_FILE

def parseCommandLine(): # Command line arguments handling
  import getopt
  options, arguments = getopt.getopt(sys.argv[1:], 'ho:i:z:',\
      ['help', 'output-file', 'input-file', 'zone'])
  return options

def main():
  
  try:
    config_file_path = None
    output_file_path = None
    shorewall_zone = 'vpn'

    # Parse command line options.
    # If no options are given on the command line, the script will use the 
    # standard input as his configuration file, and the standard output
    # as his output file.
    command_line_options = parseCommandLine()

    for option, argument in command_line_options:
      if option in ('-o', '--output-file'):
        output_file_path = argument
      if option in ('-i', '--input-file'):
        config_file_path = argument
      if option in ('-z', '--zone'):
        shorewall_zone = argument
      if option in ('-h', '--help'):
        msgHelp()

    if config_file_path != None:
      line_list = readFile(config_file_path)
      configuration_list = parseFile(line_list)
    else:
      if sys.stdin.isatty():
        msgHelp()
      else:
        line_list = sys.stdin.readlines()
        configuration_list = parseFile(line_list)
    
    # Check the syntax of the configuration file
#    checkSyntax(line_list)

    # Create dictionaries and lists of parameters
    rule_set = getRuleSet(configuration_list, shorewall_zone)

    # Write the rules to the specified file, or to screen if no output file
    # were given
    if output_file_path != None:
      writeFile(output_file_path, rule_set)
    else:
      for rule in rule_set:
        print rule.strip('\n')
    
  except FileError, error_message:
    sys.exit(error_message)
  except ConfigurationError, error_message:
    sys.exit(error_message)

if __name__ == "__main__":
  main()
