src/OsecFS.py
author ft
Mon, 09 Dec 2013 15:19:05 +0100
changeset 9 cc99197f1e08
parent 8 0a5ba0ef1058
child 10 b97aad470500
permissions -rwxr-xr-x
Added notifiaction for user (virus found, ...)
     1 #!/usr/bin/python
     2 
     3 from fuse import Fuse
     4 import fuse
     5 
     6 import ConfigParser
     7 
     8 import sys
     9 
    10 import logging
    11 import os
    12 import errno
    13 import time
    14 
    15 # ToDo replace with ikarus
    16 #import pyclamav
    17 import subprocess
    18 
    19 import urllib3
    20 import urllib
    21 import netifaces
    22 import netaddr
    23 
    24 
    25 MINOPTS = { "Main" : ["Logfile", "Mountpoint", "Rootpath", "LocalScanserverURL", "RemoteScanserverURL", "ReadOnly"]}
    26 
    27 CONFIG_NOT_READABLE = "Configfile is not readable"
    28 CONFIG_WRONG = "Something is wrong with the config"
    29 CONFIG_MISSING = "Section: \"%s\" Option: \"%s\" in configfile is missing"
    30 LOG = None
    31 LOCAL_SCANSERVER_URL = ""
    32 REMOTE_SCANSERVER_URL = ""
    33 STATUS_CODE_OK = 200
    34 STATUS_CODE_INFECTED = 210
    35 STATUS_CODE_NOT_FOUND = 404
    36 
    37 SYSTEM_FILE_COMMAND = "file"
    38 
    39 MAX_SCAN_FILE_SIZE = 50 * 0x100000
    40 SCANSERVER_RETRY_TIMEOUT = 60
    41 
    42 # Global http pool manager used to connect to the scan server
    43 remoteScanserverReachable = True
    44 scanserverTimestamp = 0
    45 httpPool = urllib3.PoolManager(num_pools = 1, timeout = 3)
    46 
    47 def checkMinimumOptions (config):
    48     for section, options in MINOPTS.iteritems ():
    49         for option in options:
    50             if (config.has_option(section, option) == False):
    51                 print (CONFIG_MISSING % (section, option))
    52                 exit (129)
    53 
    54 def printUsage ():
    55     print ("Usage:")
    56     print ("%s configfile mountpath ro/rw" % (sys.argv[0]))
    57     exit (128)
    58 
    59 def loadConfig ():
    60     print ("load config")
    61 
    62     if (len (sys.argv) < 4):
    63         printUsage ()
    64 
    65     configfile = sys.argv[1]
    66     config = ConfigParser.SafeConfigParser ()
    67 
    68     if ((os.path.exists (configfile) == False) or (os.path.isfile (configfile) == False) or (os.access (configfile, os.R_OK) == False)):
    69         print (CONFIG_NOT_READABLE)
    70         printUsage ()
    71 
    72     try:
    73         config.read (sys.argv[1])
    74     except Exception, e:
    75         print (CONFIG_WRONG)
    76         print ("Error: %s" % (e))
    77 
    78 
    79     config.set("Main", "Mountpoint", sys.argv[2])
    80     if (sys.argv[3] == "rw"):
    81         config.set("Main", "ReadOnly", "false")
    82     else:
    83         config.set("Main", "ReadOnly", "true")
    84 
    85     checkMinimumOptions (config)
    86 
    87     return config
    88 
    89 def initLog (config):
    90     print ("init log")
    91 
    92     global LOG
    93     logfile = config.get("Main", "Logfile")
    94 
    95     # ToDo move log level and maybe other things to config file
    96     logging.basicConfig(
    97                         level = logging.DEBUG,
    98                         format = "%(asctime)s %(name)-12s %(funcName)-15s %(levelname)-8s %(message)s",
    99                         datefmt = "%Y-%m-%d %H:%M:%S",
   100                         filename = logfile,
   101                         filemode = "a+",
   102     )
   103     LOG = logging.getLogger("fuse_main")
   104 
   105 
   106 def fixPath (path):
   107     return ".%s" % (path)
   108 
   109 def rootPath (rootpath, path):
   110     return "%s%s" % (rootpath, path)
   111 
   112 def flag2mode (flags):
   113     md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
   114     m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
   115 
   116     if flags | os.O_APPEND:
   117         m = m.replace('w', 'a', 1)
   118 
   119     return m
   120     
   121 def contactScanserver(url, fields):
   122     return httpPool.request_encode_body('POST', url, fields = fields, retries = 0)
   123     
   124 
   125 def scanFileIkarus (path, fileobject):
   126     global remoteScanserverReachable
   127     global scanserverTimestamp
   128 
   129     infected = False
   130     LOG.debug ("Scan File: %s" % (path))
   131 
   132     if (os.fstat(fileobject.fileno()).st_size > MAX_SCAN_FILE_SIZE):
   133         LOG.info("File max size exceeded. The file is not scanned.")
   134         return False
   135 
   136     fields = { 'up_file' : fileobject.read() }
   137 
   138     if (remoteScanserverReachable == False) and ((scanserverTimestamp + SCANSERVER_RETRY_TIMEOUT) < time.time()):
   139         remoteScanserverReachable = True
   140 
   141     if remoteScanserverReachable:
   142         try:
   143             response = contactScanserver(REMOTE_SCANSERVER_URL, fields)
   144             # We should catch socket.error here, but this does not work. Needs checking.
   145         except:
   146             LOG.info("Remote scan server unreachable, using local scan server.")
   147             LOG.info("Next check for remote server in %s seconds." % (SCANSERVER_RETRY_TIMEOUT))
   148             
   149             remoteScanserverReachable = False
   150             scanserverTimestamp = time.time()
   151 
   152             try:
   153                 response = contactScanserver(LOCAL_SCANSERVER_URL, fields)
   154             except:
   155                 LOG.error ("Connection to local scan server could not be established.")
   156                 LOG.error ("Exception: %s" %(sys.exc_info()[0]))
   157                 return False
   158     else:
   159         try:
   160             response = contactScanserver(LOCAL_SCANSERVER_URL, fields)
   161         except:
   162             LOG.error ("Connection to local scan server could not be established.")
   163             LOG.error ("Exception: %s" %(sys.exc_info()[0]))
   164             return False
   165     
   166 
   167     if response.status == STATUS_CODE_OK:
   168         infected = False
   169     elif response.status == STATUS_CODE_INFECTED:
   170         # Parse xml for info if desired
   171         #contentXML = r.content
   172         #root = ET.fromstring(contentXML)
   173         #status = root[1][2].text
   174         infected = True
   175     else:
   176         LOG.error ("Connection error to scan server.")
   177 
   178     if (infected == True):
   179         LOG.error ("Virus found, denying access.")
   180     else:
   181         LOG.debug ("No virus found.")
   182 
   183     return infected
   184 
   185 def scanFileClamAV (path):
   186     infected = False
   187 
   188     LOG.debug ("Scan File: %s" % (path))
   189 
   190     result = pyclamav.scanfile (path)
   191     LOG.debug ("Result of file \"%s\": %s" % (path, result))
   192     if (result[0] != 0):
   193         infected = True
   194 
   195     if (infected == True):
   196         LOG.error ("Virus found, deny Access %s" % (result,))
   197 
   198     return infected
   199 
   200 def whitelistFile (path):
   201     whitelisted = False;
   202 
   203     LOG.debug ("Execute \"%s\" command on \"%s\"" %(SYSTEM_FILE_COMMAND, path))
   204     
   205     result = None
   206     try:
   207         result = subprocess.check_output ([SYSTEM_FILE_COMMAND, path]);
   208         # ToDo replace with real whitelist
   209         whitelisted = True
   210     except Exception as e:
   211         LOG.error ("Call returns with an error!")
   212         LOG.error (e)
   213 
   214     LOG.debug ("Type: %s" %(result))
   215 
   216     return whitelisted
   217 
   218 def sendNotification (type, message):
   219     netifaces.ifaddresses("eth0")[2][0]["addr"]
   220     
   221     # Get first address in network (0 = network ip -> 192.168.0.0)
   222     remote_ip = netaddr.IPNetwork("%s/%s" %(netifaces.ifaddresses("eth0")[2][0]["addr"], netifaces.ifaddresses("eth0")[2][0]["netmask"]))[1]
   223     
   224     url_options = {"type" : type, "message" : message }
   225     url = ("http://%s:8090/notification?%s" %(remote_ip, urllib.urlencode(url_options)))
   226     
   227     LOG.debug ("Send notification to \"%s\"" %(url, ))
   228     
   229     try:
   230         response = httpPool.request_encode_body('GET', url, retries = 0)
   231     except:
   232         LOG.error("Remote host not reachable")
   233         LOG.error ("Exception: %s" %(sys.exc_info()[0]))
   234         return
   235     
   236     if response.status == STATUS_CODE_OK:
   237         LOG.info("Notification sent successfully")
   238     else:
   239         LOG.error("Server returned errorcode: %s" %(response.status,))
   240 
   241 def sendReadOnlyNotification():
   242     sendNotification("critical", "Filesystem is in read only mode. If you want to export files please initialize an encrypted filesystem.")
   243 
   244 class OsecFS (Fuse):
   245 
   246     __rootpath = None
   247 
   248     # default fuse init
   249     def __init__(self, rootpath, *args, **kw):
   250         self.__rootpath = rootpath
   251         Fuse.__init__ (self, *args, **kw)
   252         LOG.debug ("Init complete.")
   253         sendNotification("information", "Filesystem successfully mounted.")
   254 
   255     # defines that our working directory will be the __rootpath
   256     def fsinit(self):
   257         os.chdir (self.__rootpath)
   258 
   259     def getattr(self, path):
   260         LOG.debug ("*** getattr (%s)" % (fixPath (path)))
   261         return os.lstat (fixPath (path));
   262 
   263     def getdir(self, path):
   264         LOG.debug ("*** getdir (%s)" % (path));
   265         return os.listdir (fixPath (path))
   266 
   267     def readdir(self, path, offset):
   268         LOG.debug ("*** readdir (%s %s)" % (path, offset));
   269         for e in os.listdir (fixPath (path)):
   270             yield fuse.Direntry(e)
   271 
   272     def chmod (self, path, mode):
   273         LOG.debug ("*** chmod %s %s" % (path, oct(mode)))
   274         if (config.get("Main", "ReadOnly") == "true"):
   275             sendReadOnlyNotification()
   276             return -errno.EACCES
   277         os.chmod (fixPath (path), mode)
   278 
   279     def chown (self, path, uid, gid):
   280         LOG.debug ("*** chown %s %s %s" % (path, uid, gid))
   281         if (config.get("Main", "ReadOnly") == "true"):
   282             sendReadOnlyNotification()
   283             return -errno.EACCES
   284         os.chown (fixPath (path), uid, gid)
   285 
   286     def link (self, targetPath, linkPath):
   287         LOG.debug ("*** link %s %s" % (targetPath, linkPath))
   288         if (config.get("Main", "ReadOnly") == "true"):
   289             sendReadOnlyNotification()
   290             return -errno.EACCES
   291         os.link (fixPath (targetPath), fixPath (linkPath))
   292 
   293     def mkdir (self, path, mode):
   294         LOG.debug ("*** mkdir %s %s" % (path, oct(mode)))
   295         if (config.get("Main", "ReadOnly") == "true"):
   296             sendReadOnlyNotification()
   297             return -errno.EACCES
   298         os.mkdir (fixPath (path), mode)
   299 
   300     def mknod (self, path, mode, dev):
   301         LOG.debug ("*** mknod %s %s %s" % (path, oct (mode), dev))
   302         if (config.get("Main", "ReadOnly") == "true"):
   303             sendReadOnlyNotification()
   304             return -errno.EACCES
   305         os.mknod (fixPath (path), mode, dev)
   306 
   307     # to implement virus scan
   308     def open (self, path, flags):
   309         LOG.debug ("*** open %s %s" % (path, oct (flags)))
   310         self.file = os.fdopen (os.open (fixPath (path), flags), flag2mode (flags))
   311         self.fd = self.file.fileno ()
   312 
   313         infected = scanFileIkarus (rootPath(self.__rootpath, path), self.file)
   314         #infected = scanFileClamAV (rootPath(self.__rootpath, path))
   315         if (infected == True):
   316             self.file.close ()
   317             sendNotification("critical", "Virus found. Access denied.")
   318             return -errno.EACCES
   319         
   320         whitelisted = whitelistFile (rootPath(self.__rootpath, path))
   321         if (whitelisted == False):
   322             self.file.close ()
   323             sendNotification("critical", "File not in whitelist. Access denied.")
   324             return -errno.EACCES
   325 
   326     def read (self, path, length, offset):
   327         LOG.debug ("*** read %s %s %s" % (path, length, offset))
   328         self.file.seek (offset)
   329         return self.file.read (length)
   330 
   331     def readlink (self, path):
   332         LOG.debug ("*** readlink %s" % (path))
   333         return os.readlink (fixPath (path))
   334 
   335     def release (self, path, flags):
   336         LOG.debug ("*** release %s %s" % (path, oct (flags)))
   337         self.file.close ()
   338 
   339     def rename (self, oldPath, newPath):
   340         LOG.debug ("*** rename %s %s %s" % (oldPath, newPath, config.get("Main", "ReadOnly")))
   341         if (config.get("Main", "ReadOnly") == "true"):
   342             sendReadOnlyNotification()
   343             return -errno.EACCES
   344         os.rename (fixPath (oldPath), fixPath (newPath))
   345 
   346     def rmdir (self, path):
   347         LOG.debug ("*** rmdir %s %s" % (path, config.get("Main", "ReadOnly")))
   348         if (config.get("Main", "ReadOnly") == "true"):
   349             sendReadOnlyNotification()
   350             return -errno.EACCES
   351         os.rmdir (fixPath (path))
   352 
   353     def statfs (self):
   354         LOG.debug ("*** statfs")
   355         return os.statvfs(".")
   356 
   357     def symlink (self, targetPath, linkPath):
   358         LOG.debug ("*** symlink %s %s %s" % (targetPath, linkPath, config.get("Main", "ReadOnly")))
   359         if (config.get("Main", "ReadOnly") == "true"):
   360             sendReadOnlyNotification()
   361             return -errno.EACCES
   362         os.symlink (fixPath (targetPath), fixPath (linkPath))
   363 
   364     def truncate (self, path, length):
   365         LOG.debug ("*** truncate %s %s %s" % (path, length, config.get("Main", "ReadOnly")))
   366         if (config.get("Main", "ReadOnly") == "true"):
   367             sendReadOnlyNotification()
   368             return -errno.EACCES
   369         f = open (fixPath (path), "a")
   370         f.truncate (length)
   371         f.close ()
   372 
   373     def unlink (self, path):
   374         LOG.debug ("*** unlink %s %s" % (path, config.get("Main", "ReadOnly")))
   375         if (config.get("Main", "ReadOnly") == "true"):
   376             sendReadOnlyNotification()
   377             return -errno.EACCES
   378         os.unlink (fixPath (path))
   379 
   380     def utime (self, path, times):
   381         LOG.debug ("*** utime %s %s" % (path, times))
   382         os.utime (fixPath (path), times)
   383 
   384     def write (self, path, buf, offset):
   385         LOG.debug ("*** write %s %s %s %s" % (path, buf, offset, config.get("Main", "ReadOnly")))
   386         if (config.get("Main", "ReadOnly") == "true"):
   387             self.file.close()
   388             sendReadOnlyNotification()
   389             return -errno.EACCES
   390         self.file.seek (offset)
   391         self.file.write (buf)
   392         return len (buf)
   393 
   394     def access (self, path, mode):
   395         LOG.debug ("*** access %s %s" % (path, oct (mode)))
   396         if not os.access (fixPath (path), mode):
   397             return -errno.EACCES
   398 
   399     def create (self, path, flags, mode):
   400         LOG.debug ("*** create %s %s %s %s %s" % (fixPath (path), oct (flags), oct (mode), flag2mode (flags), config.get("Main", "ReadOnly")))
   401         if (config.get("Main", "ReadOnly") == "true"):
   402             sendReadOnlyNotification()
   403             return -errno.EACCES
   404         self.file = os.fdopen (os.open (fixPath (path), flags, mode), flag2mode (flags))
   405         self.fd = self.file.fileno ()
   406 
   407 
   408 if __name__ == "__main__":
   409     # Set api version
   410     fuse.fuse_python_api = (0, 2)
   411     fuse.feature_assert ('stateful_files', 'has_init')
   412 
   413     config = loadConfig ()
   414     initLog (config)
   415     
   416     #sendNotification("Info", "OsecFS started")
   417 
   418     scanserverTimestamp = time.time()
   419 
   420     LOCAL_SCANSERVER_URL = config.get("Main", "LocalScanserverURL")
   421     REMOTE_SCANSERVER_URL = config.get("Main", "RemoteScanserverURL")
   422     SCANSERVER_RETRY_TIMEOUT = int(config.get("Main", "RetryTimeout"))
   423 
   424     # Convert file size from MB to byte
   425     MAX_SCAN_FILE_SIZE = int(config.get("Main", "MaxFileSize")) * 0x100000
   426     
   427     osecfs = OsecFS (config.get ("Main", "Rootpath"))
   428     osecfs.flags = 0
   429     osecfs.multithreaded = 0
   430 
   431     fuse_args = [sys.argv[0], config.get ("Main", "Mountpoint")];
   432     osecfs.main (fuse_args)