#!/usr/bin/env python # -*- coding: iso-8859-1 -*- # Copyright (C) 2006, Dinko Korunic, InfoMAR d.o.o. # ipt-mgr.py # script for remote HTTP/HTTPS-based Linux Netfilter management, # to be used with Andrej Brkic's management GUI # # $Id: ipt-mgr.py 264 2006-09-30 08:34:19Z kreator $ """This program is part of Netfilter management suite. It requires working Python 2.x distribution and PySSL+OpenSSL installation. """ __copyright__ = """Copyright (C) 2006 Dinko Korunic, InfoMAR d.o.o. 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 """ __version__ = '$Id: ipt-mgr.py 264 2006-09-30 08:34:19Z kreator $' import sys import optparse import os import socket import time import BaseHTTPServer import urllib import base64 from OpenSSL import SSL class HTTPServer(BaseHTTPServer.HTTPServer): """Simple HTTP/HTTPS server class. This uses BaseHTTPServer, but overrides several methods to allow SSLized communication if needed. """ # server magic server_version = 'ipt-mgr/' + __version__ def __init__(self, server_addr, handler_class, keyfile, certfile): """Initialise with BaseHTTPServer.HTTPServer and continue with SSL stuff """ # prepare addr, port and logfile variables # by default addr is local loopback addr and port is 8080/tcp addr, port = server_addr if addr is None: addr = '127.0.0.1' if port is None: port = 8080 else: port = int(port) # call base class BaseHTTPServer.HTTPServer.__init__(self, (addr, port), handler_class) # do HTTPS only if keyfile and certfile present if keyfile and certfile: # make SSL context and use PEM-coded private and certificate ctx = SSL.Context(SSL.SSLv23_METHOD) ctx.use_privatekey_file(keyfile) ctx.use_certificate_file(certfile) # create, bind and activate SSL socket self.socket = SSL.Connection(ctx, socket.socket(self.address_family, self.socket_type)) self.server_bind() self.server_activate() # TODO: totalni iptables put (sve tablice, chainovi, sve) # TODO: iptables put samo pojedinog pravila u zeljeni chain i tablicu # TODO: totalni iptables get (sve...) # TODO: iptables get samo pojedinog pravila iz zeljenog chaina i/ili tablice class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): """Simple HTTP request handler. This handler supports GET and POST requests. It uses basic HTTP authentication. """ # we will later on decide on logfile logfile = None authdict = None realm = 'ipt-mgr' # determine platform specifics early have_fork = hasattr(os, 'fork') have_popen2 = hasattr(os, 'popen2') have_popen3 = hasattr(os, 'popen3') # unbuffered input; read line by line and parse accordingly rbufsize = 0 def setup(self): """Additional setup. This is used to additionally prepare rfile/wfile and connection for SSL type communication. """ self.connection = self.request self.rfile = socket._fileobject(self.request, 'rb', self.rbufsize) self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize) def log_message(self, format, *args): """Simple logger function. This is used to log arbitrary messages in self.logfile or stderr. Messages will be prepended with date, time and remote address. """ if self.logfile is None: logfile = sys.__stderr__ else: logfile = self.logfile print >> logfile, '%s %s -- %s' % (self.log_date_time_string(), self.address_string(), format%args) def sub_parse_request(self): """This is an additional request parser. This parser happens after parse_request in all of do_ methods. It provides generalized request parse, header parse, basic and digest authentication checking, etc. Parser and kitchen sink. """ # get scriptname and arguments script, rest = self.sub_get_scriptname() if script is None: return # setup some of basic variables request_method = self.command target = self.path path_info = urllib.unquote(rest) host = self.address_string() remote_addr = self.client_address[0] # check authorization and return error if needed if not self.sub_check_authorization(): return # rest of the headers, type, length, cookie if self.headers.typeheader is None: content_type = self.headers.type else: content_type = self.headers.typeheader length = self.headers.getheader('Content-Length') cookies = filter(None, self.headers.getheaders('Cookie')) # hand over a control to a corresponding method # FIXME: crude search/replace hack mname = 'do_' + request_method + '_' + script.replace('.', '_') if hasattr(self, mname): # continue executing self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() getattr(self, mname)() else: self.send_error(400) def sub_get_scriptname(self): """Get scriptname from POST or GET request. This happens in sub_parse_request() in each of general method handlers. """ # sanity check: should never happen according to HTTP RFC if self.path is None: return None, None # strip out leading '/' delimiter always if self.path[0] == '/': rest = self.path[1:] else: rest = self.path # check script arguments delimiter '?' i = rest.rfind('?') if i >= 0: rest, query = rest[:i], rest[i + 1:] else: query = '' # check script path delimiter '/' i = rest.rfind('/') if i >= 0: script, rest = rest[:i], rest[i:] else: script, rest = rest, '' return script, rest def sub_check_authorization(self): """Check auth headers and optionally message client with error. This happens in sub_parse_request() in each of general method handlers. """ # if no authorization data, request no authorization whatsoever if not self.authdict: return True # ask for Basic-type authorization authstr = self.headers.getheader("Authorization") if authstr and authstr in self.authdict: return True else: # prompt for user and password! self.sub_reauthorize() return False def sub_reauthorize(self): """Send reauthorize headers.""" self.send_response(401) self.send_header('WWW-Authenticate', 'Basic realm="%s"' % self.realm) self.end_headers() self.wfile.write('Reauthorizing...') def do_GET(self): """Simple GET handler""" self.sub_parse_request() def do_POST(self): """Simple general POST handler""" self.sub_parse_request() def do_GET_iptables_get_all(self): """GET request from iptables-save""" pass def do_GET_iptables_get(self): """GET request from iptables -L""" pass def do_POST_iptables_put_all(self): """POST request for iptables-restore""" pass def do_POST_iptables_put(self): """POST request for iptables load""" pass def do_GET_index_html(self): """Hello-world sample GET method""" self.wfile.write('Hello World!\r\nToday is %s' % time.asctime()) def runHTTPserver(addr=None, port=None, logfile=None, authdict=None, keyfile=None, certfile=None, server_class=HTTPServer, handler_class=HTTPRequestHandler): """Run HTTP server class. This is used to instantiate a server_class (HTTPServer) with proper handler_class (HTTPRequestHandler) and parsed arguments from command line. """ # instantiate and serve forever handler_class.logfile = logfile handler_class.authdict = authdict httpd = server_class((addr, port), handler_class, keyfile, certfile) httpd.serve_forever() def get_htpasswd(authfile=None): """Read htpasswd-style auth file and return authdict for Basic auth This is used by HTTPRequestHandler to verify Authorize header against. """ if authfile is None: return None authdict = {} for line in file(authfile, 'r'): # we don't do any syntax checking for auth lines -- if they are not # ':' delimited, well, that's not our problem... auth = 'Basic ' + base64.encodestring(line)[:-1] authdict[auth] = 1 return authdict def main(): """Check arguments and so on. """ # typical usage string usage = r'%prog [-p|--port PORT] [-a|--addr IPADDR] ' + \ '[-d|--dir DIRECTORY] [-l|--log LOGFILE] ' + \ '[-k|--key SSLKEY] [-c|--cert SSLCERT] ' + \ '[-t|--auth HTPASSWD]' # parse a dozen of arguments parser = optparse.OptionParser(usage) parser.add_option('-p', '--port', dest='port', help='port to listen on for the incoming requests', action='store') parser.add_option('-a', '--addr', dest='addr', help='IP address to listen on for the incoming requests', action='store') parser.add_option('-d', '--dir', dest='dir', help='working directory for service', action='store') parser.add_option('-l', '--log', dest='logfile', help='file to log the requests and errors', action='store') parser.add_option('-k', '--key', dest='keyfile', help='SSL PEM-formatted key file', action='store') parser.add_option('-c', '--cert', dest='certfile', help='SSL PEM-formatted certificate file', action='store') parser.add_option('-t', '--auth', dest='authfile', help='htpasswd-style authentication file', action='store') opts, args = parser.parse_args() # chdir early if possible. will throw OS exception if unsucessful if opts.dir: os.chdir(opts.dir) # ready, steady, go with service print 'Service started: ' + time.asctime() runHTTPserver(addr=opts.addr, port=opts.port, logfile=opts.logfile, keyfile=opts.keyfile, certfile=opts.certfile, authdict=get_htpasswd(opts.authfile)) if __name__ == '__main__': sys.exit(main())