ok

Mini Shell

Direktori : /opt/imunify360/venv/lib/python3.11/site-packages/restore_infected/backup_backends/
Upload File :
Current File : //opt/imunify360/venv/lib/python3.11/site-packages/restore_infected/backup_backends/r1soft.py

import json
import os
import re
import socket
import subprocess
import time
from json import JSONDecodeError

import requests

from restore_infected.backup_backends_lib import (
    BackupBase,
    backend_auth_required,
    extra,
)
from restore_infected.helpers import DateTime, from_env

TOKEN_FILE = '/var/restore_infected/r1soft_api_token.json'

auth_required = backend_auth_required(TOKEN_FILE, "Initialize R1Soft first!")


def is_suitable():
    return True


class R1SoftConnector:
    api_port = 9080
    api_addr = 'http://{}:{}/rest'
    api_get_token = 'user/authenticate'

    agent_cached = None
    machine_cached = None

    class InternalServerError(Exception):
        def __init__(self, url):
            message = 'Internal server error. {}'.format(url)
            super().__init__(message)

    class ConnectionError(Exception):
        def __init__(self, url, status_code, content):
            message = 'Request to {} failed ({}): {}' \
                .format(url, status_code, content)
            super().__init__(message)

    def __init__(self, ip, encryption_key):
        self.ip = ip
        self.__encryption_key = encryption_key

    def save_token(self, username, password):
        """
        Receives the auth token from the server and saves it in the file.
        :param username: R1Soft server username
        :param password: R1Soft server password
        """
        s = requests.Session()
        s.auth = (username, password)
        url = self._build_api_url(self.ip, self.api_get_token)
        r = s.get(url)
        self._check_response(r)

        token = r.json()['authToken']

        config = {
            'ip': self.ip,
            'token': token,
            'encryption_key': self.__encryption_key,
            'username': username,
            'timestamp': int(time.time())
        }

        self.write_token(config)

    def refresh_token(self):
        r = self._api_request(requests.get, self.api_get_token)
        token = r['authToken']
        timestamp = int(time.time())
        self._update_token_file(token=token, timestamp=timestamp)

    @classmethod
    def read_token(cls):
        with open(TOKEN_FILE) as t_file:
            return json.load(t_file)

    @classmethod
    def write_token(cls, config):
        if os.path.dirname(TOKEN_FILE):
            os.makedirs(os.path.dirname(TOKEN_FILE), exist_ok=True)
        with open(TOKEN_FILE, 'w') as t_file:
            json.dump(config, t_file)

    def remove_token(self):
        try:
            os.remove(TOKEN_FILE)
        except FileNotFoundError:
            pass

    @classmethod
    def from_token(cls):
        """
        Reads ip and encryption key from the file created by
          save_token().
        Raises an exception if the file does not exists.
        :return: new R1SoftConnector with token field not None
        """
        if not os.path.exists(TOKEN_FILE):
            raise Exception('No auth token found. Get token first.')

        with open(TOKEN_FILE) as t_file:
            config = json.load(t_file)
            ip = config['ip']
            encryption_key = config['encryption_key']

        _cls = cls(ip, encryption_key)

        return _cls

    @staticmethod
    def _check_response(response):
        """
        Exit with error status unless response code is 200
        :param response: obj -> response object
        """
        if response.status_code == 500:
            raise R1SoftConnector.InternalServerError(response.url)

        if response.status_code < 200 or response.status_code >= 400:
            raise R1SoftConnector.ConnectionError(response.url,
                                                  response.status_code,
                                                  response.content)

    @classmethod
    def _build_api_url(cls, ip, api_path):
        api_addr = cls.api_addr.format(ip, cls.api_port)

        return '{}/{}'.format(api_addr, api_path)

    def _update_token_file(self, **kwargs):
        config = self.read_token()
        for key, value in kwargs.items():
            config[key] = value

        self.write_token(config)

    @staticmethod
    def _get_machine_address():
        hostname = socket.gethostname()

        ip_a = subprocess.check_output(['ip', '-o', '-4', 'address', 'show'])
        ip_list = ip_a.decode('utf-8').splitlines()
        ip_pattern = re.compile(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
        ip_list = [ip_pattern.findall(ip)[0] for ip in ip_list]

        return ip_list, hostname

    def _api_request(self, method, api_path, data=None):
        """
        Sends an API request.
        :param method: e.g. requests.get
        :param api_path: The endpoint path that goes after rest/ (see api_addr)
        :param data: json parameters
        :return: response as dict if possible, as plain text otherwise
        """
        url = self._build_api_url(self.ip, api_path)
        token = self.read_token()['token']

        headers = {
            'AuthToken': token
        }

        r = method(url, headers=headers, json=data)
        self._check_response(r)

        try:
            res = r.json()
        except JSONDecodeError:
            res = r.text

        return res

    def _get_agents(self):
        return self._api_request(requests.get, 'agent')

    def _get_disk_safes(self):
        return self._api_request(requests.get, 'disksafe')

    def _get_machines(self):
        return self._api_request(requests.get, 'machine')

    def _get_inodes(self, recovery_point, path):
        machine = self.get_machine()
        data = {
            'passphrase': self.__encryption_key,
            'basePath': path
        }

        api_path = 'backup/{}/{}/file/id' \
            .format(machine['id'], recovery_point['recoveryPointID'])

        return self._api_request(requests.post, api_path, data)

    def _get_restore_history(self):
        """
        Retrieves restore history.
        R1Soft server may fail to generate history while starting restore
        process.
        :return: list of restore attempts on success, otherwise empty list.
        """
        try:
            history = self._api_request(requests.get, 'restore/file')

            return history
        except R1SoftConnector.InternalServerError:
            return []

    def _restore_completed(self, recovery_id):
        for recovery_entry in self._get_restore_history():
            if recovery_entry['id'] == recovery_id:
                return recovery_entry['recoveryStatus'] == 'FINISHED'

        return False

    def _find_this_agent(self, agents):
        ip_list, hostname = self._get_machine_address()
        addr_list = ip_list + [hostname]

        if agents:
            key = 'hostname' if 'hostname' in agents[0] else 'hostnameIp'
            for agent in agents:
                if agent[key] in addr_list:
                    return agent

        raise Exception('Agent with any of the addresses {} not found.'
                        .format(str(addr_list)))

    def get_agent(self):
        if self.agent_cached is None:
            agents = self._get_agents()
            self.agent_cached = self._find_this_agent(agents)

        return self.agent_cached

    def get_disk_safes(self):
        agent_id = self.get_agent()['id']
        disk_safes_all = self._get_disk_safes()
        disk_safes = []
        for disk_safe in disk_safes_all:
            if disk_safe['agentID'] == agent_id:
                disk_safes.append(disk_safe)

        return disk_safes

    def get_recovery_points(self, disk_safe):
        disk_safe_id = disk_safe['id']

        return self._api_request(
            requests.get, 'recoverypoint/{}/usable'.format(disk_safe_id))

    def restore_file(self, recovery_point, path, dst):
        just_path = os.path.dirname(path)
        just_name = os.path.basename(path)
        final_path = os.path.join(dst, just_path.strip('/'))
        final_file_name = os.path.join(final_path, just_name)
        machine_id = self.get_machine()['id']
        rec_id = recovery_point['recoveryPointID']
        data = {
            'basePath': just_path,
            'restoreMethod': 'ALTERNATE',
            'restoreToMachineId': machine_id,
            'restoreToPath': final_path,
            'childTokens': [just_name],
            'passphrase': self.__encryption_key
        }
        api_path = 'restore/file/{}/{}'.format(machine_id, rec_id)

        restore_id = self._api_request(requests.post, api_path, data)
        while not self._restore_completed(restore_id):
            pass

        return final_file_name

    def get_machine(self):
        if self.machine_cached is None:
            machines = self._get_machines()
            self.machine_cached = self._find_this_agent(machines)

        return self.machine_cached

    def get_file_entry(self, recovery_point, file_path):
        just_path = os.path.dirname(file_path)
        just_name = os.path.basename(file_path)
        machine = self.get_machine()
        inodes = self._get_inodes(recovery_point, just_path)
        inode = inodes.get(just_name, None)
        data = {
            'passphrase': self.__encryption_key,
            'basePath': just_path,
            'inodeNumbers': [inode]
        }

        if not inode:
            raise Exception(
                'No backup for \'{}\' found'.format(file_path))

        api_path = 'backup/{}/{}/file' \
            .format(machine['id'], recovery_point['recoveryPointID'])

        return self._api_request(requests.post, api_path, data)[just_name]


class R1SoftBackup(BackupBase):
    """
    R1Soft backup entry
    """

    def __init__(self, rec_point):
        self.rec_point = rec_point
        self.r1 = R1SoftConnector.from_token()
        super().__init__('', DateTime.fromtimestamp(
            rec_point['createdOnTimestampInMillis'] / 1000))

    def __repr__(self):
        return json.dumps(self.rec_point)

    def __str__(self):
        return self.__repr__()

    def close(self):
        pass

    def file_data(self, path):
        file_entry = self.r1.get_file_entry(self.rec_point, path)
        return FileData(
            path,
            DateTime.fromtimestamp(file_entry["modifyTime"] / 1000),
            file_entry["fileSize"],
        )

    def restore(self, items, destination_folder="/tmp"):
        return {
            self.r1.restore_file(
                self.rec_point, item.filename, destination_folder
            ): item.filename
            for item in items
        }


class FileData:
    """
    R1Soft FileData entry
    """

    def __init__(self, path, mtime, size):
        self.filename = path
        self.mtime = mtime
        self.size = size

    def __str__(self):
        return '{} [{} bytes] {}'.format(self.mtime, self.size, self.filename)


@from_env(
    ip="IP",
    username="ACCOUNT_NAME",
    password="PASSWORD",
    encryption_key="ENCRYPTION_KEY",
)
def init(
    ip, username, password, encryption_key,
):
    r1 = R1SoftConnector(ip, encryption_key)
    r1.save_token(username, password)


@auth_required
def backups(until=None, tmp_dir=None):
    r1 = R1SoftConnector.from_token()
    disks = r1.get_disk_safes()
    recs = r1.get_recovery_points(disks[0])
    backup_list = []
    for rec_point in recs:
        backup = R1SoftBackup(rec_point)
        if until is None or backup.created >= until:
            backup_list.append(backup)

    return backup_list


@auth_required
def info():
    return R1SoftConnector.read_token()


@auth_required
@extra
def refresh_token():
    r1 = R1SoftConnector.from_token()
    r1.refresh_token()

Zerion Mini Shell 1.0