Exemplo n.º 1
0
    def __init__(self, config):
        from ankisyncd.thread import getCollectionManager

        self.data_root = os.path.abspath(config['data_root'])
        self.base_url = config['base_url']
        self.base_media_url = config['base_media_url']
        self.setup_new_collection = None

        self.prehooks = {}
        self.posthooks = {}

        if "session_db_path" in config:
            self.session_manager = SqliteSessionManager(
                config['session_db_path'])
        else:
            self.session_manager = SimpleSessionManager()

        if "auth_db_path" in config:
            self.user_manager = SqliteUserManager(config['auth_db_path'])
        else:
            logging.warn(
                "auth_db_path not set, ankisyncd will accept any password")
            self.user_manager = SimpleUserManager()

        self.collection_manager = getCollectionManager()

        # make sure the base_url has a trailing slash
        if not self.base_url.endswith('/'):
            self.base_url += '/'
        if not self.base_media_url.endswith('/'):
            self.base_media_url += '/'
Exemplo n.º 2
0
class SimpleUserManagerTest(unittest.TestCase):
    def setUp(self):
        self.user_manager = SimpleUserManager()

    def tearDown(self):
        self._user_manager = None

    def test_authenticate(self):
        good_test_un = 'username'
        good_test_pw = 'password'
        bad_test_un = 'notAUsername'
        bad_test_pw = 'notAPassword'

        self.assertTrue(
            self.user_manager.authenticate(good_test_un, good_test_pw))
        self.assertTrue(
            self.user_manager.authenticate(bad_test_un, bad_test_pw))
        self.assertTrue(
            self.user_manager.authenticate(good_test_un, bad_test_pw))
        self.assertTrue(
            self.user_manager.authenticate(bad_test_un, good_test_pw))

    def test_userdir(self):
        username = '******'
        dirname = self.user_manager.userdir(username)
        self.assertEqual(dirname, username)
Exemplo n.º 3
0
class SimpleUserManagerTest(unittest.TestCase):
    def setUp(self):
        self.user_manager = SimpleUserManager()

    def tearDown(self):
        self._user_manager = None

    def test_authenticate(self):
        good_test_un = 'username'
        good_test_pw = 'password'
        bad_test_un = 'notAUsername'
        bad_test_pw = 'notAPassword'

        self.assertTrue(self.user_manager.authenticate(good_test_un,
                                                       good_test_pw))
        self.assertTrue(self.user_manager.authenticate(bad_test_un,
                                                       bad_test_pw))
        self.assertTrue(self.user_manager.authenticate(good_test_un,
                                                       bad_test_pw))
        self.assertTrue(self.user_manager.authenticate(bad_test_un,
                                                       good_test_pw))

    def test_userdir(self):
        username = '******'
        dirname = self.user_manager.userdir(username)
        self.assertEqual(dirname, username)
Exemplo n.º 4
0
    def __init__(self, config):
        from ankisyncd.thread import getCollectionManager

        self.data_root = os.path.abspath(config['data_root'])
        self.base_url = config['base_url']
        self.base_media_url = config['base_media_url']
        self.setup_new_collection = None

        self.prehooks = {}
        self.posthooks = {}

        if "session_db_path" in config:
            self.session_manager = SqliteSessionManager(
                config['session_db_path'])
        else:
            self.session_manager = SimpleSessionManager()

        if "auth_db_path" in config:
            self.user_manager = SqliteUserManager(config['auth_db_path'])
        else:
            logging.warn(
                "auth_db_path not set, ankisyncd will accept any password")
            self.user_manager = SimpleUserManager()

        self.collection_manager = getCollectionManager()

        # make sure the base_url has a trailing slash
        if not self.base_url.endswith('/'):
            self.base_url += '/'
        if not self.base_media_url.endswith('/'):
            self.base_media_url += '/'

        # convert base URLs to regexes, to handle Anki desktop's hostNum protocol
        self.base_url = re.compile(r'%s(?P<path>.*)' %
                                   self.base_url.replace('${hostNum}', r'\d*'))
        self.base_media_url = re.compile(
            r'%s(?P<path>.*)' %
            self.base_media_url.replace('${hostNum}', r'\d*'))
