Example #1
0
 def __init__(self,
              dataRemote: bool = False,
              allowedDirs: List[str] = None,
              allowedFileTypes: List[str] = None):
     """
     Args:
         dataRemote (bool): whether data will be served from the local instance or requests forwarded
             to a remote instance for handling.
         allowedDirs (list): list of directories from which files are allowed to be read/writting. File
             operations will not be permitted unless the file path is a child of an allowed directory.
         allowedFileTypes (list): list of file extensions, such as '.dcm', '.txt', for which file
             operations are permitted. No file operations will be done unless the file extension matches
             one on the list.
     """
     super().__init__(isRemote=dataRemote)
     if dataRemote is True:
         return
     self.initWatchSet = False
     self.watchDir = None
     self.currentStreamId = 0
     self.streamInfo = None
     self.allowedDirs = allowedDirs
     # Remove trailing slash from dir names
     if allowedDirs is not None:
         self.allowedDirs = [dir.rstrip('/') for dir in allowedDirs]
     self.allowedFileTypes = allowedFileTypes
     # make sure allowed file extensions start with '.'
     if allowedFileTypes is not None:
         if allowedFileTypes[0] != '*':
             for i in range(len(allowedFileTypes)):
                 if not allowedFileTypes[i].startswith('.'):
                     allowedFileTypes[i] = '.' + allowedFileTypes[i]
     self.fileWatchLock = threading.Lock()
     # instantiate local FileWatcher
     self.fileWatcher = FileWatcher()
Example #2
0
 def __init__(self, filesremote=False, commPipes=None):
     self.local = not filesremote
     self.commPipes = commPipes
     self.fileWatcher = None
     self.initWatchSet = False
     if self.local:
         self.fileWatcher = FileWatcher()
