def interpret(self): '''interpret a line (first call Parser.getline()) Returns 0 on success, 1 on error ''' if not self.keyword: stderr('%s: no keyword set; invalid parser state' % self) self.errors += 1 return 1 if not self.ifdef_stack[0]: if not self.keyword in ('ifdef', 'ifndef', 'else', 'endif'): debug("%s: skipping %s" % (self, self.keyword)) return 0 # get the parser function try: func = getattr(self, 'parse_%s' % self.keyword) except AttributeError: stderr("%s: unknown keyword '%s'" % (self, self.keyword)) self.errors += 1 return 1 try: func() except ParseError as parse_error: parse_error.perror() self.errors += 1 return 1 return 0
def parse_end(self): arr = self.arr if len(arr) > 2: raise ParseError("%s: syntax error, 'end' takes only one " "argument" % self) if arr[1] == 'verbatim': if not self.in_verbatim: raise ParseError("%s: 'end' can not be used here" % self) debug('end verbatim') self.in_verbatim = False bytecode_end_verbatim = firewater.globals.BYTECODE.pop() bytecode = firewater.bytecode.ByteCode() bytecode.set_verbatim(self.filename, self.lineno, self.verbatim_text) firewater.globals.BYTECODE.append(bytecode) firewater.globals.BYTECODE.append(bytecode_end_verbatim) else: raise ParseError("%s: unknown argument '%s' to 'end'" % (self, arr[1]))
def getline(self): '''read statement from input file Upon return, self.keyword should be set, as well as other members Returns True when OK, False on EOF ''' self.full_line = None self.line = '' self.comment = None self.arr = None self.keyword = None while True: # read lines from the input file # variable tmp_line is used to be able to # do multi-line reads (backslash terminated) tmp_line = self.file.readline() if not tmp_line: return False self.lineno += 1 if self.in_verbatim: # copy verbatim until the statement 'end verbatim' # is reached verbatim_line = tmp_line.strip() arr = verbatim_line.lower().split() # it is tested with an array so that both spaces # and tabs work # note that this shadows the 'end' keyword, but # only when in verbatim mode if not (len(arr) == 2 and arr[0] == 'end' and arr[1] == 'verbatim'): debug('verbatim line == [%s]' % verbatim_line) self.verbatim_text.append(verbatim_line) continue n = tmp_line.find('#') if n >= 0: self.comment = ' ' + tmp_line[n:].strip() tmp_line = tmp_line[:n] # strip comment else: self.comment = '' tmp_line = tmp_line.strip() if not tmp_line: continue if tmp_line[-1] == '\\': tmp_line = tmp_line[:-1].strip() self.line = self.line + ' ' + tmp_line continue self.line = self.line + ' ' + tmp_line self.full_line = self.line + self.comment self.arr = self.line.split() self.keyword = self.arr[0].lower() break return True
def read_input_file(filename): '''read a (included) input file Returns 0 on success, or error count on errors ''' errors = 0 parser = Parser() try: parser.open(filename) except IOError as err: stderr('failed to open %s: %s' % (filename, err.strerror)) return 1 while parser.getline(): parser.insert_comment_line() parser.interpret() if len(parser.ifdef_stack) > 1: ParseError("%s: missing 'endif' statement" % parser).perror() errors += 1 parser.close() errors = errors + parser.errors debug('errors == %d' % errors) return errors
def parse_define(self): arr = self.arr if len(arr) != 2: raise ParseError("%s: syntax error, 'define' takes only one " "argument: a symbol to define" % self) debug('parser: define "%s"' % arr[1]) firewater.globals.DEFINES.append(arr[1])
def parse_verbatim(self): arr = self.arr if len(arr) > 1: raise ParseError("%s: syntax error, 'verbatim' does not take " "any arguments" % self) debug('in verbatim') self.in_verbatim = True self.verbatim_text = []
def parse_interface(self): arr = self.arr if len(arr) < 3: raise ParseError("%s: '%s' requires at least 2 arguments: " "the interface alias and the real interface " "name" % (self, self.keyword)) alias = arr[1] if alias == 'any': raise ParseError("%s: 'any' is a reserved word" % self) iface_list = ' '.join(arr[2:]) iface_list = iface_list.split(',') elem = self.missing_comma(iface_list) if elem is not None: raise ParseError("%s: missing comma after '%s'" % (self, elem)) if alias in iface_list: raise ParseError("%s: interface %s references back to itself" % (self, alias)) if firewater.globals.INTERFACES.has_key(alias): raise ParseError("%s: redefinition of interface %s" % (self, alias)) # expand the list by filling in any previously defined aliases new_iface_list = [] while len(iface_list) > 0: iface = iface_list.pop(0) if firewater.globals.INTERFACES.has_key(iface): iface_list.extend(firewater.globals.INTERFACES[iface]) else: # treat as real system interface name if not iface in new_iface_list: new_iface_list.append(iface) debug('new interface: %s:%s' % (alias, new_iface_list)) firewater.globals.INTERFACES[alias] = new_iface_list all_ifaces = firewater.globals.INTERFACES['all'] for iface in new_iface_list: if not iface in all_ifaces: all_ifaces.append(iface)
def parse_include(self): arr = self.arr if len(arr) <= 1: raise ParseError("%s: 'include' requires a filename argument" % self) include_file = ' '.join(arr[1:]) debug('include %s' % include_file) try: # recursively read the given parse file if read_input_file(include_file) > 0: raise ParseError("%s: error in included file %s" % (self, include_file)) except IOError: raise ParseError("%s: failed to read file '%s'" % (self, include_file))
def parse_chain(self): arr = self.arr if len(arr) < 2: raise ParseError("%s: syntax error" % self) chain = arr[1] if not chain in ('incoming', 'outgoing', 'forwarding'): raise ParseError("%s: syntax error: unknown chain '%s'" % (self, chain)) if len(arr) == 5: if arr[2] != 'default' or arr[3] != 'policy': raise ParseError("%s: syntax error" % self) policy = arr[4] debug('policy == %s' % policy) if not policy in ('allow', 'deny', 'accept', 'drop'): raise ParseError("%s: syntax error: unknown policy '%s'" % (self, policy)) # allow for common aliases to be used here if policy == 'accept': policy = 'allow' if policy == 'drop': policy = 'deny' debug('set chain %s policy %s' % (chain, policy)) # emit default policy setting code bytecode = firewater.bytecode.ByteCode() bytecode.set_policy(self.filename, self.lineno, chain, policy) firewater.globals.BYTECODE.append(bytecode) else: if len(arr) == 2: # change the current chain debug('set current chain %s' % chain) bytecode = firewater.bytecode.ByteCode() bytecode.set_chain(self.filename, self.lineno, chain) firewater.globals.BYTECODE.append(bytecode) else: raise ParseError("%s: syntax error" % self)
def _parse_rule(self): '''parse a rule rule syntax: allow|deny|reject [<proto>] [from <source> [port <service>]] \ [to <dest> [port <service>]] \ [on [interface|iface] <iface> [interface]] ''' arr = self.arr allow = arr.pop(0) if len(arr) < 1: raise ParseError("%s: syntax error, premature end of line" % self) proto = None if arr[0] in firewater.globals.KNOWN_PROTOCOLS: proto = arr.pop(0) if len(arr) <= 1: raise ParseError("%s: syntax error, premature end of line" % self) # the line can be parsed using tokens source_addr = None source_port = None dest_addr = None dest_port = None interface = None while len(arr) > 0: token = arr.pop(0) if len(arr) < 1: raise ParseError("%s: syntax error, premature end of line" % self) if token == 'from': if source_addr is not None: raise ParseError("%s: syntax error ('from' is used " "multiple times)" % self) source_addr = arr.pop(0) if len(arr) > 0: # check for source port if arr[0] == 'port': arr.pop(0) if len(arr) < 1: raise ParseError("%s: syntax error, premature " "end of line" % self) source_port = arr.pop(0) continue elif token == 'to': if dest_addr is not None: raise ParseError("%s: syntax error ('to' is used " "multiple times)" % self) dest_addr = arr.pop(0) if len(arr) > 0: # check for dest port if arr[0] == 'port': arr.pop(0) if len(arr) < 1: raise ParseError("%s: syntax error, premature " "end of line" % self) dest_port = arr.pop(0) continue elif token == 'on': if interface is not None: raise ParseError("%s: syntax error ('on' is used " "multiple times)" % self) if arr[0] in ('interface', 'iface'): arr.pop(0) if len(arr) < 1: raise ParseError("%s: syntax error, premature " "end of line" % self) interface = arr.pop(0) if len(arr) > 0 and arr[0] in ('interface', 'iface'): arr.pop(0) continue else: raise ParseError("%s: syntax error, unknown token '%s'" % (self, token)) debug('rule {') debug(' %s proto %s' % (allow, proto)) debug(' source (%s, %s)' % (source_addr, source_port)) debug(' dest (%s, %s)' % (dest_addr, dest_port)) debug(' iface %s' % interface) debug('}') sources = self._parse_rule_address(source_addr) source_port = self._parse_rule_service(source_port) destinations = self._parse_rule_address(dest_addr) dest_port = self._parse_rule_service(dest_port) ifaces = self._parse_rule_interfaces(interface) debug('rule got {') debug(' sources: ' + str(sources)) debug(' port: ' + str(source_port)) debug(' destinations: ' + str(destinations)) debug(' port: ' + str(dest_port)) debug(' ifaces: ' + str(ifaces)) debug('}') if not proto and (source_port.port > 0 or dest_port.port > 0): if source_port.port > 0 and source_port.proto: proto = source_port.proto if dest_port.port > 0 and dest_port.proto: proto = dest_port.proto if not proto: raise ParseError("%s: missing protocol" % self) # save the rule in globals.BYTECODE[] # the output statements are generated later, # if there were no parse errors for src in sources: for dest in destinations: if not ifaces: debug('%s: %s %s %s eq %s %s eq %s' % (self, allow, proto, src, source_port, dest, dest_port)) bytecode = firewater.bytecode.ByteCode() bytecode.set_rule(self.filename, self.lineno, allow, proto, src, source_port, dest, dest_port, None) firewater.globals.BYTECODE.append(bytecode) else: for iface in ifaces: debug('%s: %s %s %s eq %s %s eq %s on %s' % (self, allow, proto, src, source_port, dest, dest_port, iface)) bytecode = firewater.bytecode.ByteCode() bytecode.set_rule(self.filename, self.lineno, allow, proto, src, source_port, dest, dest_port, iface) firewater.globals.BYTECODE.append(bytecode)
def parse_service(self): arr = self.arr if len(arr) < 3: raise ParseError("%s: '%s' requires at least 2 arguments: " "the service alias and at least 1 property" % (self, arr[0])) alias = arr[1] if alias == 'any': raise ParseError("%s: 'any' is a reserved word" % self) if firewater.globals.SERVICES.has_key(alias): raise ParseError("%s: redefinition of service %s" % (self, alias)) obj = firewater.service.ServiceObject(alias) if arr[2] in firewater.globals.KNOWN_PROTOCOLS: obj.proto = arr.pop(2) if len(arr) < 3: raise ParseError("%s: missing service or port number" % self) # parse port range or number, or alias, or service name m = REGEX_PORT_RANGE.match(arr[2]) if m is not None: # it's a port range port_range = arr[2] obj.port = int(m.groups()[0]) if obj.port < 0 or obj.port > 65535: raise ParseError("%s: invalid port range '%s'" % (self, port_range)) obj.endport = int(m.groups()[1]) if obj.endport < 0 or obj.endport > 65535: raise ParseError("%s: invalid port range '%s'" % (self, port_range)) else: m = REGEX_NUMERIC.match(arr[2]) if m is not None: # it's a single port number obj.port = int(arr[2]) if obj.port < 0 or obj.port > 65535: raise ParseError("%s: invalid port number '%d'" % (self, obj.port)) else: # it's a string if arr[2] == alias: raise ParseError("%s: service %s references back to " "itself" % self) if firewater.globals.SERVICES.has_key(arr[2]): obj2 = firewater.globals.SERVICES[arr[2]] # copy the other service object if not obj.proto: obj.proto = obj2.proto obj.port = obj2.port obj.endport = obj2.endport obj.iface = obj2.iface else: # treat as system service name obj.port = firewater.service.servbyname(arr[2]) if obj.port is None: raise ParseError("%s: no such service '%s'" % (self, arr[2])) if len(arr) > 3: if arr[3] in ('iface', 'interface'): if len(arr) == 5: # interface-specific service iface = arr[4] if firewater.globals.INTERFACES.has_key(iface): obj.iface = firewater.globals.INTERFACES[iface] else: # treat as real system interface obj.iface = [] obj.iface.append(arr[4]) else: raise ParseError("%s: too many arguments to '%s'" % (self, arr[0])) debug('new service: %s:%s' % (alias, obj)) firewater.globals.SERVICES[alias] = obj
def parse_group(self): arr = self.arr if len(arr) < 3: raise ParseError("%s: 'group' requires at least 2 arguments: " "the group alias and at least 1 member" % self) alias = arr[1] if alias == 'any': raise ParseError("%s: 'any' is a reserved word" % self) group_list = ','.join(arr[2:]) group_list = group_list.replace(' ', '') group_list = group_list.replace(',,', ',') group_list = group_list.split(',') elem = self.missing_comma(group_list) if elem is not None: raise ParseError("%s: missing comma after '%s'" % (self, elem)) if alias in group_list: raise ParseError("%s: range %s references back to itself" % (self, alias)) # note that group are stored in the same way as groups if firewater.globals.HOSTS.has_key(alias): raise ParseError("%s: redefinition of range or group %s" % (self, alias)) # expand the list by filling in any previously defined aliases new_group_list = [] while len(group_list) > 0: group = group_list.pop(0) if firewater.globals.HOSTS.has_key(group): group_list.extend(firewater.globals.HOSTS[group]) else: # treat as IP address or fqdn if group.find(':') > -1: # treat as IPv6 address pass elif group.find('/') > -1: # treat as network range a = group.split('/') if len(a) != 2: raise ParseError("%s: invalid address range '%s'" % (self, group)) if not _is_ipv4_address(a[0]): raise ParseError("%s: invalid address range '%s'" % (self, group)) try: bits = int(a[1]) except ValueError: raise ParseError("%s: invalid address range '%s'" % (self, group)) if bits < 0 or bits > 32: raise ParseError("%s: invalid address range '%s'" % (self, group)) else: # treat as fqdn, so resolve the address addrs = firewater.resolv.resolv(group) if addrs is None: # error raise ParseError("%s: failed to resolve '%s'" % (self, group)) for addr in addrs: if not addr in new_group_list: new_group_list.append(addr) continue if not group in new_group_list: new_group_list.append(group) debug('new group: %s:%s' % (alias, new_group_list)) firewater.globals.HOSTS[alias] = new_group_list
def parse_range(self): arr = self.arr if len(arr) < 3: raise ParseError("%s: '%s' requires at least 2 arguments: " "the range alias and the address range" % (self, arr[0])) alias = arr[1] if alias == 'any': raise ParseError("%s: 'any' is a reserved word" % self) ranges_list = ' '.join(arr[2:]) ranges_list = ranges_list.replace(' ', '') ranges_list = ranges_list.replace(',,', ',') ranges_list = ranges_list.split(',') elem = self.missing_comma(ranges_list) if elem is not None: raise ParseError("%s: missing comma after '%s'" % (self, elem)) if alias in ranges_list: raise ParseError("%s: %s %s references back to itself" % (self, arr[0], alias)) # note that ranges are stored in the same way as hosts if firewater.globals.HOSTS.has_key(alias): raise ParseError("%s: redefinition of %s or host %s" % (self, arr[0], alias)) # expand the list by filling in any previously defined aliases new_ranges_list = [] while len(ranges_list) > 0: # 'range' is a Python keyword ... # so I use 'host' instead (confusing huh?) host = ranges_list.pop(0) if firewater.globals.HOSTS.has_key(host): ranges_list.extend(firewater.globals.HOSTS[host]) else: # treat as IP address or fqdn if host.find(':') > -1: # treat as IPv6 address pass elif host.find('/') > -1: # treat as network range a = host.split('/') if len(a) != 2: raise ParseError("%s: invalid address range '%s'" % (self, host)) if not _is_ipv4_address(a[0]): raise ParseError("%s: invalid address range '%s'" % (self, host)) try: bits = int(a[1]) except ValueError: raise ParseError("%s: invalid address range '%s'" % (self, host)) if bits < 0 or bits > 32: raise ParseError("%s: invalid address range '%s'" % (self, host)) else: raise ParseError("%s: invalid address range '%s'" % (self, host)) if not host in new_ranges_list: new_ranges_list.append(host) debug('new %s: %s:%s' % (arr[0], alias, new_ranges_list)) firewater.globals.HOSTS[alias] = new_ranges_list
def parse_host(self): arr = self.arr if len(arr) < 3: raise ParseError("%s: 'host' requires at least 2 arguments: " "the host alias and the IP address or fqdn" % self) alias = arr[1] if alias == 'any': raise ParseError("%s: 'any' is a reserved word" % self) host_list = ' '.join(arr[2:]) host_list = host_list.replace(' ', '') host_list = host_list.replace(',,', ',') host_list = host_list.split(',') elem = self.missing_comma(host_list) if elem is not None: raise ParseError("%s: missing comma after '%s'" % (self, elem)) if alias in host_list: raise ParseError("%s: host %s references back to itself" % (self, alias)) if firewater.globals.HOSTS.has_key(alias): raise ParseError("%s: redefinition of host %s" % (self, alias)) # expand the list by filling in any previously defined aliases new_host_list = [] while len(host_list) > 0: host = host_list.pop(0) if firewater.globals.HOSTS.has_key(host): host_list.extend(firewater.globals.HOSTS[host]) else: # treat as IP address or fqdn if host.find(':') > -1: # treat as IPv6 address pass elif host.find('/') > -1: # treat as network range a = host.split('/') if len(a) != 2: raise ParseError("%s: invalid host address '%s'" % (self, host)) if not _is_ipv4_address(a[0]): raise ParseError("%s: invalid host address '%s'" % (self, host)) if a[1] != '32': raise ParseError("%s: invalid host address '%s'" % (self, host)) elif _is_ipv4_address(host): # treat as IPv4 address pass else: # treat as fqdn, so resolve the address addrs = firewater.resolv.resolv(host) if addrs is None: # error raise ParseError("%s: failed to resolve '%s'" % (self, host)) for addr in addrs: if not addr in new_host_list: new_host_list.append(addr) continue if not host in new_host_list: new_host_list.append(host) debug('new host: %s:%s' % (alias, new_host_list)) firewater.globals.HOSTS[alias] = new_host_list