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