Enhance HostsManager to prevent modification and movement of default system entries; add is_default_entry method to HostEntry and update sorting methods to prioritize default entries.
This commit is contained in:
parent
8c1cd2047e
commit
3084650c27
6 changed files with 122 additions and 45 deletions
|
@ -181,6 +181,11 @@ class HostsManager:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
entry = hosts_file.entries[index]
|
entry = hosts_file.entries[index]
|
||||||
|
|
||||||
|
# Prevent modification of default system entries
|
||||||
|
if entry.is_default_entry():
|
||||||
|
return False, "Cannot modify default system entries"
|
||||||
|
|
||||||
old_state = "active" if entry.is_active else "inactive"
|
old_state = "active" if entry.is_active else "inactive"
|
||||||
entry.is_active = not entry.is_active
|
entry.is_active = not entry.is_active
|
||||||
new_state = "active" if entry.is_active else "inactive"
|
new_state = "active" if entry.is_active else "inactive"
|
||||||
|
@ -207,6 +212,13 @@ class HostsManager:
|
||||||
return False, "Cannot move entry up"
|
return False, "Cannot move entry up"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
entry = hosts_file.entries[index]
|
||||||
|
target_entry = hosts_file.entries[index - 1]
|
||||||
|
|
||||||
|
# Prevent moving default system entries or moving entries above default entries
|
||||||
|
if entry.is_default_entry() or target_entry.is_default_entry():
|
||||||
|
return False, "Cannot move default system entries"
|
||||||
|
|
||||||
# Swap with previous entry
|
# Swap with previous entry
|
||||||
hosts_file.entries[index], hosts_file.entries[index - 1] = \
|
hosts_file.entries[index], hosts_file.entries[index - 1] = \
|
||||||
hosts_file.entries[index - 1], hosts_file.entries[index]
|
hosts_file.entries[index - 1], hosts_file.entries[index]
|
||||||
|
@ -232,6 +244,13 @@ class HostsManager:
|
||||||
return False, "Cannot move entry down"
|
return False, "Cannot move entry down"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
entry = hosts_file.entries[index]
|
||||||
|
target_entry = hosts_file.entries[index + 1]
|
||||||
|
|
||||||
|
# Prevent moving default system entries or moving entries below default entries
|
||||||
|
if entry.is_default_entry() or target_entry.is_default_entry():
|
||||||
|
return False, "Cannot move default system entries"
|
||||||
|
|
||||||
# Swap with next entry
|
# Swap with next entry
|
||||||
hosts_file.entries[index], hosts_file.entries[index + 1] = \
|
hosts_file.entries[index], hosts_file.entries[index + 1] = \
|
||||||
hosts_file.entries[index + 1], hosts_file.entries[index]
|
hosts_file.entries[index + 1], hosts_file.entries[index]
|
||||||
|
@ -262,6 +281,12 @@ class HostsManager:
|
||||||
return False, "Invalid entry index"
|
return False, "Invalid entry index"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
entry = hosts_file.entries[index]
|
||||||
|
|
||||||
|
# Prevent modification of default system entries
|
||||||
|
if entry.is_default_entry():
|
||||||
|
return False, "Cannot modify default system entries"
|
||||||
|
|
||||||
# Create new entry to validate
|
# Create new entry to validate
|
||||||
new_entry = HostEntry(
|
new_entry = HostEntry(
|
||||||
ip_address=ip_address,
|
ip_address=ip_address,
|
||||||
|
|
|
@ -33,6 +33,28 @@ class HostEntry:
|
||||||
"""Validate the entry after initialization."""
|
"""Validate the entry after initialization."""
|
||||||
self.validate()
|
self.validate()
|
||||||
|
|
||||||
|
def is_default_entry(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if this entry is a system default entry.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if this is a default system entry (localhost, broadcasthost, ::1)
|
||||||
|
"""
|
||||||
|
if not self.hostnames:
|
||||||
|
return False
|
||||||
|
|
||||||
|
canonical_hostname = self.hostnames[0]
|
||||||
|
default_entries = [
|
||||||
|
{"ip": "127.0.0.1", "hostname": "localhost"},
|
||||||
|
{"ip": "255.255.255.255", "hostname": "broadcasthost"},
|
||||||
|
{"ip": "::1", "hostname": "localhost"},
|
||||||
|
]
|
||||||
|
|
||||||
|
for entry in default_entries:
|
||||||
|
if entry["ip"] == self.ip_address and entry["hostname"] == canonical_hostname:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def validate(self) -> None:
|
def validate(self) -> None:
|
||||||
"""
|
"""
|
||||||
Validate the host entry data.
|
Validate the host entry data.
|
||||||
|
@ -176,13 +198,42 @@ class HostsFile:
|
||||||
"""Get all inactive entries."""
|
"""Get all inactive entries."""
|
||||||
return [entry for entry in self.entries if not entry.is_active]
|
return [entry for entry in self.entries if not entry.is_active]
|
||||||
|
|
||||||
def sort_by_ip(self) -> None:
|
def sort_by_ip(self, ascending: bool = True) -> None:
|
||||||
"""Sort entries by IP address."""
|
"""
|
||||||
self.entries.sort(key=lambda entry: ipaddress.ip_address(entry.ip_address))
|
Sort entries by IP address, keeping default entries on top.
|
||||||
|
|
||||||
def sort_by_hostname(self) -> None:
|
Args:
|
||||||
"""Sort entries by first hostname."""
|
ascending: Sort in ascending order if True, descending if False
|
||||||
self.entries.sort(key=lambda entry: entry.hostnames[0].lower())
|
"""
|
||||||
|
def sort_key(entry):
|
||||||
|
try:
|
||||||
|
ip_str = entry.ip_address.lstrip('# ')
|
||||||
|
ip_obj = ipaddress.ip_address(ip_str)
|
||||||
|
# Default entries always come first (priority 0), others get priority 1
|
||||||
|
priority = 0 if entry.is_default_entry() else 1
|
||||||
|
# Create a tuple for sorting: (priority, version, ip_int)
|
||||||
|
return (priority, ip_obj.version, int(ip_obj))
|
||||||
|
except ValueError:
|
||||||
|
# If IP parsing fails, use string comparison with high sort priority
|
||||||
|
priority = 0 if entry.is_default_entry() else 1
|
||||||
|
return (priority, 999, entry.ip_address)
|
||||||
|
|
||||||
|
self.entries.sort(key=sort_key, reverse=not ascending)
|
||||||
|
|
||||||
|
def sort_by_hostname(self, ascending: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Sort entries by first hostname, keeping default entries on top.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ascending: Sort in ascending order if True, descending if False
|
||||||
|
"""
|
||||||
|
def sort_key(entry):
|
||||||
|
# Default entries always come first (priority 0), others get priority 1
|
||||||
|
priority = 0 if entry.is_default_entry() else 1
|
||||||
|
hostname = (entry.hostnames[0] if entry.hostnames else "").lower()
|
||||||
|
return (priority, hostname)
|
||||||
|
|
||||||
|
self.entries.sort(key=sort_key, reverse=not ascending)
|
||||||
|
|
||||||
def find_entries_by_hostname(self, hostname: str) -> List[int]:
|
def find_entries_by_hostname(self, hostname: str) -> List[int]:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -240,8 +240,17 @@ class HostsManagerApp(App):
|
||||||
# Get the canonical hostname (first hostname)
|
# Get the canonical hostname (first hostname)
|
||||||
canonical_hostname = entry.hostnames[0] if entry.hostnames else ""
|
canonical_hostname = entry.hostnames[0] if entry.hostnames else ""
|
||||||
|
|
||||||
# Add row with styling based on active status
|
# Check if this is a default system entry
|
||||||
if entry.is_active:
|
is_default = entry.is_default_entry()
|
||||||
|
|
||||||
|
# Add row with styling based on active status and default entry status
|
||||||
|
if is_default:
|
||||||
|
# Default entries are always shown in dim grey regardless of active status
|
||||||
|
active_text = Text("✓" if entry.is_active else "", style="dim white")
|
||||||
|
ip_text = Text(entry.ip_address, style="dim white")
|
||||||
|
hostname_text = Text(canonical_hostname, style="dim white")
|
||||||
|
table.add_row(active_text, ip_text, hostname_text)
|
||||||
|
elif entry.is_active:
|
||||||
# Active entries in green with checkmark
|
# Active entries in green with checkmark
|
||||||
active_text = Text("✓", style="bold green")
|
active_text = Text("✓", style="bold green")
|
||||||
ip_text = Text(entry.ip_address, style="bold green")
|
ip_text = Text(entry.ip_address, style="bold green")
|
||||||
|
@ -304,6 +313,12 @@ class HostsManagerApp(App):
|
||||||
f"Status: {'Active' if entry.is_active else 'Inactive'}",
|
f"Status: {'Active' if entry.is_active else 'Inactive'}",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Add notice for default system entries
|
||||||
|
if entry.is_default_entry():
|
||||||
|
details_lines.append("")
|
||||||
|
details_lines.append("⚠️ SYSTEM DEFAULT ENTRY")
|
||||||
|
details_lines.append("This is a default system entry and cannot be modified.")
|
||||||
|
|
||||||
if entry.comment:
|
if entry.comment:
|
||||||
details_lines.append(f"Comment: {entry.comment}")
|
details_lines.append(f"Comment: {entry.comment}")
|
||||||
|
|
||||||
|
@ -379,20 +394,8 @@ class HostsManagerApp(App):
|
||||||
self.sort_column = "ip"
|
self.sort_column = "ip"
|
||||||
self.sort_ascending = True
|
self.sort_ascending = True
|
||||||
|
|
||||||
# Sort the entries
|
# Sort the entries using the new method that keeps defaults on top
|
||||||
import ipaddress
|
self.hosts_file.sort_by_ip(self.sort_ascending)
|
||||||
def ip_sort_key(entry):
|
|
||||||
try:
|
|
||||||
ip_str = entry.ip_address.lstrip('# ')
|
|
||||||
ip_obj = ipaddress.ip_address(ip_str)
|
|
||||||
# Create a tuple for sorting: (version, ip_int)
|
|
||||||
# This ensures IPv4 comes before IPv6, and within each version they're sorted numerically
|
|
||||||
return (ip_obj.version, int(ip_obj))
|
|
||||||
except ValueError:
|
|
||||||
# If IP parsing fails, use string comparison with high sort priority
|
|
||||||
return (999, entry.ip_address)
|
|
||||||
|
|
||||||
self.hosts_file.entries.sort(key=ip_sort_key, reverse=not self.sort_ascending)
|
|
||||||
self.populate_entries_table()
|
self.populate_entries_table()
|
||||||
|
|
||||||
direction = "ascending" if self.sort_ascending else "descending"
|
direction = "ascending" if self.sort_ascending else "descending"
|
||||||
|
@ -407,11 +410,8 @@ class HostsManagerApp(App):
|
||||||
self.sort_column = "hostname"
|
self.sort_column = "hostname"
|
||||||
self.sort_ascending = True
|
self.sort_ascending = True
|
||||||
|
|
||||||
# Sort the entries
|
# Sort the entries using the new method that keeps defaults on top
|
||||||
self.hosts_file.entries.sort(
|
self.hosts_file.sort_by_hostname(self.sort_ascending)
|
||||||
key=lambda entry: (entry.hostnames[0] if entry.hostnames else "").lower(),
|
|
||||||
reverse=not self.sort_ascending
|
|
||||||
)
|
|
||||||
self.populate_entries_table()
|
self.populate_entries_table()
|
||||||
|
|
||||||
direction = "ascending" if self.sort_ascending else "descending"
|
direction = "ascending" if self.sort_ascending else "descending"
|
||||||
|
|
|
@ -326,9 +326,9 @@ class TestHostsManagerApp:
|
||||||
|
|
||||||
app.action_sort_by_ip()
|
app.action_sort_by_ip()
|
||||||
|
|
||||||
# Check that entries are sorted
|
# Check that entries are sorted with default entries on top
|
||||||
assert app.hosts_file.entries[0].ip_address == "10.0.0.1"
|
assert app.hosts_file.entries[0].ip_address == "127.0.0.1" # Default entry first
|
||||||
assert app.hosts_file.entries[1].ip_address == "127.0.0.1"
|
assert app.hosts_file.entries[1].ip_address == "10.0.0.1" # Then sorted non-defaults
|
||||||
assert app.hosts_file.entries[2].ip_address == "192.168.1.1"
|
assert app.hosts_file.entries[2].ip_address == "192.168.1.1"
|
||||||
|
|
||||||
assert app.sort_column == "ip"
|
assert app.sort_column == "ip"
|
||||||
|
|
|
@ -321,7 +321,7 @@ class TestHostsManager:
|
||||||
manager.edit_mode = True
|
manager.edit_mode = True
|
||||||
|
|
||||||
hosts_file = HostsFile()
|
hosts_file = HostsFile()
|
||||||
entry = HostEntry("127.0.0.1", ["localhost"], is_active=True)
|
entry = HostEntry("192.168.1.1", ["router"], is_active=True) # Non-default entry
|
||||||
hosts_file.entries.append(entry)
|
hosts_file.entries.append(entry)
|
||||||
|
|
||||||
success, message = manager.toggle_entry(hosts_file, 0)
|
success, message = manager.toggle_entry(hosts_file, 0)
|
||||||
|
@ -363,7 +363,7 @@ class TestHostsManager:
|
||||||
manager.edit_mode = True
|
manager.edit_mode = True
|
||||||
|
|
||||||
hosts_file = HostsFile()
|
hosts_file = HostsFile()
|
||||||
entry1 = HostEntry("127.0.0.1", ["localhost"])
|
entry1 = HostEntry("10.0.0.1", ["test1"]) # Non-default entries
|
||||||
entry2 = HostEntry("192.168.1.1", ["router"])
|
entry2 = HostEntry("192.168.1.1", ["router"])
|
||||||
hosts_file.entries.extend([entry1, entry2])
|
hosts_file.entries.extend([entry1, entry2])
|
||||||
|
|
||||||
|
@ -372,7 +372,7 @@ class TestHostsManager:
|
||||||
assert success
|
assert success
|
||||||
assert "moved up" in message
|
assert "moved up" in message
|
||||||
assert hosts_file.entries[0].hostnames[0] == "router"
|
assert hosts_file.entries[0].hostnames[0] == "router"
|
||||||
assert hosts_file.entries[1].hostnames[0] == "localhost"
|
assert hosts_file.entries[1].hostnames[0] == "test1"
|
||||||
|
|
||||||
def test_move_entry_up_invalid_index(self):
|
def test_move_entry_up_invalid_index(self):
|
||||||
"""Test moving entry up with invalid index."""
|
"""Test moving entry up with invalid index."""
|
||||||
|
@ -396,7 +396,7 @@ class TestHostsManager:
|
||||||
manager.edit_mode = True
|
manager.edit_mode = True
|
||||||
|
|
||||||
hosts_file = HostsFile()
|
hosts_file = HostsFile()
|
||||||
entry1 = HostEntry("127.0.0.1", ["localhost"])
|
entry1 = HostEntry("10.0.0.1", ["test1"]) # Non-default entries
|
||||||
entry2 = HostEntry("192.168.1.1", ["router"])
|
entry2 = HostEntry("192.168.1.1", ["router"])
|
||||||
hosts_file.entries.extend([entry1, entry2])
|
hosts_file.entries.extend([entry1, entry2])
|
||||||
|
|
||||||
|
@ -405,7 +405,7 @@ class TestHostsManager:
|
||||||
assert success
|
assert success
|
||||||
assert "moved down" in message
|
assert "moved down" in message
|
||||||
assert hosts_file.entries[0].hostnames[0] == "router"
|
assert hosts_file.entries[0].hostnames[0] == "router"
|
||||||
assert hosts_file.entries[1].hostnames[0] == "localhost"
|
assert hosts_file.entries[1].hostnames[0] == "test1"
|
||||||
|
|
||||||
def test_move_entry_down_invalid_index(self):
|
def test_move_entry_down_invalid_index(self):
|
||||||
"""Test moving entry down with invalid index."""
|
"""Test moving entry down with invalid index."""
|
||||||
|
@ -429,7 +429,7 @@ class TestHostsManager:
|
||||||
manager.edit_mode = True
|
manager.edit_mode = True
|
||||||
|
|
||||||
hosts_file = HostsFile()
|
hosts_file = HostsFile()
|
||||||
entry = HostEntry("127.0.0.1", ["localhost"])
|
entry = HostEntry("10.0.0.1", ["test"]) # Non-default entry
|
||||||
hosts_file.entries.append(entry)
|
hosts_file.entries.append(entry)
|
||||||
|
|
||||||
success, message = manager.update_entry(
|
success, message = manager.update_entry(
|
||||||
|
@ -449,7 +449,7 @@ class TestHostsManager:
|
||||||
manager.edit_mode = True
|
manager.edit_mode = True
|
||||||
|
|
||||||
hosts_file = HostsFile()
|
hosts_file = HostsFile()
|
||||||
entry = HostEntry("127.0.0.1", ["localhost"])
|
entry = HostEntry("127.0.0.1", ["localhost"]) # Default entry - cannot be modified
|
||||||
hosts_file.entries.append(entry)
|
hosts_file.entries.append(entry)
|
||||||
|
|
||||||
success, message = manager.update_entry(
|
success, message = manager.update_entry(
|
||||||
|
@ -457,7 +457,7 @@ class TestHostsManager:
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not success
|
assert not success
|
||||||
assert "Invalid entry data" in message
|
assert "Cannot modify default system entries" in message
|
||||||
|
|
||||||
@patch('tempfile.NamedTemporaryFile')
|
@patch('tempfile.NamedTemporaryFile')
|
||||||
@patch('subprocess.run')
|
@patch('subprocess.run')
|
||||||
|
|
|
@ -224,10 +224,10 @@ class TestHostsFile:
|
||||||
assert inactive_entries[0] == inactive_entry
|
assert inactive_entries[0] == inactive_entry
|
||||||
|
|
||||||
def test_sort_by_ip(self):
|
def test_sort_by_ip(self):
|
||||||
"""Test sorting entries by IP address."""
|
"""Test sorting entries by IP address with default entries on top."""
|
||||||
hosts_file = HostsFile()
|
hosts_file = HostsFile()
|
||||||
entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["router"])
|
entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["router"])
|
||||||
entry2 = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"])
|
entry2 = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]) # Default entry
|
||||||
entry3 = HostEntry(ip_address="10.0.0.1", hostnames=["test"])
|
entry3 = HostEntry(ip_address="10.0.0.1", hostnames=["test"])
|
||||||
|
|
||||||
hosts_file.add_entry(entry1)
|
hosts_file.add_entry(entry1)
|
||||||
|
@ -236,8 +236,9 @@ class TestHostsFile:
|
||||||
|
|
||||||
hosts_file.sort_by_ip()
|
hosts_file.sort_by_ip()
|
||||||
|
|
||||||
assert hosts_file.entries[0].ip_address == "10.0.0.1"
|
# Default entries should come first, then sorted non-default entries
|
||||||
assert hosts_file.entries[1].ip_address == "127.0.0.1"
|
assert hosts_file.entries[0].ip_address == "127.0.0.1" # Default entry first
|
||||||
|
assert hosts_file.entries[1].ip_address == "10.0.0.1" # Then sorted non-defaults
|
||||||
assert hosts_file.entries[2].ip_address == "192.168.1.1"
|
assert hosts_file.entries[2].ip_address == "192.168.1.1"
|
||||||
|
|
||||||
def test_sort_by_hostname(self):
|
def test_sort_by_hostname(self):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue