Files
iOS/Sources/App/Frontend/ExternalMessageBus/SafeScriptMessageHandler.swift
Bruno Pantaleão Gonçalves 92214b873d Use explicit URL.port and remove fallback in SafeScriptMessageHandler (#4483)
<!-- Thank you for submitting a Pull Request and helping to improve Home
Assistant. Please complete the following sections to help the processing
and review of your changes. Please do not delete anything from this
template. -->

## Summary
<!-- Provide a brief summary of the changes you have made and most
importantly what they aim to achieve -->

Remove the portWithFallback helper and update port comparisons to use
URL.port directly. baseIsEqual now compares ports via url.port (no
80/443 fallback), and SafeScriptMessageHandler passes url.port ?? 0 when
building origin keys (security origin uses port 0 when unspecified).
This simplifies port handling and avoids implicit defaulting to standard
ports.

## Screenshots
<!-- If this is a user-facing change not in the frontend, please include
screenshots in light and dark mode. -->

## Link to pull request in Documentation repository
<!-- Pull requests that add, change or remove functionality must have a
corresponding pull request in the Companion App Documentation repository
(https://github.com/home-assistant/companion.home-assistant). Please add
the number of this pull request after the "#" -->
Documentation: home-assistant/companion.home-assistant#

## Any other notes
<!-- If there is any other information of note, like if this Pull
Request is part of a bigger change, please include it here. -->
2026-04-07 16:47:39 +02:00

79 lines
2.5 KiB
Swift

import Foundation
import Shared
import WebKit
/// Use to avoid holding webview alive when adding WKScriptMessageHandler
final class SafeScriptMessageHandler: NSObject, WKScriptMessageHandler {
let server: Server
weak var delegate: WKScriptMessageHandler?
init(server: Server, delegate: WKScriptMessageHandler) {
self.server = server
self.delegate = delegate
super.init()
}
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
// Only the top-level document on an allowed server origin may talk to the native bridge.
guard shouldAllowMessage(
isMainFrame: message.frameInfo.isMainFrame,
scheme: message.frameInfo.securityOrigin.protocol,
host: message.frameInfo.securityOrigin.host,
port: message.frameInfo.securityOrigin.port // Security origin port is 0 whenever not specified
) else {
return
}
delegate?.userContentController(
userContentController, didReceive: message
)
}
func shouldAllowMessage(isMainFrame: Bool, scheme: String, host: String, port: Int) -> Bool {
guard isMainFrame, let origin = originKey(scheme: scheme, host: host, port: port) else {
return false
}
return allowedOrigins.contains(origin)
}
private var allowedOrigins: Set<String> {
let urls = [
server.info.connection.address(for: .internal),
server.info.connection.address(for: .external),
server.info.connection.address(for: .remoteUI),
]
return Set(urls.compactMap(originKey(url:)))
}
private func originKey(url: URL?) -> String? {
guard let url, let scheme = url.scheme?.lowercased(), let host = url.host else {
return nil
}
return originKey(scheme: scheme, host: host, port: url.port)
}
private func originKey(scheme: String, host: String, port: Int?) -> String? {
guard let normalizedPort = normalizedPort(for: scheme, port: port) else {
return nil
}
return "\(scheme.lowercased())://\(host.lowercased()):\(normalizedPort)"
}
private func normalizedPort(for scheme: String, port: Int?) -> Int? {
if let port, port != 0 {
return port
}
switch scheme.lowercased() {
case "http": return 80
case "https": return 443
default: return port
}
}
}