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