peerfinder.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. #!/usr/bin/env python3
  2. from flask import Flask
  3. from flask import request, render_template
  4. from flask.ext.sqlalchemy import SQLAlchemy
  5. from flask.ext.script import Server, Manager
  6. #from flask import session, request, url_for, redirect, render_template
  7. import netaddr
  8. from netaddr import IPAddress
  9. # Hack for python3
  10. from netaddr.strategy.ipv4 import packed_to_int as unpack_v4
  11. from netaddr.strategy.ipv6 import packed_to_int as unpack_v6
  12. import socket
  13. from datetime import datetime
  14. from uuid import uuid4
  15. app = Flask(__name__)
  16. app.config.from_pyfile('config.py')
  17. db = SQLAlchemy(app)
  18. manager = Manager(app)
  19. manager.add_command("runserver", Server(host='0.0.0.0', port=8888))
  20. def unpack(ip):
  21. if len(ip) == 4:
  22. return unpack_v4(ip)
  23. elif len(ip) == 16:
  24. return unpack_v6(ip)
  25. def is_valid_ip(ip):
  26. return netaddr.valid_ipv4(ip) or netaddr.valid_ipv6(ip)
  27. def resolve_name(hostname):
  28. try:
  29. return list({s[4][0] for s in socket.getaddrinfo(hostname, None)})
  30. except socket.gaierror:
  31. return []
  32. class Target(db.Model):
  33. """Target IP to ping"""
  34. id = db.Column(db.Integer, primary_key=True)
  35. # Unique ID for accessing the results (privacy reasons)
  36. unique_id = db.Column(db.String)
  37. # IP addresses are encoded as their binary representation
  38. ip = db.Column(db.BINARY(length=16))
  39. # Date at which a user asked for measurements to this target
  40. submitted = db.Column(db.DateTime)
  41. def __init__(self, ip):
  42. self.unique_id = str(uuid4())
  43. self.ip = IPAddress(ip).packed
  44. self.submitted = datetime.now()
  45. def get_ip(self):
  46. return IPAddress(unpack(self.ip))
  47. def is_v4(self):
  48. return self.get_ip().version == 4
  49. def is_v6(self):
  50. return self.get_ip().version == 6
  51. def __repr__(self):
  52. return '%r' % self.get_ip()
  53. def __str__(self):
  54. return str(self.get_ip())
  55. # Many-to-many table to record which target has been given to which
  56. # participant.
  57. handled_targets = db.Table('handled_targets',
  58. db.Column('target_id', db.Integer, db.ForeignKey('target.id')),
  59. db.Column('participant_id', db.Integer, db.ForeignKey('participant.id'))
  60. )
  61. class Participant(db.Model):
  62. """Participant in the ping network"""
  63. id = db.Column(db.Integer, primary_key=True)
  64. # Used both as identification and password
  65. uuid = db.Column(db.String, unique=True)
  66. # Name of the machine
  67. name = db.Column(db.String)
  68. # Mostly free-form (nick, mail address, ...)
  69. contact = db.Column(db.String)
  70. # Whether we accept this participant or not
  71. active = db.Column(db.Boolean)
  72. # Many-to-many relationship
  73. targets = db.relationship('Target',
  74. secondary=handled_targets,
  75. backref=db.backref('participants', lazy='dynamic'),
  76. lazy='dynamic')
  77. def __init__(self, name, contact):
  78. self.uuid = str(uuid4())
  79. self.name = name
  80. self.contact = contact
  81. self.active = False
  82. def __str__(self):
  83. return "{} ({})".format(self.name, self.contact)
  84. class Result(db.Model):
  85. """Result of a ping measurement"""
  86. id = db.Column(db.Integer, primary_key=True)
  87. target_id = db.Column(db.Integer, db.ForeignKey('target.id'))
  88. target = db.relationship('Target',
  89. backref=db.backref('results', lazy='dynamic'))
  90. participant_id = db.Column(db.Integer, db.ForeignKey('participant.id'))
  91. participant = db.relationship('Participant',
  92. backref=db.backref('results', lazy='dynamic'))
  93. # Date at which the result was reported back to us
  94. date = db.Column(db.DateTime)
  95. # In milliseconds
  96. avgrtt = db.Column(db.Float)
  97. # All these are optional
  98. minrtt = db.Column(db.Float)
  99. maxrtt = db.Column(db.Float)
  100. jitter = db.Column(db.Float)
  101. # Number of ping requests
  102. probes_sent = db.Column(db.Integer)
  103. # Number of successful probes
  104. probes_received = db.Column(db.Integer)
  105. def __init__(self, target_id, participant_uuid, avgrtt, minrtt, maxrtt,
  106. jitter, probes_sent, probes_received):
  107. target = Target.query.get_or_404(int(target_id))
  108. participant = Participant.query.filter_by(uuid=participant_uuid,
  109. active=True).first_or_404()
  110. self.target = target
  111. self.participant = participant
  112. self.date = datetime.now()
  113. self.avgrtt = float(avgrtt)
  114. self.minrtt = float(minrtt) if minrtt is not None else None
  115. self.maxrtt = float(maxrtt) if maxrtt is not None else None
  116. self.jitter = float(jitter) if jitter is not None else None
  117. self.probes_sent = int(probes_sent) if probes_sent is not None else None
  118. self.probes_received = int(probes_received) if probes_received is not None else None
  119. def init_db():
  120. db.create_all()
  121. def get_targets(uuid):
  122. """Returns the queryset of potential targets for the given participant
  123. UUID, that is, targets that have not already been handed out to this
  124. participant.
  125. """
  126. participant = Participant.query.filter_by(uuid=uuid, active=True).first_or_404()
  127. # We want to get all targets that do not have a relationship with the
  128. # given participant. Note that the following lines manipulate SQL
  129. # queries, which are only executed at the very end.
  130. # This gives all targets that have already been sent to the given
  131. # participant.
  132. already_done = Target.query.join(handled_targets).filter_by(participant_id=participant.id).with_entities(Target.id)
  133. # This takes the negation of the previous set.
  134. return Target.query.filter(~Target.id.in_(already_done))
  135. @app.route('/')
  136. def homepage():
  137. return render_template('home.html')
  138. @app.route('/static/<path:path>')
  139. def static_proxy(path):
  140. # send_static_file will guess the correct MIME type
  141. return app.send_static_file(path)
  142. @app.route('/robots.txt')
  143. def robots():
  144. return app.send_static_file("robots.txt")
  145. @app.route('/submit', methods=['POST'])
  146. def submit_job():
  147. if 'target' in request.form:
  148. target = request.form['target'].strip()
  149. if is_valid_ip(target):
  150. # Explicit IP
  151. targets = [Target(target)]
  152. else:
  153. # DNS name, might give multiple IP
  154. targets = [Target(ip) for ip in resolve_name(target)]
  155. if targets == []:
  156. return render_template('submit_error.html', target=request.form['target'])
  157. for t in targets:
  158. db.session.add(t)
  159. db.session.commit()
  160. return render_template('submit.html', targets=targets)
  161. else:
  162. return "Invalid arguments"
  163. @app.route('/create/participant', methods=['POST'])
  164. def create_participant():
  165. if {'name', 'contact'}.issubset(request.form) and request.form['name']:
  166. participant = Participant(request.form['name'], request.form['contact'])
  167. db.session.add(participant)
  168. db.session.commit()
  169. return render_template('participant.html', participant=participant,
  170. uuid=participant.uuid,
  171. peerfinder=app.config["PEERFINDER_DN42"])
  172. else:
  173. return "Invalid arguments"
  174. @app.route('/target/<uuid>')
  175. def get_next_target(uuid):
  176. """"Returns the next target to ping for the given participant"""
  177. target = get_targets(uuid).first()
  178. if target is not None:
  179. return "{} {}".format(target.id, target)
  180. else:
  181. return ""
  182. @app.route('/target/<uuid>/<family>')
  183. def get_next_target_family(uuid, family):
  184. """Same as above, but for a specific family ("ipv4" or "ipv6")"""
  185. if family not in ("ipv4", "ipv6"):
  186. return "Invalid family, should be ipv4 or ipv6\n"
  187. predicate = lambda t: t.is_v4() if family == "ipv4" else t.is_v6()
  188. targets = [t for t in get_targets(uuid).all() if predicate(t)]
  189. if not targets:
  190. return ""
  191. return "{} {}".format(targets[0].id, targets[0])
  192. @app.route('/result/report/<uuid>', methods=['POST'])
  193. def report_result(uuid):
  194. if {'avgrtt', 'target'}.issubset(request.form):
  195. target_id = request.form['target']
  196. avgrtt = request.form['avgrtt']
  197. optional_args = [request.form.get(f) for f in
  198. ('minrtt', 'maxrtt', 'jitter', 'probes_sent',
  199. 'probes_received')]
  200. result = Result(target_id, uuid, avgrtt, *optional_args)
  201. db.session.add(result)
  202. # Record that the participant has returned a result
  203. participant = result.participant
  204. participant.targets.append(result.target)
  205. db.session.commit()
  206. return "OK\n"
  207. else:
  208. return "Invalid arguments\n"
  209. @app.route('/result/show/<target_uniqueid>')
  210. def show_results(target_uniqueid):
  211. target = Target.query.filter_by(unique_id=target_uniqueid).first_or_404()
  212. results = target.results.order_by('avgrtt').all()
  213. return render_template('results.html', target=target, results=results)
  214. if __name__ == '__main__':
  215. init_db()
  216. manager.run()