peerfinder.py 7.2 KB

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