xref: /aosp_15_r20/external/autotest/client/cros/netprotos/cros_p2p.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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