#!/usr/bin/env python3 from flask import Flask from flask import request, render_template from flask.ext.sqlalchemy import SQLAlchemy #from flask import session, request, url_for, redirect, render_template import netaddr from netaddr import IPAddress # Hack for python3 from netaddr.strategy.ipv4 import packed_to_int as unpack_v4 from netaddr.strategy.ipv6 import packed_to_int as unpack_v6 import socket from datetime import datetime from uuid import uuid4 app = Flask(__name__) app.config.from_pyfile('config.py') db = SQLAlchemy(app) def unpack(ip): if len(ip) == 4: return unpack_v4(ip) elif len(ip) == 16: return unpack_v6(ip) def is_valid_ip(ip): return netaddr.valid_ipv4(ip) or netaddr.valid_ipv6(ip) def resolve_name(hostname): return list({s[4][0] for s in socket.getaddrinfo(hostname, None)}) class Target(db.Model): """Target IP to ping""" id = db.Column(db.Integer, primary_key=True) # Unique ID for accessing the results (privacy reasons) unique_id = db.Column(db.String) # IP addresses are encoded as their binary representation ip = db.Column(db.BINARY(length=16)) # Date at which a user asked for measurements to this target submitted = db.Column(db.DateTime) def __init__(self, ip): self.unique_id = str(uuid4()) self.ip = IPAddress(ip).packed self.submitted = datetime.now() def get_ip(self): return IPAddress(unpack(self.ip)) def is_v4(self): return self.get_ip().version == 4 def is_v6(self): return self.get_ip().version == 6 def __repr__(self): return '%r' % self.get_ip() def __str__(self): return str(self.get_ip()) # Many-to-many table to record which target has been given to which # participant. handled_targets = db.Table('handled_targets', db.Column('target_id', db.Integer, db.ForeignKey('target.id')), db.Column('participant_id', db.Integer, db.ForeignKey('participant.id')) ) class Participant(db.Model): """Participant in the ping network""" id = db.Column(db.Integer, primary_key=True) # Used both as identification and password uuid = db.Column(db.String, unique=True) # Name of the machine name = db.Column(db.String) # Mostly free-form (nick, mail address, ...) contact = db.Column(db.String) # Whether we accept this participant or not active = db.Column(db.Boolean) # Many-to-many relationship targets = db.relationship('Target', secondary=handled_targets, backref=db.backref('participants', lazy='dynamic'), lazy='dynamic') def __init__(self, name, contact): self.uuid = str(uuid4()) self.name = name self.contact = contact self.active = False def __str__(self): return "{} ({})".format(self.name, self.contact) class Result(db.Model): """Result of a ping measurement""" id = db.Column(db.Integer, primary_key=True) target_id = db.Column(db.Integer, db.ForeignKey('target.id')) target = db.relationship('Target', backref=db.backref('results', lazy='dynamic')) participant_id = db.Column(db.Integer, db.ForeignKey('participant.id')) participant = db.relationship('Participant', backref=db.backref('results', lazy='dynamic')) # In milliseconds rtt = db.Column(db.Float) # Date at which the result was reported back to us date = db.Column(db.DateTime) def __init__(self, target_id, participant_uuid, rtt): target = Target.query.get_or_404(int(target_id)) participant = Participant.query.filter_by(uuid=participant_uuid, active=True).first_or_404() self.target = target self.participant = participant self.rtt = float(rtt) self.date = datetime.now() def init_db(): db.create_all() def get_targets(uuid): """Returns the queryset of potential targets for the given participant UUID, that is, targets that have not already been handed out to this participant. """ participant = Participant.query.filter_by(uuid=uuid, active=True).first_or_404() # We want to get all targets that do not have a relationship with the # given participant. Note that the following lines manipulate SQL # queries, which are only executed at the very end. # This gives all targets that have already been sent to the given # participant. already_done = Target.query.join(handled_targets).filter_by(participant_id=participant.id).with_entities(Target.id) # This takes the negation of the previous set. return Target.query.filter(~Target.id.in_(already_done)) @app.route('/') def homepage(): return render_template('home.html') @app.route('/robots.txt') def robots(): return "User-agent: *\nDisallow: /target/\nDisallow: /result/" @app.route('/submit', methods=['POST']) def submit_job(): if 'target' in request.form: target = request.form['target'] if is_valid_ip(target): # Explicit IP targets = [Target(target)] else: # DNS name, might give multiple IP targets = [Target(ip) for ip in resolve_name(target)] for t in targets: db.session.add(t) db.session.commit() return render_template('submit.html', targets=targets) else: return "Invalid arguments" @app.route('/create/participant', methods=['POST']) def create_participant(): if {'name', 'contact'}.issubset(request.form) and request.form['name']: participant = Participant(request.form['name'], request.form['contact']) db.session.add(participant) db.session.commit() return render_template('participant.html', participant=participant, uuid=participant.uuid) else: return "Invalid arguments" @app.route('/target/') def get_next_target(uuid): """"Returns the next target to ping for the given participant""" target = get_targets(uuid).first() if target is not None: return "{} {}".format(target.id, target) else: return "" @app.route('/target//') def get_next_target_family(uuid, family): """Same as above, but for a specific family ("ipv4" or "ipv6")""" if family not in ("ipv4", "ipv6"): return "Invalid family, should be ipv4 or ipv6\n" predicate = lambda t: t.is_v4() if family == "ipv4" else t.is_v6() targets = [t for t in get_targets(uuid).all() if predicate(t)] if not targets: return "" return "{} {}".format(targets[0].id, targets[0]) @app.route('/result/report/', methods=['POST']) def report_result(uuid): if {'rtt', 'target'}.issubset(request.form): target_id = request.form['target'] rtt = request.form['rtt'] result = Result(target_id, uuid, rtt) db.session.add(result) # Record that the participant has returned a result participant = result.participant participant.targets.append(result.target) db.session.commit() return "OK\n" else: return "Invalid arguments\n" @app.route('/result/show/') def show_results(target_uniqueid): target = Target.query.filter_by(unique_id=target_uniqueid).first_or_404() results = target.results.order_by('rtt').all() return render_template('results.html', target=target, results=results) if __name__ == '__main__': init_db() app.run(host='0.0.0.0', port=8888)