# Ë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 . import ipaddress import json import functools import statistics from pathlib import Path from datetime import datetime import jsonschema import strict_rfc3339 import requests from flask import current_app from pkg_resources import resource_filename 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() ) 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') 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 url = 'https://onionoo.torproject.org/weights?lookup={}'.format( self.fingerprint ) data = requests.get(url) try: data.raise_for_status() except requests.HTTPError as e: raise e else: try: data = data.json()['relays'][0] except IndexError as e: raise RuntimeError from e 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 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