# -*- coding: utf-8 -*- # state.py # Copyright (C) 2015,2016 LEAP # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Server state using CouchDatabase as backend. """ import re import treq from six.moves.urllib.parse import urljoin from twisted.internet import defer from urlparse import urlsplit from leap.soledad.common.log import getLogger from leap.soledad.common.couch import CouchDatabase from leap.soledad.common.couch import CONFIG_DOC_ID from leap.soledad.common.couch import SCHEMA_VERSION from leap.soledad.common.couch import SCHEMA_VERSION_KEY from leap.soledad.common.command import exec_validated_cmd from leap.soledad.common.l2db.remote.server_state import ServerState from leap.soledad.common.l2db.errors import Unauthorized from leap.soledad.common.errors import WrongCouchSchemaVersionError from leap.soledad.common.errors import MissingCouchConfigDocumentError logger = getLogger(__name__) def is_db_name_valid(name): """ Validate a user database using a regular expression. :param name: database name. :type name: str :return: boolean for name vailidity :rtype: bool """ db_name_regex = "^user-[a-f0-9]+$" return re.match(db_name_regex, name) is not None @defer.inlineCallbacks def _check_db(url, db, auth, agent=None): # if there are documents, ensure that a config doc exists db_url = urljoin(url, '%s/' % db) config_doc_url = urljoin(db_url, CONFIG_DOC_ID) res = yield treq.get(config_doc_url, auth=auth, agent=agent) raise Exception if res.code != 200 and res.code != 404: raise Exception if res.code == 404: res = yield treq.get(urljoin(db_url, '_all_docs'), auth=auth, params={'limit': 1}, agent=agent) docs = yield res.json() if docs['total_rows'] != 0: logger.error( "Missing couch config document in database %s" % db) raise MissingCouchConfigDocumentError(db) if res.code == 200: config_doc = yield res.json() if config_doc[SCHEMA_VERSION_KEY] != SCHEMA_VERSION: logger.error( "Unsupported database schema in database %s" % db) raise WrongCouchSchemaVersionError(db) @defer.inlineCallbacks def check_schema_versions(couch_url, agent=None): """ Check that all user databases use the correct couch schema. """ url = urlsplit(couch_url) auth = (url.username, url.password) if url.username else None url = "%s://%s:%d" % (url.scheme, url.hostname, url.port) res = yield treq.get(urljoin(url, '_all_dbs'), auth=auth, agent=agent) dbs = yield res.json() deferreds = [] semaphore = defer.DeferredSemaphore(20) for db in dbs: if not db.startswith('user-'): continue d = semaphore.run(_check_db, url, db, auth, agent=agent) deferreds.append(d) yield defer.gatherResults(deferreds) class CouchServerState(ServerState): """ Inteface of the WSGI server with the CouchDB backend. """ def __init__(self, couch_url, create_cmd=None): """ Initialize the couch server state. :param couch_url: The URL for the couch database. :type couch_url: str :param create_cmd: Command to be executed for user db creation. It will receive a properly sanitized parameter with user db name and should access CouchDB with necessary privileges, which server lacks for security reasons. :type create_cmd: str """ self.couch_url = couch_url self.create_cmd = create_cmd def open_database(self, dbname): """ Open a couch database. :param dbname: The name of the database to open. :type dbname: str :return: The SoledadBackend object. :rtype: SoledadBackend """ url = urljoin(self.couch_url, dbname) db = CouchDatabase.open_database(url, create=False) return db def ensure_database(self, dbname): """ Ensure couch database exists. :param dbname: The name of the database to ensure. :type dbname: str :raise Unauthorized: If disabled or other error was raised. :return: The SoledadBackend object and its replica_uid. :rtype: (SoledadBackend, str) """ if not self.create_cmd: raise Unauthorized() else: code, out = exec_validated_cmd(self.create_cmd, dbname, validator=is_db_name_valid) if code is not 0: logger.error(""" Error while creating database (%s) with (%s) command. Output: %s Exit code: %d """ % (dbname, self.create_cmd, out, code)) raise Unauthorized() db = self.open_database(dbname) return db, db.replica_uid def delete_database(self, dbname): """ Delete couch database. :param dbname: The name of the database to delete. :type dbname: str :raise Unauthorized: Always, because Soledad server is not allowed to delete databases. """ raise Unauthorized()