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 += '/'
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)
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)
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*'))
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
def setUp(self): self.user_manager = SimpleUserManager()