# HG changeset patch # User Dennis Fink # Date 2016-12-30 12:39:15 # Node ID 6f9fd557a0887c4a8b8d1244e20c3b68fa7af764 # Parent 414cbe0f5a4b4097bf4c0ecad88ef82423ea5188 # Parent 550edaffafa75d661fc3c3994003b22d73ffd206 Merged dev diff --git a/ennstatus/api/model.py b/ennstatus/api/model.py --- a/ennstatus/api/model.py +++ b/ennstatus/api/model.py @@ -255,10 +255,13 @@ class Server: except: raise NotImplementedError - if data is not None: - self.flags = data.relays[0].flags - else: - raise NotImplementedError + try: + if data is not None: + self.flags = data.relays[0].flags + else: + raise NotImplementedError + except IndexError as e: + raise NotImplementedError from e def check_status(self): diff --git a/ennstatus/api/views.py b/ennstatus/api/views.py --- a/ennstatus/api/views.py +++ b/ennstatus/api/views.py @@ -115,15 +115,16 @@ def update(): current_app.logger.warning(' '.join([str(e), str(data)])) return str(e), 409, {'Content-Type': 'text/plain'} - try: - server.update_weights() - except NotImplementedError: - pass + if current_app.config['ENNSTATUS_ENABLE_ONIONOO']: + try: + server.update_weights() + except NotImplementedError: + pass - try: - server.update_flags() - except NotImplementedError: - pass + try: + server.update_flags() + except NotImplementedError: + pass try: server.save() diff --git a/ennstatus/cli/__init__.py b/ennstatus/cli/__init__.py --- a/ennstatus/cli/__init__.py +++ b/ennstatus/cli/__init__.py @@ -15,9 +15,13 @@ # along with this program. If not, see . import pathlib +import importlib +import operator import click +from .commands import __all__ as commands_all + @click.group() @click.option('-p', '--path', default='/srv/http/enn.lu', @@ -35,9 +39,10 @@ def cli(ctx, path): ctx.obj['config_file'] = path / 'config.json' ctx.obj['data_dir'] = path / 'data' - -from .commands import config -cli.add_command(config, 'config') +subcommands = importlib.import_module('ennstatus.cli.commands') +for command in commands_all: + get = operator.attrgetter(command) + cli.add_command(get(subcommands), command) if __name__ == '__main__': cli() diff --git a/ennstatus/cli/commands/__init__.py b/ennstatus/cli/commands/__init__.py --- a/ennstatus/cli/commands/__init__.py +++ b/ennstatus/cli/commands/__init__.py @@ -15,5 +15,6 @@ # along with this program. If not, see . from .config import config +from .stats import stats -__all__ = ['config'] +__all__ = ['config', 'stats'] diff --git a/ennstatus/cli/commands/config.py b/ennstatus/cli/commands/config.py --- a/ennstatus/cli/commands/config.py +++ b/ennstatus/cli/commands/config.py @@ -16,6 +16,7 @@ import json import ipaddress +import subprocess from pprint import pprint @@ -83,6 +84,7 @@ def add(obj, name, ips, password, bridge except KeyError: config['ENNSTATUS_BRIDGE_PROGRAM'] = [name] + click.echo('%s password: %s' % (name, password)) with obj['config_file'].open(mode='w', encoding='utf-8') as f: json.dump(config, f, indent=4, sort_keys=True) @@ -103,7 +105,7 @@ def delete(obj, name): try: config['ENNSTATUS_BRIDGE_PROGRAM'].remove(name) - except KeyError: + except (KeyError, ValueError): pass with obj['config_file'].open(mode='w', encoding='utf-8') as f: @@ -114,3 +116,37 @@ def delete(obj, name): filename.unlink() except FileNotFoundError: pass + + +@config.group(short_help='Configure more servers at once') +def servers(): + pass + + +@servers.command('add', short_help='Add servers') +@click.argument('names', nargs=-1, required=True) +@click.option('-i', '--ips', prompt='IPs (comma separated)') +@click.option('--bridgeprogram/--no-bridgeprogram', default=False) +@click.pass_context +def adds(ctx, names, ips, bridgeprogram): + + for name in names: + password = subprocess.check_output(['pwgen', '8', '1']) + password = password.decode('utf-8') + password = password.strip() + ctx.invoke( + add, + name=name, + ips=ips, + password=password, + bridgeprogram=bridgeprogram + ) + + +@servers.command('delete', short_help='Delete servers') +@click.argument('names', nargs=-1, required=True) +@click.pass_context +def dels(ctx, names): + + for name in names: + ctx.invoke(delete, name=name) diff --git a/ennstatus/cli/commands/stats.py b/ennstatus/cli/commands/stats.py new file mode 100644 --- /dev/null +++ b/ennstatus/cli/commands/stats.py @@ -0,0 +1,232 @@ +import json + +from collections import defaultdict + +import click + +from ennstatus import create_app +from ...status.functions import split_all_servers_to_types + + +@click.group(short_help='Get statistics') +def stats(): + pass + + +@stats.command('count') +@click.option('--by-type', 'by_type', is_flag=True, default=False) +@click.pass_obj +def count(obj, by_type): + + def calculate_host_number(config, type='all', servers=None): + + hosts = set() + if type == 'all': + + for values in config['ENNSTATUS_SERVERS'].values(): + ips = frozenset(values['IPS']) + hosts.add(ips) + + return len(hosts) + else: + for server in servers[servertype]: + ips = frozenset( + config['ENNSTATUS_SERVERS'][server.name.lower()]['IPS'] + ) + hosts.add(ips) + return len(hosts) + + app = create_app() + + with app.app_context(): + app.logger.disabled = True + servers = split_all_servers_to_types() + + if not by_type: + + click.echo( + 'We have %s servers in total!' % ( + click.style( + str(sum(len(x) for x in servers.values())), + fg='blue' + ) + ) + ) + + click.echo( + 'We have %s different hosts!' % ( + click.style( + str(calculate_host_number(app.config)), + fg='blue' + ) + ) + ) + else: + for servertype, server in servers.items(): + click.echo( + 'We have %s %s servers!' % ( + click.style( + str(len(server)), + fg='blue' + ), + click.style( + servertype, + fg='red' + ) + ) + ) + click.echo( + 'We have %s different %s hosts!' % ( + click.style( + str( + calculate_host_number( + app.config, + type=servertype, + servers=servers + ) + ), + fg='blue' + ), + click.style( + servertype, + fg='red' + ) + ) + ) + + +@stats.command('countries') +@click.option('--by-type', 'by_type', is_flag=True, default=False) +@click.pass_obj +def countries(obj, by_type): + app = create_app() + + with app.app_context(): + app.logger.disabled = True + servers = split_all_servers_to_types() + + if not by_type: + countries = defaultdict(int) + + for key, value in servers.items(): + for server in value: + countries[server.country] += 1 + + for key, value in sorted(countries.items(), key=lambda x: x[1]): + click.echo( + '%s: %s' % ( + click.style(key, fg='green'), + click.style(str(value), fg='blue') + ) + ) + + click.echo( + 'We are hosted in %s different countries' % click.style( + str(len(countries.keys())), + fg='blue' + ) + ) + else: + type_countries = { + 'exit': defaultdict(int), + 'relay': defaultdict(int), + 'bridge': defaultdict(int) + } + + for key, value in servers.items(): + for server in value: + type_countries[key][server.country] += 1 + + type_countries = dict((k, v) for k, v in type_countries.items() if v) + + for key, value in sorted(type_countries.items(), key=lambda x: x[0]): + click.echo( + click.style( + key.capitalize(), + fg='red', + bold=True, + underline=True + ) + ) + + for country, count in sorted(type_countries[key].items(), key=lambda x: x[1]): + click.echo( + '%s: %s' % ( + click.style(country, fg='green'), + click.style(str(count), fg='blue') + ) + ) + + click.echo( + '%s are hosted in %s different countries' % ( + key, + click.style( + str(len(type_countries[key].keys())), + fg='blue' + ) + ) + ) + + +@stats.command('exit_probability') +@click.option('--by-server', 'by_server', is_flag=True, default=False) +@click.pass_obj +def exit_probability(obj, by_server): + + app = create_app() + with app.app_context(): + app.logger.disabled = True + servers = split_all_servers_to_types() + + if not by_server: + exit_probability = defaultdict(int) + for server in servers['exit']: + for subkey in ('1_week', '1_month', '3_months', '1_year', '5_years'): + try: + if server.mean_exit_probability[subkey] is not None: + exit_probability[subkey] += server.mean_exit_probability[subkey] + except KeyError: + continue + for subkey in ('1_week', '1_month', '3_months', '1_year', '5_years'): + try: + click.echo( + 'Mean exit probability over %s: %s' % ( + click.style( + subkey, + fg='blue' + ), + click.style( + str(round(exit_probability[subkey], 2)) + '%', + fg='red' + ) + ) + ) + except KeyError: + continue + else: + for server in servers['exit']: + click.echo( + click.style( + server.name.capitalize(), + fg='red', + bold=True, + underline=True + ) + ) + for subkey in ('1_week', '1_month', '3_months', '1_year', '5_years'): + try: + if server.mean_exit_probability[subkey] is not None: + click.echo( + 'Mean exit probabilty over %s: %s' % ( + click.style( + subkey, + fg='blue' + ), + click.style( + str(round(server.mean_exit_probability[subkey], 2)) + "%", + fg='red' + ) + ) + ) + except KeyError: + continue diff --git a/ennstatus/config.py b/ennstatus/config.py --- a/ennstatus/config.py +++ b/ennstatus/config.py @@ -36,3 +36,5 @@ def init_app(app): config.setdefault('ENNSTATUS_MOMENTJS_FORMAT', 'DD MMMM YYYY HH:mm:ss') config.setdefault('ENNSTATUS_STRFTIME_FORMAT', '%d %B %Y %H:%M:%S') + + config.setdefault('ENNSTATUS_ENABLE_ONIONOO', False) diff --git a/ennstatus/donate/views.py b/ennstatus/donate/views.py --- a/ennstatus/donate/views.py +++ b/ennstatus/donate/views.py @@ -22,40 +22,12 @@ from babel.numbers import parse_decimal, from ennstatus.donate.forms import DateForm from ennstatus.donate.functions import load_csv, get_choices -from ennstatus.root.forms import BPMForm -from ennstatus.root.constants import BPM_ADDRESSES - donate_page = Blueprint('donate', __name__) -@donate_page.route('/', methods=('GET', 'POST')) +@donate_page.route('/') def index(): - - current_app.logger.info('Handling index') - form = BPMForm() - country_choices = [choice[0] for choice in form.country.choices] - - if request.method == 'POST': - current_app.logger.debug('Validating form') - if form.validate_on_submit(): - country = form.country.data - return redirect(url_for('donate.index', country=country)) - else: - if 'country' in request.args: - country = request.args['country'] - if country in country_choices: - current_app.logger.info('Showing country %s' % country) - else: - current_app.logger.warn('Country %s not found' % country) - country = 'luxembourg' - else: - current_app.logger.info('Using default country') - country = 'luxembourg' - - form.country.data = country - address = BPM_ADDRESSES[country] - - return render_template('donate/index.html', form=form, address=address) + return render_template('donate/index.html') @donate_page.route('/received', diff --git a/ennstatus/root/constants.py b/ennstatus/root/constants.py deleted file mode 100644 --- a/ennstatus/root/constants.py +++ /dev/null @@ -1,43 +0,0 @@ -# Ë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 . - - -BPM_ADDRESSES = { - 'germany': { - 'address': 'Zum Bürgerwehr 28', - 'postal_code': 'D-54516', - 'city': 'Wittlich', - 'country': 'Germany', - }, - 'belgium': { - 'address': '3, Rue des Deux Luxembourg', - 'postal_code': 'B-6791', - 'city': 'Athus', - 'country': 'Belgium', - }, - 'france': { - 'address': 'Les Maragolles', - 'postal_code': 'F-54720', - 'city': 'Lexy', - 'country': 'France', - }, - 'luxembourg': { - 'address': '34, Rue Gabriel Lippmann', - 'postal_code': 'L-5365', - 'city': 'Munsbach', - 'country': 'Luxembourg', - }, -} diff --git a/ennstatus/root/forms.py b/ennstatus/root/forms.py --- a/ennstatus/root/forms.py +++ b/ennstatus/root/forms.py @@ -15,28 +15,12 @@ # along with this program. If not, see . from flask_wtf import Form -from wtforms import (SelectField, - StringField, +from wtforms import (StringField, RadioField, BooleanField, SubmitField ) -from wtforms.validators import InputRequired, Email, Length, DataRequired - - -COUNTRIES = [ - ('luxembourg', 'Luxembourg'), - ('belgium', 'Belgium'), - ('france', 'France'), - ('germany', 'Germany'), -] - - -class BPMForm(Form): - country = SelectField('Country', - validators=[DataRequired()], - choices=COUNTRIES) - submit = SubmitField('Submit') +from wtforms.validators import InputRequired, Email, Length class MembershipForm(Form): diff --git a/ennstatus/root/views.py b/ennstatus/root/views.py --- a/ennstatus/root/views.py +++ b/ennstatus/root/views.py @@ -17,8 +17,7 @@ from flask import (Blueprint, render_template, current_app, request, redirect, url_for, flash) -from ennstatus.root.forms import BPMForm, MembershipForm, BridgeprogramForm -from ennstatus.root.constants import BPM_ADDRESSES +from ennstatus.root.forms import MembershipForm, BridgeprogramForm from ennstatus.root.functions import (create_membership_ticket, create_bridgeprogram_ticket) @@ -80,35 +79,9 @@ def mirrors(): return render_template('root/mirrors.html') -@root_page.route('/contact', methods=('GET', 'POST')) +@root_page.route('/contact') def contact(): - - current_app.logger.info('Handling contact') - form = BPMForm() - country_choices = [choice[0] for choice in form.country.choices] - - if request.method == 'POST': - current_app.logger.debug('Validating form') - if form.validate_on_submit(): - country = form.country.data - return redirect(url_for('root.contact', country=country)) - else: - if 'country' in request.args: - country = request.args['country'] - if country in country_choices: - current_app.logger.info('Showing country %s' % country) - else: - current_app.logger.warn('Country %s not found' % country) - country = 'luxembourg' - else: - current_app.logger.info('Using default country') - country = 'luxembourg' - - form.country.data = country - - address = BPM_ADDRESSES[country] - - return render_template('root/contact.html', form=form, address=address) + return render_template('root/contact.html') @root_page.route('/abuse') diff --git a/ennstatus/static/css/ennstatus.css b/ennstatus/static/css/ennstatus.css --- a/ennstatus/static/css/ennstatus.css +++ b/ennstatus/static/css/ennstatus.css @@ -165,3 +165,12 @@ a, a:hover, a:active, a:visited { footer { margin-bottom: 20px; } + +@media (min-width: 768px) { + .dl-horizontal dd { + margin-left: 200px + } + .dl-horizontal dt { + width: 180px + } +} diff --git a/ennstatus/templates/base.html b/ennstatus/templates/base.html --- a/ennstatus/templates/base.html +++ b/ennstatus/templates/base.html @@ -111,6 +111,8 @@ @@ -131,18 +133,20 @@ {% block content %} {% endblock %} - + + {% block scripts %} diff --git a/ennstatus/templates/donate/index.html b/ennstatus/templates/donate/index.html --- a/ennstatus/templates/donate/index.html +++ b/ennstatus/templates/donate/index.html @@ -83,21 +83,13 @@

