feat: tunnel WireGuard vers le serveur familyd et mode propriétaire de l'appareil
- Firebase et le pare-feu local (trou noir, liste blanche par application, coupure cyclique locale) sont retirés - Client WireGuard officiel (GoBackend) : tout le trafic passe par le serveur familyd, qui filtre - Appairage par code ou lien familyd:// ; clé privée WireGuard et jeton chiffrés par l'Android Keystore - Remontées par le tunnel : état, temps d'écran, applications installées, évènements (file persistante) - Mode propriétaire (Device Owner) : VPN toujours actif avec blocage hors VPN, réglages verrouillés, garde-fou si le serveur ne répond plus pendant 15 min - Plan de la refonte : docs/PLAN-wireguard.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R9CBCVP5oDu2sfoYPisNCQ
This commit is contained in:
1 parent
f382eeb4fe
commit
33ea4ff947
40 files changed
+1680
-1635
No files matched your search
@@ -0,0 +1,88 @@
|
||||
package com.jttools.datacontrol.remote
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
/** Erreur renvoyée par le serveur ([code] HTTP) ou réseau ([code] = 0), avec un message lisible. */
|
||||
class ApiException(val code: Int, message: String) : IOException(message)
|
||||
|
||||
/**
|
||||
* Client de l'API familyd. Appels bloquants : à lancer sur Dispatchers.IO.
|
||||
* Les routes des appareils passent par le tunnel ([FamilyLink.apiBase], http://10.78.0.1:8100,
|
||||
* déjà chiffré par WireGuard) ; l'appairage passe par l'adresse publique en HTTPS.
|
||||
*/
|
||||
class FamilyApi(private val apiBase: String, private val token: String) {
|
||||
|
||||
fun policy(): JSONObject = JSONObject(request("GET", "$apiBase/api/device/policy", token, null))
|
||||
|
||||
fun serverTime(): Long = JSONObject(request("GET", "$apiBase/api/device/time", token, null)).getLong("time")
|
||||
|
||||
fun state(state: JSONObject) {
|
||||
request("POST", "$apiBase/api/device/state", token, state)
|
||||
}
|
||||
|
||||
fun usage(date: String, totalMs: Long, apps: Map<String, Long>) {
|
||||
request("POST", "$apiBase/api/device/usage", token,
|
||||
JSONObject().put("date", date).put("total_ms", totalMs).put("apps", JSONObject(apps as Map<*, *>)))
|
||||
}
|
||||
|
||||
fun event(type: String, detail: String, at: Long) {
|
||||
request("POST", "$apiBase/api/device/events", token,
|
||||
JSONObject().put("type", type).put("detail", detail.take(500)).put("at", at))
|
||||
}
|
||||
|
||||
fun apps(apps: JSONArray) {
|
||||
request("POST", "$apiBase/api/device/apps", token, JSONObject().put("apps", apps))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONNECT_TIMEOUT_MS = 10_000
|
||||
private const val READ_TIMEOUT_MS = 15_000
|
||||
|
||||
/** Appairage (public, HTTPS) : renvoie la réponse brute, à lire avec [FamilyLink.fromPairResponse]. */
|
||||
fun pair(serverUrl: String, code: String, publicKey: String, name: String, model: String, appVersion: String): String {
|
||||
require(serverUrl.startsWith("https://")) { "L'adresse du serveur doit commencer par https://" }
|
||||
val body = JSONObject().put("code", code).put("public_key", publicKey).put("name", name)
|
||||
.put("model", model).put("app_version", appVersion)
|
||||
return request("POST", "${serverUrl.trimEnd('/')}/api/pair", null, body)
|
||||
}
|
||||
|
||||
fun request(method: String, url: String, token: String?, body: JSONObject?): String {
|
||||
val conn = try {
|
||||
(URL(url).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = method
|
||||
connectTimeout = CONNECT_TIMEOUT_MS
|
||||
readTimeout = READ_TIMEOUT_MS
|
||||
setRequestProperty("Accept", "application/json")
|
||||
token?.let { setRequestProperty("Authorization", "Bearer $it") }
|
||||
if (body != null) {
|
||||
doOutput = true
|
||||
setRequestProperty("Content-Type", "application/json; charset=utf-8")
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw ApiException(0, "Serveur injoignable : ${e.message}")
|
||||
}
|
||||
try {
|
||||
if (body != null) conn.outputStream.use { it.write(body.toString().toByteArray(Charsets.UTF_8)) }
|
||||
val code = conn.responseCode
|
||||
val text = (if (code in 200..299) conn.inputStream else conn.errorStream)
|
||||
?.bufferedReader()?.use { it.readText() }.orEmpty()
|
||||
if (code !in 200..299) {
|
||||
val detail = runCatching { JSONObject(text).optString("detail") }.getOrNull()
|
||||
throw ApiException(code, detail?.takeIf { it.isNotBlank() } ?: "Erreur du serveur ($code)")
|
||||
}
|
||||
return text
|
||||
} catch (e: ApiException) {
|
||||
throw e
|
||||
} catch (e: IOException) {
|
||||
throw ApiException(0, "Serveur injoignable : ${e.message}")
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user