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:
claudi 2026-01-28 11:33:37 +01:00
parent 6bef2f6119
commit 86034358b7
6 changed files with 529 additions and 33 deletions

View file

@ -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.

View 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