SnailMail

-
-
- {{ form.hidden_tag() }} -
- {{ form.country(class_='form-control input-sm', onchange='this.form.submit()') }} - -
-
-
-
- Frënn vun der Ënn, ASBL
- BPM 381892
- {{ address['address'] }}
- {{ address['postal_code'] }}, {{ address['city'] }}
- {{ address['country'] }} +
+ Frënn vun der Ënn, ASBL
+ {{ config['ENNSTATUS_ADDRESS_CO'] }}
+ {{ config['ENNSTATUS_HOUSE_NAME'] }}
+ {{ config['ENNSTATUS_STREET'] }}
+ {{ config['ENNSTATUS_POSTAL_CODE'] }}, {{ config['ENNSTATUS_CITY'] }}
+ {{ config['ENNSTATUS_COUNTRY'] }}, {{ config['ENNSTATUS_CONTINENT'] }}
@@ -204,13 +196,12 @@
-

BPM Points

+

Patreon

-

For our parcel station and international mail boxes.

-

- Send your BPM voucher code to:
- : info@enn.lu GPG: 0x02225522 -

+

Support us!

+ + Patreon +
diff --git a/ennstatus/templates/root/about.html b/ennstatus/templates/root/about.html --- a/ennstatus/templates/root/about.html +++ b/ennstatus/templates/root/about.html @@ -46,8 +46,9 @@

