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
yougotmapped/cli.py
CHANGED
|
@@ -1,174 +1,217 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
1
|
import argparse
|
|
4
|
-
import ipaddress
|
|
5
|
-
from datetime import datetime
|
|
6
2
|
from pathlib import Path
|
|
3
|
+
from datetime import datetime
|
|
7
4
|
|
|
8
5
|
from yougotmapped.utils.dependencies import check_dependencies
|
|
9
|
-
from yougotmapped.utils.network import get_public_ip, get_geolocation
|
|
10
|
-
from yougotmapped.utils.
|
|
11
|
-
from yougotmapped.utils.
|
|
12
|
-
from yougotmapped.utils.
|
|
13
|
-
from yougotmapped.utils.
|
|
6
|
+
from yougotmapped.utils.network import get_public_ip, get_geolocation
|
|
7
|
+
from yougotmapped.utils.ping import ping_target, format_ping_result
|
|
8
|
+
from yougotmapped.utils.jitter import jitter_test, format_jitter_result
|
|
9
|
+
from yougotmapped.utils.mss import discover_mss, format_mss_result
|
|
10
|
+
from yougotmapped.utils.bandwidth import estimate_bandwidth, format_bandwidth_result
|
|
11
|
+
from yougotmapped.utils.trace import run_traceroute, format_traceroute
|
|
14
12
|
from yougotmapped.utils.anonymity import detect_anonymity, format_anonymity_result
|
|
15
|
-
from yougotmapped.utils.
|
|
13
|
+
from yougotmapped.utils.mapping import (
|
|
14
|
+
plot_ip_location,
|
|
15
|
+
plot_multiple_ip_locations,
|
|
16
|
+
plot_traceroute_path,
|
|
17
|
+
save_map,
|
|
18
|
+
)
|
|
19
|
+
from yougotmapped.utils.output import write_output
|
|
16
20
|
|
|
21
|
+
def parse_args():
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
description="Geolocate IPs/domains, analyze network paths, and visualize routing."
|
|
24
|
+
)
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
parser
|
|
20
|
-
parser.add_argument('-i', '--ip', nargs='*', help="One or more IPs/domains separated by space")
|
|
21
|
-
parser.add_argument('-f', '--file', type=str, help="Path to a file containing IPs/domains (one per line)")
|
|
22
|
-
parser.add_argument('-p', '--ping', action='store_true', help="Ping each IP or domain and show latency")
|
|
23
|
-
parser.add_argument('-t', '--trace', action='store_true', help="Show traceroute to each IP or domain")
|
|
24
|
-
parser.add_argument('-c', '--hidecheck', action='store_true', help="Check if the IP is a Tor exit node or VPN")
|
|
25
|
-
parser.add_argument('-o', '--output', type=str, help="Specify output file or use format shorthand (e.g., f:csv, f:json, f:normal)")
|
|
26
|
-
parser.add_argument('--no-map', action='store_true', help="Do not generate a map")
|
|
27
|
-
parser.add_argument('--delete-map', action='store_true', help="Delete the map after generating")
|
|
28
|
-
args = parser.parse_args()
|
|
26
|
+
parser.add_argument("-i", "--ip", nargs="*", help="IP addresses or domains")
|
|
27
|
+
parser.add_argument("-f", "--file", help="File with IPs/domains (one per line)")
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
parser.add_argument("-p", "--ping", action="store_true", help="Ping test")
|
|
30
|
+
parser.add_argument("-j", "--jitter", action="store_true", help="Jitter analysis")
|
|
31
|
+
parser.add_argument("-m", "--mtu", action="store_true", help="MTU/MSS discovery")
|
|
32
|
+
parser.add_argument("-b", "--bandwidth", action="store_true", help="Bandwidth estimate")
|
|
33
|
+
parser.add_argument("-t", "--trace", action="store_true", help="Traceroute")
|
|
34
|
+
|
|
35
|
+
parser.add_argument("-c", "--hidecheck", action="store_true", help="Anonymity detection")
|
|
36
|
+
parser.add_argument("-a", "--all", action="store_true", help="Run all modules")
|
|
37
|
+
|
|
38
|
+
parser.add_argument("--no-map", action="store_true", help="Disable map output")
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-o",
|
|
41
|
+
"--output",
|
|
42
|
+
help="Output file or shorthand (f:json, f:csv, f:normal)",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return parser.parse_args()
|
|
31
46
|
|
|
32
|
-
API_TOKEN = get_api_token()
|
|
33
|
-
if not API_TOKEN:
|
|
34
|
-
return
|
|
35
47
|
|
|
36
|
-
|
|
48
|
+
def collect_targets(args) -> list[str]:
|
|
49
|
+
targets: list[str] = []
|
|
37
50
|
|
|
38
51
|
if args.ip:
|
|
39
52
|
targets.extend(args.ip)
|
|
40
53
|
|
|
41
54
|
if args.file:
|
|
42
55
|
try:
|
|
43
|
-
with open(args.file,
|
|
56
|
+
with open(args.file, "r") as f:
|
|
44
57
|
targets.extend(line.strip() for line in f if line.strip())
|
|
45
58
|
except FileNotFoundError:
|
|
46
59
|
print(f"File not found: {args.file}")
|
|
47
|
-
return
|
|
48
60
|
|
|
49
61
|
if not targets:
|
|
50
|
-
print("No
|
|
51
|
-
|
|
52
|
-
if
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
print("No input provided. Defaulting to public IP.")
|
|
63
|
+
public_ip = get_public_ip()
|
|
64
|
+
if public_ip:
|
|
65
|
+
targets.append(public_ip)
|
|
66
|
+
|
|
67
|
+
return targets
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_geo_rich(geo: dict) -> None:
|
|
71
|
+
raw = geo.get("raw", {})
|
|
72
|
+
connection = raw.get("connection", {})
|
|
73
|
+
timezone = raw.get("timezone", {})
|
|
74
|
+
|
|
75
|
+
print("\n[ GEOLOCATION ]")
|
|
76
|
+
print(f"IP: {raw.get('ip')}")
|
|
77
|
+
print(f"Type: {raw.get('type')}")
|
|
78
|
+
print(f"ASN: AS{connection.get('asn')}" if connection.get("asn") else "ASN: N/A")
|
|
79
|
+
print(f"ISP / Org: {connection.get('org') or connection.get('isp') or 'N/A'}")
|
|
80
|
+
print(f"Domain: {connection.get('domain') or 'N/A'}")
|
|
81
|
+
|
|
82
|
+
print("\n[ LOCATION ]")
|
|
83
|
+
print(f"Continent: {raw.get('continent')} ({raw.get('continent_code')})")
|
|
84
|
+
print(f"Country: {raw.get('country')} ({raw.get('country_code')})")
|
|
85
|
+
print(f"Region: {raw.get('region')} ({raw.get('region_code')})")
|
|
86
|
+
print(f"City: {raw.get('city')}")
|
|
87
|
+
print(f"Postal Code: {raw.get('postal') or ''}")
|
|
88
|
+
print(f"Latitude: {raw.get('latitude')}")
|
|
89
|
+
print(f"Longitude: {raw.get('longitude')}")
|
|
90
|
+
print(f"Capital: {raw.get('capital')}")
|
|
91
|
+
print(f"Borders: {raw.get('borders')}")
|
|
92
|
+
|
|
93
|
+
print("\n[ TIMEZONE ]")
|
|
94
|
+
print(f"Zone: {timezone.get('id')}")
|
|
95
|
+
print(f"Abbreviation: {timezone.get('abbr')}")
|
|
96
|
+
print(f"UTC Offset: {timezone.get('utc')}")
|
|
97
|
+
print(f"DST Active: {timezone.get('is_dst')}")
|
|
98
|
+
print(f"Local Time: {timezone.get('current_time')}")
|
|
56
99
|
|
|
57
|
-
|
|
58
|
-
|
|
100
|
+
|
|
101
|
+
def main():
|
|
102
|
+
args = parse_args()
|
|
103
|
+
check_dependencies()
|
|
104
|
+
|
|
105
|
+
if args.all:
|
|
106
|
+
args.ping = True
|
|
107
|
+
args.jitter = True
|
|
108
|
+
args.mtu = True
|
|
109
|
+
args.bandwidth = True
|
|
110
|
+
args.trace = True
|
|
111
|
+
args.hidecheck = True
|
|
112
|
+
|
|
113
|
+
targets = collect_targets(args)
|
|
114
|
+
if not targets:
|
|
115
|
+
print("No valid targets to process.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
results = []
|
|
119
|
+
geos_for_map = []
|
|
120
|
+
last_trace_result = None
|
|
59
121
|
|
|
60
122
|
for target in targets:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
123
|
+
print(f"\nTarget: {target}")
|
|
124
|
+
|
|
125
|
+
geo = get_geolocation(target)
|
|
126
|
+
if not geo:
|
|
127
|
+
print("Failed to retrieve geolocation data.")
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
results.append(geo)
|
|
131
|
+
geos_for_map.append(geo)
|
|
132
|
+
|
|
133
|
+
print_geo_rich(geo)
|
|
134
|
+
|
|
135
|
+
ping_result = None
|
|
136
|
+
trace_result = None
|
|
137
|
+
|
|
138
|
+
if args.ping:
|
|
139
|
+
print("\n[ PING ]")
|
|
140
|
+
ping_result = ping_target(target)
|
|
141
|
+
format_ping_result(ping_result)
|
|
142
|
+
geo["ping"] = ping_result
|
|
143
|
+
|
|
144
|
+
if not ping_result.get("reachable"):
|
|
145
|
+
print("Stopping analysis: host unreachable.")
|
|
71
146
|
continue
|
|
72
147
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
148
|
+
if args.jitter:
|
|
149
|
+
print("\n[ JITTER ]")
|
|
150
|
+
jitter_result = jitter_test(target)
|
|
151
|
+
format_jitter_result(jitter_result)
|
|
152
|
+
geo["jitter"] = jitter_result
|
|
153
|
+
|
|
154
|
+
if args.mtu:
|
|
155
|
+
print("\n[ MSS ]")
|
|
156
|
+
mss_result = discover_mss(target)
|
|
157
|
+
format_mss_result(mss_result)
|
|
158
|
+
geo["mss"] = mss_result
|
|
159
|
+
|
|
160
|
+
if args.bandwidth:
|
|
161
|
+
print("\n[ BANDWIDTH ]")
|
|
162
|
+
bandwidth_result = estimate_bandwidth(ping_result)
|
|
163
|
+
format_bandwidth_result(bandwidth_result)
|
|
164
|
+
geo["bandwidth"] = bandwidth_result
|
|
165
|
+
|
|
166
|
+
if args.trace:
|
|
167
|
+
print("\n[ TRACEROUTE ]")
|
|
168
|
+
trace_result = run_traceroute(target)
|
|
169
|
+
format_traceroute(trace_result)
|
|
170
|
+
geo["traceroute"] = trace_result
|
|
171
|
+
last_trace_result = trace_result
|
|
172
|
+
|
|
173
|
+
if args.hidecheck:
|
|
174
|
+
print("\n[ ANONYMITY ]")
|
|
175
|
+
anonymity = detect_anonymity(geo)
|
|
176
|
+
format_anonymity_result(anonymity)
|
|
177
|
+
geo["anonymity"] = anonymity
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
map_path = None
|
|
181
|
+
if geos_for_map and not args.no_map:
|
|
182
|
+
if len(geos_for_map) == 1:
|
|
183
|
+
m = plot_ip_location(geos_for_map[0])
|
|
184
|
+
if m and args.trace and last_trace_result:
|
|
185
|
+
plot_traceroute_path(last_trace_result, m)
|
|
102
186
|
else:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
map_path = Path("ip_geolocation_map.html").resolve()
|
|
108
|
-
log_path = None
|
|
109
|
-
|
|
110
|
-
if not args.no_map and geolocated:
|
|
111
|
-
if len(geolocated) == 1:
|
|
112
|
-
m = plot_ip_location(geolocated[0], color="red")
|
|
113
|
-
|
|
114
|
-
if args.trace and isinstance(trace_output, str):
|
|
115
|
-
lines = trace_output.strip().split("\n")
|
|
116
|
-
for line in reversed(lines):
|
|
117
|
-
if "(" in line and ")" in line:
|
|
118
|
-
last_ip = line.split()[1]
|
|
119
|
-
try:
|
|
120
|
-
ipaddress.ip_address(last_ip)
|
|
121
|
-
last_data = get_geolocation(last_ip, API_TOKEN)
|
|
122
|
-
if last_data:
|
|
123
|
-
plot_ip_location(last_data, color="blue", map_object=m)
|
|
124
|
-
except Exception:
|
|
125
|
-
pass
|
|
126
|
-
break
|
|
127
|
-
if m:
|
|
128
|
-
m.save(str(map_path))
|
|
129
|
-
map_saved = True
|
|
130
|
-
|
|
131
|
-
elif len(geolocated) > 1:
|
|
132
|
-
plot_multiple_ip_locations(geolocated)
|
|
133
|
-
map_saved = True
|
|
134
|
-
|
|
135
|
-
if args.delete_map:
|
|
136
|
-
try:
|
|
137
|
-
os.remove(map_path)
|
|
138
|
-
map_saved = False
|
|
139
|
-
except FileNotFoundError:
|
|
140
|
-
pass
|
|
187
|
+
m = plot_multiple_ip_locations(geos_for_map)
|
|
188
|
+
|
|
189
|
+
if m:
|
|
190
|
+
map_path = save_map(m, "ip_geolocation_map.html")
|
|
141
191
|
|
|
192
|
+
|
|
193
|
+
output_path = None
|
|
142
194
|
if args.output:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
date_str = now.strftime("%m-%d-%y")
|
|
146
|
-
time_str = now.strftime("%H-%M")
|
|
195
|
+
Path("logs").mkdir(exist_ok=True)
|
|
196
|
+
timestamp = datetime.now().strftime("%m-%d-%y--%H-%M")
|
|
147
197
|
|
|
148
198
|
if args.output.startswith("f:"):
|
|
149
|
-
|
|
150
|
-
ext =
|
|
151
|
-
|
|
152
|
-
filename = f"{
|
|
199
|
+
fmt = args.output[2:].lower()
|
|
200
|
+
ext = "json" if fmt == "json" else "csv" if fmt == "csv" else "txt"
|
|
201
|
+
name = targets[0].replace(":", "-").replace("/", "-")
|
|
202
|
+
filename = f"{name}--{timestamp}--YouGotMapped.{ext}"
|
|
153
203
|
else:
|
|
154
204
|
filename = args.output
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
print("\n----- OUTPUT SUMMARY -----")
|
|
166
|
-
if map_saved:
|
|
167
|
-
print(f"Map Location: {map_path}")
|
|
168
|
-
print(f"Map URL: file://{map_path}")
|
|
169
|
-
if log_path:
|
|
170
|
-
print(f"Log File: {log_path}")
|
|
171
|
-
print(f"Log URL: file://{log_path}")
|
|
205
|
+
fmt = filename.split(".")[-1].lower()
|
|
206
|
+
|
|
207
|
+
output_path = write_output(results, Path("logs") / filename, fmt=fmt)
|
|
208
|
+
|
|
209
|
+
if map_path or output_path:
|
|
210
|
+
print("\nOutput Summary")
|
|
211
|
+
if map_path:
|
|
212
|
+
print(f" Map: file://{map_path}")
|
|
213
|
+
if output_path:
|
|
214
|
+
print(f" Log: file://{output_path}")
|
|
172
215
|
|
|
173
216
|
|
|
174
217
|
if __name__ == "__main__":
|
yougotmapped/utils/anonymity.py
CHANGED
|
@@ -1,53 +1,87 @@
|
|
|
1
|
-
# utils/anonymity.py
|
|
2
|
-
import requests
|
|
3
|
-
import socket
|
|
1
|
+
# yougotmapped/utils/anonymity.py
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
import requests
|
|
4
|
+
from functools import lru_cache
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
"vpn", "m247", "nord", "express", "ovh", "digitalocean", "linode",
|
|
9
|
-
"datacamp", "host", "colo", "server", "vultr", "heficed", "leaseweb"
|
|
10
|
-
]
|
|
6
|
+
TOR_EXIT_LIST_URL = "https://check.torproject.org/torbulkexitlist"
|
|
11
7
|
|
|
8
|
+
VPN_KEYWORDS = {
|
|
9
|
+
"vpn", "proxy", "tor", "hosting", "cloud", "server", "colo",
|
|
10
|
+
"digitalocean", "linode", "ovh", "vultr", "hetzner", "leaseweb",
|
|
11
|
+
"m247", "nord", "express", "surfshark", "proton", "fastly",
|
|
12
|
+
"akamai", "cloudflare", "google", "amazon", "aws", "azure"
|
|
13
|
+
}
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
@lru_cache(maxsize=1)
|
|
16
|
+
def _get_tor_exit_nodes() -> set[str]:
|
|
14
17
|
try:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
+
resp = requests.get(TOR_EXIT_LIST_URL, timeout=10)
|
|
19
|
+
resp.raise_for_status()
|
|
20
|
+
return set(
|
|
21
|
+
line.strip()
|
|
22
|
+
for line in resp.text.splitlines()
|
|
23
|
+
if line and not line.startswith("#")
|
|
24
|
+
)
|
|
18
25
|
except Exception:
|
|
26
|
+
return set()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_tor_exit_node(ip: str) -> bool:
|
|
30
|
+
if not ip:
|
|
19
31
|
return False
|
|
32
|
+
return ip in _get_tor_exit_nodes()
|
|
20
33
|
|
|
34
|
+
def _contains_vpn_keyword(value: str | None) -> bool:
|
|
35
|
+
if not value:
|
|
36
|
+
return False
|
|
37
|
+
value = value.lower()
|
|
38
|
+
return any(keyword in value for keyword in VPN_KEYWORDS)
|
|
21
39
|
|
|
22
|
-
def detect_anonymity(ip_data):
|
|
23
|
-
org = ip_data.get("org", "").lower()
|
|
24
|
-
hostname = ip_data.get("hostname", "").lower()
|
|
40
|
+
def detect_anonymity(ip_data: dict) -> dict:
|
|
25
41
|
ip = ip_data.get("ip")
|
|
42
|
+
hostname = ip_data.get("hostname")
|
|
43
|
+
org = ip_data.get("org")
|
|
44
|
+
raw = ip_data.get("raw", {})
|
|
26
45
|
|
|
27
46
|
tor = is_tor_exit_node(ip)
|
|
28
|
-
vpn = any(keyword in org or keyword in hostname for keyword in VPN_KEYWORDS)
|
|
29
47
|
|
|
30
|
-
|
|
48
|
+
# ipwho.is connection hints
|
|
49
|
+
connection = raw.get("connection", {})
|
|
50
|
+
isp = connection.get("isp")
|
|
51
|
+
domain = connection.get("domain")
|
|
52
|
+
asn = connection.get("asn")
|
|
53
|
+
|
|
54
|
+
vpn = any([
|
|
55
|
+
_contains_vpn_keyword(org),
|
|
56
|
+
_contains_vpn_keyword(hostname),
|
|
57
|
+
_contains_vpn_keyword(isp),
|
|
58
|
+
_contains_vpn_keyword(domain),
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
return {
|
|
31
62
|
"ip": ip,
|
|
32
63
|
"tor": tor,
|
|
33
64
|
"vpn": vpn,
|
|
34
|
-
"org": org,
|
|
35
65
|
"hostname": hostname,
|
|
66
|
+
"org": org,
|
|
67
|
+
"isp": isp,
|
|
68
|
+
"asn": asn,
|
|
69
|
+
"confidence": _confidence_score(tor, vpn),
|
|
36
70
|
}
|
|
37
71
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
else
|
|
53
|
-
|
|
72
|
+
def _confidence_score(tor: bool, vpn: bool) -> str:
|
|
73
|
+
if tor:
|
|
74
|
+
return "very high"
|
|
75
|
+
if vpn:
|
|
76
|
+
return "high"
|
|
77
|
+
return "low"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def format_anonymity_result(result: dict) -> None:
|
|
81
|
+
print(f"IP: {result.get('ip')}")
|
|
82
|
+
print(f"Hostname: {result.get('hostname') or 'N/A'}")
|
|
83
|
+
print(f"Org / ISP: {result.get('org') or result.get('isp') or 'N/A'}")
|
|
84
|
+
|
|
85
|
+
print(f"Tor Exit Node: {'YES' if result['tor'] else 'No'}")
|
|
86
|
+
print(f"VPN / Proxy Suspected: {'YES' if result['vpn'] else 'No'}")
|
|
87
|
+
print(f"Confidence: {result.get('confidence', 'unknown').upper()}")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# yougotmapped/utils/bandwidth.py
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
DEFAULT_MSS_BYTES = 1460 # Ethernet MTU 1500 - IP/TCP headers
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def estimate_bandwidth(ping_result: dict, mss_bytes: int = DEFAULT_MSS_BYTES) -> dict:
|
|
10
|
+
if not ping_result or not ping_result.get("reachable"):
|
|
11
|
+
return {
|
|
12
|
+
"available": False,
|
|
13
|
+
"reason": "no_ping_data",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
rtt_ms = ping_result["rtt_ms"]["median"]
|
|
17
|
+
loss_percent = ping_result.get("packet_loss_percent", 0.0)
|
|
18
|
+
|
|
19
|
+
rtt_sec = rtt_ms / 1000.0
|
|
20
|
+
loss = loss_percent / 100.0
|
|
21
|
+
|
|
22
|
+
if rtt_sec <= 0:
|
|
23
|
+
return {
|
|
24
|
+
"available": False,
|
|
25
|
+
"reason": "invalid_rtt",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if loss == 0:
|
|
29
|
+
throughput_bps = (mss_bytes * 8) / rtt_sec
|
|
30
|
+
limiting_factor = "latency"
|
|
31
|
+
else:
|
|
32
|
+
throughput_bps = (mss_bytes * 8) / (rtt_sec * math.sqrt(loss))
|
|
33
|
+
limiting_factor = "packet_loss"
|
|
34
|
+
|
|
35
|
+
throughput_mbps = round(throughput_bps / 1_000_000, 2)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"available": True,
|
|
39
|
+
"estimated_mbps": throughput_mbps,
|
|
40
|
+
"rtt_ms": rtt_ms,
|
|
41
|
+
"packet_loss_percent": loss_percent,
|
|
42
|
+
"limiting_factor": limiting_factor,
|
|
43
|
+
"model": "tcp_mathis",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def format_bandwidth_result(result: dict) -> None:
|
|
48
|
+
if not result.get("available"):
|
|
49
|
+
print("Bandwidth estimation unavailable.")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
print(f"Estimated Throughput: ~{result['estimated_mbps']} Mbps")
|
|
53
|
+
print(f"Limiting Factor: {result['limiting_factor'].replace('_', ' ').title()}")
|
|
54
|
+
print("Model: TCP Mathis approximation")
|
|
@@ -1,33 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dependency Definitions
|
|
2
|
+
|
|
2
3
|
import importlib.util
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
REQUIRED_PACKAGES = {
|
|
9
|
+
"requests": "requests",
|
|
10
|
+
"folium": "folium",
|
|
11
|
+
"ping3": "ping3",
|
|
12
|
+
"scapy": "scapy"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_installed(module_name: str) -> bool:
|
|
17
|
+
return importlib.util.find_spec(module_name) is not None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _install_package(package_name: str) -> bool:
|
|
21
|
+
try:
|
|
22
|
+
subprocess.check_call(
|
|
23
|
+
[sys.executable, "-m", "pip", "install", package_name],
|
|
24
|
+
stdout=subprocess.DEVNULL,
|
|
25
|
+
stderr=subprocess.DEVNULL,
|
|
26
|
+
)
|
|
27
|
+
return True
|
|
28
|
+
except subprocess.CalledProcessError:
|
|
29
|
+
return False
|
|
14
30
|
|
|
31
|
+
|
|
32
|
+
def check_dependencies(interactive: bool = True) -> None:
|
|
15
33
|
print("\nChecking dependencies:\n")
|
|
16
34
|
|
|
17
|
-
for
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
for package, module in REQUIRED_PACKAGES.items():
|
|
36
|
+
if _is_installed(module):
|
|
37
|
+
print(f" [OK] {package} found")
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
print(f" [MISSING] {package} not found")
|
|
41
|
+
|
|
42
|
+
if not interactive:
|
|
43
|
+
print(f" Install '{package}' manually and try again.")
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
choice = input(f" Install '{package}' now? (yes/no): ").strip().lower()
|
|
47
|
+
if choice not in ("yes", "y"):
|
|
48
|
+
print(f" Cannot continue without '{package}'. Exiting.")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
if _install_package(package):
|
|
52
|
+
print(f" '{package}' successfully installed")
|
|
21
53
|
else:
|
|
22
|
-
print(f"
|
|
23
|
-
|
|
24
|
-
if choice in ['yes', 'y']:
|
|
25
|
-
try:
|
|
26
|
-
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
|
|
27
|
-
print(f" '{package_name}' successfully installed\n")
|
|
28
|
-
except subprocess.CalledProcessError:
|
|
29
|
-
print(f" Failed to install '{package_name}'. Please install manually.")
|
|
30
|
-
sys.exit(1)
|
|
31
|
-
else:
|
|
32
|
-
print(f" Cannot continue without '{package_name}'. Exiting.")
|
|
33
|
-
sys.exit(1)
|
|
54
|
+
print(f" Failed to install '{package}'. Install manually.")
|
|
55
|
+
sys.exit(1)
|