Files @ 47d07b5cb8db
Branch filter:

Location: FVDE/ennstatus/ennstatus/api/model.py

Dennis Fink
Added flags
# Ënnstatus
# Copyright (C) 2015  Dennis Fink
#
# 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 <http://www.gnu.org/licenses/>.

import ipaddress
import json
import functools
import statistics

from pathlib import Path
from datetime import datetime

import jsonschema
import strict_rfc3339

from flask import current_app
from pkg_resources import resource_filename
from onion_py.manager import Manager
from onion_py.caching import OnionSimpleCache

from ..utils import check_ip


schema = json.load(
    open(
        resource_filename('ennstatus.api', 'schema/server.json'),
        encoding='utf-8'
    )
)

validate = functools.partial(
    jsonschema.validate,
    schema=schema,
    format_checker=jsonschema.FormatChecker()
)

manager = Manager(OnionSimpleCache())


def calculate_weight(data):

    obj = {}

    for subkey in ('1_week', '1_month', '3_months', '1_year', '5_years'):

        try:
            subdata = data[subkey]
        except KeyError:
            continue

        factor = subdata.factor

        values = [x * factor for x in subdata.values if x is not None]

        if values:
            obj[subkey] = statistics.mean(values) * 100
        else:
            obj[subkey] = None

    return obj


class ServerEncoder(json.JSONEncoder):

    def default(self, obj):

        if isinstance(obj, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
            return str(obj)

        if isinstance(obj, datetime):
            return strict_rfc3339.timestamp_to_rfc3339_utcoffset(
                obj.timestamp()
            )

        return json.JSONEncoder.default(self, obj)


class ServerDecoder(json.JSONDecoder):

    def decode(self, json_string):

        default_obj = super().decode(json_string)

        for key in ('ip', 'ip6'):
            if key in default_obj:
                current_app.logger.debug('{}: {}'.format(
                    key, default_obj[key]
                )
                )
                default_obj[key] = ipaddress.ip_address(default_obj[key])

        current_app.logger.debug('Loading last_updated')
        default_obj['last_updated'] = datetime.fromtimestamp(
            strict_rfc3339.rfc3339_to_timestamp(default_obj['last_updated'])
        )

        return default_obj


class Server:

    def __init__(self, *args, **kwargs):

        self.name = kwargs['name']
        self.type = kwargs['type']
        self.status = kwargs.get('status')
        self.fingerprint = kwargs['fingerprint']
        self.last_updated = kwargs['last_updated']
        self.country = kwargs['country']
        self.bandwidth = kwargs.get('bandwidth')
        self.flags = kwargs.get('flags')

        if self.type == 'bridge':
            self.obfs = kwargs.get('obfs')
            self.fteproxy = kwargs.get('fteproxy')
            self.flashproxy = kwargs.get('flashproxy')
            self.meek = kwargs.get('meek')
        else:
            self.ip = kwargs['ip']

            if 'ip6' in kwargs:
                self.ip6 = kwargs['ip6']

            default_weights = {
                '1_week': None,
                '1_month': None,
                '3_months': None,
                '1_year': None,
                '5_years': None
            }

            self.mean_consensus_weight = kwargs.get(
                'mean_consensus_weight',
                default_weights
            )
            self.mean_guard_probability = kwargs.get(
                'mean_guard_probability',
                default_weights
            )
            self.mean_exit_probability = kwargs.get(
                'mean_exit_probability',
                default_weights
            )
            self.mean_consensus_weight_fraction = kwargs.get(
                'mean_consensus_weight_fraction',
                default_weights
            )
            self.mean_middle_probability = kwargs.get(
                'mean_middle_probability',
                default_weights
            )

    @classmethod
    def from_file_by_name(cls, name):

        filepath = Path('data') / (name.lower() + '.json')

        current_app.logger.info('Loading {}'.format(str(filepath)))
        if filepath.exists() and filepath.is_file():
            try:
                with filepath.open(encoding='utf-8') as f:
                    data = json.load(f, cls=ServerDecoder)
            except (IOError, ValueError):
                current_app.logger.error('IOError or ValueError')
                return False
            else:
                return cls(**data)
        else:
            current_app.logger.error('File error!')
            return False

    @classmethod
    def from_json(cls, server):

        try:
            if cls.check_json_format(json.loads(server)):
                decoded = json.loads(server, cls=ServerDecoder)
                return cls(**decoded)
        except (jsonschema.ValidationError, ValueError) as e:
            raise e

    @classmethod
    def from_dict(cls, server):
        return cls.from_json(json.dumps(server))

    def json(self):
        return json.dumps(self.__dict__, cls=ServerEncoder)

    @staticmethod
    def check_json_format(server):

        try:
            validate(server)
        except jsonschema.ValidationError as e:
            raise e

        for key in ('ip', 'ip6'):
            if key in server:
                address = ipaddress.ip_address(server[key])
                if not check_ip(address):
                    raise ValueError('{} is not accepted!\n'.format(key))
        return True

    def save(self):

        filepath = Path('data') / (self.name.lower() + '.json')

        try:
            with filepath.open(mode='w', encoding='utf-8') as f:
                json.dump(self.__dict__, f, cls=ServerEncoder)
        except Exception as e:
            raise e

    def update_weights(self):

        if self.type not in ('exit', 'relay'):
            raise NotImplementedError

        try:
            data = manager.query('weights', lookup=self.fingerprint)
        except:
            raise NotImplementedError

        if data is not None:
            data = data.relays[0]

        self.mean_consensus_weight = calculate_weight(data.consensus_weight)
        self.mean_exit_probability = calculate_weight(data.exit_probability)
        self.mean_guard_probability = calculate_weight(
            data.guard_probability
        )
        self.mean_middle_probability = calculate_weight(
            data.middle_probability
        )
        self.mean_consensus_weight_fraction = calculate_weight(
            data.consensus_weight_fraction
        )

    def update_flags(self):

        try:
            data = manager.query('details', lookup=self.fingerprint)
        except:
            raise NotImplementedError

        self.flags = data.relays[0].flags

    def check_status(self):

        now = datetime.utcnow()
        delta = now - self.last_updated

        if delta.seconds >= 3600:
            self.status = False
        elif delta.seconds >= 600:
            self.status = None