yougotmapped 1.0.1__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yougotmapped/cli.py +181 -138
- yougotmapped/utils/anonymity.py +68 -34
- yougotmapped/utils/bandwidth.py +54 -0
- yougotmapped/utils/dependencies.py +46 -24
- yougotmapped/utils/ingress.py +67 -0
- yougotmapped/utils/jitter.py +78 -0
- yougotmapped/utils/mapping.py +90 -39
- yougotmapped/utils/mss.py +132 -0
- yougotmapped/utils/mtu.py +81 -0
- yougotmapped/utils/network.py +81 -20
- yougotmapped/utils/output.py +61 -48
- yougotmapped/utils/ping.py +90 -39
- yougotmapped/utils/trace.py +73 -48
- yougotmapped-1.1.0.dist-info/METADATA +196 -0
- yougotmapped-1.1.0.dist-info/RECORD +21 -0
- {yougotmapped-1.0.1.dist-info → yougotmapped-1.1.0.dist-info}/WHEEL +1 -1
- {yougotmapped-1.0.1.dist-info → yougotmapped-1.1.0.dist-info}/licenses/LICENSE +3 -9
- yougotmapped/utils/token.py +0 -31
- yougotmapped-1.0.1.dist-info/METADATA +0 -133
- yougotmapped-1.0.1.dist-info/RECORD +0 -17
- {yougotmapped-1.0.1.dist-info → yougotmapped-1.1.0.dist-info}/entry_points.txt +0 -0
- {yougotmapped-1.0.1.dist-info → yougotmapped-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Ingress Geography Finder
|
|
2
|
+
|
|
3
|
+
from yougotmapped.utils.network import get_hop_location
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def find_ingress_geography(trace_result: dict, target_asn: int | None) -> dict:
|
|
7
|
+
|
|
8
|
+
if not trace_result or not target_asn:
|
|
9
|
+
return {
|
|
10
|
+
"available": False,
|
|
11
|
+
"reason": "missing_data",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
hops = trace_result.get("hops", [])
|
|
15
|
+
previous_asn = None
|
|
16
|
+
|
|
17
|
+
for hop in hops:
|
|
18
|
+
if hop.get("private"):
|
|
19
|
+
continue
|
|
20
|
+
|
|
21
|
+
hop_asn = hop.get("asn")
|
|
22
|
+
hop_ip = hop.get("ip")
|
|
23
|
+
|
|
24
|
+
if hop_asn is None:
|
|
25
|
+
previous_asn = hop_asn
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
if previous_asn != hop_asn and hop_asn == target_asn:
|
|
29
|
+
loc = get_hop_location(hop_ip)
|
|
30
|
+
if not loc:
|
|
31
|
+
return {
|
|
32
|
+
"available": False,
|
|
33
|
+
"reason": "geo_lookup_failed",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
lat, lon = loc
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"available": True,
|
|
40
|
+
"ingress_hop": hop["hop"],
|
|
41
|
+
"ip": hop_ip,
|
|
42
|
+
"asn": hop_asn,
|
|
43
|
+
"latitude": lat,
|
|
44
|
+
"longitude": lon,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
previous_asn = hop_asn
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"available": False,
|
|
51
|
+
"reason": "ingress_not_detected",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def format_ingress_result(result: dict) -> None:
|
|
56
|
+
|
|
57
|
+
if not result.get("available"):
|
|
58
|
+
print("Ingress geography could not be determined.")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
print("Ingress Geography:")
|
|
62
|
+
print(f" Hop: {result['ingress_hop']}")
|
|
63
|
+
print(f" IP: {result['ip']}")
|
|
64
|
+
print(f" ASN: AS{result['asn']}")
|
|
65
|
+
print(
|
|
66
|
+
f" Coordinates:{result['latitude']:.3f}, {result['longitude']:.3f}"
|
|
67
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Jitter Utilities
|
|
2
|
+
|
|
3
|
+
from ping3 import ping
|
|
4
|
+
import statistics
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def jitter_test(host: str, count: int = 20, timeout: float = 1.0) -> dict:
|
|
8
|
+
|
|
9
|
+
rtts = []
|
|
10
|
+
|
|
11
|
+
for _ in range(count):
|
|
12
|
+
try:
|
|
13
|
+
delay = ping(host, timeout=timeout)
|
|
14
|
+
if delay is not None:
|
|
15
|
+
rtts.append(delay * 1000) # ms
|
|
16
|
+
except Exception:
|
|
17
|
+
continue
|
|
18
|
+
|
|
19
|
+
sent = count
|
|
20
|
+
received = len(rtts)
|
|
21
|
+
lost = sent - received
|
|
22
|
+
|
|
23
|
+
if received < 2:
|
|
24
|
+
return {
|
|
25
|
+
"reachable": False,
|
|
26
|
+
"sent": sent,
|
|
27
|
+
"received": received,
|
|
28
|
+
"packet_loss_percent": round((lost / sent) * 100, 1),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
median_rtt = statistics.median(rtts)
|
|
32
|
+
|
|
33
|
+
jitter_ms = statistics.mean(abs(rtt - median_rtt) for rtt in rtts)
|
|
34
|
+
|
|
35
|
+
min_rtt = round(min(rtts), 2)
|
|
36
|
+
max_rtt = round(max(rtts), 2)
|
|
37
|
+
median_rtt = round(median_rtt, 2)
|
|
38
|
+
jitter_ms = round(jitter_ms, 2)
|
|
39
|
+
|
|
40
|
+
if jitter_ms < 3:
|
|
41
|
+
stability = "stable"
|
|
42
|
+
elif jitter_ms < 10:
|
|
43
|
+
stability = "moderate"
|
|
44
|
+
else:
|
|
45
|
+
stability = "unstable"
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"reachable": True,
|
|
49
|
+
"sent": sent,
|
|
50
|
+
"received": received,
|
|
51
|
+
"packet_loss_percent": round((lost / sent) * 100, 1),
|
|
52
|
+
"rtt_ms": {
|
|
53
|
+
"min": min_rtt,
|
|
54
|
+
"median": median_rtt,
|
|
55
|
+
"max": max_rtt,
|
|
56
|
+
},
|
|
57
|
+
"jitter_ms": jitter_ms,
|
|
58
|
+
"stability": stability,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def format_jitter_result(result: dict) -> None:
|
|
63
|
+
|
|
64
|
+
if not result.get("reachable"):
|
|
65
|
+
print("Jitter test failed (insufficient replies).")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
print(f"Packets: sent={result['sent']} received={result['received']}")
|
|
69
|
+
print(f"Packet loss: {result['packet_loss_percent']}%")
|
|
70
|
+
|
|
71
|
+
rtt = result["rtt_ms"]
|
|
72
|
+
print(
|
|
73
|
+
f"RTT (ms): min={rtt['min']} "
|
|
74
|
+
f"median={rtt['median']} max={rtt['max']}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print(f"Jitter: {result['jitter_ms']} ms")
|
|
78
|
+
print(f"Stability: {result['stability'].upper()}")
|
yougotmapped/utils/mapping.py
CHANGED
|
@@ -1,66 +1,117 @@
|
|
|
1
|
-
|
|
1
|
+
from pathlib import Path
|
|
2
2
|
import folium
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
if 'loc' not in ip_data:
|
|
6
|
-
print("Location data not available.")
|
|
7
|
-
return
|
|
4
|
+
DEFAULT_GEO_RADIUS_KM = 40 # IP geolocation uncertainty radius
|
|
8
5
|
|
|
9
|
-
latitude, longitude = map(float, ip_data['loc'].split(','))
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
def plot_ip_location(geo: dict):
|
|
12
8
|
|
|
9
|
+
lat = geo.get("latitude")
|
|
10
|
+
lon = geo.get("longitude")
|
|
11
|
+
|
|
12
|
+
if lat is None or lon is None:
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
m = folium.Map(location=[lat, lon], zoom_start=6)
|
|
16
|
+
|
|
17
|
+
# Destination marker
|
|
13
18
|
folium.Marker(
|
|
14
|
-
[
|
|
15
|
-
popup=f"IP: {
|
|
16
|
-
icon=folium.Icon(color=
|
|
19
|
+
location=[lat, lon],
|
|
20
|
+
popup=f"IP: {geo.get('ip')}",
|
|
21
|
+
icon=folium.Icon(color="red", icon="info-sign"),
|
|
17
22
|
).add_to(m)
|
|
18
23
|
|
|
24
|
+
# Uncertainty circle
|
|
19
25
|
folium.Circle(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
color=
|
|
26
|
+
location=[lat, lon],
|
|
27
|
+
radius=DEFAULT_GEO_RADIUS_KM * 1000, # meters
|
|
28
|
+
color="red",
|
|
23
29
|
fill=True,
|
|
24
|
-
fill_color=
|
|
25
|
-
fill_opacity=0.
|
|
26
|
-
popup="Approximate
|
|
30
|
+
fill_color="red",
|
|
31
|
+
fill_opacity=0.15,
|
|
32
|
+
popup="Approximate IP geolocation",
|
|
27
33
|
).add_to(m)
|
|
28
34
|
|
|
29
35
|
return m
|
|
30
36
|
|
|
31
37
|
|
|
32
|
-
def plot_multiple_ip_locations(
|
|
33
|
-
if not ip_data_list:
|
|
34
|
-
print("No valid IP data to plot.")
|
|
35
|
-
return
|
|
38
|
+
def plot_multiple_ip_locations(geos: list[dict]):
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
coords = [
|
|
41
|
+
(g.get("latitude"), g.get("longitude"))
|
|
42
|
+
for g in geos
|
|
43
|
+
if g.get("latitude") is not None and g.get("longitude") is not None
|
|
44
|
+
]
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
if not coords:
|
|
47
|
+
return None
|
|
44
48
|
|
|
45
|
-
for
|
|
46
|
-
|
|
49
|
+
avg_lat = sum(lat for lat, _ in coords) / len(coords)
|
|
50
|
+
avg_lon = sum(lon for _, lon in coords) / len(coords)
|
|
51
|
+
|
|
52
|
+
m = folium.Map(location=[avg_lat, avg_lon], zoom_start=3)
|
|
53
|
+
|
|
54
|
+
for geo in geos:
|
|
55
|
+
lat = geo.get("latitude")
|
|
56
|
+
lon = geo.get("longitude")
|
|
57
|
+
if lat is None or lon is None:
|
|
47
58
|
continue
|
|
48
|
-
|
|
59
|
+
|
|
49
60
|
folium.Marker(
|
|
50
|
-
[lat, lon],
|
|
51
|
-
popup=f"IP: {
|
|
52
|
-
icon=folium.Icon(color=
|
|
61
|
+
location=[lat, lon],
|
|
62
|
+
popup=f"IP: {geo.get('ip')}",
|
|
63
|
+
icon=folium.Icon(color="red", icon="info-sign"),
|
|
53
64
|
).add_to(m)
|
|
54
65
|
|
|
55
66
|
folium.Circle(
|
|
56
|
-
radius=10000,
|
|
57
67
|
location=[lat, lon],
|
|
58
|
-
|
|
68
|
+
radius=DEFAULT_GEO_RADIUS_KM * 1000,
|
|
69
|
+
color="red",
|
|
70
|
+
fill=True,
|
|
71
|
+
fill_color="red",
|
|
72
|
+
fill_opacity=0.15,
|
|
73
|
+
).add_to(m)
|
|
74
|
+
|
|
75
|
+
return m
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def plot_traceroute_path(trace_result: dict, m):
|
|
79
|
+
|
|
80
|
+
hops = trace_result.get("hops", [])
|
|
81
|
+
prev = None
|
|
82
|
+
|
|
83
|
+
for hop in hops:
|
|
84
|
+
lat = hop.get("latitude")
|
|
85
|
+
lon = hop.get("longitude")
|
|
86
|
+
|
|
87
|
+
if lat is None or lon is None:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
point = [lat, lon]
|
|
91
|
+
|
|
92
|
+
# Hop marker (no circle)
|
|
93
|
+
folium.CircleMarker(
|
|
94
|
+
location=point,
|
|
95
|
+
radius=4,
|
|
96
|
+
color="blue",
|
|
59
97
|
fill=True,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
popup="Approximate Area"
|
|
98
|
+
fill_opacity=0.9,
|
|
99
|
+
popup=f"Hop {hop.get('hop')} ({hop.get('ip')})",
|
|
63
100
|
).add_to(m)
|
|
64
101
|
|
|
65
|
-
|
|
66
|
-
|
|
102
|
+
if prev:
|
|
103
|
+
folium.PolyLine(
|
|
104
|
+
locations=[prev, point],
|
|
105
|
+
color="blue",
|
|
106
|
+
weight=2,
|
|
107
|
+
opacity=0.6,
|
|
108
|
+
).add_to(m)
|
|
109
|
+
|
|
110
|
+
prev = point
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def save_map(m, filename: str) -> str:
|
|
114
|
+
|
|
115
|
+
path = Path(filename).resolve()
|
|
116
|
+
m.save(str(path))
|
|
117
|
+
return str(path)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import time
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from scapy.all import IP, TCP, sr1, conf
|
|
7
|
+
SCAPY_AVAILABLE = True
|
|
8
|
+
conf.verb = 0
|
|
9
|
+
except Exception:
|
|
10
|
+
SCAPY_AVAILABLE = False
|
|
11
|
+
|
|
12
|
+
def _probe_mss(
|
|
13
|
+
host: str,
|
|
14
|
+
port: int,
|
|
15
|
+
mss: int,
|
|
16
|
+
timeout: float = 1.0,
|
|
17
|
+
) -> bool:
|
|
18
|
+
sport = random.randint(1024, 65535)
|
|
19
|
+
|
|
20
|
+
pkt = (
|
|
21
|
+
IP(dst=host, flags="DF")
|
|
22
|
+
/ TCP(
|
|
23
|
+
sport=sport,
|
|
24
|
+
dport=port,
|
|
25
|
+
flags="S",
|
|
26
|
+
seq=1000,
|
|
27
|
+
options=[("MSS", mss)],
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
resp = sr1(pkt, timeout=timeout)
|
|
32
|
+
if resp is None:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
if resp.haslayer(TCP):
|
|
36
|
+
flags = resp[TCP].flags
|
|
37
|
+
return (flags & 0x12) == 0x12 # SYN+ACK
|
|
38
|
+
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _estimate_mss(
|
|
43
|
+
rtt_ms: Optional[float],
|
|
44
|
+
traceroute_ok: bool,
|
|
45
|
+
) -> Dict:
|
|
46
|
+
if rtt_ms is None or not traceroute_ok:
|
|
47
|
+
return {
|
|
48
|
+
"mss": 1360,
|
|
49
|
+
"confidence": "low",
|
|
50
|
+
"reason": "mobile_or_cgnat_likely",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if rtt_ms > 40:
|
|
54
|
+
return {
|
|
55
|
+
"mss": 1360,
|
|
56
|
+
"confidence": "low",
|
|
57
|
+
"reason": "high_rtt_path",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if rtt_ms > 20:
|
|
61
|
+
return {
|
|
62
|
+
"mss": 1420,
|
|
63
|
+
"confidence": "medium",
|
|
64
|
+
"reason": "possible_tunneling",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"mss": 1460,
|
|
69
|
+
"confidence": "medium",
|
|
70
|
+
"reason": "standard_ethernet_assumed",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def discover_mss(
|
|
74
|
+
host: str,
|
|
75
|
+
port: int = 443,
|
|
76
|
+
*,
|
|
77
|
+
median_rtt_ms: Optional[float] = None,
|
|
78
|
+
traceroute_ok: bool = True,
|
|
79
|
+
) -> Dict:
|
|
80
|
+
MIN_MSS = 536
|
|
81
|
+
MAX_MSS = 1460
|
|
82
|
+
|
|
83
|
+
if SCAPY_AVAILABLE:
|
|
84
|
+
try:
|
|
85
|
+
if _probe_mss(host, port, MIN_MSS):
|
|
86
|
+
low = MIN_MSS
|
|
87
|
+
high = MAX_MSS
|
|
88
|
+
best = MIN_MSS
|
|
89
|
+
|
|
90
|
+
while low <= high:
|
|
91
|
+
mid = (low + high) // 2
|
|
92
|
+
if _probe_mss(host, port, mid):
|
|
93
|
+
best = mid
|
|
94
|
+
low = mid + 1
|
|
95
|
+
else:
|
|
96
|
+
high = mid - 1
|
|
97
|
+
time.sleep(0.05)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"reachable": True,
|
|
101
|
+
"method": "tcp-mss",
|
|
102
|
+
"mss": best,
|
|
103
|
+
"confidence": "high",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
except PermissionError:
|
|
107
|
+
pass
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
est = _estimate_mss(median_rtt_ms, traceroute_ok)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"reachable": True,
|
|
115
|
+
"method": "estimated",
|
|
116
|
+
"mss": est["mss"],
|
|
117
|
+
"confidence": est["confidence"],
|
|
118
|
+
"reason": est["reason"],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def format_mss_result(result: Dict) -> None:
|
|
123
|
+
if not result.get("reachable"):
|
|
124
|
+
print("MSS unavailable.")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
print(f"TCP MSS: {result['mss']} bytes")
|
|
128
|
+
print(f"Method: {result['method'].upper()}")
|
|
129
|
+
print(f"Confidence: {result.get('confidence', 'unknown').upper()}")
|
|
130
|
+
|
|
131
|
+
if result.get("method") == "estimated":
|
|
132
|
+
print(f"Reason: {result.get('reason')}")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _ping_df(host: str, payload_size: int, timeout: int = 1) -> bool:
|
|
6
|
+
|
|
7
|
+
system = platform.system()
|
|
8
|
+
|
|
9
|
+
if system == "Windows":
|
|
10
|
+
cmd = [
|
|
11
|
+
"ping",
|
|
12
|
+
"-n", "1",
|
|
13
|
+
"-f",
|
|
14
|
+
"-l", str(payload_size),
|
|
15
|
+
host,
|
|
16
|
+
]
|
|
17
|
+
else:
|
|
18
|
+
cmd = [
|
|
19
|
+
"ping",
|
|
20
|
+
"-c", "1",
|
|
21
|
+
"-M", "do",
|
|
22
|
+
"-s", str(payload_size),
|
|
23
|
+
host,
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
cmd,
|
|
29
|
+
stdout=subprocess.DEVNULL,
|
|
30
|
+
stderr=subprocess.DEVNULL,
|
|
31
|
+
timeout=timeout,
|
|
32
|
+
)
|
|
33
|
+
return result.returncode == 0
|
|
34
|
+
except Exception:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def discover_mtu(host: str) -> dict:
|
|
39
|
+
low = 1200
|
|
40
|
+
high = 1472 # Ethernet MTU (1500)
|
|
41
|
+
best = None
|
|
42
|
+
|
|
43
|
+
while low <= high:
|
|
44
|
+
mid = (low + high) // 2
|
|
45
|
+
|
|
46
|
+
if _ping_df(host, mid):
|
|
47
|
+
best = mid
|
|
48
|
+
low = mid + 1
|
|
49
|
+
else:
|
|
50
|
+
high = mid - 1
|
|
51
|
+
|
|
52
|
+
if best is None:
|
|
53
|
+
return {
|
|
54
|
+
"reachable": False,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
mtu = best + 28 # add IP (20) + ICMP (8)
|
|
58
|
+
|
|
59
|
+
if mtu >= 1500:
|
|
60
|
+
path_type = "standard"
|
|
61
|
+
elif mtu >= 1400:
|
|
62
|
+
path_type = "pppoe / light tunneling"
|
|
63
|
+
else:
|
|
64
|
+
path_type = "vpn / heavy tunneling"
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"reachable": True,
|
|
68
|
+
"mtu": mtu,
|
|
69
|
+
"payload_size": best,
|
|
70
|
+
"path_type": path_type,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def format_mtu_result(result: dict) -> None:
|
|
75
|
+
if not result.get("reachable"):
|
|
76
|
+
print("MTU discovery failed (host unreachable or blocked).")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
print(f"Path MTU: {result['mtu']} bytes")
|
|
80
|
+
print(f"Payload Size: {result['payload_size']} bytes")
|
|
81
|
+
print(f"Inference: {result['path_type'].upper()}")
|
yougotmapped/utils/network.py
CHANGED
|
@@ -1,34 +1,95 @@
|
|
|
1
|
-
# utils/network.py
|
|
2
1
|
import requests
|
|
3
|
-
import ipaddress
|
|
4
2
|
import socket
|
|
3
|
+
import ipaddress
|
|
4
|
+
|
|
5
|
+
def is_private_ip(ip: str) -> bool:
|
|
6
|
+
try:
|
|
7
|
+
return ipaddress.ip_address(ip).is_private
|
|
8
|
+
except ValueError:
|
|
9
|
+
return False
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_valid_ip(value: str) -> bool:
|
|
13
|
+
try:
|
|
14
|
+
ipaddress.ip_address(value)
|
|
15
|
+
return True
|
|
16
|
+
except ValueError:
|
|
17
|
+
return False
|
|
5
18
|
|
|
19
|
+
def get_public_ip() -> str | None:
|
|
20
|
+
try:
|
|
21
|
+
r = requests.get("https://api.ipify.org", timeout=5)
|
|
22
|
+
r.raise_for_status()
|
|
23
|
+
return r.text.strip()
|
|
24
|
+
except Exception:
|
|
25
|
+
return None
|
|
6
26
|
|
|
7
|
-
def
|
|
27
|
+
def resolve_domain_to_ip(domain: str) -> str | None:
|
|
8
28
|
try:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return response.text
|
|
12
|
-
except requests.exceptions.RequestException as e:
|
|
13
|
-
print(f"Error fetching public IP: {e}")
|
|
29
|
+
return socket.gethostbyname(domain)
|
|
30
|
+
except socket.gaierror:
|
|
14
31
|
return None
|
|
15
32
|
|
|
16
33
|
|
|
17
|
-
def get_geolocation(
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
def get_geolocation(target: str) -> dict | None:
|
|
35
|
+
ip = target
|
|
36
|
+
|
|
37
|
+
if not is_valid_ip(target):
|
|
38
|
+
ip = resolve_domain_to_ip(target)
|
|
39
|
+
if not ip:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
if is_private_ip(ip):
|
|
43
|
+
return {
|
|
44
|
+
"ip": ip,
|
|
45
|
+
"private": True,
|
|
46
|
+
"note": "Private / non-routable address",
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
try:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
r = requests.get(
|
|
51
|
+
f"https://ipwho.is/{ip}",
|
|
52
|
+
timeout=6,
|
|
53
|
+
)
|
|
54
|
+
data = r.json()
|
|
55
|
+
except Exception:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
if not data.get("success", False):
|
|
26
59
|
return None
|
|
27
60
|
|
|
61
|
+
return {
|
|
62
|
+
"ip": ip,
|
|
63
|
+
"hostname": data.get("hostname"),
|
|
64
|
+
"org": data.get("connection", {}).get("isp"),
|
|
65
|
+
"country": data.get("country"),
|
|
66
|
+
"region": data.get("region"),
|
|
67
|
+
"city": data.get("city"),
|
|
68
|
+
"postal": data.get("postal"),
|
|
69
|
+
"latitude": data.get("latitude"),
|
|
70
|
+
"longitude": data.get("longitude"),
|
|
71
|
+
"timezone": data.get("timezone", {}).get("id"),
|
|
72
|
+
"raw": data,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_hop_location(ip: str) -> dict | None:
|
|
77
|
+
if is_private_ip(ip):
|
|
78
|
+
return None
|
|
28
79
|
|
|
29
|
-
def resolve_domain_to_ip(domain):
|
|
30
80
|
try:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
81
|
+
r = requests.get(
|
|
82
|
+
f"http://ip-api.com/json/{ip}?fields=status,lat,lon",
|
|
83
|
+
timeout=4,
|
|
84
|
+
)
|
|
85
|
+
data = r.json()
|
|
86
|
+
except Exception:
|
|
34
87
|
return None
|
|
88
|
+
|
|
89
|
+
if data.get("status") != "success":
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"latitude": data.get("lat"),
|
|
94
|
+
"longitude": data.get("lon"),
|
|
95
|
+
}
|