def _load(self):
        if not os.path.isfile(self._config.get(Setting.DATA_CACHE_FILE_PATH)):
            self._data = {KEY_SNPASHOTS: {}}
        else:
            with open(self._config.get(Setting.DATA_CACHE_FILE_PATH)) as f:
                self._data = json.load(f)

        # Check for an upgrade.
        if KEY_LAST_VERSION in self._data:
            self._last_version = Version.parse(self._data[KEY_LAST_VERSION])
        if self.previousVersion != self.currentVersion:
            # add an upgrade marker
            if KEY_UPGRADES not in self._data:
                self._data[KEY_UPGRADES] = []
            self._data[KEY_UPGRADES].append({
                'prev_version':
                str(self.previousVersion),
                'new_version':
                str(self.currentVersion),
                'date':
                self._time.now().isoformat()
            })
            self._data[KEY_LAST_VERSION] = str(self.currentVersion)
            self.makeDirty()
            self.saveIfDirty()

        if self.previousVersion == Version.default(
        ) and self.currentVersion > Version.parse("0.103"):
            self._flags.add(UpgradeFlags.DONT_IGNORE_LEGACY_SNAPSHOTS)
    async def authorize(self, request: Request):
        if 'redirectbacktoken' in request.query:
            version = Version.parse(request.query.get('version', "0"))
            token_url = request.query.get('redirectbacktoken')
            return_url = request.query.get('return', None)
            state = {
                'v': str(version),
                'token': token_url,
                'return': return_url,
                'bg': self.base_context(request).get('backgroundColor'),
                'ac': self.base_context(request).get('accentColor'),
            }
            # Someone is trying to authenticate with the add-on, direct them to the google auth url
            raise HTTPSeeOther(await self.exchanger.getAuthorizationUrl(
                json.dumps(state)))
        elif 'state' in request.query and 'code' in request.query:
            state = json.loads(unquote(request.query.get('state')))
            code = request.query.get('code')
            try:
                version = Version.parse(state["v"])
                creds = (await self.exchanger.exchange(code)).serialize(
                    include_secret=False)

                if version < NEW_AUTH_MINIMUM:
                    # Redirect back to the addon, since this is the older addon
                    url = URL(state['token']).with_query(
                        {'creds': json.dumps(creds)})
                    raise HTTPSeeOther(url)

                serialized_creds = str(
                    base64.b64encode(json.dumps(creds).encode("utf-8")),
                    "utf-8")
                url = URL(state['token']).with_query({
                    'creds': serialized_creds,
                    'host': state['return']
                })
                context = {
                    **self.base_context(request),
                    'redirect_url': str(url),
                    'credentials_serialized': serialized_creds,
                }
                if 'bg' in state:
                    context['backgroundColor'] = state['bg']
                if 'ac' in state:
                    context['accentColor'] = state['ac']
                return aiohttp_jinja2.render_template("authorize.jinja2",
                                                      request, context)
            except Exception as e:
                if isinstance(e, HTTPSeeOther):
                    # expected, pass this thorugh
                    raise
                self.logError(request, e)
                content = "The server encountered an error while processing this request: " + str(
                    e) + "<br/>"
                content += "Please <a href='https://github.com/sabeechen/hassio-google-drive-backup/issues'>file an issue</a> on Home Assistant Google Backup's GitHub page so I'm aware of this problem or attempt authorizing with Google Drive again."
                return Response(status=500, body=content)
        else:
            raise HTTPBadRequest()
async def test_backup_supervisor_path(ha: HaSource,
                                      supervisor: SimulatedSupervisor,
                                      interceptor: RequestInterceptor):
    supervisor._super_version = Version(2021, 7)
    await ha.get()
    assert not interceptor.urlWasCalled(URL_MATCH_BACKUPS)
    assert interceptor.urlWasCalled(URL_MATCH_SNAPSHOT)
예제 #4
0
    def _load(self):
        if not os.path.isfile(self._config.get(Setting.DATA_CACHE_FILE_PATH)):
            self._data = {NECESSARY_OLD_BACKUP_PLURAL_NAME: {}}
        else:
            with open(self._config.get(Setting.DATA_CACHE_FILE_PATH)) as f:
                self._data = json.load(f)

        # Check for an upgrade.
        if KEY_LAST_VERSION in self._data:
            self._last_version = Version.parse(self._data[KEY_LAST_VERSION])
        if self.previousVersion != self.currentVersion:
            # add an upgrade marker
            if KEY_UPGRADES not in self._data:
                self._data[KEY_UPGRADES] = []
            self._data[KEY_UPGRADES].append({
                'prev_version':
                str(self.previousVersion),
                'new_version':
                str(self.currentVersion),
                'date':
                self._time.now().isoformat()
            })
            self._data[KEY_LAST_VERSION] = str(self.currentVersion)
            self.makeDirty()
            self.saveIfDirty()
    def __init__(self, config: Config, ports: Ports, time: Time):
        self._config = config
        self._time = time
        self._ports = ports
        self._auth_token = "test_header"
        self._backups: Dict[str, Any] = {}
        self._backup_data: Dict[str, bytearray] = {}
        self._backup_lock = asyncio.Lock()
        self._backup_inner_lock = asyncio.Lock()
        self._entities = {}
        self._events = []
        self._attributes = {}
        self._notification = None
        self._min_backup_size = 1024 * 1024 * 5
        self._max_backup_size = 1024 * 1024 * 5
        self._addon_slug = "self_slug"
        self._options = self.defaultOptions()
        self._username = "******"
        self._password = "******"
        self._addons = all_addons.copy()
        self._super_version = Version(2021, 8)

        self.installAddon(self._addon_slug, "Home Assistant Google drive Backup")
        self.installAddon("42", "The answer")
        self.installAddon("sgadg", "sdgsagsdgsggsd")
 def __init__(self, config: Config, time: Time):
     self._config = config
     self._data = {}
     self._dirty = {}
     self._time = time
     self._last_version = Version.default()
     self._flags = set()
     self._load()
