1# Copyright (c) 2016 RIPE NCC
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16import logging
17
18from calendar import timegm
19
20from .base import Result, ParsingDict
21
22
23class IcmpHeader(ParsingDict):
24    """
25    But why did we stop here?  Why not go all the way and define subclasses for
26    each object and for `mpls`?  it comes down to a question of complexity vs.
27    usefulness.  This is such a fringe case that it's probably fine to just
28    dump the data in to `self.objects` and let people work from there.  If
29    however you feel that this needs expansion, pull requests are welcome :-)
30
31    Further information regarding the structure and meaning of the data in
32    this class can be found here: http://localhost:8000/docs/data_struct/
33    """
34
35    def __init__(self, data, **kwargs):
36
37        ParsingDict.__init__(self, **kwargs)
38
39        self.raw_data = data
40
41        self.version = self.ensure("version", int)
42        self.rfc4884 = self.ensure("rfc4884", bool)
43        self.objects = self.ensure("obj", list)
44
45
46class Packet(ParsingDict):
47
48    ERROR_CONDITIONS = {
49        "N": "Network unreachable",
50        "H": "Destination unreachable",
51        "A": "Administratively prohibited",
52        "P": "Protocol unreachable",
53        "p": "Port unreachable",
54    }
55
56    def __init__(self, data, **kwargs):
57
58        ParsingDict.__init__(self, **kwargs)
59
60        self.raw_data = data
61
62        self.origin = self.ensure("from", str)
63        self.rtt = self.ensure("rtt", float)
64        self.size = self.ensure("size", int)
65        self.ttl = self.ensure("ttl", int)
66        self.mtu = self.ensure("mtu", int)
67        self.destination_option_size = self.ensure("dstoptsize", int)
68        self.hop_by_hop_option_size = self.ensure("hbhoptsize", int)
69        self.arrived_late_by = self.ensure("late", int, 0)
70        self.internal_ttl = self.ensure("ittl", int, 1)
71
72        if self.rtt:
73            self.rtt = round(self.rtt, 3)
74
75        error = self.ensure("err", str)
76        if error:
77            self._handle_error(self.ERROR_CONDITIONS.get(error, error))
78
79        icmp_header = self.ensure("icmpext", dict)
80
81        self.icmp_header = None
82        if icmp_header:
83            self.icmp_header = IcmpHeader(icmp_header, **kwargs)
84
85    def __str__(self):
86        return self.origin
87
88
89class Hop(ParsingDict):
90
91    def __init__(self, data, **kwargs):
92
93        ParsingDict.__init__(self, **kwargs)
94
95        self.raw_data = data
96
97        self.index = self.ensure("hop", int)
98
99        error = self.ensure("error", str)
100        if error:
101            self._handle_error(error)
102
103        self.packets = []
104        packet_rtts = []
105        if "result" in self.raw_data:
106            for raw_packet in self.raw_data["result"]:
107                if "late" not in raw_packet:
108                    packet = Packet(raw_packet, **kwargs)
109                    if packet.rtt:
110                        packet_rtts.append(packet.rtt)
111                    self.packets.append(packet)
112        self.median_rtt = Result.calculate_median(packet_rtts)
113
114    def __str__(self):
115        return str(self.index)
116
117
118class TracerouteResult(Result):
119
120    def __init__(self, data, **kwargs):
121
122        Result.__init__(self, data, **kwargs)
123
124        self.af = self.ensure("af", int)
125        self.destination_address = self.ensure("dst_addr", str)
126        self.destination_name = self.ensure("dst_name", str)
127        self.source_address = self.ensure("src_addr", str)
128        self.end_time = self.ensure("endtime", "datetime")
129        self.paris_id = self.ensure("paris_id", int)
130        self.size = self.ensure("size", int)
131
132        if 0 < self.firmware < 4460:
133            self.af = self.ensure("pf", int)
134
135        self.protocol = self.clean_protocol(self.ensure("proto", str))
136
137        self.hops = []
138        self.total_hops = 0
139        self.last_median_rtt = None
140
141        # Used by a few response tests below
142        self.destination_ip_responded = False
143        self.last_hop_responded = False
144        self.is_success = False
145        self.last_hop_errors = []
146
147        self._parse_hops(**kwargs)  # Sets hops, last_median_rtt, and total_hops
148
149    @property
150    def last_rtt(self):
151        logging.warning(
152            '"last_rtt" is deprecated and will be removed in future versions. '
153            'Instead, use "last_median_rtt".')
154        return self.last_median_rtt
155
156    @property
157    def target_responded(self):
158        logging.warning(
159            'The "target_responded" property is deprecated and will be removed '
160            'in future versions.  Instead, use "destination_ip_responded".'
161        )
162        return self.destination_ip_responded
163
164    def set_destination_ip_responded(self, last_hop):
165        """Sets the flag if destination IP responded."""
166        if not self.destination_address:
167            return
168
169        for packet in last_hop.packets:
170            if packet.origin and \
171                    self.destination_address == packet.origin:
172                self.destination_ip_responded = True
173                break
174
175    def set_last_hop_responded(self, last_hop):
176        """Sets the flag if last hop responded."""
177        for packet in last_hop.packets:
178            if packet.rtt:
179                self.last_hop_responded = True
180                break
181
182    def set_is_success(self, last_hop):
183        """Sets the flag if traceroute result is successful or not."""
184        for packet in last_hop.packets:
185            if packet.rtt and not packet.is_error:
186                self.is_success = True
187                break
188        else:
189            self.set_last_hop_errors(last_hop)
190
191    def set_last_hop_errors(self, last_hop):
192        """Sets the last hop's errors."""
193        if last_hop.is_error:
194            self.last_hop_errors.append(last_hop.error_message)
195            return
196
197        for packet in last_hop.packets:
198            if packet.is_error:
199                self.last_hop_errors.append(packet.error_message)
200
201    @property
202    def end_time_timestamp(self):
203        return timegm(self.end_time.timetuple())
204
205    @property
206    def ip_path(self):
207        """
208        Returns just the IPs from the traceroute.
209        """
210        r = []
211        for hop in self.hops:
212            r.append([packet.origin for packet in hop.packets])
213        return r
214
215    def _parse_hops(self, parse_all_hops=True, **kwargs):
216
217        try:
218            hops = self.raw_data["result"]
219            assert(isinstance(hops, list))
220        except (KeyError, AssertionError):
221            self._handle_malformation("Legacy formats not supported")
222            return
223
224        num_hops = len(hops)
225        # Go through the hops in reverse so that if
226        # parse_all_hops is False we can stop processing as
227        # soon as possible.
228        for index, raw_hop in reversed(list(enumerate(hops))):
229
230            hop = Hop(raw_hop, **kwargs)
231
232            # If last hop set several useful attributes
233            if index + 1 == num_hops:
234                self.set_destination_ip_responded(hop)
235                self.set_last_hop_responded(hop)
236                self.set_is_success(hop)
237                # We always store the last hop
238                self.hops.insert(0, hop)
239            elif parse_all_hops:
240                self.hops.insert(0, hop)
241
242            if hop.median_rtt and not self.last_median_rtt:
243                self.last_median_rtt = hop.median_rtt
244                if not parse_all_hops:
245                    # Now that we have the last RTT we can stop
246                    break
247        self.total_hops = num_hops
248
249
250__all__ = (
251    "TracerouteResult",
252)
253