feat(ios): Extensions melden Protection-State ans Backend
- RebreakProtectionModule.setExtensionCredentials() speichert Token, deviceId + baseURL in App-Group Shared UserDefaults. - Auth-Store ruft setExtensionCredentials bei Session-Änderungen auf. - ContentFilter-Extension (FilterDataProvider) sendet bei stopFilter() /api/protection/event active=false mit x-extension-secret. - PacketTunnel-Extension (PacketTunnelProvider) sendet bei stopTunnel() /api/protection/event active=false mit x-extension-secret.
This commit is contained in:
parent
b0a7091ac7
commit
c477b300ad
@ -17,6 +17,46 @@ import NetworkExtension
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
// ─── Extension → Backend reporting ─────────────────────────────────────────────
|
||||
|
||||
private let EXTENSION_SECRET = "5067b8a065ef655a32631640d2e3f86cab8abbfaf6173aabb58598172c57c5fe"
|
||||
private let APP_GROUP = "group.org.rebreak.app"
|
||||
|
||||
private func reportProtectionEventToBackend(active: Bool, source: String, reason: String) {
|
||||
guard let defaults = UserDefaults(suiteName: APP_GROUP) else { return }
|
||||
guard let token = defaults.string(forKey: "rebreak_extension_token"), !token.isEmpty,
|
||||
let deviceId = defaults.string(forKey: "rebreak_extension_device_id"), !deviceId.isEmpty,
|
||||
let baseURL = defaults.string(forKey: "rebreak_extension_base_url"), !baseURL.isEmpty
|
||||
else {
|
||||
SharedLogStore.append("⚠️ reportProtectionEvent: keine Credentials in App-Group")
|
||||
return
|
||||
}
|
||||
|
||||
let url = URL(string: "\(baseURL)/api/protection/event")!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(EXTENSION_SECRET, forHTTPHeaderField: "x-extension-secret")
|
||||
let body: [String: Any] = [
|
||||
"active": active,
|
||||
"source": source,
|
||||
"deviceId": deviceId,
|
||||
"reason": reason,
|
||||
]
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
SharedLogStore.append("📡 reportProtectionEvent active=\(active) source=\(source)")
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
SharedLogStore.append("❌ reportProtectionEvent failed: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||
SharedLogStore.append("✅ reportProtectionEvent HTTP \(status)")
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/// Shared Log-Store via App-Group UserDefaults (für Container-App-Debug-Page).
|
||||
enum SharedLogStore {
|
||||
static let appGroup = "group.org.rebreak.app"
|
||||
@ -187,8 +227,13 @@ class FilterDataProvider: NEFilterDataProvider {
|
||||
|
||||
override func stopFilter(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
SharedLogStore.append("🛑 stopFilter() reason=\(reason.rawValue)")
|
||||
reportProtectionEventToBackend(active: false, source: "mdm", reason: "NEFilter stopped (reason=\(reason.rawValue))")
|
||||
// Kurze Verzögerung, damit der Fire-and-Forget-Request abgesetzt wird,
|
||||
// bevor die Extension beendet wird. iOS gibt uns hier keine Garantie.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
|
||||
override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict {
|
||||
guard let browserFlow = flow as? NEFilterBrowserFlow,
|
||||
|
||||
@ -41,6 +41,44 @@ import os
|
||||
private let APP_GROUP = "group.org.rebreak.app"
|
||||
private let BLOCKLIST_FILENAME = "blocklist.bin"
|
||||
private let DARWIN_NOTIF = "rebreak.blocklist.updated"
|
||||
private let EXTENSION_SECRET = "5067b8a065ef655a32631640d2e3f86cab8abbfaf6173aabb58598172c57c5fe"
|
||||
|
||||
// ─── Extension → Backend reporting ─────────────────────────────────────────────
|
||||
|
||||
private func reportProtectionEventToBackend(active: Bool, source: String, reason: String) {
|
||||
guard let defaults = UserDefaults(suiteName: APP_GROUP),
|
||||
let token = defaults.string(forKey: "rebreak_extension_token"), !token.isEmpty,
|
||||
let deviceId = defaults.string(forKey: "rebreak_extension_device_id"), !deviceId.isEmpty,
|
||||
let baseURL = defaults.string(forKey: "rebreak_extension_base_url"), !baseURL.isEmpty
|
||||
else {
|
||||
ExtLog.write("⚠️ reportProtectionEvent: keine Credentials in App-Group")
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = URL(string: "\(baseURL)/api/protection/event") else { return }
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(EXTENSION_SECRET, forHTTPHeaderField: "x-extension-secret")
|
||||
let body: [String: Any] = [
|
||||
"active": active,
|
||||
"source": source,
|
||||
"deviceId": deviceId,
|
||||
"reason": reason,
|
||||
]
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
ExtLog.write("📡 reportProtectionEvent active=\(active) source=\(source)")
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
ExtLog.write("❌ reportProtectionEvent failed: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||
ExtLog.write("✅ reportProtectionEvent HTTP \(status)")
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
// Virtuelles Subnet — identisch zur Android-Wahl (RebreakVpnService).
|
||||
private let TUNNEL_LOCAL_ADDR = "10.0.0.2"
|
||||
@ -241,8 +279,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
d.set(false, forKey: "vpn_tunnel_running")
|
||||
}
|
||||
}
|
||||
|
||||
reportProtectionEventToBackend(active: false, source: "vpn", reason: "PacketTunnel stopped (reason=\(reason.rawValue))")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Read-Loop ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@ -90,6 +90,22 @@ public class RebreakProtectionModule: Module {
|
||||
|
||||
Events("onLayerChange")
|
||||
|
||||
// ───────── setExtensionCredentials: Auth-Token + deviceId an Extensions ───
|
||||
// Die Network-Extensions laufen als eigene Prozesse und haben keinen Zugriff
|
||||
// auf das Supabase-SDK. Damit sie bei stopFilter()/stopTunnel() selbst den
|
||||
// Protection-State ans Backend melden können, schreibt die Haupt-App Token
|
||||
// + deviceId in die App-Group Shared UserDefaults.
|
||||
AsyncFunction("setExtensionCredentials") { (token: String, deviceId: String, baseURL: String) in
|
||||
guard let defaults = UserDefaults(suiteName: APP_GROUP) else {
|
||||
SharedLogStore.append("⚠️ setExtensionCredentials: App-Group nicht erreichbar")
|
||||
return
|
||||
}
|
||||
defaults.set(token, forKey: "rebreak_extension_token")
|
||||
defaults.set(deviceId, forKey: "rebreak_extension_device_id")
|
||||
defaults.set(baseURL, forKey: "rebreak_extension_base_url")
|
||||
SharedLogStore.append("🔐 Extension-Credentials gespeichert (deviceId len=\(deviceId.count))")
|
||||
}
|
||||
|
||||
// ───────── activate: Family Controls + NEFilter + denyAppRemoval ─────────
|
||||
|
||||
// ───────── probeContentFilter: Try NEFilter, retourniert ob das Device es
|
||||
|
||||
@ -16,6 +16,13 @@ import type {
|
||||
} from './RebreakProtection.types';
|
||||
|
||||
declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEvents> {
|
||||
/**
|
||||
* iOS: schreibt Auth-Token + deviceId + baseURL in die App-Group Shared
|
||||
* UserDefaults, damit die Network-Extensions bei stopFilter()/stopTunnel()
|
||||
* selbst den Protection-State ans Backend melden können.
|
||||
*/
|
||||
setExtensionCredentials(token: string, deviceId: string, baseURL: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* iOS: read-only check ob NEFilter aktiv ist (egal ob via App-Code-saveToPreferences
|
||||
* oder via System-installiertes webcontent-filter Profile). Wenn `enabled=true`
|
||||
|
||||
@ -6,7 +6,10 @@ import * as Linking from 'expo-linking';
|
||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { apiFetch } from '../lib/api';
|
||||
import { getDeviceId } from '../lib/deviceId';
|
||||
import Constants from 'expo-constants';
|
||||
import i18n from '../lib/i18n';
|
||||
import RebreakProtection from '../modules/rebreak-protection';
|
||||
import { syncLanguageFromUserMetadata } from './language';
|
||||
|
||||
const SUPPORTED_LOCALES = ['de', 'en', 'fr', 'ar'] as const;
|
||||
@ -16,6 +19,21 @@ function currentLocale(): SupportedLocale {
|
||||
const raw = (i18n.language ?? 'en').split('-')[0].toLowerCase();
|
||||
return (SUPPORTED_LOCALES as readonly string[]).includes(raw) ? (raw as SupportedLocale) : 'en';
|
||||
}
|
||||
|
||||
const API_BASE = (Constants.expoConfig?.extra?.apiUrl as string) ?? 'https://staging.rebreak.org';
|
||||
|
||||
async function syncExtensionCredentials(session: Session | null) {
|
||||
try {
|
||||
if (!session?.access_token) {
|
||||
await RebreakProtection.setExtensionCredentials('', '', API_BASE);
|
||||
return;
|
||||
}
|
||||
const deviceId = await getDeviceId();
|
||||
await RebreakProtection.setExtensionCredentials(session.access_token, deviceId, API_BASE);
|
||||
} catch {
|
||||
// Best-effort; Extension kann bei fehlenden Credentials nicht melden.
|
||||
}
|
||||
}
|
||||
import { invalidateMe } from '../hooks/useMe';
|
||||
import { useDevicesStore } from './devices';
|
||||
import { useDeviceLimitStore } from './deviceLimit';
|
||||
@ -73,12 +91,14 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
if (data.session?.user) {
|
||||
void syncLanguageFromUserMetadata(data.session.user);
|
||||
}
|
||||
void syncExtensionCredentials(data.session);
|
||||
|
||||
supabase.auth.onAuthStateChange((_event, session) => {
|
||||
set({ session, user: session?.user ?? null });
|
||||
if (session?.user) {
|
||||
void syncLanguageFromUserMetadata(session.user);
|
||||
}
|
||||
void syncExtensionCredentials(session);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user