Example #3
0
class WsFileWatcher:
    ''' A server that watches for files on the scanner computer and replies to
        cloud service requests with the file data. The communication connection
        is made with webSockets (ws)
    '''
    fileWatcher = FileWatcher()
    allowedDirs = None
    allowedTypes = None
    serverAddr = None
    sessionCookie = None
    needLogin = True
    shouldExit = False
    validationError = None
    # Synchronizing across threads
    clientLock = threading.Lock()
    fileWatchLock = threading.Lock()

    @staticmethod
    def runFileWatcher(serverAddr,
                       retryInterval=10,
                       allowedDirs=defaultAllowedDirs,
                       allowedTypes=defaultAllowedTypes,
                       username=None,
                       password=None,
                       testMode=False):
        WsFileWatcher.serverAddr = serverAddr
        WsFileWatcher.allowedDirs = allowedDirs
        for i in range(len(allowedTypes)):
            if not allowedTypes[i].startswith('.'):
                allowedTypes[i] = '.' + allowedTypes[i]
        WsFileWatcher.allowedTypes = allowedTypes
        # go into loop trying to do webSocket connection periodically
        WsFileWatcher.shouldExit = False
        while not WsFileWatcher.shouldExit:
            try:
                if WsFileWatcher.needLogin or WsFileWatcher.sessionCookie is None:
                    WsFileWatcher.sessionCookie = login(serverAddr,
                                                        username,
                                                        password,
                                                        testMode=testMode)
                wsAddr = os.path.join('wss://', serverAddr, 'wsData')
                if testMode:
                    print(
                        "Warning: using non-encrypted connection for test mode"
                    )
                    wsAddr = os.path.join('ws://', serverAddr, 'wsData')
                logging.log(DebugLevels.L6, "Trying connection: %s", wsAddr)
                ws = websocket.WebSocketApp(
                    wsAddr,
                    on_message=WsFileWatcher.on_message,
                    on_close=WsFileWatcher.on_close,
                    on_error=WsFileWatcher.on_error,
                    cookie="login="******"Connected to: %s", wsAddr)
                print("Connected to: {}".format(wsAddr))
                ws.run_forever(sslopt={"ca_certs": certFile})
            except Exception as err:
                logging.log(
                    logging.INFO, "WSFileWatcher Exception {}: {}".format(
                        type(err).__name__, str(err)))
                print('sleep {}'.format(retryInterval))
                time.sleep(retryInterval)

    @staticmethod
    def stop():
        WsFileWatcher.shouldExit = True

    @staticmethod
    def on_message(client, message):
        fileWatcher = WsFileWatcher.fileWatcher
        response = {'status': 400, 'error': 'unhandled request'}
        try:
            request = json.loads(message)
            response = request.copy()
            if 'data' in response: del response['data']
            cmd = request.get('cmd')
            dir = request.get('dir')
            filename = request.get('filename')
            timeout = request.get('timeout', 0)
            compress = request.get('compress', False)
            logging.log(logging.INFO, "{}: {} {}".format(cmd, dir, filename))
            # Do Validation Checks
            if cmd not in ['getAllowedFileTypes', 'ping', 'error']:
                # All other commands must have a filename or directory parameter
                if dir is None and filename is not None:
                    dir, filename = os.path.split(filename)
                if filename is None:
                    errStr = "{}: Missing filename param".format(cmd)
                    return send_error_response(client, response, errStr)
                if dir is None:
                    errStr = "{}: Missing dir param".format(cmd)
                    return send_error_response(client, response, errStr)
                if cmd in ('watchFile', 'getFile', 'getNewestFile'):
                    if not os.path.isabs(dir):
                        # make path relative to the watch dir
                        dir = os.path.join(fileWatcher.watchDir, dir)
                if WsFileWatcher.validateRequestedFile(dir, filename,
                                                       cmd) is False:
                    errStr = '{}: {}'.format(cmd,
                                             WsFileWatcher.validationError)
                    return send_error_response(client, response, errStr)
                if cmd in ('putTextFile', 'putBinaryFile', 'dataLog'):
                    if not os.path.exists(dir):
                        os.makedirs(dir)
                if not os.path.exists(dir):
                    errStr = '{}: No such directory: {}'.format(cmd, dir)
                    return send_error_response(client, response, errStr)
            # Now handle requests
            if cmd == 'initWatch':
                minFileSize = request.get('minFileSize')
                demoStep = request.get('demoStep')
                if minFileSize is None:
                    errStr = "InitWatch: Missing minFileSize param"
                    return send_error_response(client, response, errStr)
                WsFileWatcher.fileWatchLock.acquire()
                try:
                    fileWatcher.initFileNotifier(dir, filename, minFileSize,
                                                 demoStep)
                finally:
                    WsFileWatcher.fileWatchLock.release()
                response.update({'status': 200})
                return send_response(client, response)
            elif cmd == 'watchFile':
                WsFileWatcher.fileWatchLock.acquire()
                filename = os.path.join(dir, filename)
                try:
                    retVal = fileWatcher.waitForFile(filename, timeout=timeout)
                finally:
                    WsFileWatcher.fileWatchLock.release()
                if retVal is None:
                    errStr = "WatchFile: 408 Timeout {}s: {}".format(
                        timeout, filename)
                    response.update({'status': 408, 'error': errStr})
                    logging.log(logging.WARNING, errStr)
                    return send_response(client, response)
                else:
                    response.update({'status': 200, 'filename': filename})
                    return send_data_response(client, response, compress)
            elif cmd == 'getFile':
                filename = os.path.join(dir, filename)
                if not os.path.exists(filename):
                    errStr = "GetFile: File not found {}".format(filename)
                    return send_error_response(client, response, errStr)
                response.update({'status': 200, 'filename': filename})
                return send_data_response(client, response, compress)
            elif cmd == 'getNewestFile':
                resultFilename = findNewestFile(dir, filename)
                if resultFilename is None or not os.path.exists(
                        resultFilename):
                    errStr = 'GetNewestFile: file not found: {}'.format(
                        os.path.join(dir, filename))
                    return send_error_response(client, response, errStr)
                response.update({'status': 200, 'filename': resultFilename})
                return send_data_response(client, response, compress)
            elif cmd == 'listFiles':
                if not os.path.isabs(dir):
                    errStr = "listFiles must have an absolute path: {}".format(
                        dir)
                    return send_error_response(client, response, errStr)
                filePattern = os.path.join(dir, filename)
                fileList = [x for x in glob.iglob(filePattern, recursive=True)]
                fileList = WsFileWatcher.filterFileList(fileList)
                response.update({
                    'status': 200,
                    'filePattern': filePattern,
                    'fileList': fileList
                })
                return send_response(client, response)
            elif cmd == 'getAllowedFileTypes':
                response.update({
                    'status': 200,
                    'fileTypes': WsFileWatcher.allowedTypes
                })
                return send_response(client, response)
            elif cmd == 'putTextFile':
                text = request.get('text')
                if text is None:
                    errStr = 'PutTextFile: Missing text field'
                    return send_error_response(client, response, errStr)
                elif type(text) is not str:
                    errStr = "PutTextFile: Only text data allowed"
                    return send_error_response(client, response, errStr)
                fullPath = os.path.join(dir, filename)
                with open(fullPath, 'w') as volFile:
                    volFile.write(text)
                response.update({'status': 200})
                return send_response(client, response)
            elif cmd == 'putBinaryFile':
                try:
                    data = unpackDataMessage(request)
                except Exception as err:
                    errStr = 'putBinaryFile: {}'.format(err)
                    return send_error_response(client, response, errStr)
                # If data is None - Incomplete multipart data, more will follow
                if data is not None:
                    fullPath = os.path.join(dir, filename)
                    with open(fullPath, 'wb') as binFile:
                        binFile.write(data)
                response.update({'status': 200})
                return send_response(client, response)
            elif cmd == 'dataLog':
                logLine = request.get('logLine')
                if logLine is None:
                    errStr = 'DataLog: Missing logLine field'
                    return send_error_response(client, response, errStr)
                fullPath = os.path.join(dir, filename)
                with open(fullPath, 'a') as logFile:
                    logFile.write(logLine + '\n')
                response.update({'status': 200})
                return send_response(client, response)
            elif cmd == 'ping':
                response.update({'status': 200})
                return send_response(client, response)
            elif cmd == 'error':
                errorCode = request.get('status', 400)
                errorMsg = request.get('error', 'missing error msg')
                if errorCode == 401:
                    WsFileWatcher.needLogin = True
                    WsFileWatcher.sessionCookie = None
                errStr = 'Error {}: {}'.format(errorCode, errorMsg)
                logging.log(logging.ERROR, errStr)
                return
            else:
                errStr = 'OnMessage: Unrecognized command {}'.format(cmd)
                return send_error_response(client, response, errStr)
        except Exception as err:
            errStr = "OnMessage Exception: {}: {}".format(cmd, err)
            send_error_response(client, response, errStr)
            if cmd == 'error':
                sys.exit()
            return
        errStr = 'unhandled request'
        send_error_response(client, response, errStr)
        return

    @staticmethod
    def on_close(client):
        logging.info('connection closed')

    @staticmethod
    def on_error(client, error):
        if type(error) is KeyboardInterrupt:
            WsFileWatcher.shouldExit = True
        else:
            logging.log(
                logging.WARNING, "on_error: WSFileWatcher: {} {}".format(
                    type(error), str(error)))

    @staticmethod
    def validateRequestedFile(dir, file, cmd):
        textFileTypeOnly = False
        wildcardAllowed = False
        if cmd in ('putTextFile', 'dataLog'):
            textFileTypeOnly = True
        if cmd in ('listFiles'):
            wildcardAllowed = True
        # Restrict requests to certain directories and file types
        WsFileWatcher.validationError = None
        if WsFileWatcher.allowedDirs is None or WsFileWatcher.allowedTypes is None:
            raise StateError(
                'FileServer: Allowed Directories or File Types is not set')
        if file is not None and file != '':
            fileDir, filename = os.path.split(file)
            fileExtension = Path(filename).suffix
            if textFileTypeOnly:
                if fileExtension != '.txt':
                    WsFileWatcher.validationError = \
                        'Only .txt files allowed with command putTextFile() or dataLog()'
                    return False
            if wildcardAllowed:
                pass  # wildcard searches will be filtered for filetype later
            elif fileExtension not in WsFileWatcher.allowedTypes:
                WsFileWatcher.validationError = \
                    "File type {} not in list of allowed file types {}. " \
                    "Specify allowed filetypes with FileServer -f parameter.". \
                    format(fileExtension, WsFileWatcher.allowedTypes)
                return False
            if fileDir is not None and fileDir != '':  # and os.path.isabs(fileDir):
                dirMatch = False
                for allowedDir in WsFileWatcher.allowedDirs:
                    if fileDir.startswith(allowedDir):
                        dirMatch = True
                        break
                if dirMatch is False:
                    WsFileWatcher.validationError = \
                        "Path {} not within list of allowed directories {}. " \
                        "Make sure you specified a full (absolute) path. " \
                        "Specify allowed directories with FileServer -d parameter.". \
                        format(fileDir, WsFileWatcher.allowedDirs)
                    return False
        if dir is not None and dir != '':
            for allowedDir in WsFileWatcher.allowedDirs:
                if dir.startswith(allowedDir):
                    return True
            WsFileWatcher.validationError = \
                "Path {} not within list of allowed directories {}. " \
                "Make sure you specified a full (absolute) path. " \
                "Specify allowed directories with FileServer -d parameter.". \
                format(dir, WsFileWatcher.allowedDirs)
            return False
        # default case
        return True

    @staticmethod
    def filterFileList(fileList):
        filteredList = []
        for filename in fileList:
            if os.path.isdir(filename):
                continue
            fileExtension = Path(filename).suffix
            if fileExtension in WsFileWatcher.allowedTypes:
                filteredList.append(filename)
        return filteredList