1732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 2732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo# Use of this source code is governed by a BSD-style license that can be 3732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo# found in the LICENSE file. 4732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 5e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymoimport dpkt 6e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymoimport re 7e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 8e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 9732dfe9c1124b51b906cb6510f219ad84be58836Alex DeymoCROS_P2P_PROTO = '_cros_p2p._tcp' 10732dfe9c1124b51b906cb6510f219ad84be58836Alex DeymoCROS_P2P_PORT = 16725 11732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 12e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 13732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymoclass CrosP2PDaemon(object): 14732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo """Simulates a P2P server. 15732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 16732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo The simulated P2P server will instruct the underlying ZeroconfDaemon to 17732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo reply to requests sharing the files registered on this server. 18732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo """ 19732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo def __init__(self, zeroconf, port=CROS_P2P_PORT): 20732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo """Initialize the CrosP2PDaemon. 21732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 22732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo @param zeroconf: A ZeroconfDaemon instance where this P2P server will be 23732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo announced. 24732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo @param port: The port where the HTTP server part of the P2P protocol is 25732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo listening. The HTTP server is assumend to be running on the same host as 26732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo the provided ZeroconfDaemon server. 27732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo """ 28732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo self._zeroconf = zeroconf 29732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo self._files = {} 30732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo self._num_connections = 0 31732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 32732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo self._p2p_domain = CROS_P2P_PROTO + '.' + zeroconf.domain 33732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo # Register the HTTP Server. 34732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo zeroconf.register_SRV(zeroconf.hostname, CROS_P2P_PROTO, 0, 0, port) 35732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo # Register the P2P running on this server. 36732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo zeroconf.register_PTR(self._p2p_domain, zeroconf.hostname) 372c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo self._update_records(False) 38732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 39732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 402c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo def add_file(self, file_id, file_size, announce=False): 41732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo """Add or update a shared file. 42732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 43732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo @param file_id: The name of the file (without .p2p extension). 44732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo @param file_size: The expected total size of the file. 452c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo @param announce: If True, the method will also announce the changes 462c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo on the network. 47732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo """ 48732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo self._files[file_id] = file_size 492c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo self._update_records(announce) 50732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 51732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 522c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo def remove_file(self, file_id, announce=False): 53732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo """Remove a shared file. 54732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 55732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo @param file_id: The name of the file (without .p2p extension). 562c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo @param announce: If True, the method will also announce the changes 572c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo on the network. 58732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo """ 59732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo del self._files[file_id] 602c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo self._update_records(announce) 61732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 62732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 632c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo def set_num_connections(self, num_connections, announce=False): 64732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo """Sets the number of connections that the HTTP server is handling. 65732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 66732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo This method allows the P2P server to properly announce the number of 67732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo connections it is currently handling. 68732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 69732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo @param num_connections: An integer with the number of connections. 702c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo @param announce: If True, the method will also announce the changes 712c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo on the network. 72732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo """ 73732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo self._num_connections = num_connections 742c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo self._update_records(announce) 75732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 76732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo 772c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo def _update_records(self, announce): 78732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo # Build the TXT records: 79732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo txts = ['num_connections=%d' % self._num_connections] 80732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo for file_id, file_size in self._files.iteritems(): 81732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo txts.append('id_%s=%d' % (file_id, file_size)) 82732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo self._zeroconf.register_TXT( 832c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo self._zeroconf.hostname + '.' + self._p2p_domain, txts, announce) 84e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 85e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 86e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymoclass CrosP2PClient(object): 87e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo """Simulates a P2P client. 88e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 89e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo The P2P client interacts with a ZeroconfDaemon instance that inquires the 90e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo network and collects the mDNS responses. A P2P client instance decodes those 91e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo responses according to the P2P protocol implemented over mDNS. 92e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo """ 93e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo def __init__(self, zeroconf): 94e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo self._zeroconf = zeroconf 95e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo self._p2p_domain = CROS_P2P_PROTO + '.' + zeroconf.domain 96c39594fae840955bf530305e9349694be5953c8fAlex Deymo self._in_query = 0 97c39594fae840955bf530305e9349694be5953c8fAlex Deymo zeroconf.add_answer_observer(self._new_answers) 98c39594fae840955bf530305e9349694be5953c8fAlex Deymo 99c39594fae840955bf530305e9349694be5953c8fAlex Deymo 100c39594fae840955bf530305e9349694be5953c8fAlex Deymo def start_query(self): 101c39594fae840955bf530305e9349694be5953c8fAlex Deymo """Sends queries to gather all the p2p information on the network. 102c39594fae840955bf530305e9349694be5953c8fAlex Deymo 103c39594fae840955bf530305e9349694be5953c8fAlex Deymo When a response that requires to send a new query to the peer is 104c39594fae840955bf530305e9349694be5953c8fAlex Deymo received, such query will be sent until stop_query() is called. 105c39594fae840955bf530305e9349694be5953c8fAlex Deymo Responses received when no query is running will not generate a new. 106c39594fae840955bf530305e9349694be5953c8fAlex Deymo """ 107c39594fae840955bf530305e9349694be5953c8fAlex Deymo self._in_query += 1 108c39594fae840955bf530305e9349694be5953c8fAlex Deymo ts = self._zeroconf.send_request([(self._p2p_domain, dpkt.dns.DNS_PTR)]) 109c39594fae840955bf530305e9349694be5953c8fAlex Deymo # Also send requests for all the known PTR records. 110c39594fae840955bf530305e9349694be5953c8fAlex Deymo queries = [] 111c39594fae840955bf530305e9349694be5953c8fAlex Deymo 112c39594fae840955bf530305e9349694be5953c8fAlex Deymo 113c39594fae840955bf530305e9349694be5953c8fAlex Deymo # The PTR record points to a SRV name. 114c39594fae840955bf530305e9349694be5953c8fAlex Deymo ptr_recs = self._zeroconf.cached_results( 115c39594fae840955bf530305e9349694be5953c8fAlex Deymo self._p2p_domain, dpkt.dns.DNS_PTR, ts) 116c39594fae840955bf530305e9349694be5953c8fAlex Deymo for _rrname, _rrtype, p2p_peer, _deadline in ptr_recs: 117c39594fae840955bf530305e9349694be5953c8fAlex Deymo # Request all the information for that peer. 118c39594fae840955bf530305e9349694be5953c8fAlex Deymo queries.append((p2p_peer, dpkt.dns.DNS_ANY)) 119c39594fae840955bf530305e9349694be5953c8fAlex Deymo # The SRV points to a hostname, port, etc. 120c39594fae840955bf530305e9349694be5953c8fAlex Deymo srv_recs = self._zeroconf.cached_results( 121c39594fae840955bf530305e9349694be5953c8fAlex Deymo p2p_peer, dpkt.dns.DNS_SRV, ts) 122c39594fae840955bf530305e9349694be5953c8fAlex Deymo for _rrname, _rrtype, service, _deadline in srv_recs: 123c39594fae840955bf530305e9349694be5953c8fAlex Deymo srvname, _priority, _weight, port = service 124c39594fae840955bf530305e9349694be5953c8fAlex Deymo # Request all the information for the host name. 125c39594fae840955bf530305e9349694be5953c8fAlex Deymo queries.append((srvname, dpkt.dns.DNS_ANY)) 126c39594fae840955bf530305e9349694be5953c8fAlex Deymo if queries: 127c39594fae840955bf530305e9349694be5953c8fAlex Deymo self._zeroconf.send_request(queries) 128c39594fae840955bf530305e9349694be5953c8fAlex Deymo 129c39594fae840955bf530305e9349694be5953c8fAlex Deymo 130c39594fae840955bf530305e9349694be5953c8fAlex Deymo def stop_query(self): 131c39594fae840955bf530305e9349694be5953c8fAlex Deymo """Stops a started query.""" 132c39594fae840955bf530305e9349694be5953c8fAlex Deymo self._in_query -= 1 133c39594fae840955bf530305e9349694be5953c8fAlex Deymo 134c39594fae840955bf530305e9349694be5953c8fAlex Deymo 135c39594fae840955bf530305e9349694be5953c8fAlex Deymo def _new_answers(self, answers): 136c39594fae840955bf530305e9349694be5953c8fAlex Deymo if not self._in_query: 137c39594fae840955bf530305e9349694be5953c8fAlex Deymo return 138c39594fae840955bf530305e9349694be5953c8fAlex Deymo queries = [] 139c39594fae840955bf530305e9349694be5953c8fAlex Deymo for rrname, rrtype, data in answers: 140c39594fae840955bf530305e9349694be5953c8fAlex Deymo if rrname == self._p2p_domain and rrtype == dpkt.dns.DNS_PTR: 141c39594fae840955bf530305e9349694be5953c8fAlex Deymo # data is a "ptrname" string. 142c39594fae840955bf530305e9349694be5953c8fAlex Deymo queries.append((ptrname, dpkt.dns.DNS_ANY)) 143c39594fae840955bf530305e9349694be5953c8fAlex Deymo if queries: 144c39594fae840955bf530305e9349694be5953c8fAlex Deymo self._zeroconf.send_request(queries) 145e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 146e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 147e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo def get_peers(self, timestamp=None): 148e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo """Return the cached list of peers. 149e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 150e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo @param timestamp: The deadline timestamp to consider the responses. 151e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo @return: A list of tuples of the form (peer_name, hostname, list_of_IPs, 152e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo port). 153e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo """ 154e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo res = [] 155e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo # The PTR record points to a SRV name. 156e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo ptr_recs = self._zeroconf.cached_results( 157e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo self._p2p_domain, dpkt.dns.DNS_PTR, timestamp) 158e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo for _rrname, _rrtype, p2p_peer, _deadline in ptr_recs: 159e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo # The SRV points to a hostname, port, etc. 160e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo srv_recs = self._zeroconf.cached_results( 161e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo p2p_peer, dpkt.dns.DNS_SRV, timestamp) 162e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo for _rrname, _rrtype, service, _deadline in srv_recs: 163e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo srvname, _priority, _weight, port = service 164e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo # Each service points to a hostname (srvname). 165e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo a_recs = self._zeroconf.cached_results( 166e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo srvname, dpkt.dns.DNS_A, timestamp) 167e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo ip_list = [ip for _rrname, _rrtype, ip, _deadline in a_recs] 168e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo res.append((p2p_peer, srvname, ip_list, port)) 169e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo return res 170e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 171e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 172e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo def get_peer_files(self, peer_name, timestamp=None): 173e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo """Returns the cached list of files of the given peer. 174e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 175e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo @peer_name: The peer_name as provided by get_peers(). 176e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo @param timestamp: The deadline timestamp to consider the responses. 177e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo @return: A list of tuples of the form (file_name, current_size). 178e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo """ 179e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo res = [] 180e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo txt_records = self._zeroconf.cached_results( 181e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo peer_name, dpkt.dns.DNS_TXT, timestamp) 182e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo for _rrname, _rrtype, txt_list, _deadline in txt_records: 183e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo for txt in txt_list: 184e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo m = re.match(r'^id_(.*)=([0-9]+)$', txt) 185e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo if not m: 186e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo continue 187e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo file_name, size = m.groups() 188e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo res.append((file_name, int(size))) 189e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo return res 190e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 191e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 192e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo def get_peer_connections(self, peer_name, timestamp=None): 193e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo """Returns the cached num_connections of the given peer. 194e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo 195e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo @peer_name: The peer_name as provided by get_peers(). 196e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo @param timestamp: The deadline timestamp to consider the responses. 197e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo @return: A list of tuples of the form (file_name, current_size). 198e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo """ 199e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo txt_records = self._zeroconf.cached_results( 200e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo peer_name, dpkt.dns.DNS_TXT, timestamp) 201e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo for _rrname, _rrtype, txt_list, _deadline in txt_records: 202e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo for txt in txt_list: 203e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo m = re.match(r'num_connections=(\d+)$', txt) 204e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo if m: 205e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo return int(m.group(1)) 206e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo return None # No num_connections found. 207