Add URL whitelist enforcement for Kiosk-mode and enhance configuration management
- Introduced `allowed_urls` in configuration to specify whitelisted domains/patterns. - Implemented `RestrictedWebEngineView` to enforce URL restrictions in the web view. - Updated `MainWindow` to utilize the new restricted web view and added navigation toolbar. - Enhanced unit tests for configuration and restricted web view to cover new functionality.
This commit is contained in:
parent
6bef2f6119
commit
86034358b7
6 changed files with 529 additions and 33 deletions
|
|
@ -3,13 +3,14 @@
|
|||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import Qt, QUrl
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
from PySide6.QtWidgets import QMainWindow, QVBoxLayout, QWidget
|
||||
from PySide6.QtCore import QSize, Qt, QUrl
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import QMainWindow, QToolBar, QVBoxLayout, QWidget
|
||||
|
||||
from webdrop_bridge.config import Config
|
||||
from webdrop_bridge.core.drag_interceptor import DragInterceptor
|
||||
from webdrop_bridge.core.validator import PathValidator
|
||||
from webdrop_bridge.ui.restricted_web_view import RestrictedWebEngineView
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
|
|
@ -43,7 +44,10 @@ class MainWindow(QMainWindow):
|
|||
)
|
||||
|
||||
# Create web engine view
|
||||
self.web_view = QWebEngineView()
|
||||
self.web_view = RestrictedWebEngineView(config.allowed_urls)
|
||||
|
||||
# Create navigation toolbar (Kiosk-mode navigation)
|
||||
self._create_navigation_toolbar()
|
||||
|
||||
# Create drag interceptor
|
||||
self.drag_interceptor = DragInterceptor()
|
||||
|
|
@ -135,6 +139,54 @@ class MainWindow(QMainWindow):
|
|||
# Can be extended with logging or user notification
|
||||
pass
|
||||
|
||||
def _create_navigation_toolbar(self) -> None:
|
||||
"""Create navigation toolbar with Home, Back, Forward, Refresh buttons.
|
||||
|
||||
In Kiosk-mode, users can navigate history but cannot freely browse.
|
||||
"""
|
||||
toolbar = QToolBar("Navigation")
|
||||
toolbar.setMovable(False)
|
||||
toolbar.setIconSize(QSize(24, 24))
|
||||
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar)
|
||||
|
||||
# Back button
|
||||
back_action = self.web_view.pageAction(
|
||||
self.web_view.WebAction.Back
|
||||
)
|
||||
toolbar.addAction(back_action)
|
||||
|
||||
# Forward button
|
||||
forward_action = self.web_view.pageAction(
|
||||
self.web_view.WebAction.Forward
|
||||
)
|
||||
toolbar.addAction(forward_action)
|
||||
|
||||
# Separator
|
||||
toolbar.addSeparator()
|
||||
|
||||
# Home button
|
||||
home_action = toolbar.addAction("Home")
|
||||
home_action.triggered.connect(self._navigate_home)
|
||||
|
||||
# Refresh button
|
||||
refresh_action = self.web_view.pageAction(
|
||||
self.web_view.WebAction.Reload
|
||||
)
|
||||
toolbar.addAction(refresh_action)
|
||||
|
||||
def _navigate_home(self) -> None:
|
||||
"""Navigate to the home (start) URL."""
|
||||
home_url = self.config.webapp_url
|
||||
if home_url.startswith("http://") or home_url.startswith("https://"):
|
||||
self.web_view.load(QUrl(home_url))
|
||||
else:
|
||||
try:
|
||||
file_path = Path(home_url).resolve()
|
||||
file_url = file_path.as_uri()
|
||||
self.web_view.load(QUrl(file_url))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
"""Handle window close event.
|
||||
|
||||
|
|
|
|||
101
src/webdrop_bridge/ui/restricted_web_view.py
Normal file
101
src/webdrop_bridge/ui/restricted_web_view.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"""Restricted web view with URL whitelist enforcement for Kiosk-mode."""
|
||||
|
||||
import fnmatch
|
||||
from typing import List, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from PySide6.QtCore import QUrl
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
from PySide6.QtWebEngineCore import QWebEngineNavigationRequest
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
|
||||
|
||||
class RestrictedWebEngineView(QWebEngineView):
|
||||
"""Web view that enforces URL whitelist for Kiosk-mode security.
|
||||
|
||||
If allowed_urls is empty, no restrictions are applied.
|
||||
If allowed_urls is not empty, only matching URLs are loaded in the view.
|
||||
Non-matching URLs open in the system default browser.
|
||||
"""
|
||||
|
||||
def __init__(self, allowed_urls: Optional[List[str]] = None):
|
||||
"""Initialize the restricted web view.
|
||||
|
||||
Args:
|
||||
allowed_urls: List of allowed URL patterns (empty = no restriction)
|
||||
Patterns support wildcards: *.example.com, localhost, etc.
|
||||
"""
|
||||
super().__init__()
|
||||
self.allowed_urls = allowed_urls or []
|
||||
|
||||
# Connect to navigation request handler
|
||||
self.page().navigationRequested.connect(self._on_navigation_requested)
|
||||
|
||||
def _on_navigation_requested(
|
||||
self, request: QWebEngineNavigationRequest
|
||||
) -> None:
|
||||
"""Handle navigation requests and enforce URL whitelist.
|
||||
|
||||
Args:
|
||||
request: Navigation request to process
|
||||
"""
|
||||
url = request.url
|
||||
|
||||
# If no restrictions, allow all URLs
|
||||
if not self.allowed_urls:
|
||||
return
|
||||
|
||||
# Check if URL matches whitelist
|
||||
if self._is_url_allowed(url):
|
||||
# Allow the navigation (default behavior)
|
||||
return
|
||||
|
||||
# URL not whitelisted - open in system browser
|
||||
request.reject()
|
||||
QDesktopServices.openUrl(url)
|
||||
|
||||
def _is_url_allowed(self, url: QUrl) -> bool:
|
||||
"""Check if a URL matches the whitelist.
|
||||
|
||||
Supports:
|
||||
- Exact domain matches: example.com
|
||||
- Wildcard patterns: *.example.com
|
||||
- Localhost variations: localhost, 127.0.0.1
|
||||
- File URLs: file://...
|
||||
|
||||
Args:
|
||||
url: QUrl to check
|
||||
|
||||
Returns:
|
||||
True if URL is allowed, False otherwise
|
||||
"""
|
||||
url_str = url.toString()
|
||||
host = url.host()
|
||||
scheme = url.scheme()
|
||||
|
||||
# Allow file:// URLs (local webapp)
|
||||
if scheme == "file":
|
||||
return True
|
||||
|
||||
# If no whitelist, allow all URLs
|
||||
if not self.allowed_urls:
|
||||
return True
|
||||
|
||||
# Check against whitelist patterns
|
||||
for pattern in self.allowed_urls:
|
||||
# Check full URL pattern
|
||||
if fnmatch.fnmatch(url_str, pattern):
|
||||
return True
|
||||
|
||||
# Check host pattern (for http/https URLs)
|
||||
if scheme in ("http", "https") and host:
|
||||
if fnmatch.fnmatch(host, pattern):
|
||||
return True
|
||||
|
||||
# Also check with www prefix variants
|
||||
if fnmatch.fnmatch(f"www.{host}", pattern):
|
||||
return True
|
||||
if fnmatch.fnmatch(host.replace("www.", ""), pattern):
|
||||
return True
|
||||
|
||||
return False
|
||||
Loading…
Add table
Add a link
Reference in a new issue