President
Sam Grüneisen
-
Secretary
Gilles Hamen
+
Secretary
Nadine Schneider
Treasurer
Patrick Kahr
+
International Ambassador
Christophe Kemp
Developers
Dennis Fink (Ënnstatus Lead Developer), Sam Grüneisen

diff --git a/ennstatus/templates/root/contact.html b/ennstatus/templates/root/contact.html --- a/ennstatus/templates/root/contact.html +++ b/ennstatus/templates/root/contact.html @@ -26,26 +26,16 @@

Contact

-
-

General

-
-
- {{ form.hidden_tag()}} -
- {{ form.country(class_='form-control input-sm', onchange='this.form.submit()') }} - -
-
-
-
+

General

Please mail all general inquiries to:

Frënn vun der Ënn, ASBL
- BPM 381892
- {{ address['address'] }}
- {{ address['postal_code'] }}, {{ address['city'] }}
- {{ address['country'] }}
+ {{ config['ENNSTATUS_ADDRESS_CO'] }}
+ {{ config['ENNSTATUS_HOUSE_NAME'] }}
+ {{ config['ENNSTATUS_STREET'] }}
+ {{ config['ENNSTATUS_POSTAL_CODE'] }}, {{ config['ENNSTATUS_CITY'] }}
+ {{ config['ENNSTATUS_COUNTRY'] }}, {{ config['ENNSTATUS_CONTINENT'] }}
: info@enn.lu GPG: 0x02225522
: enn.lu/ or {{ config['ENNSTATUS_ONION_ADDRESS'] }}
diff --git a/ennstatus/templates/root/membership.html b/ennstatus/templates/root/membership.html --- a/ennstatus/templates/root/membership.html +++ b/ennstatus/templates/root/membership.html @@ -80,7 +80,6 @@ {{ form.submit(class_='btn btn-enn') }}
-

Field marked with * are required!

1: If you have decided to apply for the double membership, the membership fees are 150€/year for the regular diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def _get_requirements(): setup(name='Ennstatus', - version='5.4.2-dev', + version='5.6.3', description=('Ennstatus provides the user with vital information about ' 'the status of the organizations Tor servers.'), author='Frënn vun der Ënn',