async def test_old_delete_path(ha: HaSource, supervisor: SimulatedSupervisor,
                               interceptor: RequestInterceptor,
                               time: FakeTime):
    supervisor._super_version = Version(2020, 8)
    await ha.get()
    backup: HABackup = await ha.create(CreateOptions(time.now(), "Test Name"))
    full = DummyBackup(backup.name(), backup.date(), backup.size(),
                       backup.slug(), "dummy")
    full.addSource(backup)
    await ha.delete(full)
    assert interceptor.urlWasCalled("/snapshots/{0}/remove".format(
        backup.slug()))
 async def picker(self, request: Request):
     version = Version.parse(request.query.get('version', "0"))
     bg = request.query.get('bg', self.config.get(Setting.BACKGROUND_COLOR))
     ac = request.query.get('ac', self.config.get(Setting.ACCENT_COLOR))
     return {
         **self.base_context(request),
         "client_id": self.config.get(Setting.DEFAULT_DRIVE_CLIENT_ID),
         "developer_key": self.config.get(Setting.DRIVE_PICKER_API_KEY),
         "app_id": self.config.get(Setting.DEFAULT_DRIVE_CLIENT_ID).split("-")[0],
         'backgroundColor': bg,
         'accentColor': ac,
         "do_redirect": str(version < NEW_AUTH_MINIMUM).lower()
     }
 def getUpgradeTime(self, version: Version):
     for upgrade in self._data[KEY_UPGRADES]:
         if Version.parse(upgrade['new_version']) >= version:
             return self._time.parse(upgrade['date'])
     return self._time.now()
 def currentVersion(self):
     return Version.parse(VERSION)
async def test_version_upgrades(time: Time, injector: Injector,
                                config: Config) -> None:
    # Simluate upgrading from an un-tracked version
    assert not os.path.exists(config.get(Setting.DATA_CACHE_FILE_PATH))
    cache = injector.get(DataCache)
    upgrade_time = time.now()
    assert cache.previousVersion == Version.default()
    assert cache.currentVersion == Version.parse(VERSION)
    assert cache.checkFlag(UpgradeFlags.DONT_IGNORE_LEGACY_SNAPSHOTS)

    assert os.path.exists(config.get(Setting.DATA_CACHE_FILE_PATH))
    with open(config.get(Setting.DATA_CACHE_FILE_PATH)) as f:
        data = json.load(f)
        assert data["upgrades"] == [{
            "prev_version": str(Version.default()),
            "new_version": VERSION,
            "date": upgrade_time.isoformat()
        }]

    # Reload the data cache, verify there is no upgrade.
    time.advance(days=1)
    cache = DataCache(config, time)
    assert cache.previousVersion == Version.parse(VERSION)
    assert cache.currentVersion == Version.parse(VERSION)
    assert not cache.checkFlag(UpgradeFlags.DONT_IGNORE_LEGACY_SNAPSHOTS)
    assert os.path.exists(config.get(Setting.DATA_CACHE_FILE_PATH))

    with open(config.get(Setting.DATA_CACHE_FILE_PATH)) as f:
        data = json.load(f)
        assert data["upgrades"] == [{
            "prev_version": str(Version.default()),
            "new_version": VERSION,
            "date": upgrade_time.isoformat()
        }]

    # simulate upgrading to a new version, verify an upgrade gets identified.
    upgrade_version = Version.parse("200")

    class UpgradeCache(DataCache):
        def __init__(self):
            super().__init__(config, time)

        @property
        def currentVersion(self):
            return upgrade_version

    cache = UpgradeCache()
    assert cache.previousVersion == Version.parse(VERSION)
    assert cache.currentVersion == upgrade_version
    assert os.path.exists(config.get(Setting.DATA_CACHE_FILE_PATH))

    with open(config.get(Setting.DATA_CACHE_FILE_PATH)) as f:
        data = json.load(f)
        assert data["upgrades"] == [{
            "prev_version": str(Version.default()),
            "new_version": VERSION,
            "date": upgrade_time.isoformat()
        }, {
            "prev_version": VERSION,
            "new_version": str(upgrade_version),
            "date": time.now().isoformat()
        }]

    next_upgrade_time = time.now()
    time.advance(days=1)
    # Verify version upgrade time queries work as expected
    assert cache.getUpgradeTime(Version.parse(VERSION)) == upgrade_time
    assert cache.getUpgradeTime(Version.default()) == upgrade_time
    assert cache.getUpgradeTime(upgrade_version) == next_upgrade_time

    # degenerate case, should never happen but a sensible value needs to be returned
    assert cache.getUpgradeTime(Version.parse("201")) == time.now()
