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