package com.jttools.datacontrol.remote import org.json.JSONArray import org.json.JSONObject import java.net.URI import java.net.URLDecoder /** * Liaison avec le serveur familyd, reçue à l'appairage (sans les secrets : jeton de l'appareil et clé * privée WireGuard sont stockés à part, chiffrés par l'Android Keystore, voir SecretBox). */ data class FamilyLink( val serverUrl: String, val deviceId: String, val name: String, val address: String, val dns: String, val mtu: Int, val serverPublicKey: String, val endpoint: String, val allowedIps: List, val keepalive: Int, val apiBase: String ) { /** Adresse du téléphone dans le tunnel, sans le /32. */ val tunnelIp: String get() = address.substringBefore('/') fun toJson(): String = JSONObject() .put("serverUrl", serverUrl).put("deviceId", deviceId).put("name", name) .put("address", address).put("dns", dns).put("mtu", mtu) .put("serverPublicKey", serverPublicKey).put("endpoint", endpoint) .put("allowedIps", JSONArray(allowedIps)).put("keepalive", keepalive).put("apiBase", apiBase) .toString() /** Configuration WireGuard au format wg-quick (lue par la bibliothèque officielle). */ fun wgQuickConfig(privateKey: String): String = buildString { appendLine("[Interface]") appendLine("PrivateKey = $privateKey") appendLine("Address = $address") appendLine("DNS = $dns") appendLine("MTU = $mtu") appendLine() appendLine("[Peer]") appendLine("PublicKey = $serverPublicKey") appendLine("Endpoint = $endpoint") appendLine("AllowedIPs = ${allowedIps.joinToString(", ")}") appendLine("PersistentKeepalive = $keepalive") } companion object { fun fromJson(json: String?): FamilyLink? { if (json.isNullOrBlank()) return null return runCatching { val o = JSONObject(json) val ips = o.getJSONArray("allowedIps") FamilyLink( serverUrl = o.getString("serverUrl"), deviceId = o.getString("deviceId"), name = o.optString("name"), address = o.getString("address"), dns = o.getString("dns"), mtu = o.optInt("mtu", 1280), serverPublicKey = o.getString("serverPublicKey"), endpoint = o.getString("endpoint"), allowedIps = (0 until ips.length()).map { ips.getString(it) }, keepalive = o.optInt("keepalive", 25), apiBase = o.getString("apiBase") ) }.getOrNull() } /** Réponse de POST /api/pair → liaison + jeton de l'appareil. */ fun fromPairResponse(serverUrl: String, json: String): Pair { val o = JSONObject(json) val wg = o.getJSONObject("wireguard") val ips = wg.getJSONArray("allowed_ips") val link = FamilyLink( serverUrl = serverUrl, deviceId = o.getString("device_id"), name = o.optString("name"), address = wg.getString("address"), dns = wg.getString("dns"), mtu = wg.optInt("mtu", 1280), serverPublicKey = wg.getString("server_public_key"), endpoint = wg.getString("endpoint"), allowedIps = (0 until ips.length()).map { ips.getString(it) }, keepalive = wg.optInt("persistent_keepalive", 25), apiBase = o.getString("api").trimEnd('/') ) require(link.serverPublicKey.isNotBlank()) { "Le serveur n'a pas fourni sa clé WireGuard." } return link to o.getString("token") } } } /** * Ce que l'utilisateur saisit pour s'associer : le code seul (« ABCD 2345 »), ou le lien * « familyd://pair?url=https://…&code=… » affiché par l'interface des parents. */ data class PairingInput(val serverUrl: String, val code: String) { companion object { const val DEFAULT_SERVER = "https://family.tuxtech.fr" private val CODE = Regex("^[A-HJ-NP-Z2-9]{8}$") // même alphabet que le serveur (sans I, O, 0, 1) fun parse(input: String, defaultServer: String = DEFAULT_SERVER): PairingInput? { val text = input.trim() if (text.startsWith("familyd://", ignoreCase = true)) { val query = runCatching { URI(text).rawQuery }.getOrNull() ?: return null val params = query.split('&').mapNotNull { val (k, v) = it.split('=', limit = 2).takeIf { p -> p.size == 2 } ?: return@mapNotNull null k to URLDecoder.decode(v, "UTF-8") }.toMap() val url = params["url"]?.trimEnd('/') ?: return null val code = normalize(params["code"] ?: return null) ?: return null if (!url.startsWith("https://")) return null return PairingInput(url, code) } return normalize(text)?.let { PairingInput(defaultServer, it) } } private fun normalize(code: String): String? = code.uppercase().replace(" ", "").replace("-", "").takeIf { CODE.matches(it) } } }