Exemplo n.º 5
0
class SyncApp:
    valid_urls = SyncCollectionHandler.operations + SyncMediaHandler.operations + [
        'hostKey', 'upload', 'download'
    ]

    def __init__(self, config):
        from ankisyncd.thread import getCollectionManager

        self.data_root = os.path.abspath(config['data_root'])
        self.base_url = config['base_url']
        self.base_media_url = config['base_media_url']
        self.setup_new_collection = None

        self.prehooks = {}
        self.posthooks = {}

        if "session_db_path" in config:
            self.session_manager = SqliteSessionManager(
                config['session_db_path'])
        else:
            self.session_manager = SimpleSessionManager()

        if "auth_db_path" in config:
            self.user_manager = SqliteUserManager(config['auth_db_path'])
        else:
            logging.warn(
                "auth_db_path not set, ankisyncd will accept any password")
            self.user_manager = SimpleUserManager()

        self.collection_manager = getCollectionManager()

        # make sure the base_url has a trailing slash
        if not self.base_url.endswith('/'):
            self.base_url += '/'
        if not self.base_media_url.endswith('/'):
            self.base_media_url += '/'

        # convert base URLs to regexes, to handle Anki desktop's hostNum protocol
        self.base_url = re.compile(r'%s(?P<path>.*)' %
                                   self.base_url.replace('${hostNum}', r'\d*'))
        self.base_media_url = re.compile(
            r'%s(?P<path>.*)' %
            self.base_media_url.replace('${hostNum}', r'\d*'))

    # backwards compat
    @property
    def hook_pre_sync(self):
        return self.prehooks.get("start")

    @hook_pre_sync.setter
    def hook_pre_sync(self, value):
        self.prehooks['start'] = value

    @property
    def hook_post_sync(self):
        return self.posthooks.get("finish")

    @hook_post_sync.setter
    def hook_post_sync(self, value):
        self.posthooks['finish'] = value

    @property
    def hook_upload(self):
        return self.prehooks.get("upload")

    @hook_upload.setter
    def hook_upload(self, value):
        self.prehooks['upload'] = value

    @property
    def hook_download(self):
        return self.posthooks.get("download")

    @hook_download.setter
    def hook_download(self, value):
        self.posthooks['download'] = value

    def generateHostKey(self, username):
        """Generates a new host key to be used by the given username to identify their session.
        This values is random."""

        import hashlib, time, random, string
        chars = string.ascii_letters + string.digits
        val = ':'.join([
            username,
            str(int(time.time())),
            ''.join(random.choice(chars) for x in range(8))
        ]).encode()
        return hashlib.md5(val).hexdigest()

    def create_session(self, username, user_path):
        return SyncUserSession(username, user_path, self.collection_manager,
                               self.setup_new_collection)

    def _decode_data(self, data, compression=0):
        if compression:
            with gzip.GzipFile(mode="rb", fileobj=io.BytesIO(data)) as gz:
                data = gz.read()

        try:
            data = json.loads(data.decode())
        except (ValueError, UnicodeDecodeError):
            data = {'data': data}

        return data

    def operation_hostKey(self, username, password):
        if not self.user_manager.authenticate(username, password):
            return

        dirname = self.user_manager.userdir(username)
        if dirname is None:
            return

        hkey = self.generateHostKey(username)
        user_path = os.path.join(self.data_root, dirname)
        session = self.create_session(username, user_path)
        self.session_manager.save(hkey, session)

        return {'key': hkey}

    def operation_upload(self, col, data, session):
        # Verify integrity of the received database file before replacing our
        # existing db.
        temp_db_path = session.get_collection_path() + ".tmp"
        with open(temp_db_path, 'wb') as f:
            f.write(data)

        try:
            with anki.db.DB(temp_db_path) as test_db:
                if test_db.scalar("pragma integrity_check") != "ok":
                    raise HTTPBadRequest("Integrity check failed for uploaded "
                                         "collection database file.")
        except sqlite.Error as e:
            raise HTTPBadRequest("Uploaded collection database file is "
                                 "corrupt.")

        # Overwrite existing db.
        col.close()
        try:
            os.rename(temp_db_path, session.get_collection_path())
        finally:
            col.reopen()
            col.load()

        return "OK"

    def operation_download(self, col, session):
        col.close()
        try:
            data = open(session.get_collection_path(), 'rb').read()
        finally:
            col.reopen()
            col.load()
        return data

    @wsgify
    def __call__(self, req):
        # Get and verify the session
        try:
            hkey = req.POST['k']
        except KeyError:
            hkey = None

        session = self.session_manager.load(hkey, self.create_session)

        if session is None:
            try:
                skey = req.POST['sk']
                session = self.session_manager.load_from_skey(
                    skey, self.create_session)
            except KeyError:
                skey = None

        try:
            compression = int(req.POST['c'])
        except KeyError:
            compression = 0

        try:
            data = req.POST['data'].file.read()
            data = self._decode_data(data, compression)
        except KeyError:
            data = {}

        base_url_match = self.base_url.match(req.path)
        base_media_url_match = self.base_media_url.match(req.path)
        if base_url_match:
            url = base_url_match.group('path')
            if url not in self.valid_urls:
                raise HTTPNotFound()

            if url == 'hostKey':
                result = self.operation_hostKey(data.get("u"), data.get("p"))
                if result:
                    return json.dumps(result)
                else:
                    # TODO: do I have to pass 'null' for the client to receive None?
                    raise HTTPForbidden('null')

            if session is None:
                raise HTTPForbidden()

            if url in SyncCollectionHandler.operations + SyncMediaHandler.operations:
                # 'meta' passes the SYNC_VER but it isn't used in the handler
                if url == 'meta':
                    if session.skey == None and 's' in req.POST:
                        session.skey = req.POST['s']
                    if 'v' in data:
                        session.version = data['v']
                    if 'cv' in data:
                        session.client_version = data['cv']

                    self.session_manager.save(hkey, session)
                    session = self.session_manager.load(
                        hkey, self.create_session)

                thread = session.get_thread()

                if url in self.prehooks:
                    thread.execute(self.prehooks[url], [session])

                result = self._execute_handler_method_in_thread(
                    url, data, session)

                # If it's a complex data type, we convert it to JSON
                if type(result) not in (str, bytes, Response):
                    result = json.dumps(result)

                if url in self.posthooks:
                    thread.execute(self.posthooks[url], [session])

                return result

            elif url == 'upload':
                thread = session.get_thread()
                if url in self.prehooks:
                    thread.execute(self.prehooks[url], [session])
                result = thread.execute(self.operation_upload,
                                        [data['data'], session])
                if url in self.posthooks:
                    thread.execute(self.posthooks[url], [session])
                return result

            elif url == 'download':
                thread = session.get_thread()
                if url in self.prehooks:
                    thread.execute(self.prehooks[url], [session])
                result = thread.execute(self.operation_download, [session])
                if url in self.posthooks:
                    thread.execute(self.posthooks[url], [session])
                return result

            # This was one of our operations but it didn't get handled... Oops!
            raise HTTPInternalServerError()

        # media sync
        elif base_media_url_match:
            if session is None:
                raise HTTPForbidden()

            url = base_media_url_match.group('path')

            if url not in self.valid_urls:
                raise HTTPNotFound()

            if url == "begin":
                data['skey'] = session.skey

            result = self._execute_handler_method_in_thread(url, data, session)

            # If it's a complex data type, we convert it to JSON
            if type(result) not in (str, bytes):
                result = json.dumps(result)

            return result

        return "Anki Sync Server"

    @staticmethod
    def _execute_handler_method_in_thread(method_name, keyword_args, session):
        """
        Gets and runs the handler method specified by method_name inside the
        thread for session. The handler method will access the collection as
        self.col.
        """
        def run_func(col):
            # Retrieve the correct handler method.
            handler = session.get_handler_for_operation(method_name, col)
            handler_method = getattr(handler, method_name)

            res = handler_method(**keyword_args)

            col.save()
            return res

        run_func.__name__ = method_name  # More useful debugging messages.

        # Send the closure to the thread for execution.
        thread = session.get_thread()
        result = thread.execute(run_func)

        return result
Exemplo n.º 6
0
 def setUp(self):
     self.user_manager = SimpleUserManager()
Exemplo n.º 7
0
 def setUp(self):
     self.user_manager = SimpleUserManager()