-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add QR scanner view and improve registration logic for QR callback #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -91,12 +91,20 @@ async def async_setup(hass: HomeAssistant, config: dict): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Set up the Siedle component.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hass.data.setdefault(DOMAIN, {}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Register QR callback API endpoint only once | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not any(isinstance(view, SiedleQRCallbackView) for view in hass.http.app.router._resources): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Register QR callback and scanner API endpoints only once | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| registered_names = {getattr(r, 'name', None) for r in hass.http.app.router._resources} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "api:siedle:qr_callback" not in registered_names: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _LOGGER.info("Registering Siedle QR callback view at /api/siedle/qr_callback") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hass.http.register_view(SiedleQRCallbackView()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _LOGGER.info("Siedle QR callback view already registered") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _LOGGER.debug("Siedle QR callback view already registered") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "api:siedle:qr_scanner" not in registered_names: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _LOGGER.info("Registering Siedle QR scanner view at /api/siedle/qr_scanner") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hass.http.register_view(SiedleQRScannerView()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _LOGGER.debug("Siedle QR scanner view already registered") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -709,6 +717,339 @@ async def _async_update_data(self): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise UpdateFailed(f"Error communicating with API: {err}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| QR_SCANNER_HTML = """<!DOCTYPE html> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <html lang="de"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <head> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
719
to
+722
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| QR_SCANNER_HTML = """<!DOCTYPE html> | |
| <html lang="de"> | |
| <head> | |
| # NOTE: The QR scanner HTML below is intentionally provided in German (lang="de") | |
| # because the Siedle integration primarily targets German-speaking users. | |
| # If this integration is extended for broader international use, this template | |
| # should be refactored to support multiple languages and/or localized strings. | |
| QR_SCANNER_HTML = """<!DOCTYPE html> | |
| <html lang="de"> | |
| <head> | |
| <!-- This page is intentionally in German; update if internationalization is added. --> |
Copilot
AI
Feb 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The updateStatus function directly injects the message parameter into innerHTML without sanitization. This is vulnerable to XSS if the message contains user-controlled data. While the current usage appears to only use hardcoded German strings, this creates a potential security risk if the function is ever called with dynamic data. Consider using textContent instead of innerHTML, or properly sanitize the input.
| statusDiv.innerHTML = showSpinner | |
| ? '<div class="spinner"></div>' + message | |
| : message; | |
| } | |
| function onScanSuccess(decodedText) { | |
| if (isScanning) return; | |
| isScanning = true; | |
| updateStatus('✅ QR-Code erfolgreich gescannt!', 'success'); | |
| html5QrCode.stop().then(function() { | |
| if (callbackUrl && configFlowId) { | |
| updateStatus('🔄 Weiterleitung zu Home Assistant...', 'info', true); | |
| var encodedResult = encodeURIComponent(decodedText); | |
| var redirectUrl = callbackUrl + '?config_flow_id=' + configFlowId + '&result=' + encodedResult; | |
| setTimeout(function() { window.location.href = redirectUrl; }, 1000); | |
| } else { | |
| updateStatus('❌ Fehlende Parameter: callback_url oder config_flow_id', 'error'); | |
| // Clear any existing content safely | |
| statusDiv.innerHTML = ''; | |
| // Optionally add a spinner element | |
| if (showSpinner) { | |
| var spinner = document.createElement('div'); | |
| spinner.className = 'spinner'; | |
| statusDiv.appendChild(spinner); | |
| } | |
| // Add the status message as plain text to avoid XSS | |
| statusDiv.appendChild(document.createTextNode(message)); | |
| } | |
| function onScanSuccess(decodedText) { | |
| if (isScanning) return; | |
| isScanning = true; | |
| updateStatus('✅ QR-Code erfolgreich gescannt!', 'success'); | |
| html5QrCode.stop().then(function() { | |
| if (callbackUrl && configFlowId) { | |
| updateStatus('🔄 Weiterleitung zu Home Assistant...', 'info', true); | |
| var encodedResult = encodeURIComponent(decodedText); | |
| var redirectUrl = callbackUrl + '?config_flow_id=' + configFlowId + '&result=' + encodedResult; | |
| setTimeout(function() { window.location.href = redirectUrl; }, 1000); | |
| } else { | |
| updateStatus('❌ Fehlende Parameter: callback_url oder config_flow_id', 'error'); |
Copilot
AI
Feb 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message directly exposes the exception details to the user. This could leak sensitive information about the internal implementation. Consider using a generic error message for the user while logging the detailed error server-side only.
| showCameraError('❌ Fehler beim Zugriff auf die Kamera: ' + err2); | |
| showCameraError('❌ Fehler beim Zugriff auf die Kamera.'); |
Copilot
AI
Feb 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This view is marked as requires_auth = False, which means anyone can access the QR scanner page without authentication. While this might be intentional for the configuration flow, it could potentially be abused. Consider whether authentication should be required, or at least document why it's disabled. If the page needs to be accessible during initial setup before authentication is configured, ensure that the config_flow_id provides sufficient security.
| requires_auth = False | |
| async def get(self, request): | |
| """Serve the QR scanner HTML page.""" | |
| from aiohttp import web | |
| config_flow_id = request.query.get("config_flow_id", "") | |
| # Authentication is intentionally disabled for this view because it is | |
| # used during the initial configuration flow, before Home Assistant | |
| # authentication may be fully set up. Access is scoped by the opaque | |
| # config_flow_id query parameter, which is generated by Home Assistant | |
| # and should be hard to guess. | |
| requires_auth = False | |
| async def get(self, request): | |
| """Serve the QR scanner HTML page.""" | |
| from aiohttp import web | |
| config_flow_id = request.query.get("config_flow_id") | |
| if not config_flow_id: | |
| _LOGGER.warning( | |
| "QR scanner view requested without config_flow_id; rejecting request" | |
| ) | |
| return web.Response(text="Missing config_flow_id", status=400) |
Uh oh!
There was an error while loading. Please reload this page.