# Twisted, the Framework of Your Internet
# Copyright (C) 2001-2002 Matthew W. Lefkowitz
# 
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
# 
# This library 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
# Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser 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
# 
"""Parser for inetd.conf files

Stability: stable

Maintainer: U{Andrew Bennetts<mailto:spiv@twistedmatrix.com>}

Future Plans: xinetd configuration file support?
"""

# Various exceptions
class InvalidConfError(Exception):
    """Invalid configuration file"""


class InvalidInetdConfError(InvalidConfError):
    """Invalid inetd.conf file"""


class InvalidServicesConfError(InvalidConfError):
    """Invalid services file"""


class InvalidRPCServicesConfError(InvalidConfError):
    """Invalid rpc services file"""


class UnknownService(Exception):
    """Unknown service name"""


class SimpleConfFile:
    """Simple configuration file parser superclass.

    Filters out comments and empty lines (which includes lines that only 
    contain comments).

    To use this class, override parseLine or parseFields.
    """
    
    commentChar = '#'
    defaultFilename = None
    
    def parseFile(self, file=None):
        """Parse a configuration file
        
        If file is None and self.defaultFilename is set, it will open
        defaultFilename and use it.
        """
        if file is None and self.defaultFilename:
            file = open(self.defaultFilename,'r')
            
        for line in file.readlines():
            # Strip out comments
            comment = line.find(self.commentChar)
            if comment != -1:
                line = line[:comment]

            # Strip whitespace
            line = line.strip()

            # Skip empty lines (and lines which only contain comments)
            if not line:
                continue

            self.parseLine(line)

    def parseLine(self, line):
        """Override this.
        
        By default, this will split the line on whitespace and call
        self.parseFields (catching any errors).
        """
        try:
            self.parseFields(*line.split())
        except ValueError:
            raise InvalidInetdConfError, 'Invalid line: ' + repr(line)
    
    def parseFields(self, *fields):
        """Override this."""


class InetdService:
    """A simple description of an inetd service."""
    name = None
    port = None
    socketType = None
    protocol = None
    wait = None
    user = None
    group = None
    program = None
    programArgs = None
    
    def __init__(self, name, port, socketType, protocol, wait, user, group,
                 program, programArgs):
        self.name = name
        self.port = port
        self.socketType = socketType
        self.protocol = protocol
        self.wait = wait
        self.user = user
        self.group = group
        self.program = program
        self.programArgs = programArgs


class InetdConf(SimpleConfFile):
    """Configuration parser for a traditional UNIX inetd(8)"""

    defaultFilename = '/etc/inetd.conf'
    
    def __init__(self, knownServices=None):
        self.services = []
        
        if knownServices is None:
            knownServices = ServicesConf()
            knownServices.parseFile()
        self.knownServices = knownServices

    def parseFields(self, serviceName, socketType, protocol, wait, user,
                    program, *programArgs):
        """Parse an inetd.conf file.

        Implemented from the description in the Debian inetd.conf man page.
        """
        # Extract user (and optional group)
        user, group = (user.split('.') + [None])[:2]

        # Find the port for a service
        port = self.knownServices.services.get((serviceName, protocol), None)
        if not port and not protocol.startswith('rpc/'):
            # FIXME: Should this be discarded/ignored, rather than throwing
            #        an exception?
            try:
                port = int(serviceName)
                serviceName = 'unknown'
            except:
                raise UnknownService, "Unknown service: %s (%s)" \
                      % (serviceName, protocol)

        self.services.append(InetdService(serviceName, port, socketType,
                                          protocol, wait, user, group, program,
                                          programArgs))
            
            
class ServicesConf(SimpleConfFile):
    """/etc/services parser
    
    @ivar services: dict mapping service names to (port, protocol) tuples.
    """
    
    defaultFilename = '/etc/services'

    def __init__(self):
        self.services = {}

    def parseFields(self, name, portAndProtocol, *aliases):
        try:
            port, protocol = portAndProtocol.split('/')
            port = long(port)
        except:
            raise InvalidServicesConfError, 'Invalid port/protocol:' + \
                                            repr(portAndProtocol)

        self.services[(name, protocol)] = port
        for alias in aliases:
            self.services[(alias, protocol)] = port


class RPCServicesConf(SimpleConfFile):
    """/etc/rpc parser

    @ivar self.services: dict mapping rpc service names to rpc ports.
    """

    defaultFilename = '/etc/rpc'

    def __init__(self):
        self.services = {}
    
    def parseFields(self, name, port, *aliases):
        try:
            port = long(port)
        except:
            raise InvalidRPCServicesConfError, 'Invalid port:' + repr(port)
                        
        self.services[name] = port
        for alias in aliases:
            self.services[alias] = port


