element-ios/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift
2022-12-07 14:34:27 +00:00

236 lines
8.4 KiB
Swift

//
// Copyright 2021 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 MatrixSDK
import Combine
struct UISIAutoReportData {
let eventId: String?
let roomId: String?
let senderKey: String?
let deviceId: String?
let userId: String?
let sessionId: String?
}
extension UISIAutoReportData: Codable {
enum CodingKeys: String, CodingKey {
case eventId = "event_id"
case roomId = "room_id"
case senderKey = "sender_key"
case deviceId = "device_id"
case userId = "user_id"
case sessionId = "session_id"
}
}
/// Listens for failed decryption events and silently sends reports RageShake server.
/// Also requests that message senders send a matching report to have both sides of the interaction.
@objcMembers class UISIAutoReporter: NSObject, UISIDetectorDelegate {
struct ReportInfo: Hashable {
let roomId: String
let sessionId: String
}
// MARK: - Properties
private static let autoRsRequest = "im.vector.auto_rs_request"
private static let reportSpacing = 60
private let bugReporter: MXBugReportRestClient
private let dispatchQueue = DispatchQueue(label: "io.element.UISIAutoReporter.queue")
// Simple in memory cache of already sent report
private var alreadyReportedUisi = Set<ReportInfo>()
private let e2eDetectedSubject = PassthroughSubject<UISIDetectedMessage, Never>()
private let matchingRSRequestSubject = PassthroughSubject<MXEvent, Never>()
private var cancellables = Set<AnyCancellable>()
private var sessions = [MXSession]()
private var enabled = false {
didSet {
guard oldValue != enabled else { return }
detector.enabled = enabled
}
}
// MARK: - Setup
override init() {
self.bugReporter = MXBugReportRestClient.vc_bugReportRestClient(appName: BuildSettings.bugReportUISIId)
super.init()
// Simple rate limiting, for any rage-shakes emitted we guarantee a spacing between requests.
e2eDetectedSubject
.bufferAndSpace(spacingDelay: Self.reportSpacing)
.sink { [weak self] in
guard let self = self else { return }
self.sendRageShake(source: $0)
}.store(in: &cancellables)
matchingRSRequestSubject
.bufferAndSpace(spacingDelay: Self.reportSpacing)
.sink { [weak self] in
guard let self = self else { return }
self.sendMatchingRageShake(source: $0)
}.store(in: &cancellables)
self.enabled = RiotSettings.shared.enableUISIAutoReporting
RiotSettings.shared.publisher(for: RiotSettings.UserDefaultsKeys.enableUISIAutoReporting)
.sink { [weak self] _ in
guard let self = self else { return }
self.enabled = RiotSettings.shared.enableUISIAutoReporting
}
.store(in: &cancellables)
}
private lazy var detector: UISIDetector = {
let detector = UISIDetector()
detector.delegate = self
return detector
}()
var reciprocateToDeviceEventType: String {
return Self.autoRsRequest
}
// MARK: - Public
func uisiDetected(source: UISIDetectedMessage) {
dispatchQueue.async {
let reportInfo = ReportInfo(roomId: source.roomId, sessionId: source.sessionId)
let alreadySent = self.alreadyReportedUisi.contains(reportInfo)
if !alreadySent {
self.alreadyReportedUisi.insert(reportInfo)
self.e2eDetectedSubject.send(source)
}
}
}
func add(_ session: MXSession) {
sessions.append(session)
detector.enabled = enabled
session.eventStreamService.add(eventStreamListener: detector)
}
func remove(_ session: MXSession) {
if let index = sessions.firstIndex(of: session) {
sessions.remove(at: index)
}
session.eventStreamService.remove(eventStreamListener: detector)
}
func uisiReciprocateRequest(source: MXEvent) {
guard source.type == Self.autoRsRequest else { return }
self.matchingRSRequestSubject.send(source)
}
// MARK: - Private
private func sendRageShake(source: UISIDetectedMessage) {
MXLog.debug("[UISIAutoReporter] sendRageShake")
guard let session = sessions.first else { return }
let uisiData = UISIAutoReportData(
eventId: source.eventId,
roomId: source.roomId,
senderKey: source.senderKey,
deviceId: source.senderDeviceId,
userId: source.senderUserId,
sessionId: source.sessionId
).jsonString ?? ""
self.bugReporter.vc_sendBugReport(
description: "Auto-reporting decryption error",
sendLogs: true,
sendCrashLog: true,
additionalLabels: [
"Z-UISI",
"ios",
"uisi-recipient"
],
customFields: ["auto_uisi": uisiData],
success: { reportUrl in
let contentMap = MXUsersDevicesMap<NSDictionary>()
let content = [
"event_id": source.eventId,
"room_id": source.roomId,
"session_id": source.sessionId,
"device_id": source.senderDeviceId,
"user_id": source.senderUserId,
"sender_key": source.senderKey,
"recipient_rageshake": reportUrl
]
contentMap.setObject(content as NSDictionary, forUser: source.senderUserId, andDevice: source.senderDeviceId)
session.matrixRestClient.sendDirectToDevice(
payload: .init(
eventType: Self.autoRsRequest,
contentMap: contentMap
)
) { response in
if response.isFailure {
MXLog.warning("failed to send auto-uisi to device")
}
}
},
failure: { [weak self] error in
guard let self = self else { return }
self.dispatchQueue.async {
self.alreadyReportedUisi.remove(ReportInfo(roomId: source.roomId, sessionId: source.sessionId))
}
})
}
private func sendMatchingRageShake(source: MXEvent) {
MXLog.debug("[UISIAutoReporter] sendMatchingRageShake")
let eventId = source.content["event_id"] as? String
let roomId = source.content["room_id"] as? String
let sessionId = source.content["session_id"] as? String
let deviceId = source.content["device_id"] as? String
let userId = source.content["user_id"] as? String
let senderKey = source.content["sender_key"] as? String
let matchingIssue = source.content["recipient_rageshake"] as? String
var description = "Auto-reporting decryption error (sender)"
if let matchingIssue = matchingIssue {
description += "\nRecipient rageshake: \(matchingIssue)"
}
let uisiData = UISIAutoReportData(
eventId: eventId,
roomId: roomId,
senderKey: senderKey,
deviceId: deviceId,
userId: userId,
sessionId: sessionId
).jsonString ?? ""
self.bugReporter.vc_sendBugReport(
description: description,
sendLogs: true,
sendCrashLog: true,
additionalLabels: [
"Z-UISI",
"ios",
"uisi-sender"
],
customFields: [
"auto_uisi": uisiData,
"recipient_rageshake": matchingIssue ?? ""
]
)
}
}