1*9c5db199SXin Li# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 3*9c5db199SXin Li# found in the LICENSE file. 4*9c5db199SXin Li 5*9c5db199SXin Liimport dpkt 6*9c5db199SXin Liimport re 7*9c5db199SXin Li 8*9c5db199SXin Li 9*9c5db199SXin LiCROS_P2P_PROTO = '_cros_p2p._tcp' 10*9c5db199SXin LiCROS_P2P_PORT = 16725 11*9c5db199SXin Li 12*9c5db199SXin Li 13*9c5db199SXin Liclass CrosP2PDaemon(object): 14*9c5db199SXin Li """Simulates a P2P server. 15*9c5db199SXin Li 16*9c5db199SXin Li The simulated P2P server will instruct the underlying ZeroconfDaemon to 17*9c5db199SXin Li reply to requests sharing the files registered on this server. 18*9c5db199SXin Li """ 19*9c5db199SXin Li def __init__(self, zeroconf, port=CROS_P2P_PORT): 20*9c5db199SXin Li """Initialize the CrosP2PDaemon. 21*9c5db199SXin Li 22*9c5db199SXin Li @param zeroconf: A ZeroconfDaemon instance where this P2P server will be 23*9c5db199SXin Li announced. 24*9c5db199SXin Li @param port: The port where the HTTP server part of the P2P protocol is 25*9c5db199SXin Li listening. The HTTP server is assumend to be running on the same host as 26*9c5db199SXin Li the provided ZeroconfDaemon server. 27*9c5db199SXin Li """ 28*9c5db199SXin Li self._zeroconf = zeroconf 29*9c5db199SXin Li self._files = {} 30*9c5db199SXin Li self._num_connections = 0 31*9c5db199SXin Li 32*9c5db199SXin Li self._p2p_domain = CROS_P2P_PROTO + '.' + zeroconf.domain 33*9c5db199SXin Li # Register the HTTP Server. 34*9c5db199SXin Li zeroconf.register_SRV(zeroconf.hostname, CROS_P2P_PROTO, 0, 0, port) 35*9c5db199SXin Li # Register the P2P running on this server. 36*9c5db199SXin Li zeroconf.register_PTR(self._p2p_domain, zeroconf.hostname) 37*9c5db199SXin Li self._update_records(False) 38*9c5db199SXin Li 39*9c5db199SXin Li 40*9c5db199SXin Li def add_file(self, file_id, file_size, announce=False): 41*9c5db199SXin Li """Add or update a shared file. 42*9c5db199SXin Li 43*9c5db199SXin Li @param file_id: The name of the file (without .p2p extension). 44*9c5db199SXin Li @param file_size: The expected total size of the file. 45*9c5db199SXin Li @param announce: If True, the method will also announce the changes 46*9c5db199SXin Li on the network. 47*9c5db199SXin Li """ 48*9c5db199SXin Li self._files[file_id] = file_size 49*9c5db199SXin Li self._update_records(announce) 50*9c5db199SXin Li 51*9c5db199SXin Li 52*9c5db199SXin Li def remove_file(self, file_id, announce=False): 53*9c5db199SXin Li """Remove a shared file. 54*9c5db199SXin Li 55*9c5db199SXin Li @param file_id: The name of the file (without .p2p extension). 56*9c5db199SXin Li @param announce: If True, the method will also announce the changes 57*9c5db199SXin Li on the network. 58*9c5db199SXin Li """ 59*9c5db199SXin Li del self._files[file_id] 60*9c5db199SXin Li self._update_records(announce) 61*9c5db199SXin Li 62*9c5db199SXin Li 63*9c5db199SXin Li def set_num_connections(self, num_connections, announce=False): 64*9c5db199SXin Li """Sets the number of connections that the HTTP server is handling. 65*9c5db199SXin Li 66*9c5db199SXin Li This method allows the P2P server to properly announce the number of 67*9c5db199SXin Li connections it is currently handling. 68*9c5db199SXin Li 69*9c5db199SXin Li @param num_connections: An integer with the number of connections. 70*9c5db199SXin Li @param announce: If True, the method will also announce the changes 71*9c5db199SXin Li on the network. 72*9c5db199SXin Li """ 73*9c5db199SXin Li self._num_connections = num_connections 74*9c5db199SXin Li self._update_records(announce) 75*9c5db199SXin Li 76*9c5db199SXin Li 77*9c5db199SXin Li def _update_records(self, announce): 78*9c5db199SXin Li # Build the TXT records: 79*9c5db199SXin Li txts = ['num_connections=%d' % self._num_connections] 80*9c5db199SXin Li for file_id, file_size in self._files.iteritems(): 81*9c5db199SXin Li txts.append('id_%s=%d' % (file_id, file_size)) 82*9c5db199SXin Li self._zeroconf.register_TXT( 83*9c5db199SXin Li self._zeroconf.hostname + '.' + self._p2p_domain, txts, announce) 84*9c5db199SXin Li 85*9c5db199SXin Li 86*9c5db199SXin Liclass CrosP2PClient(object): 87*9c5db199SXin Li """Simulates a P2P client. 88*9c5db199SXin Li 89*9c5db199SXin Li The P2P client interacts with a ZeroconfDaemon instance that inquires the 90*9c5db199SXin Li network and collects the mDNS responses. A P2P client instance decodes those 91*9c5db199SXin Li responses according to the P2P protocol implemented over mDNS. 92*9c5db199SXin Li """ 93*9c5db199SXin Li def __init__(self, zeroconf): 94*9c5db199SXin Li self._zeroconf = zeroconf 95*9c5db199SXin Li self._p2p_domain = CROS_P2P_PROTO + '.' + zeroconf.domain 96*9c5db199SXin Li self._in_query = 0 97*9c5db199SXin Li zeroconf.add_answer_observer(self._new_answers) 98*9c5db199SXin Li 99*9c5db199SXin Li 100*9c5db199SXin Li def start_query(self): 101*9c5db199SXin Li """Sends queries to gather all the p2p information on the network. 102*9c5db199SXin Li 103*9c5db199SXin Li When a response that requires to send a new query to the peer is 104*9c5db199SXin Li received, such query will be sent until stop_query() is called. 105*9c5db199SXin Li Responses received when no query is running will not generate a new. 106*9c5db199SXin Li """ 107*9c5db199SXin Li self._in_query += 1 108*9c5db199SXin Li ts = self._zeroconf.send_request([(self._p2p_domain, dpkt.dns.DNS_PTR)]) 109*9c5db199SXin Li # Also send requests for all the known PTR records. 110*9c5db199SXin Li queries = [] 111*9c5db199SXin Li 112*9c5db199SXin Li 113*9c5db199SXin Li # The PTR record points to a SRV name. 114*9c5db199SXin Li ptr_recs = self._zeroconf.cached_results( 115*9c5db199SXin Li self._p2p_domain, dpkt.dns.DNS_PTR, ts) 116*9c5db199SXin Li for _rrname, _rrtype, p2p_peer, _deadline in ptr_recs: 117*9c5db199SXin Li # Request all the information for that peer. 118*9c5db199SXin Li queries.append((p2p_peer, dpkt.dns.DNS_ANY)) 119*9c5db199SXin Li # The SRV points to a hostname, port, etc. 120*9c5db199SXin Li srv_recs = self._zeroconf.cached_results( 121*9c5db199SXin Li p2p_peer, dpkt.dns.DNS_SRV, ts) 122*9c5db199SXin Li for _rrname, _rrtype, service, _deadline in srv_recs: 123*9c5db199SXin Li srvname, _priority, _weight, port = service 124*9c5db199SXin Li # Request all the information for the host name. 125*9c5db199SXin Li queries.append((srvname, dpkt.dns.DNS_ANY)) 126*9c5db199SXin Li if queries: 127*9c5db199SXin Li self._zeroconf.send_request(queries) 128*9c5db199SXin Li 129*9c5db199SXin Li 130*9c5db199SXin Li def stop_query(self): 131*9c5db199SXin Li """Stops a started query.""" 132*9c5db199SXin Li self._in_query -= 1 133*9c5db199SXin Li 134*9c5db199SXin Li 135*9c5db199SXin Li def _new_answers(self, answers): 136*9c5db199SXin Li if not self._in_query: 137*9c5db199SXin Li return 138*9c5db199SXin Li queries = [] 139*9c5db199SXin Li for rrname, rrtype, data in answers: 140*9c5db199SXin Li if rrname == self._p2p_domain and rrtype == dpkt.dns.DNS_PTR: 141*9c5db199SXin Li # data is a "ptrname" string. 142*9c5db199SXin Li queries.append((ptrname, dpkt.dns.DNS_ANY)) 143*9c5db199SXin Li if queries: 144*9c5db199SXin Li self._zeroconf.send_request(queries) 145*9c5db199SXin Li 146*9c5db199SXin Li 147*9c5db199SXin Li def get_peers(self, timestamp=None): 148*9c5db199SXin Li """Return the cached list of peers. 149*9c5db199SXin Li 150*9c5db199SXin Li @param timestamp: The deadline timestamp to consider the responses. 151*9c5db199SXin Li @return: A list of tuples of the form (peer_name, hostname, list_of_IPs, 152*9c5db199SXin Li port). 153*9c5db199SXin Li """ 154*9c5db199SXin Li res = [] 155*9c5db199SXin Li # The PTR record points to a SRV name. 156*9c5db199SXin Li ptr_recs = self._zeroconf.cached_results( 157*9c5db199SXin Li self._p2p_domain, dpkt.dns.DNS_PTR, timestamp) 158*9c5db199SXin Li for _rrname, _rrtype, p2p_peer, _deadline in ptr_recs: 159*9c5db199SXin Li # The SRV points to a hostname, port, etc. 160*9c5db199SXin Li srv_recs = self._zeroconf.cached_results( 161*9c5db199SXin Li p2p_peer, dpkt.dns.DNS_SRV, timestamp) 162*9c5db199SXin Li for _rrname, _rrtype, service, _deadline in srv_recs: 163*9c5db199SXin Li srvname, _priority, _weight, port = service 164*9c5db199SXin Li # Each service points to a hostname (srvname). 165*9c5db199SXin Li a_recs = self._zeroconf.cached_results( 166*9c5db199SXin Li srvname, dpkt.dns.DNS_A, timestamp) 167*9c5db199SXin Li ip_list = [ip for _rrname, _rrtype, ip, _deadline in a_recs] 168*9c5db199SXin Li res.append((p2p_peer, srvname, ip_list, port)) 169*9c5db199SXin Li return res 170*9c5db199SXin Li 171*9c5db199SXin Li 172*9c5db199SXin Li def get_peer_files(self, peer_name, timestamp=None): 173*9c5db199SXin Li """Returns the cached list of files of the given peer. 174*9c5db199SXin Li 175*9c5db199SXin Li @peer_name: The peer_name as provided by get_peers(). 176*9c5db199SXin Li @param timestamp: The deadline timestamp to consider the responses. 177*9c5db199SXin Li @return: A list of tuples of the form (file_name, current_size). 178*9c5db199SXin Li """ 179*9c5db199SXin Li res = [] 180*9c5db199SXin Li txt_records = self._zeroconf.cached_results( 181*9c5db199SXin Li peer_name, dpkt.dns.DNS_TXT, timestamp) 182*9c5db199SXin Li for _rrname, _rrtype, txt_list, _deadline in txt_records: 183*9c5db199SXin Li for txt in txt_list: 184*9c5db199SXin Li m = re.match(r'^id_(.*)=([0-9]+)$', txt) 185*9c5db199SXin Li if not m: 186*9c5db199SXin Li continue 187*9c5db199SXin Li file_name, size = m.groups() 188*9c5db199SXin Li res.append((file_name, int(size))) 189*9c5db199SXin Li return res 190*9c5db199SXin Li 191*9c5db199SXin Li 192*9c5db199SXin Li def get_peer_connections(self, peer_name, timestamp=None): 193*9c5db199SXin Li """Returns the cached num_connections of the given peer. 194*9c5db199SXin Li 195*9c5db199SXin Li @peer_name: The peer_name as provided by get_peers(). 196*9c5db199SXin Li @param timestamp: The deadline timestamp to consider the responses. 197*9c5db199SXin Li @return: A list of tuples of the form (file_name, current_size). 198*9c5db199SXin Li """ 199*9c5db199SXin Li txt_records = self._zeroconf.cached_results( 200*9c5db199SXin Li peer_name, dpkt.dns.DNS_TXT, timestamp) 201*9c5db199SXin Li for _rrname, _rrtype, txt_list, _deadline in txt_records: 202*9c5db199SXin Li for txt in txt_list: 203*9c5db199SXin Li m = re.match(r'num_connections=(\d+)$', txt) 204*9c5db199SXin Li if m: 205*9c5db199SXin Li return int(m.group(1)) 206*9c5db199SXin Li return None # No num_connections found. 207