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