Add a certificate validator hook. (#3069)

* Add a certificate validator hook.

* General tidy up of AppHooks.

* Don't worry about the generic call links.
This commit is contained in:
Doug
2024-07-19 17:05:54 +01:00
committed by GitHub
parent 74907d321f
commit cdd763efd4
21 changed files with 227 additions and 104 deletions
+49
View File
@@ -0,0 +1,49 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
class AppHooks: AppHooksProtocol {
#if IS_MAIN_APP
private(set) var appSettingsHook: AppSettingsHookProtocol = DefaultAppSettingsHook()
func registerAppSettingsHook(_ hook: AppSettingsHookProtocol) {
appSettingsHook = hook
}
private(set) var bugReportHook: BugReportHookProtocol = DefaultBugReportHook()
func registerBugReportHook(_ hook: BugReportHookProtocol) {
bugReportHook = hook
}
private(set) var certificateValidatorHook: CertificateValidatorHookProtocol = DefaultCertificateValidator()
func registerCertificateValidatorHook(_ hook: CertificateValidatorHookProtocol) {
certificateValidatorHook = hook
}
#endif
private(set) var clientBuilderHook: ClientBuilderHookProtocol = DefaultClientBuilderHook()
func registerClientBuilderHook(_ hook: ClientBuilderHookProtocol) {
clientBuilderHook = hook
}
}
protocol AppHooksProtocol {
func configure()
}
extension AppHooksProtocol {
func configure() { }
}
@@ -0,0 +1,25 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
protocol AppSettingsHookProtocol {
func configure(_ appSettings: AppSettings) -> AppSettings
}
struct DefaultAppSettingsHook: AppSettingsHookProtocol {
func configure(_ appSettings: AppSettings) -> AppSettings { appSettings }
}
@@ -0,0 +1,25 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
protocol BugReportHookProtocol {
func update(_ bugReport: BugReport) -> BugReport
}
struct DefaultBugReportHook: BugReportHookProtocol {
func update(_ bugReport: BugReport) -> BugReport { bugReport }
}
@@ -0,0 +1,27 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
protocol CertificateValidatorHookProtocol {
func respondTo(_ challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?)
}
struct DefaultCertificateValidator: CertificateValidatorHookProtocol {
func respondTo(_ challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
(.performDefaultHandling, nil)
}
}
@@ -0,0 +1,25 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import MatrixRustSDK
protocol ClientBuilderHookProtocol {
func configure(_ builder: ClientBuilder) -> ClientBuilder
}
struct DefaultClientBuilderHook: ClientBuilderHookProtocol {
func configure(_ builder: ClientBuilder) -> ClientBuilder { builder }
}
@@ -74,7 +74,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
windowManager = WindowManager(appDelegate: appDelegate)
appMediator = AppMediator(windowManager: windowManager)
let appSettings = appHooks.runAppSettingsHook(AppSettings())
let appSettings = appHooks.appSettingsHook.configure(AppSettings())
MXLog.configure(logLevel: appSettings.logLevel)
@@ -534,6 +534,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
roomTimelineControllerFactory: RoomTimelineControllerFactory(),
appMediator: appMediator,
appSettings: appSettings,
appHooks: appHooks,
analytics: ServiceLocator.shared.analytics,
notificationManager: notificationManager,
isNewLogin: isNewLogin)
@@ -32,6 +32,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private let elementCallService: ElementCallServiceProtocol
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings
private let appHooks: AppHooks
private let analytics: AnalyticsService
private let notificationManager: NotificationManagerProtocol
@@ -74,6 +75,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
appMediator: AppMediatorProtocol,
appSettings: AppSettings,
appHooks: AppHooks,
analytics: AnalyticsService,
notificationManager: NotificationManagerProtocol,
isNewLogin: Bool) {
@@ -85,6 +87,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.appMediator = appMediator
self.appSettings = appSettings
self.appHooks = appHooks
self.analytics = analytics
self.notificationManager = notificationManager
@@ -561,7 +564,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
clientID: InfoPlistReader.main.bundleIdentifier,
elementCallBaseURL: appSettings.elementCallBaseURL,
elementCallBaseURLOverride: appSettings.elementCallBaseURLOverride,
colorScheme: colorScheme))
colorScheme: colorScheme,
appHooks: appHooks))
callScreenCoordinator.actions
.sink { [weak self] action in
-78
View File
@@ -1,78 +0,0 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import MatrixRustSDK
// MARK: Registration
class AppHooks: AppHooksProtocol {
#if IS_MAIN_APP
private var appSettingsHook: AppSettingsHookProtocol?
func registerAppSettingsHook(_ hook: AppSettingsHookProtocol) {
appSettingsHook = hook
}
func runAppSettingsHook(_ appSettings: AppSettings) -> AppSettings {
guard let appSettingsHook else { return appSettings }
return appSettingsHook.run(appSettings: appSettings)
}
private var bugReportHook: BugReportHookProtocol?
func registerBugReportHook(_ hook: BugReportHookProtocol) {
bugReportHook = hook
}
func runBugReportHook(_ bugReport: BugReport) -> BugReport {
guard let bugReportHook else { return bugReport }
return bugReportHook.run(bugReport: bugReport)
}
#endif
private var clientBuilderHook: ClientBuilderHookProtocol?
func registerClientBuilderHook(_ hook: ClientBuilderHookProtocol) {
clientBuilderHook = hook
}
func runClientBuilderHook(_ clientBuilder: ClientBuilder) -> ClientBuilder {
guard let clientBuilderHook else { return clientBuilder }
return clientBuilderHook.run(builder: clientBuilder)
}
}
protocol AppHooksProtocol {
func configure()
}
extension AppHooksProtocol {
func configure() { }
}
// MARK: Protocols
#if IS_MAIN_APP
protocol AppSettingsHookProtocol {
func run(appSettings: AppSettings) -> AppSettings
}
protocol BugReportHookProtocol {
func run(bugReport: BugReport) -> BugReport
}
#endif
protocol ClientBuilderHookProtocol {
func run(builder: ClientBuilder) -> ClientBuilder
}
@@ -42,6 +42,6 @@ extension ClientBuilder {
builder = builder.proxy(url: httpProxy)
}
return appHooks.runClientBuilderHook(builder)
return appHooks.clientBuilderHook.configure(builder)
}
}
@@ -25,6 +25,7 @@ struct CallScreenCoordinatorParameters {
let elementCallBaseURL: URL
let elementCallBaseURLOverride: URL?
let colorScheme: ColorScheme
let appHooks: AppHooks
}
enum CallScreenCoordinatorAction {
@@ -47,7 +48,8 @@ final class CallScreenCoordinator: CoordinatorProtocol {
clientID: parameters.clientID,
elementCallBaseURL: parameters.elementCallBaseURL,
elementCallBaseURLOverride: parameters.elementCallBaseURLOverride,
colorScheme: parameters.colorScheme)
colorScheme: parameters.colorScheme,
appHooks: parameters.appHooks)
}
func start() {
@@ -24,6 +24,9 @@ struct CallScreenViewState: BindableState {
let messageHandler: String
let script: String?
var url: URL?
let certificateValidator: CertificateValidatorHookProtocol
var bindings = Bindings()
}
@@ -43,7 +43,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
clientID: String,
elementCallBaseURL: URL,
elementCallBaseURLOverride: URL?,
colorScheme: ColorScheme) {
colorScheme: ColorScheme,
appHooks: AppHooks) {
guard let deviceID = clientProxy.deviceID else { fatalError("Missing device ID for the call.") }
self.elementCallService = elementCallService
@@ -52,7 +53,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
widgetDriver = roomProxy.elementCallWidgetDriver(deviceID: deviceID)
super.init(initialViewState: CallScreenViewState(messageHandler: Self.eventHandlerName,
script: Self.eventHandlerInjectionScript))
script: Self.eventHandlerInjectionScript,
certificateValidator: appHooks.certificateValidatorHook))
state.bindings.javaScriptMessageHandler = { [weak self] message in
guard let self,
@@ -54,6 +54,7 @@ private struct WebView: UIViewRepresentable {
@MainActor
class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate {
private weak var viewModelContext: CallScreenViewModel.Context?
private let certificateValidator: CertificateValidatorHookProtocol
private(set) var webView: WKWebView!
@@ -61,6 +62,7 @@ private struct WebView: UIViewRepresentable {
init(viewModelContext: CallScreenViewModel.Context) {
self.viewModelContext = viewModelContext
certificateValidator = viewModelContext.viewState.certificateValidator
super.init()
@@ -131,6 +133,10 @@ private struct WebView: UIViewRepresentable {
// MARK: - WKNavigationDelegate
func webView(_ webView: WKWebView, respondTo challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
await certificateValidator.respondTo(challenge)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
// Allow any content from the main URL.
if navigationAction.request.url?.host == url.host {
@@ -193,7 +199,8 @@ struct CallScreen_Previews: PreviewProvider {
clientID: "io.element.elementx",
elementCallBaseURL: "https://call.element.io",
elementCallBaseURLOverride: nil,
colorScheme: .light)
colorScheme: .light,
appHooks: AppHooks())
}()
static var previews: some View {
@@ -58,7 +58,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
// swiftlint:disable:next cyclomatic_complexity
func submitBugReport(_ bugReport: BugReport,
progressListener: CurrentValueSubject<Double, Never>) async -> Result<SubmitBugReportResponse, BugReportServiceError> {
let bugReport = appHooks.runBugReportHook(bugReport)
let bugReport = appHooks.bugReportHook.update(bugReport)
var params = [
MultipartFormData(key: "text", type: .text(value: bugReport.text)),
@@ -532,6 +532,7 @@ class MockScreen: Identifiable {
roomTimelineControllerFactory: RoomTimelineControllerFactoryMock(configuration: .init()),
appMediator: appMediator,
appSettings: appSettings,
appHooks: AppHooks(),
analytics: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
isNewLogin: false)
@@ -648,6 +649,7 @@ class MockScreen: Identifiable {
roomTimelineControllerFactory: RoomTimelineControllerFactoryMock(configuration: .init(timelineController: timelineController)),
appMediator: AppMediatorMock.default,
appSettings: appSettings,
appHooks: AppHooks(),
analytics: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
isNewLogin: false)