PORTify, a firewall passive port knocking by listening to Spotify tracks#1
PORTify, a firewall passive port knocking by listening to Spotify tracks#1LavaTime wants to merge 14 commits intoinitial_commitfrom
Conversation
…clarity, Use Pydantic settings instead of dotenv
…e, we will create a block rule when closed, and remove it when openining, thus managing only already opened ports
|
@jules Could you perform a Code Review on the PR? |
|
/gemini review |
1 similar comment
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an interesting project, PORTify, which uses Spotify's currently playing track to control firewall ports. The overall structure is good, leveraging Pydantic for settings and splitting logic into modules. However, I've identified several critical and high-severity issues that need to be addressed. The most critical issue is in libs/firewall.py, where the method of calling shell commands is unreliable and will likely fail on Windows. There are also several high-severity issues related to missing error handling and unsafe data access that could cause the application to crash unexpectedly. I've provided detailed comments and suggestions to fix these problems and improve the robustness and maintainability of the code.
| command_to_run: List[str] = cls.LINUX_FIREWALL_RULE_TEMPLATE.substitute( | ||
| maybe_delete=" delete", port_number=port | ||
| ).split() | ||
| print(f"Closing port: {port}") | ||
| try: | ||
| subprocess.run(command_to_run, check=False, capture_output=True) | ||
| except subprocess.CalledProcessError as e: | ||
| print(f"Failed closing port: {port} Crashing aggresively") | ||
| raise e |
There was a problem hiding this comment.
There are two critical issues with how subprocess.run is used throughout this file:
string.split()is not safe for parsing shell commands. It will fail for commands with quoted arguments containing spaces (like the PowerShell commands). You should useshlex.split()from theshlexmodule for robust parsing.check=Falsewithexcept CalledProcessErroris incorrect. Whencheck=False,subprocess.runwill not raise aCalledProcessErroron a non-zero exit code, making yourexceptblocks unreachable. You must usecheck=Trueto catch command execution errors.
These issues make the firewall manipulation logic unreliable and prone to silent failures. This needs to be fixed for all subprocess.run calls in this file.
| command_to_run: List[str] = cls.LINUX_FIREWALL_RULE_TEMPLATE.substitute( | |
| maybe_delete=" delete", port_number=port | |
| ).split() | |
| print(f"Closing port: {port}") | |
| try: | |
| subprocess.run(command_to_run, check=False, capture_output=True) | |
| except subprocess.CalledProcessError as e: | |
| print(f"Failed closing port: {port} Crashing aggresively") | |
| raise e | |
| command_to_run: List[str] = shlex.split(cls.LINUX_FIREWALL_RULE_TEMPLATE.substitute( | |
| maybe_delete=" delete", port_number=port | |
| )) | |
| print(f"Closing port: {port}") | |
| try: | |
| subprocess.run(command_to_run, check=True, capture_output=True) | |
| except subprocess.CalledProcessError as e: | |
| print(f"Failed closing port: {port} Crashing aggressively") | |
| raise e |
| current_playing_track_id: Optional[str] = get_current_playing_track_id( | ||
| spotify_client=spotify_client | ||
| ) | ||
| if current_playing_track_id == settings.SECRET_OPEN_TRACK_ID: | ||
| print("User is playing the secret open track - Opening firewall's ports") | ||
| open_firewall_ports(firewall=firewall, ports=settings.PORTS_TO_MANAGE) | ||
| time_to_remove_rules = updated_closing_ports_countdown( | ||
| timeout=settings.OPEN_PORTS_TIMEOUT_MINUTES | ||
| ) | ||
| elif current_playing_track_id == settings.SECRET_CLOSE_TRACK_ID: | ||
| print("User is playing the secret close track - Closing firewall's ports") | ||
| close_firewall_ports(firewall=firewall, ports=settings.PORTS_TO_MANAGE) | ||
| else: | ||
| maybe_close_ports_on_timeout( | ||
| firewall=firewall, | ||
| ports=settings.PORTS_TO_MANAGE, | ||
| time_to_remove_rules=time_to_remove_rules, | ||
| ) |
There was a problem hiding this comment.
The main loop does not handle exceptions that might be raised from get_current_playing_track_id (e.g., network issues, API changes). An unhandled exception here would crash the entire daemon. It's crucial to wrap the logic inside the while loop with a try...except block to gracefully handle errors and ensure the service remains running.
try:
current_playing_track_id: Optional[str] = get_current_playing_track_id(
spotify_client=spotify_client
)
if current_playing_track_id == settings.SECRET_OPEN_TRACK_ID:
print("User is playing the secret open track - Opening firewall's ports")
open_firewall_ports(firewall=firewall, ports=settings.PORTS_TO_MANAGE)
time_to_remove_rules = updated_closing_ports_countdown(
timeout=settings.OPEN_PORTS_TIMEOUT_MINUTES
)
elif current_playing_track_id == settings.SECRET_CLOSE_TRACK_ID:
print("User is playing the secret close track - Closing firewall's ports")
close_firewall_ports(firewall=firewall, ports=settings.PORTS_TO_MANAGE)
else:
maybe_close_ports_on_timeout(
firewall=firewall,
ports=settings.PORTS_TO_MANAGE,
time_to_remove_rules=time_to_remove_rules,
)
except Exception as e:
# It's better to use a logger here
print(f"An unexpected error occurred: {e}. Continuing...")| user_current_playing_track: Dict[str, Any] = ( | ||
| spotify_client.current_user_playing_track() | ||
| ) | ||
| is_playing: bool = user_current_playing_track["is_playing"] | ||
| print(f"is_playing: {is_playing}") | ||
| if not is_playing: | ||
| return None | ||
| current_playing_track_id: str = user_current_playing_track["item"]["id"] | ||
| print(f"current_playing_track_id: {current_playing_track_id}") | ||
| return current_playing_track_id |
There was a problem hiding this comment.
This function is not safely handling the response from spotify_client.current_user_playing_track(). According to the spotipy documentation, this method can return None if no device is active or nothing is playing. Also, the item object can be None (e.g., during an ad) or may not have an id key. The current implementation will crash with a TypeError or KeyError in these common scenarios. You should add checks for None and use safe dictionary access with .get().
| user_current_playing_track: Dict[str, Any] = ( | |
| spotify_client.current_user_playing_track() | |
| ) | |
| is_playing: bool = user_current_playing_track["is_playing"] | |
| print(f"is_playing: {is_playing}") | |
| if not is_playing: | |
| return None | |
| current_playing_track_id: str = user_current_playing_track["item"]["id"] | |
| print(f"current_playing_track_id: {current_playing_track_id}") | |
| return current_playing_track_id | |
| user_current_playing_track: Optional[Dict[str, Any]] = ( | |
| spotify_client.current_user_playing_track() | |
| ) | |
| if not user_current_playing_track or not user_current_playing_track.get("is_playing"): | |
| return None | |
| item = user_current_playing_track.get("item") | |
| if not item: | |
| return None | |
| current_playing_track_id = item.get("id") | |
| if current_playing_track_id: | |
| print(f"current_playing_track_id: {current_playing_track_id}") | |
| return current_playing_track_id |
| @@ -0,0 +1,5 @@ | |||
| # Using setuptools | |||
| ```cmd | |||
| cd PORTify | |||
There was a problem hiding this comment.
| firewall: type[Firewall], ports: frozenset[int], time_to_remove_rules: datetime | ||
| ) -> None: | ||
| if datetime.now() > time_to_remove_rules: | ||
| print("Timeout has reached, closing ports!") |
There was a problem hiding this comment.
Throughout the application, print() is used for output. For a daemon/service, it's highly recommended to use a configurable logging framework (like Python's built-in logging module). This allows for better control over verbosity, log formatting, and directing output to files or system logs instead of just stdout. This applies to other print statements in this file as well (e.g., lines 66, 72).
|
|
||
|
|
||
| class Firewall: | ||
| pass |
|
|
||
| class LinuxFirewall(Firewall): | ||
| LINUX_FIREWALL_RULE_TEMPLATE: Final[Template] = Template( | ||
| "sudo ufw$maybe_delete allow $port_number comment PORTify_automaticly_managed_rule:_allow_port_$port_number" |
There was a problem hiding this comment.
There is a typo in the rule comment: "automaticly" should be "automatically". This typo appears in all firewall rule templates in this file (lines 51, 54, 57). It's best to correct it everywhere for consistency.
| "sudo ufw$maybe_delete allow $port_number comment PORTify_automaticly_managed_rule:_allow_port_$port_number" | |
| "sudo ufw$maybe_delete allow $port_number comment PORTify_automatically_managed_rule:_allow_port_$port_number" |
| @classmethod | ||
| def open_port(cls, port: int) -> None: | ||
| if cls.check_rule_exists(port=port): | ||
| print("Rule exists, openinig port") |
| spotify_client.current_user_playing_track() | ||
| ) | ||
| is_playing: bool = user_current_playing_track["is_playing"] | ||
| print(f"is_playing: {is_playing}") |
| "pydantic_settings", | ||
| "setuptools", | ||
| ] | ||
| requires-python = ">3.8" |
There was a problem hiding this comment.
No description provided.