예제 #12
0
import base64
from os.path import abspath, join
from aiohttp.web import Application, json_response, Request, TCPSite, AppRunner, post, Response, static, get
from aiohttp.client_exceptions import ClientResponseError, ClientConnectorError, ServerConnectionError, ServerDisconnectedError, ServerTimeoutError
from aiohttp.web_exceptions import HTTPBadRequest, HTTPSeeOther
from backup.creds import Exchanger
from backup.config import Config, Setting, VERSION
from backup.exceptions import GoogleCredentialsExpired, ensureKey, KnownError
from injector import ClassAssistedBuilder, inject, singleton
from .errorstore import ErrorStore
from .cloudlogger import CloudLogger
from yarl import URL
from backup.config import Version
from urllib.parse import unquote

NEW_AUTH_MINIMUM = Version(0, 101, 3)


@singleton
class Server():
    @inject
    def __init__(self,
                 config: Config,
                 exchanger_builder: ClassAssistedBuilder[Exchanger],
                 logger: CloudLogger,
                 error_store: ErrorStore):
        self.exchanger = exchanger_builder.build(
            client_id=config.get(Setting.DEFAULT_DRIVE_CLIENT_ID),
            client_secret=config.get(Setting.DEFAULT_DRIVE_CLIENT_SECRET),
            redirect=URL(config.get(Setting.AUTHORIZATION_HOST)).with_path("/drive/authorize"))
        self.logger = logger
from backup.logger import getLogger, getHistory, TraceLogger
from backup.creds import Exchanger, MANUAL_CODE_REDIRECT_URI, Creds
from backup.debugworker import DebugWorker
from backup.drive import FolderFinder
from backup.const import FOLDERS
from .debug import Debug
from yarl import URL

logger = getLogger(__name__)

# Used to Google's oauth verification
SCOPE: str = 'https://www.googleapis.com/auth/drive.file'

MIME_TEXT_HTML = "text/html"
MIME_JSON = "application/json"
VERSION_CREATION_TRACKING = Version(0, 104, 0)


@singleton
class UiServer(Trigger, Startable):
    @inject
    def __init__(self, debug: Debug, coord: Coordinator, ha_source: HaSource, harequests: HaRequests,
                 time: Time, config: Config, global_info: GlobalInfo, estimator: Estimator,
                 session: ClientSession, exchanger_builder: ClassAssistedBuilder[Exchanger],
                 debug_worker: DebugWorker, folder_finder: FolderFinder, data_cache: DataCache,
                 haupdater: HaUpdater):
        super().__init__()
        # Currently running server tasks
        self.runners = []
        self.exchanger_builder = exchanger_builder
        self._coord = coord
예제 #14
0
def test_version():
    assert Version(1, 2, 3) == Version(1, 2, 3)
    assert Version(1, 2, 3) >= Version(1, 2, 3)
    assert Version(1, 2, 3) <= Version(1, 2, 3)
    assert Version(1, 2, 3) > Version(1, 2)
    assert Version(1) < Version(2)
    assert Version(2) > Version(1)
    assert Version(1) != Version(2)
    assert Version(1, 2) > Version(1)
    assert Version(1) < Version(1, 2)
예제 #15
0
def test_broken_versions():
    assert Version.parse("") == Version.default()
    assert Version.parse(".") == Version.default()
    assert Version.parse("empty") == Version.default()
    assert Version.parse("no.version.here") == Version.default()
예제 #16
0
def test_junk_strings():
    assert Version.parse("1-.2.3.1") == Version(1, 2, 3, 1)
    assert Version.parse("ignore-1.2.3.1") == Version(1, 2, 3, 1)
    assert Version.parse(
        "1.2.ignore.this.text.3.and...andhere.too.1") == Version(1, 2, 3, 1)
예제 #17
0
def test_default():
    assert Version.default() == Version.default()
    assert not Version.default() > Version.default()
    assert not Version.default() < Version.default()
    assert not Version.default() != Version.default()
    assert Version.default() >= Version.default()
    assert Version.default() <= Version.default()
예제 #18
0
def test_parse_staging():
    assert Version.parse("1.0.staging.1") == Version(1, 0, 1)
    assert Version.parse("1.0.staging.1").staging
    assert Version.parse("1.0.staging.1") > Version(1.0)
    assert Version.parse("1.2.3") == Version(1, 2, 3)
예제 #19
0
def test_parse():
    assert Version.parse("1.0") == Version(1, 0)
    assert Version.parse("1.2.3") == Version(1, 2, 3)