diff --git a/android/app/src/main/kotlin/tech/threefold/mycelium/HttpToSocksProxy.kt b/android/app/src/main/kotlin/tech/threefold/mycelium/HttpToSocksProxy.kt new file mode 100644 index 0000000..4359f1e --- /dev/null +++ b/android/app/src/main/kotlin/tech/threefold/mycelium/HttpToSocksProxy.kt @@ -0,0 +1,320 @@ +package tech.threefold.mycelium + +import android.util.Log +import kotlinx.coroutines.* +import java.io.* +import java.net.* +import java.nio.charset.StandardCharsets + +/** + * HTTP-to-SOCKS proxy server that accepts HTTP requests and forwards them through SOCKS5 + * This bridges Android's HTTP proxy support with mycelium's SOCKS5 proxy + */ +class HttpToSocksProxy { + companion object { + private const val TAG = "HttpToSocksProxy" + private const val HTTP_PROXY_PORT = 8080 + private const val SOCKS_HOST = "127.0.0.1" + private const val SOCKS_PORT = 1080 + } + + private var serverSocket: ServerSocket? = null + private var isRunning = false + private var proxyJob: Job? = null + + /** + * Start the HTTP-to-SOCKS proxy server + */ + fun start() { + if (isRunning) { + Log.w(TAG, "HTTP-to-SOCKS proxy already running") + return + } + + isRunning = true + Log.i(TAG, "Starting HTTP-to-SOCKS proxy on port $HTTP_PROXY_PORT") + + // Test if mycelium SOCKS proxy is running + testSocksConnection() + + proxyJob = CoroutineScope(Dispatchers.IO).launch { + try { + serverSocket = ServerSocket(HTTP_PROXY_PORT) + Log.i(TAG, "HTTP-to-SOCKS proxy listening on port $HTTP_PROXY_PORT") + + while (isRunning) { + try { + val clientSocket = serverSocket?.accept() + if (clientSocket != null) { + Log.d(TAG, "New HTTP client connection from ${clientSocket.remoteSocketAddress}") + CoroutineScope(Dispatchers.IO).launch { + handleHttpClient(clientSocket) + } + } + } catch (e: Exception) { + if (isRunning) { + Log.e(TAG, "Error accepting HTTP client: ${e.message}") + } + } + } + } catch (e: Exception) { + Log.e(TAG, "HTTP-to-SOCKS proxy server error: ${e.message}") + } + } + } + + /** + * Stop the HTTP-to-SOCKS proxy server + */ + fun stop() { + Log.i(TAG, "Stopping HTTP-to-SOCKS proxy") + isRunning = false + proxyJob?.cancel() + + try { + serverSocket?.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing server socket: ${e.message}") + } + serverSocket = null + } + + private suspend fun handleHttpClient(clientSocket: Socket) { + try { + val clientInput = BufferedReader(InputStreamReader(clientSocket.getInputStream())) + val clientOutput = clientSocket.getOutputStream() + + // Read HTTP request + val requestLine = clientInput.readLine() + if (requestLine == null) { + clientSocket.close() + return + } + + Log.d(TAG, "HTTP request: $requestLine") + + // Parse HTTP request + val parts = requestLine.split(" ") + if (parts.size < 3) { + sendHttpError(clientOutput, "400 Bad Request") + clientSocket.close() + return + } + + val method = parts[0] + val url = parts[1] + val httpVersion = parts[2] + + // Handle CONNECT method (HTTPS tunneling) + if (method == "CONNECT") { + handleHttpsConnect(url, clientInput, clientOutput, clientSocket) + } else { + // Handle regular HTTP requests + handleHttpRequest(method, url, httpVersion, clientInput, clientOutput, clientSocket) + } + + } catch (e: Exception) { + Log.e(TAG, "Error handling HTTP client: ${e.message}") + } finally { + try { + clientSocket.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing client socket: ${e.message}") + } + } + } + + private suspend fun handleHttpsConnect( + hostPort: String, + clientInput: BufferedReader, + clientOutput: OutputStream, + clientSocket: Socket + ) { + try { + // Parse host:port + val parts = hostPort.split(":") + val host = parts[0] + val port = if (parts.size > 1) parts[1].toInt() else 443 + + Log.d(TAG, "HTTPS CONNECT to $host:$port") + + // Connect through SOCKS proxy + val socksSocket = connectThroughSocks(host, port) + if (socksSocket == null) { + sendHttpError(clientOutput, "502 Bad Gateway") + return + } + + // Send 200 Connection Established + clientOutput.write("HTTP/1.1 200 Connection Established\r\n\r\n".toByteArray()) + clientOutput.flush() + + // Start bidirectional forwarding + val job1 = CoroutineScope(Dispatchers.IO).launch { + forwardData(clientSocket.getInputStream(), socksSocket.getOutputStream(), "client->socks") + } + val job2 = CoroutineScope(Dispatchers.IO).launch { + forwardData(socksSocket.getInputStream(), clientOutput, "socks->client") + } + + // Wait for either direction to close + try { + job1.join() + } catch (e: Exception) { + // One direction closed, cancel the other + } + + job1.cancel() + job2.cancel() + socksSocket.close() + + } catch (e: Exception) { + Log.e(TAG, "Error in HTTPS CONNECT: ${e.message}") + } + } + + private suspend fun handleHttpRequest( + method: String, + url: String, + httpVersion: String, + clientInput: BufferedReader, + clientOutput: OutputStream, + clientSocket: Socket + ) { + try { + // Parse URL to extract host and port + val uri = URI(url) + val host = uri.host ?: return + val port = if (uri.port != -1) uri.port else 80 + + Log.d(TAG, "HTTP $method to $host:$port") + + // Connect through SOCKS proxy + val socksSocket = connectThroughSocks(host, port) + if (socksSocket == null) { + sendHttpError(clientOutput, "502 Bad Gateway") + return + } + + val socksOutput = socksSocket.getOutputStream() + val socksInput = socksSocket.getInputStream() + + // Forward the original request + socksOutput.write("$method ${uri.path}${if (uri.query != null) "?" + uri.query else ""} $httpVersion\r\n".toByteArray()) + + // Forward headers + var line: String? + while (clientInput.readLine().also { line = it } != null && line!!.isNotEmpty()) { + socksOutput.write("$line\r\n".toByteArray()) + } + socksOutput.write("\r\n".toByteArray()) + socksOutput.flush() + + // Forward response back to client + forwardData(socksInput, clientOutput, "socks->client") + socksSocket.close() + + } catch (e: Exception) { + Log.e(TAG, "Error in HTTP request: ${e.message}") + } + } + + private suspend fun connectThroughSocks(host: String, port: Int): Socket? { + return withContext(Dispatchers.IO) { + try { + val socksSocket = Socket(SOCKS_HOST, SOCKS_PORT) + socksSocket.soTimeout = 10000 + + val input = socksSocket.getInputStream() + val output = socksSocket.getOutputStream() + + // SOCKS5 handshake + output.write(byteArrayOf(0x05, 0x01, 0x00)) // Version 5, 1 method, no auth + output.flush() + + val authResponse = ByteArray(2) + if (input.read(authResponse) != 2 || authResponse[0] != 0x05.toByte() || authResponse[1] != 0x00.toByte()) { + Log.e(TAG, "SOCKS auth failed") + socksSocket.close() + return@withContext null + } + + // SOCKS5 connect request + val hostBytes = host.toByteArray(StandardCharsets.UTF_8) + val request = ByteArrayOutputStream() + request.write(0x05) // Version + request.write(0x01) // Connect command + request.write(0x00) // Reserved + request.write(0x03) // Domain name address type + request.write(hostBytes.size) // Domain name length + request.write(hostBytes) // Domain name + request.write(port shr 8) // Port high byte + request.write(port and 0xFF) // Port low byte + + output.write(request.toByteArray()) + output.flush() + + // Read connect response + val response = ByteArray(10) + val bytesRead = input.read(response) + if (bytesRead < 4 || response[0] != 0x05.toByte() || response[1] != 0x00.toByte()) { + Log.e(TAG, "SOCKS connect failed") + socksSocket.close() + return@withContext null + } + + Log.d(TAG, "Successfully connected to $host:$port through SOCKS") + socksSocket + } catch (e: Exception) { + Log.e(TAG, "Failed to connect through SOCKS: ${e.message}") + null + } + } + } + + private suspend fun forwardData(input: InputStream, output: OutputStream, direction: String) { + withContext(Dispatchers.IO) { + try { + val buffer = ByteArray(8192) + while (true) { + val bytesRead = input.read(buffer) + if (bytesRead <= 0) break + output.write(buffer, 0, bytesRead) + output.flush() + } + } catch (e: Exception) { + Log.d(TAG, "Data forwarding stopped ($direction): ${e.message}") + } + } + } + + private fun sendHttpError(output: OutputStream, error: String) { + try { + val response = "HTTP/1.1 $error\r\nContent-Length: 0\r\n\r\n" + output.write(response.toByteArray()) + output.flush() + } catch (e: Exception) { + Log.e(TAG, "Error sending HTTP error: ${e.message}") + } + } + + private fun testSocksConnection() { + CoroutineScope(Dispatchers.IO).launch { + try { + Log.i(TAG, "Testing connection to mycelium SOCKS proxy at $SOCKS_HOST:$SOCKS_PORT") + val testSocket = Socket() + testSocket.connect(InetSocketAddress(SOCKS_HOST, SOCKS_PORT), 5000) + testSocket.close() + Log.i(TAG, "✅ Mycelium SOCKS proxy is reachable at $SOCKS_HOST:$SOCKS_PORT") + } catch (e: Exception) { + Log.e(TAG, "❌ Cannot connect to mycelium SOCKS proxy at $SOCKS_HOST:$SOCKS_PORT: ${e.message}") + Log.e(TAG, "Make sure mycelium is running and SOCKS proxy is enabled") + } + } + } + + /** + * Get the port number this proxy is listening on + */ + fun getPort(): Int = HTTP_PROXY_PORT +} diff --git a/android/app/src/main/kotlin/tech/threefold/mycelium/MainActivity.kt b/android/app/src/main/kotlin/tech/threefold/mycelium/MainActivity.kt index b8f28da..baaea61 100644 --- a/android/app/src/main/kotlin/tech/threefold/mycelium/MainActivity.kt +++ b/android/app/src/main/kotlin/tech/threefold/mycelium/MainActivity.kt @@ -5,6 +5,7 @@ import android.app.Activity import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.content.IntentFilter import android.net.VpnService import android.os.Build @@ -16,6 +17,11 @@ import io.flutter.plugin.common.MethodChannel import tech.threefold.mycelium.rust.uniffi.mycelmob.addressFromSecretKey import tech.threefold.mycelium.rust.uniffi.mycelmob.generateSecretKey import tech.threefold.mycelium.rust.uniffi.mycelmob.getPeerStatus +import tech.threefold.mycelium.rust.uniffi.mycelmob.proxyConnect +import tech.threefold.mycelium.rust.uniffi.mycelmob.proxyDisconnect +import tech.threefold.mycelium.rust.uniffi.mycelmob.startProxyProbe +import tech.threefold.mycelium.rust.uniffi.mycelmob.stopProxyProbe +import tech.threefold.mycelium.rust.uniffi.mycelmob.listProxies private const val tag = "[Myceliumflut]" @@ -23,14 +29,17 @@ class MainActivity: FlutterActivity() { private val channelName = "tech.threefold.mycelium/tun" private val vpnRequestCode = 0x0F - // these two variables are only used during VPN permission flow. + // these variables are only used during VPN permission flow. private var vpnPermissionPeers: List? = null private var vpnPermissionSecretKey: ByteArray? = null + private var vpnPermissionSocksEnabled: Boolean = false private lateinit var channel : MethodChannel + private lateinit var prefs: SharedPreferences override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) + prefs = getSharedPreferences("mycelium_prefs", MODE_PRIVATE) channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName) channel.setMethodCallHandler { // This method is invoked on the main thread. @@ -48,8 +57,9 @@ class MainActivity: FlutterActivity() { "startVpn" -> { val peers = call.argument>("peers")!! val secretKey = call.argument("secretKey")!! - Log.d("tff", "peers = $peers") - val started = startVpn(peers, secretKey) + val socksEnabled = call.argument("socksEnabled") ?: false + Log.d("tff", "peers = $peers, socksEnabled = $socksEnabled") + val started = startVpn(peers, secretKey, socksEnabled) result.success(started) } "stopVpn" -> { @@ -66,6 +76,63 @@ class MainActivity: FlutterActivity() { result.error("PEER_STATUS_ERROR", e.message, null) } } + "proxyConnect" -> { + try { + val remote = call.argument("remote") ?: "" + val proxyResult = proxyConnect(remote) + result.success(proxyResult) + } catch (e: Exception) { + Log.e(tag, "Error in proxyConnect: ${e.message}") + result.error("PROXY_CONNECT_ERROR", e.message, null) + } + } + "proxyDisconnect" -> { + try { + val proxyResult = proxyDisconnect() + result.success(proxyResult) + } catch (e: Exception) { + Log.e(tag, "Error in proxyDisconnect: ${e.message}") + result.error("PROXY_DISCONNECT_ERROR", e.message, null) + } + } + "startProxyProbe" -> { + try { + val proxyResult = startProxyProbe() + result.success(proxyResult) + } catch (e: Exception) { + Log.e(tag, "Error in startProxyProbe: ${e.message}") + result.error("START_PROXY_PROBE_ERROR", e.message, null) + } + } + "stopProxyProbe" -> { + try { + val proxyResult = stopProxyProbe() + result.success(proxyResult) + } catch (e: Exception) { + Log.e(tag, "Error in stopProxyProbe: ${e.message}") + result.error("STOP_PROXY_PROBE_ERROR", e.message, null) + } + } + "listProxies" -> { + try { + val proxyResult = listProxies() + result.success(proxyResult) + } catch (e: Exception) { + Log.e(tag, "Error in listProxies: ${e.message}") + result.error("LIST_PROXIES_ERROR", e.message, null) + } + } + "queryStatus" -> { + // Immediately report last known state while also querying the service + val running = prefs.getBoolean("mycelium_running", false) + if (running) { + channel.invokeMethod("notifyMyceliumStarted","") + } else { + channel.invokeMethod("notifyMyceliumFinished","") + } + queryStatus() + result.success(true) + } else -> result.notImplemented() } } @@ -79,6 +146,9 @@ class MainActivity: FlutterActivity() { TunService.EVENT_MYCELIUM_FINISHED -> { channel.invokeMethod("notifyMyceliumFinished","") } + TunService.EVENT_MYCELIUM_RUNNING -> { + channel.invokeMethod("notifyMyceliumStarted", "") + } TunService.EVENT_MYCELIUM_FAILED -> { channel.invokeMethod("notifyMyceliumFailed", "") } @@ -94,7 +164,7 @@ class MainActivity: FlutterActivity() { if (requestCode == vpnRequestCode) { if (resultCode == Activity.RESULT_OK) { Log.i(tag, "VPN permission granted by the user") - startVpn(this.vpnPermissionPeers ?: emptyList(), this.vpnPermissionSecretKey ?: ByteArray(0)) + startVpn(this.vpnPermissionPeers ?: emptyList(), this.vpnPermissionSecretKey ?: ByteArray(0), this.vpnPermissionSocksEnabled) } else { // The user denied the VPN permission, // TODO: handle this case as needed @@ -104,19 +174,20 @@ class MainActivity: FlutterActivity() { } // checkAskVpnPermission will return true if we need to ask for permission, // false otherwise. - private fun checkAskVpnPermission(peers: List, secretKey: ByteArray): Boolean{ + private fun checkAskVpnPermission(peers: List, secretKey: ByteArray, socksEnabled: Boolean): Boolean{ val intent = VpnService.prepare(this) if (intent != null) { this.vpnPermissionPeers = peers this.vpnPermissionSecretKey = secretKey + this.vpnPermissionSocksEnabled = socksEnabled startActivityForResult(intent, vpnRequestCode) return true } else { return false } } - private fun startVpn(peers: List, secretKey: ByteArray): Boolean { - if (checkAskVpnPermission(peers, secretKey)) { + private fun startVpn(peers: List, secretKey: ByteArray, socksEnabled: Boolean = false): Boolean { + if (checkAskVpnPermission(peers, secretKey, socksEnabled)) { // need to ask for permission, so stop the flow here. // permission handler will be handled by onActivityResult function return false @@ -125,6 +196,7 @@ class MainActivity: FlutterActivity() { val intent = Intent(this, TunService::class.java) intent.action = TunService.ACTION_START intent.putExtra("secret_key", secretKey) + intent.putExtra("socks_enabled", socksEnabled) intent.putStringArrayListExtra("peers", ArrayList(peers)) startService(intent) @@ -139,6 +211,12 @@ class MainActivity: FlutterActivity() { return true } + private fun queryStatus() { + val intent = Intent(this, TunService::class.java) + intent.action = TunService.ACTION_QUERY + startService(intent) + } + @SuppressLint("UnspecifiedRegisterReceiverFlag", "WrongConstant") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -187,10 +265,6 @@ class MainActivity: FlutterActivity() { override fun onDestroy() { Log.e(tag, "onDestroy") - - Log.i(tag, "onDestroy:Stopping VPN service") - stopVpn() - super.onDestroy() // Activity is about to be destroyed. @@ -211,4 +285,4 @@ class MainActivity: FlutterActivity() { // Restore UI state from the savedInstanceState bundle. } -} \ No newline at end of file +} diff --git a/android/app/src/main/kotlin/tech/threefold/mycelium/ProxyTester.kt b/android/app/src/main/kotlin/tech/threefold/mycelium/ProxyTester.kt new file mode 100644 index 0000000..006cfca --- /dev/null +++ b/android/app/src/main/kotlin/tech/threefold/mycelium/ProxyTester.kt @@ -0,0 +1,122 @@ +package tech.threefold.mycelium + +import android.util.Log +import kotlinx.coroutines.* +import java.io.* +import java.net.* + +/** + * Simple utility to test proxy connections and debug routing issues + */ +class ProxyTester { + companion object { + private const val TAG = "ProxyTester" + } + + /** + * Test if we can connect to the mycelium SOCKS proxy + */ + fun testSocksProxy() { + CoroutineScope(Dispatchers.IO).launch { + try { + Log.i(TAG, "Testing SOCKS proxy connection to 127.0.0.1:1080") + val socket = Socket() + socket.connect(InetSocketAddress("127.0.0.1", 1080), 5000) + + // Try SOCKS5 handshake + val output = socket.getOutputStream() + val input = socket.getInputStream() + + // Send SOCKS5 greeting + output.write(byteArrayOf(0x05, 0x01, 0x00)) // Version 5, 1 method, no auth + output.flush() + + // Read response + val response = ByteArray(2) + val bytesRead = input.read(response) + + if (bytesRead == 2 && response[0] == 0x05.toByte() && response[1] == 0x00.toByte()) { + Log.i(TAG, "✅ SOCKS proxy is working correctly") + } else { + Log.e(TAG, "❌ SOCKS proxy handshake failed: ${response.contentToString()}") + } + + socket.close() + } catch (e: Exception) { + Log.e(TAG, "❌ SOCKS proxy test failed: ${e.message}") + } + } + } + + /** + * Test HTTP request through our HTTP-to-SOCKS bridge + */ + fun testHttpProxy() { + CoroutineScope(Dispatchers.IO).launch { + try { + Log.i(TAG, "Testing HTTP proxy connection to 127.0.0.1:8080") + val socket = Socket() + socket.connect(InetSocketAddress("127.0.0.1", 8080), 5000) + + val output = socket.getOutputStream() + val input = BufferedReader(InputStreamReader(socket.getInputStream())) + + // Send simple HTTP request + val request = "GET http://httpbin.org/ip HTTP/1.1\r\n" + + "Host: httpbin.org\r\n" + + "Connection: close\r\n\r\n" + + output.write(request.toByteArray()) + output.flush() + + // Read response + val response = StringBuilder() + var line: String? + while (input.readLine().also { line = it } != null) { + response.append(line).append("\n") + if (response.length > 1000) break // Limit response size + } + + Log.i(TAG, "HTTP proxy response: $response") + socket.close() + } catch (e: Exception) { + Log.e(TAG, "❌ HTTP proxy test failed: ${e.message}") + } + } + } + + /** + * Test direct connection to check what IP we're actually using + */ + fun testDirectConnection() { + CoroutineScope(Dispatchers.IO).launch { + try { + Log.i(TAG, "Testing direct connection to httpbin.org") + val socket = Socket() + socket.connect(InetSocketAddress("httpbin.org", 80), 10000) + + val output = socket.getOutputStream() + val input = BufferedReader(InputStreamReader(socket.getInputStream())) + + val request = "GET /ip HTTP/1.1\r\n" + + "Host: httpbin.org\r\n" + + "Connection: close\r\n\r\n" + + output.write(request.toByteArray()) + output.flush() + + val response = StringBuilder() + var line: String? + while (input.readLine().also { line = it } != null) { + response.append(line).append("\n") + if (response.length > 1000) break + } + + Log.i(TAG, "Direct connection response: $response") + socket.close() + } catch (e: Exception) { + Log.e(TAG, "❌ Direct connection test failed: ${e.message}") + } + } + } +} diff --git a/android/app/src/main/kotlin/tech/threefold/mycelium/SocksProxy.kt b/android/app/src/main/kotlin/tech/threefold/mycelium/SocksProxy.kt new file mode 100644 index 0000000..8211d3e --- /dev/null +++ b/android/app/src/main/kotlin/tech/threefold/mycelium/SocksProxy.kt @@ -0,0 +1,129 @@ +package tech.threefold.mycelium + +import android.util.Log +import kotlinx.coroutines.* +import java.io.* +import java.net.* +import java.nio.ByteBuffer +import java.nio.channels.SocketChannel + +/** + * SOCKS5 client implementation for routing TUN traffic through local SOCKS proxy + */ +class SocksProxy { + companion object { + private const val TAG = "SocksProxy" + private const val SOCKS_VERSION = 0x05.toByte() + private const val SOCKS_CMD_CONNECT = 0x01.toByte() + private const val SOCKS_ATYP_IPV4 = 0x01.toByte() + private const val SOCKS_ATYP_IPV6 = 0x04.toByte() + private const val SOCKS_RSV = 0x00.toByte() + private const val SOCKS_AUTH_NONE = 0x00.toByte() + private const val SOCKS_SUCCESS = 0x00.toByte() + } + + /** + * Establish SOCKS5 connection to target through local proxy + */ + suspend fun connectThroughSocks(targetHost: String, targetPort: Int): Socket? { + return withContext(Dispatchers.IO) { + try { + val socksSocket = Socket("127.0.0.1", 1080) + socksSocket.soTimeout = 10000 // 10 second timeout + + val input = socksSocket.getInputStream() + val output = socksSocket.getOutputStream() + + // Step 1: Authentication negotiation + if (!performSocksHandshake(input, output)) { + Log.e(TAG, "SOCKS handshake failed") + socksSocket.close() + return@withContext null + } + + // Step 2: Connection request + if (!performSocksConnect(input, output, targetHost, targetPort)) { + Log.e(TAG, "SOCKS connect failed") + socksSocket.close() + return@withContext null + } + + Log.d(TAG, "Successfully connected to $targetHost:$targetPort through SOCKS") + socksSocket + } catch (e: Exception) { + Log.e(TAG, "Failed to connect through SOCKS: ${e.message}") + null + } + } + } + + private fun performSocksHandshake(input: InputStream, output: OutputStream): Boolean { + try { + // Send authentication methods + val authRequest = byteArrayOf( + SOCKS_VERSION, // Version + 0x01, // Number of methods + SOCKS_AUTH_NONE // No authentication + ) + output.write(authRequest) + output.flush() + + // Read server response + val response = ByteArray(2) + if (input.read(response) != 2) { + Log.e(TAG, "Failed to read SOCKS auth response") + return false + } + + if (response[0] != SOCKS_VERSION || response[1] != SOCKS_AUTH_NONE) { + Log.e(TAG, "SOCKS auth failed: version=${response[0]}, method=${response[1]}") + return false + } + + return true + } catch (e: Exception) { + Log.e(TAG, "SOCKS handshake error: ${e.message}") + return false + } + } + + private fun performSocksConnect(input: InputStream, output: OutputStream, host: String, port: Int): Boolean { + try { + val hostBytes = host.toByteArray() + val hostAddress = InetAddress.getByName(host).address + val request = byteArrayOf( + SOCKS_VERSION, // Version + SOCKS_CMD_CONNECT, // Command: CONNECT + SOCKS_RSV, // Reserved + SOCKS_ATYP_IPV4, // Address type: IPv4 + hostAddress[0], // IP byte 1 + hostAddress[1], // IP byte 2 + hostAddress[2], // IP byte 3 + hostAddress[3], // IP byte 4 + (port shr 8).toByte(), // Port high byte + (port and 0xFF).toByte() // Port low byte + ) + + output.write(request) + output.flush() + + // Read response + val response = ByteArray(10) // Max response size for IPv4 + val bytesRead = input.read(response) + if (bytesRead < 4) { + Log.e(TAG, "Invalid SOCKS connect response length: $bytesRead") + return false + } + + if (response[0] != SOCKS_VERSION || response[1] != SOCKS_SUCCESS) { + Log.e(TAG, "SOCKS connect failed: version=${response[0]}, status=${response[1]}") + return false + } + + return true + } catch (e: Exception) { + Log.e(TAG, "SOCKS connect error: ${e.message}") + return false + } + } +} diff --git a/android/app/src/main/kotlin/tech/threefold/mycelium/TunService.kt b/android/app/src/main/kotlin/tech/threefold/mycelium/TunService.kt index de09d69..7906d7b 100644 --- a/android/app/src/main/kotlin/tech/threefold/mycelium/TunService.kt +++ b/android/app/src/main/kotlin/tech/threefold/mycelium/TunService.kt @@ -1,6 +1,9 @@ package tech.threefold.mycelium import android.content.Intent +import android.net.ProxyInfo +import android.content.Context +import android.content.SharedPreferences import android.net.VpnService import android.os.ParcelFileDescriptor import android.system.OsConstants @@ -12,7 +15,6 @@ import tech.threefold.mycelium.rust.uniffi.mycelmob.stopMycelium import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext - private const val tag = "[TunService]" class TunService : VpnService(), CoroutineScope { @@ -20,13 +22,17 @@ class TunService : VpnService(), CoroutineScope { companion object { const val ACTION_START = "tech.threefold.mycelium.TunService.START" const val ACTION_STOP = "tech.threefold.mycelium.TunService.STOP" + const val ACTION_QUERY = "tech.threefold.mycelium.TunService.QUERY" const val EVENT_INTENT = "tech.threefold.mycelium.TunService.EVENT" const val EVENT_MYCELIUM_FAILED = "mycelium_failed" const val EVENT_MYCELIUM_FINISHED = "mycelium_finished" + const val EVENT_MYCELIUM_RUNNING = "mycelium_running" } private var started = AtomicBoolean() private var parcel: ParcelFileDescriptor? = null + private var httpToSocksProxy: HttpToSocksProxy? = null + private lateinit var prefs: SharedPreferences private val job = Job() @@ -36,6 +42,7 @@ class TunService : VpnService(), CoroutineScope { override fun onCreate() { Log.d(tag, "tun service created") super.onCreate() + prefs = getSharedPreferences("mycelium_prefs", Context.MODE_PRIVATE) } override fun onDestroy() { @@ -60,9 +67,18 @@ class TunService : VpnService(), CoroutineScope { ACTION_START -> { val secretKey = intent.getByteArrayExtra("secret_key") ?: ByteArray(0) val peers = intent.getStringArrayListExtra("peers") ?: emptyList() - start(peers.toList(), secretKey) + val socksEnabled = intent.getBooleanExtra("socks_enabled", false) + start(peers.toList(), secretKey, socksEnabled) START_STICKY } + ACTION_QUERY -> { + if (started.get()) { + sendMyceliumEvent(EVENT_MYCELIUM_RUNNING) + } else { + sendMyceliumEvent(EVENT_MYCELIUM_FINISHED) + } + START_NOT_STICKY + } else -> { Log.e(tag, "unknown command") @@ -71,23 +87,43 @@ class TunService : VpnService(), CoroutineScope { } - private fun start(peers: List, secretKey: ByteArray): Int { + private fun start(peers: List, secretKey: ByteArray, socksEnabled: Boolean = false): Int { if (!started.compareAndSet(false, true)) { return 0 } + prefs.edit().putBoolean("mycelium_running", true).apply() val nodeAddress = addressFromSecretKey(secretKey) Log.i(tag, "creating TUN device with node address: $nodeAddress") val builder = Builder() .addAddress(nodeAddress, 64) .addRoute("400::", 7) - .allowBypass() + .addDnsServer("8.8.8.8") + .addDnsServer("8.8.4.4") + .addDnsServer("2001:4860:4860::8888") + .addDnsServer("2001:4860:4860::8844") .allowFamily(OsConstants.AF_INET) - //.allowFamily(OsConstants.AF_INET6) - //.setBlocking(true) - //.setMtu(1400) + .allowFamily(OsConstants.AF_INET6) + .setMtu(1420) .setSession("mycelium") + // If SOCKS proxy is enabled, route all internet traffic through VPN + if (socksEnabled) { + // Route all traffic except localhost to prevent loops + builder.addRoute("1.0.0.0", 8) // 1.0.0.0/8 + builder.addRoute("2.0.0.0", 7) // 2.0.0.0/7 (covers 2.0.0.0-3.255.255.255) + builder.addRoute("4.0.0.0", 6) // 4.0.0.0/6 (covers 4.0.0.0-7.255.255.255) + builder.addRoute("8.0.0.0", 5) // 8.0.0.0/5 (covers 8.0.0.0-15.255.255.255) + builder.addRoute("16.0.0.0", 4) // 16.0.0.0/4 (covers 16.0.0.0-31.255.255.255) + builder.addRoute("32.0.0.0", 3) // 32.0.0.0/3 (covers 32.0.0.0-63.255.255.255) + builder.addRoute("64.0.0.0", 2) // 64.0.0.0/2 (covers 64.0.0.0-127.255.255.255) + builder.addRoute("128.0.0.0", 1) // 128.0.0.0/1 (covers 128.0.0.0-255.255.255.255) + // This excludes 127.0.0.0/8 (localhost) to prevent SOCKS proxy loops + + // Set HTTP proxy to use our HTTP-to-SOCKS bridge + builder.setHttpProxy(ProxyInfo.buildDirectProxy("127.0.0.1", 8080)) + } + parcel = builder.establish() @@ -100,6 +136,21 @@ class TunService : VpnService(), CoroutineScope { } Log.d(tag, "starting mycelium with parcel fd: " + parcel.fd) + + // Start HTTP-to-SOCKS proxy bridge if SOCKS is enabled + if (socksEnabled) { + Log.i(tag, "Starting HTTP-to-SOCKS proxy bridge") + httpToSocksProxy = HttpToSocksProxy() + httpToSocksProxy?.start() + Log.i(tag, "SOCKS proxy enabled - HTTP traffic will be routed through 127.0.0.1:8080 -> 127.0.0.1:1080") + + // Run diagnostic tests + val tester = ProxyTester() + tester.testSocksProxy() + tester.testHttpProxy() + tester.testDirectConnection() + } + launch { try { startMycelium(peers, parcel.fd, secretKey) @@ -109,7 +160,7 @@ class TunService : VpnService(), CoroutineScope { sendMyceliumEvent(EVENT_MYCELIUM_FAILED) } else { Log.i(tag, "mycelium finished cleanly") - sendMyceliumEvent(EVENT_MYCELIUM_FINISHED) + stop(true) } } catch (e: Exception) { @@ -127,6 +178,13 @@ class TunService : VpnService(), CoroutineScope { Log.d(tag, "got stop when not started") return } + + // Stop HTTP-to-SOCKS proxy bridge + httpToSocksProxy?.stop() + httpToSocksProxy = null + Log.d(tag, "Cleaned up SOCKS proxy resources") + + prefs.edit().putBoolean("mycelium_running", false).apply() if (stopMycelium) { stopMycelium() } @@ -139,4 +197,4 @@ class TunService : VpnService(), CoroutineScope { intent.putExtra("event", event) sendBroadcast(intent) } -} \ No newline at end of file +} diff --git a/android/app/src/main/kotlin/tech/threefold/mycelium/TunToSocksForwarder.kt b/android/app/src/main/kotlin/tech/threefold/mycelium/TunToSocksForwarder.kt new file mode 100644 index 0000000..c9633cc --- /dev/null +++ b/android/app/src/main/kotlin/tech/threefold/mycelium/TunToSocksForwarder.kt @@ -0,0 +1,277 @@ +package tech.threefold.mycelium + +import android.os.ParcelFileDescriptor +import android.util.Log +import kotlinx.coroutines.* +import java.io.* +import java.net.* +import java.nio.ByteBuffer +import java.nio.channels.FileChannel +import java.util.concurrent.ConcurrentHashMap + +/** + * TUN-to-SOCKS forwarder that intercepts packets from TUN interface + * and routes them through the local SOCKS proxy + */ +class TunToSocksForwarder(private val tunFd: ParcelFileDescriptor) { + companion object { + private const val TAG = "TunToSocksForwarder" + private const val MTU = 1500 + private const val TCP_PROTOCOL = 6 + private const val UDP_PROTOCOL = 17 + } + + private val socksProxy = SocksProxy() + private val activeConnections = ConcurrentHashMap() + private var isRunning = false + private var forwarderJob: Job? = null + + /** + * Start the TUN-to-SOCKS forwarding process + */ + fun start() { + if (isRunning) { + Log.w(TAG, "Forwarder already running") + return + } + + isRunning = true + Log.i(TAG, "Starting TUN-to-SOCKS forwarder") + + forwarderJob = CoroutineScope(Dispatchers.IO).launch { + try { + val tunInput = FileInputStream(tunFd.fileDescriptor) + val tunOutput = FileOutputStream(tunFd.fileDescriptor) + val buffer = ByteArray(MTU) + + Log.i(TAG, "TUN forwarder started, reading packets...") + + while (isRunning) { + try { + val bytesRead = tunInput.read(buffer) + if (bytesRead > 0) { + Log.d(TAG, "Read $bytesRead bytes from TUN interface") + processPacket(buffer, bytesRead, tunOutput) + } else if (bytesRead == 0) { + // No data available, small delay to prevent busy waiting + delay(10) + } + } catch (e: Exception) { + if (isRunning) { + Log.e(TAG, "Error processing packet: ${e.message}") + delay(100) // Prevent rapid error loops + } + } + } + } catch (e: Exception) { + Log.e(TAG, "TUN forwarder error: ${e.message}") + } + } + } + + /** + * Stop the forwarder and close all connections + */ + fun stop() { + Log.i(TAG, "Stopping TUN-to-SOCKS forwarder") + isRunning = false + forwarderJob?.cancel() + + // Close all active connections + activeConnections.values.forEach { socket -> + try { + socket.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing connection: ${e.message}") + } + } + activeConnections.clear() + } + + private suspend fun processPacket(buffer: ByteArray, length: Int, tunOutput: FileOutputStream) { + try { + val packet = parseIPPacket(buffer, length) ?: return + + // Only handle TCP for now (HTTP/HTTPS traffic) + if (packet.protocol != TCP_PROTOCOL) { + return + } + + val connectionKey = "${packet.srcIP}:${packet.srcPort}->${packet.dstIP}:${packet.dstPort}" + + // Check if this is a new connection + if (!activeConnections.containsKey(connectionKey)) { + handleNewConnection(packet, connectionKey, tunOutput) + } else { + // Forward data for existing connection + forwardData(packet, connectionKey, tunOutput) + } + } catch (e: Exception) { + Log.e(TAG, "Error processing packet: ${e.message}") + } + } + + private suspend fun handleNewConnection(packet: IPPacket, connectionKey: String, tunOutput: FileOutputStream) { + try { + Log.d(TAG, "New connection: $connectionKey") + + // Connect through SOCKS proxy + val socksSocket = socksProxy.connectThroughSocks(packet.dstIP, packet.dstPort) + if (socksSocket == null) { + Log.e(TAG, "Failed to establish SOCKS connection for $connectionKey") + return + } + + activeConnections[connectionKey] = socksSocket + + // Start forwarding data in both directions + CoroutineScope(Dispatchers.IO).launch { + forwardFromSocksToTun(socksSocket, packet, tunOutput, connectionKey) + } + + // Forward the initial packet data + if (packet.payload.isNotEmpty()) { + socksSocket.getOutputStream().write(packet.payload) + socksSocket.getOutputStream().flush() + } + + } catch (e: Exception) { + Log.e(TAG, "Error handling new connection: ${e.message}") + } + } + + private suspend fun forwardData(packet: IPPacket, connectionKey: String, tunOutput: FileOutputStream) { + try { + val socket = activeConnections[connectionKey] ?: return + + if (packet.payload.isNotEmpty()) { + socket.getOutputStream().write(packet.payload) + socket.getOutputStream().flush() + } + } catch (e: Exception) { + Log.e(TAG, "Error forwarding data: ${e.message}") + // Remove failed connection + activeConnections.remove(connectionKey)?.close() + } + } + + private suspend fun forwardFromSocksToTun( + socket: Socket, + originalPacket: IPPacket, + tunOutput: FileOutputStream, + connectionKey: String + ) { + try { + val input = socket.getInputStream() + val buffer = ByteArray(MTU) + + while (isRunning && !socket.isClosed) { + val bytesRead = input.read(buffer) + if (bytesRead <= 0) break + + // Create response packet and write to TUN + val responsePacket = createResponsePacket( + originalPacket, + buffer.copyOf(bytesRead) + ) + tunOutput.write(responsePacket) + tunOutput.flush() + } + } catch (e: Exception) { + Log.e(TAG, "Error forwarding from SOCKS to TUN: ${e.message}") + } finally { + activeConnections.remove(connectionKey) + try { + socket.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing socket: ${e.message}") + } + } + } + + private fun parseIPPacket(buffer: ByteArray, length: Int): IPPacket? { + try { + if (length < 20) return null // Minimum IP header size + + val version = (buffer[0].toInt() shr 4) and 0xF + if (version != 4) return null // Only IPv4 for now + + val headerLength = (buffer[0].toInt() and 0xF) * 4 + val protocol = buffer[9].toInt() and 0xFF + + // Extract source and destination IPs + val srcIP = "${buffer[12].toUByte()}.${buffer[13].toUByte()}.${buffer[14].toUByte()}.${buffer[15].toUByte()}" + val dstIP = "${buffer[16].toUByte()}.${buffer[17].toUByte()}.${buffer[18].toUByte()}.${buffer[19].toUByte()}" + + if (protocol != TCP_PROTOCOL) return null + + // Extract TCP ports + val tcpHeaderOffset = headerLength + if (length < tcpHeaderOffset + 4) return null + + val srcPort = ((buffer[tcpHeaderOffset].toInt() and 0xFF) shl 8) or (buffer[tcpHeaderOffset + 1].toInt() and 0xFF) + val dstPort = ((buffer[tcpHeaderOffset + 2].toInt() and 0xFF) shl 8) or (buffer[tcpHeaderOffset + 3].toInt() and 0xFF) + + val tcpHeaderLength = ((buffer[tcpHeaderOffset + 12].toInt() and 0xFF) shr 4) * 4 + val payloadOffset = tcpHeaderOffset + tcpHeaderLength + val payload = if (payloadOffset < length) { + buffer.copyOfRange(payloadOffset, length) + } else { + byteArrayOf() + } + + return IPPacket(srcIP, srcPort, dstIP, dstPort, protocol, payload) + } catch (e: Exception) { + Log.e(TAG, "Error parsing IP packet: ${e.message}") + return null + } + } + + private fun createResponsePacket(originalPacket: IPPacket, payload: ByteArray): ByteArray { + // Create a simple TCP response packet + // This is a simplified implementation - in production you'd want proper TCP state management + val ipHeader = ByteArray(20) + val tcpHeader = ByteArray(20) + + // IP Header (simplified) + ipHeader[0] = 0x45.toByte() // Version 4, Header length 5*4=20 + ipHeader[1] = 0x00.toByte() // Type of service + val totalLength = 20 + 20 + payload.size + ipHeader[2] = (totalLength shr 8).toByte() + ipHeader[3] = (totalLength and 0xFF).toByte() + ipHeader[9] = TCP_PROTOCOL.toByte() + + // Source IP (swap with destination) + val dstIPParts = originalPacket.dstIP.split(".") + ipHeader[12] = dstIPParts[0].toInt().toByte() + ipHeader[13] = dstIPParts[1].toInt().toByte() + ipHeader[14] = dstIPParts[2].toInt().toByte() + ipHeader[15] = dstIPParts[3].toInt().toByte() + + // Destination IP (swap with source) + val srcIPParts = originalPacket.srcIP.split(".") + ipHeader[16] = srcIPParts[0].toInt().toByte() + ipHeader[17] = srcIPParts[1].toInt().toByte() + ipHeader[18] = srcIPParts[2].toInt().toByte() + ipHeader[19] = srcIPParts[3].toInt().toByte() + + // TCP Header (simplified) + tcpHeader[0] = (originalPacket.dstPort shr 8).toByte() + tcpHeader[1] = (originalPacket.dstPort and 0xFF).toByte() + tcpHeader[2] = (originalPacket.srcPort shr 8).toByte() + tcpHeader[3] = (originalPacket.srcPort and 0xFF).toByte() + tcpHeader[12] = 0x50.toByte() // Header length 5*4=20 + tcpHeader[13] = 0x18.toByte() // PSH + ACK flags + + return ipHeader + tcpHeader + payload + } + + data class IPPacket( + val srcIP: String, + val srcPort: Int, + val dstIP: String, + val dstPort: Int, + val protocol: Int, + val payload: ByteArray + ) +} diff --git a/android/app/src/main/kotlin/tech/threefold/mycelium/rust/uniffi/mycelmob/mycelmob.kt b/android/app/src/main/kotlin/tech/threefold/mycelium/rust/uniffi/mycelmob/mycelmob.kt index 68d87e2..89875f9 100644 --- a/android/app/src/main/kotlin/tech/threefold/mycelium/rust/uniffi/mycelmob/mycelmob.kt +++ b/android/app/src/main/kotlin/tech/threefold/mycelium/rust/uniffi/mycelmob/mycelmob.kt @@ -714,6 +714,16 @@ internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback { + + + + + + + + + + @@ -750,10 +760,20 @@ internal interface UniffiLib : Library { ): Int fun uniffi_mycelmob_fn_func_hello_mycelios(uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue + fun uniffi_mycelmob_fn_func_list_proxies(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_mycelmob_fn_func_proxy_connect(`remote`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_mycelmob_fn_func_proxy_disconnect(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue fun uniffi_mycelmob_fn_func_start_mycelium(`peers`: RustBuffer.ByValue,`tunFd`: Int,`secretKey`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Unit + fun uniffi_mycelmob_fn_func_start_proxy_probe(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue fun uniffi_mycelmob_fn_func_stop_mycelium(uniffi_out_err: UniffiRustCallStatus, ): Unit + fun uniffi_mycelmob_fn_func_stop_proxy_probe(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue fun ffi_mycelmob_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue fun ffi_mycelmob_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, @@ -876,10 +896,20 @@ internal interface UniffiLib : Library { ): Short fun uniffi_mycelmob_checksum_func_hello_mycelios( ): Short + fun uniffi_mycelmob_checksum_func_list_proxies( + ): Short + fun uniffi_mycelmob_checksum_func_proxy_connect( + ): Short + fun uniffi_mycelmob_checksum_func_proxy_disconnect( + ): Short fun uniffi_mycelmob_checksum_func_start_mycelium( ): Short + fun uniffi_mycelmob_checksum_func_start_proxy_probe( + ): Short fun uniffi_mycelmob_checksum_func_stop_mycelium( ): Short + fun uniffi_mycelmob_checksum_func_stop_proxy_probe( + ): Short fun ffi_mycelmob_uniffi_contract_version( ): Int @@ -912,12 +942,27 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_mycelmob_checksum_func_hello_mycelios() != 48239.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_mycelmob_checksum_func_list_proxies() != 65480.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_mycelmob_checksum_func_proxy_connect() != 60646.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_mycelmob_checksum_func_proxy_disconnect() != 62431.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_mycelmob_checksum_func_start_mycelium() != 61012.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_mycelmob_checksum_func_start_proxy_probe() != 23502.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_mycelmob_checksum_func_stop_mycelium() != 28488.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_mycelmob_checksum_func_stop_proxy_probe() != 19306.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } } // Async support @@ -1135,6 +1180,33 @@ public object FfiConverterSequenceString: FfiConverterRustBuffer { + return FfiConverterSequenceString.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_mycelmob_fn_func_list_proxies( + _status) +} + ) + } + + fun `proxyConnect`(`remote`: kotlin.String): List { + return FfiConverterSequenceString.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_mycelmob_fn_func_proxy_connect( + FfiConverterString.lower(`remote`),_status) +} + ) + } + + fun `proxyDisconnect`(): List { + return FfiConverterSequenceString.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_mycelmob_fn_func_proxy_disconnect( + _status) +} + ) + } + fun `startMycelium`(`peers`: List, `tunFd`: kotlin.Int, `secretKey`: kotlin.ByteArray) = uniffiRustCall() { _status -> @@ -1143,6 +1215,15 @@ public object FfiConverterSequenceString: FfiConverterRustBuffer { + return FfiConverterSequenceString.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_mycelmob_fn_func_start_proxy_probe( + _status) +} + ) + } + fun `stopMycelium`() = uniffiRustCall() { _status -> @@ -1151,5 +1232,14 @@ public object FfiConverterSequenceString: FfiConverterRustBuffer { + return FfiConverterSequenceString.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_mycelmob_fn_func_stop_proxy_probe( + _status) +} + ) + } + diff --git a/assets/images/tray_icon.ico b/assets/images/tray_icon.ico new file mode 100644 index 0000000..4319b81 Binary files /dev/null and b/assets/images/tray_icon.ico differ diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 5f970b5..7c54774 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -61,8 +61,57 @@ import OSLog self.stopMycelium() result(true) case "getPeerStatus": - let peerStatus = getPeerStatus() - result(peerStatus) + do { + let peerStatus = getPeerStatus() + result(peerStatus) + } catch { + debuglog("Error getting peer status: \(error.localizedDescription)") + result(FlutterError(code: "PEER_STATUS_ERROR", message: error.localizedDescription, details: nil)) + } + case "proxyConnect": + do { + let remote = call.arguments as? String ?? "" + let proxyResult = proxyConnect(remote: remote) + result(proxyResult) + } catch { + debuglog("Error in proxyConnect: \(error.localizedDescription)") + result(FlutterError(code: "PROXY_CONNECT_ERROR", message: error.localizedDescription, details: nil)) + } + case "proxyDisconnect": + do { + let proxyResult = proxyDisconnect() + result(proxyResult) + } catch { + debuglog("Error in proxyDisconnect: \(error.localizedDescription)") + result(FlutterError(code: "PROXY_DISCONNECT_ERROR", message: error.localizedDescription, details: nil)) + } + case "startProxyProbe": + do { + let proxyResult = startProxyProbe() + result(proxyResult) + } catch { + debuglog("Error in startProxyProbe: \(error.localizedDescription)") + result(FlutterError(code: "START_PROXY_PROBE_ERROR", message: error.localizedDescription, details: nil)) + } + case "stopProxyProbe": + do { + let proxyResult = stopProxyProbe() + result(proxyResult) + } catch { + debuglog("Error in stopProxyProbe: \(error.localizedDescription)") + result(FlutterError(code: "STOP_PROXY_PROBE_ERROR", message: error.localizedDescription, details: nil)) + } + case "listProxies": + do { + let proxyResult = listProxies() + result(proxyResult) + } catch { + debuglog("Error in listProxies: \(error.localizedDescription)") + result(FlutterError(code: "LIST_PROXIES_ERROR", message: error.localizedDescription, details: nil)) + } + case "queryStatus": + self.queryStatus() + result(true) default: result(FlutterMethodNotImplemented) } @@ -79,7 +128,6 @@ import OSLog override func applicationWillTerminate(_ application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. // Insert code here to handle when the app is about to terminate - self.stopMycelium() super.applicationWillTerminate(application) } @@ -230,6 +278,31 @@ import OSLog self.vpnManager?.connection.stopVPNTunnel() } + func queryStatus() { + NETunnelProviderManager.loadAllFromPreferences { (providers: [NETunnelProviderManager]?, error: Error?) in + if let error = error { + errlog("queryStatus loadAllFromPref failed:" + error.localizedDescription) + self.flutterChannel?.invokeMethod("notifyMyceliumFinished", arguments: nil) + return + } + guard let providers = providers else { + self.flutterChannel?.invokeMethod("notifyMyceliumFinished", arguments: nil) + return + } + let myProvider = providers.first(where: { $0.protocolConfiguration?.serverAddress==self.vpnServerAddress }) + if let vpnManager = myProvider { + switch vpnManager.connection.status { + case .connected, .connecting, .reasserting: + self.flutterChannel?.invokeMethod("notifyMyceliumStarted", arguments: nil) + default: + self.flutterChannel?.invokeMethod("notifyMyceliumFinished", arguments: nil) + } + } else { + self.flutterChannel?.invokeMethod("notifyMyceliumFinished", arguments: nil) + } + } + } + /* TODO: add some handlers to below func func applicationWillResignActive(_ application: UIApplication) { diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fa..d0d98aa 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 1a42d6c..9951c4c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 4af4d16..c627c47 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 95f895e..56a7aae 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 8c83b70..0768076 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index a231957..c4a314c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index 0c365bb..2b94976 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index c7c00f4..805e21a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 95f895e..56a7aae 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index e720e5b..c75908f 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 7baf56f..2fef36a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..73b0278 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..bf075d0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..06903d9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..c8a45a8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 7baf56f..2fef36a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 28d547f..3ead9ea 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..b01c27a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..f19aa89 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 3ce7e34..9a47102 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 12075aa..c1d279f 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 6f8b4f3..4b1c002 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/mycelmob.swift b/ios/Runner/mycelmob.swift new file mode 100644 index 0000000..66a2fc8 --- /dev/null +++ b/ios/Runner/mycelmob.swift @@ -0,0 +1,594 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +// swiftlint:disable all +import Foundation + +// Depending on the consumer's build setup, the low-level FFI code +// might be in a separate module, or it might be compiled inline into +// this module. This is a bit of light hackery to work with both. +#if canImport(mycelmobFFI) +import mycelmobFFI +#endif + +fileprivate extension RustBuffer { + // Allocate a new buffer, copying the contents of a `UInt8` array. + init(bytes: [UInt8]) { + let rbuf = bytes.withUnsafeBufferPointer { ptr in + RustBuffer.from(ptr) + } + self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data) + } + + static func empty() -> RustBuffer { + RustBuffer(capacity: 0, len:0, data: nil) + } + + static func from(_ ptr: UnsafeBufferPointer) -> RustBuffer { + try! rustCall { ffi_mycelmob_rustbuffer_from_bytes(ForeignBytes(bufferPointer: ptr), $0) } + } + + // Frees the buffer in place. + // The buffer must not be used after this is called. + func deallocate() { + try! rustCall { ffi_mycelmob_rustbuffer_free(self, $0) } + } +} + +fileprivate extension ForeignBytes { + init(bufferPointer: UnsafeBufferPointer) { + self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) + } +} + +// For every type used in the interface, we provide helper methods for conveniently +// lifting and lowering that type from C-compatible data, and for reading and writing +// values of that type in a buffer. + +// Helper classes/extensions that don't change. +// Someday, this will be in a library of its own. + +fileprivate extension Data { + init(rustBuffer: RustBuffer) { + self.init( + bytesNoCopy: rustBuffer.data!, + count: Int(rustBuffer.len), + deallocator: .none + ) + } +} + +// Define reader functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. +// +// With external types, one swift source file needs to be able to call the read +// method on another source file's FfiConverter, but then what visibility +// should Reader have? +// - If Reader is fileprivate, then this means the read() must also +// be fileprivate, which doesn't work with external types. +// - If Reader is internal/public, we'll get compile errors since both source +// files will try define the same type. +// +// Instead, the read() method and these helper functions input a tuple of data + +fileprivate func createReader(data: Data) -> (data: Data, offset: Data.Index) { + (data: data, offset: 0) +} + +// Reads an integer at the current offset, in big-endian order, and advances +// the offset on success. Throws if reading the integer would move the +// offset past the end of the buffer. +fileprivate func readInt(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { + let range = reader.offset...size + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + if T.self == UInt8.self { + let value = reader.data[reader.offset] + reader.offset += 1 + return value as! T + } + var value: T = 0 + let _ = withUnsafeMutableBytes(of: &value, { reader.data.copyBytes(to: $0, from: range)}) + reader.offset = range.upperBound + return value.bigEndian +} + +// Reads an arbitrary number of bytes, to be used to read +// raw bytes, this is useful when lifting strings +fileprivate func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> Array { + let range = reader.offset..<(reader.offset+count) + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + var value = [UInt8](repeating: 0, count: count) + value.withUnsafeMutableBufferPointer({ buffer in + reader.data.copyBytes(to: buffer, from: range) + }) + reader.offset = range.upperBound + return value +} + +// Reads a float at the current offset. +fileprivate func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { + return Float(bitPattern: try readInt(&reader)) +} + +// Reads a float at the current offset. +fileprivate func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { + return Double(bitPattern: try readInt(&reader)) +} + +// Indicates if the offset has reached the end of the buffer. +fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { + return reader.offset < reader.data.count +} + +// Define writer functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. See the above discussion on Readers for details. + +fileprivate func createWriter() -> [UInt8] { + return [] +} + +fileprivate func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { + writer.append(contentsOf: byteArr) +} + +// Writes an integer in big-endian order. +// +// Warning: make sure what you are trying to write +// is in the correct type! +fileprivate func writeInt(_ writer: inout [UInt8], _ value: T) { + var value = value.bigEndian + withUnsafeBytes(of: &value) { writer.append(contentsOf: $0) } +} + +fileprivate func writeFloat(_ writer: inout [UInt8], _ value: Float) { + writeInt(&writer, value.bitPattern) +} + +fileprivate func writeDouble(_ writer: inout [UInt8], _ value: Double) { + writeInt(&writer, value.bitPattern) +} + +// Protocol for types that transfer other types across the FFI. This is +// analogous to the Rust trait of the same name. +fileprivate protocol FfiConverter { + associatedtype FfiType + associatedtype SwiftType + + static func lift(_ value: FfiType) throws -> SwiftType + static func lower(_ value: SwiftType) -> FfiType + static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType + static func write(_ value: SwiftType, into buf: inout [UInt8]) +} + +// Types conforming to `Primitive` pass themselves directly over the FFI. +fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { } + +extension FfiConverterPrimitive { +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lift(_ value: FfiType) throws -> SwiftType { + return value + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lower(_ value: SwiftType) -> FfiType { + return value + } +} + +// Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`. +// Used for complex types where it's hard to write a custom lift/lower. +fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} + +extension FfiConverterRustBuffer { +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lift(_ buf: RustBuffer) throws -> SwiftType { + var reader = createReader(data: Data(rustBuffer: buf)) + let value = try read(from: &reader) + if hasRemaining(reader) { + throw UniffiInternalError.incompleteData + } + buf.deallocate() + return value + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lower(_ value: SwiftType) -> RustBuffer { + var writer = createWriter() + write(value, into: &writer) + return RustBuffer(bytes: writer) + } +} +// An error type for FFI errors. These errors occur at the UniFFI level, not +// the library level. +fileprivate enum UniffiInternalError: LocalizedError { + case bufferOverflow + case incompleteData + case unexpectedOptionalTag + case unexpectedEnumCase + case unexpectedNullPointer + case unexpectedRustCallStatusCode + case unexpectedRustCallError + case unexpectedStaleHandle + case rustPanic(_ message: String) + + public var errorDescription: String? { + switch self { + case .bufferOverflow: return "Reading the requested value would read past the end of the buffer" + case .incompleteData: return "The buffer still has data after lifting its containing value" + case .unexpectedOptionalTag: return "Unexpected optional tag; should be 0 or 1" + case .unexpectedEnumCase: return "Raw enum value doesn't match any cases" + case .unexpectedNullPointer: return "Raw pointer value was null" + case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" + case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" + case .unexpectedStaleHandle: return "The object in the handle map has been dropped already" + case let .rustPanic(message): return message + } + } +} + +fileprivate extension NSLock { + func withLock(f: () throws -> T) rethrows -> T { + self.lock() + defer { self.unlock() } + return try f() + } +} + +fileprivate let CALL_SUCCESS: Int8 = 0 +fileprivate let CALL_ERROR: Int8 = 1 +fileprivate let CALL_UNEXPECTED_ERROR: Int8 = 2 +fileprivate let CALL_CANCELLED: Int8 = 3 + +fileprivate extension RustCallStatus { + init() { + self.init( + code: CALL_SUCCESS, + errorBuf: RustBuffer.init( + capacity: 0, + len: 0, + data: nil + ) + ) + } +} + +private func rustCall(_ callback: (UnsafeMutablePointer) -> T) throws -> T { + let neverThrow: ((RustBuffer) throws -> Never)? = nil + return try makeRustCall(callback, errorHandler: neverThrow) +} + +private func rustCallWithError( + _ errorHandler: @escaping (RustBuffer) throws -> E, + _ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: errorHandler) +} + +private func makeRustCall( + _ callback: (UnsafeMutablePointer) -> T, + errorHandler: ((RustBuffer) throws -> E)? +) throws -> T { + uniffiEnsureInitialized() + var callStatus = RustCallStatus.init() + let returnedVal = callback(&callStatus) + try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: errorHandler) + return returnedVal +} + +private func uniffiCheckCallStatus( + callStatus: RustCallStatus, + errorHandler: ((RustBuffer) throws -> E)? +) throws { + switch callStatus.code { + case CALL_SUCCESS: + return + + case CALL_ERROR: + if let errorHandler = errorHandler { + throw try errorHandler(callStatus.errorBuf) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.unexpectedRustCallError + } + + case CALL_UNEXPECTED_ERROR: + // When the rust code sees a panic, it tries to construct a RustBuffer + // with the message. But if that code panics, then it just sends back + // an empty buffer. + if callStatus.errorBuf.len > 0 { + throw UniffiInternalError.rustPanic(try FfiConverterString.lift(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } + + case CALL_CANCELLED: + fatalError("Cancellation not supported yet") + + default: + throw UniffiInternalError.unexpectedRustCallStatusCode + } +} + +private func uniffiTraitInterfaceCall( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + writeReturn: (T) -> () +) { + do { + try writeReturn(makeCall()) + } catch let error { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) + } +} + +private func uniffiTraitInterfaceCallWithError( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + writeReturn: (T) -> (), + lowerError: (E) -> RustBuffer +) { + do { + try writeReturn(makeCall()) + } catch let error as E { + callStatus.pointee.code = CALL_ERROR + callStatus.pointee.errorBuf = lowerError(error) + } catch { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) + } +} +fileprivate class UniffiHandleMap { + private var map: [UInt64: T] = [:] + private let lock = NSLock() + private var currentHandle: UInt64 = 1 + + func insert(obj: T) -> UInt64 { + lock.withLock { + let handle = currentHandle + currentHandle += 1 + map[handle] = obj + return handle + } + } + + func get(handle: UInt64) throws -> T { + try lock.withLock { + guard let obj = map[handle] else { + throw UniffiInternalError.unexpectedStaleHandle + } + return obj + } + } + + @discardableResult + func remove(handle: UInt64) throws -> T { + try lock.withLock { + guard let obj = map.removeValue(forKey: handle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return obj + } + } + + var count: Int { + get { + map.count + } + } +} + + +// Public interface members begin here. + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterInt32: FfiConverterPrimitive { + typealias FfiType = Int32 + typealias SwiftType = Int32 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int32 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int32, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterString: FfiConverter { + typealias SwiftType = String + typealias FfiType = RustBuffer + + public static func lift(_ value: RustBuffer) throws -> String { + defer { + value.deallocate() + } + if value.data == nil { + return String() + } + let bytes = UnsafeBufferPointer(start: value.data!, count: Int(value.len)) + return String(bytes: bytes, encoding: String.Encoding.utf8)! + } + + public static func lower(_ value: String) -> RustBuffer { + return value.utf8CString.withUnsafeBufferPointer { ptr in + // The swift string gives us int8_t, we want uint8_t. + ptr.withMemoryRebound(to: UInt8.self) { ptr in + // The swift string gives us a trailing null byte, we don't want it. + let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1)) + return RustBuffer.from(buf) + } + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String { + let len: Int32 = try readInt(&buf) + return String(bytes: try readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! + } + + public static func write(_ value: String, into buf: inout [UInt8]) { + let len = Int32(value.utf8.count) + writeInt(&buf, len) + writeBytes(&buf, value.utf8) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterData: FfiConverterRustBuffer { + typealias SwiftType = Data + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Data { + let len: Int32 = try readInt(&buf) + return Data(try readBytes(&buf, count: Int(len))) + } + + public static func write(_ value: Data, into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + writeBytes(&buf, value) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceString: FfiConverterRustBuffer { + typealias SwiftType = [String] + + public static func write(_ value: [String], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterString.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [String] { + let len: Int32 = try readInt(&buf) + var seq = [String]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterString.read(from: &buf)) + } + return seq + } +} +public func addressFromSecretKey(data: Data) -> String { + return try! FfiConverterString.lift(try! rustCall() { + uniffi_mycelmob_fn_func_address_from_secret_key( + FfiConverterData.lower(data),$0 + ) +}) +} +public func generateSecretKey() -> Data { + return try! FfiConverterData.lift(try! rustCall() { + uniffi_mycelmob_fn_func_generate_secret_key($0 + ) +}) +} +public func getPeerStatus() -> [String] { + return try! FfiConverterSequenceString.lift(try! rustCall() { + uniffi_mycelmob_fn_func_get_peer_status($0 + ) +}) +} +public func helloInt() -> Int32 { + return try! FfiConverterInt32.lift(try! rustCall() { + uniffi_mycelmob_fn_func_hello_int($0 + ) +}) +} +public func helloMycelios() -> String { + return try! FfiConverterString.lift(try! rustCall() { + uniffi_mycelmob_fn_func_hello_mycelios($0 + ) +}) +} +public func startMycelium(peers: [String], tunFd: Int32, secretKey: Data) {try! rustCall() { + uniffi_mycelmob_fn_func_start_mycelium( + FfiConverterSequenceString.lower(peers), + FfiConverterInt32.lower(tunFd), + FfiConverterData.lower(secretKey),$0 + ) +} +} +public func stopMycelium() {try! rustCall() { + uniffi_mycelmob_fn_func_stop_mycelium($0 + ) +} +} + +private enum InitializationResult { + case ok + case contractVersionMismatch + case apiChecksumMismatch +} +// Use a global variable to perform the versioning checks. Swift ensures that +// the code inside is only computed once. +private var initializationResult: InitializationResult = { + // Get the bindings contract version from our ComponentInterface + let bindings_contract_version = 26 + // Get the scaffolding contract version by calling the into the dylib + let scaffolding_contract_version = ffi_mycelmob_uniffi_contract_version() + if bindings_contract_version != scaffolding_contract_version { + return InitializationResult.contractVersionMismatch + } + if (uniffi_mycelmob_checksum_func_address_from_secret_key() != 39059) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_mycelmob_checksum_func_generate_secret_key() != 63601) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_mycelmob_checksum_func_get_peer_status() != 1198) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_mycelmob_checksum_func_hello_int() != 31063) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_mycelmob_checksum_func_hello_mycelios() != 48239) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_mycelmob_checksum_func_start_mycelium() != 61012) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_mycelmob_checksum_func_stop_mycelium() != 28488) { + return InitializationResult.apiChecksumMismatch + } + + return InitializationResult.ok +}() + +private func uniffiEnsureInitialized() { + switch initializationResult { + case .ok: + break + case .contractVersionMismatch: + fatalError("UniFFI contract version mismatch: try cleaning and rebuilding your project") + case .apiChecksumMismatch: + fatalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} + +// swiftlint:enable all \ No newline at end of file diff --git a/lib/app/theme/app_theme.dart b/lib/app/theme/app_theme.dart index 93b358f..e845b86 100644 --- a/lib/app/theme/app_theme.dart +++ b/lib/app/theme/app_theme.dart @@ -56,7 +56,12 @@ class AppTheme { } return colorScheme.primary; }), - foregroundColor: MaterialStateProperty.all(Colors.white), + foregroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return colorScheme.onSurface.withOpacity(0.38); + } + return colorScheme.onPrimary; + }), minimumSize: MaterialStateProperty.all(const Size.fromHeight(48)), shape: MaterialStateProperty.all( RoundedRectangleBorder( @@ -136,7 +141,12 @@ class AppTheme { } return colorScheme.primary; }), - foregroundColor: MaterialStateProperty.all(Colors.white), + foregroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return colorScheme.onSurface.withOpacity(0.38); + } + return colorScheme.onPrimary; + }), minimumSize: MaterialStateProperty.all(const Size.fromHeight(48)), shape: MaterialStateProperty.all( RoundedRectangleBorder( diff --git a/lib/app/widgets/app_button.dart b/lib/app/widgets/app_button.dart index e59cffa..df3dbfa 100644 --- a/lib/app/widgets/app_button.dart +++ b/lib/app/widgets/app_button.dart @@ -6,27 +6,71 @@ class AppButton extends StatelessWidget { final VoidCallback? onPressed; final bool isPrimary; final bool isLoading; + final Color? backgroundColor; + final Color? foregroundColor; - const AppButton({super.key, required this.label, this.onPressed, this.isPrimary = true, this.isLoading = false}); + const AppButton( + {super.key, + required this.label, + this.onPressed, + this.isPrimary = true, + this.isLoading = false, + this.backgroundColor, + this.foregroundColor}); @override Widget build(BuildContext context) { - final style = isPrimary - ? Theme.of(context).elevatedButtonTheme.style - : ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.secondary, - foregroundColor: Colors.white, - minimumSize: const Size.fromHeight(48), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadii.md)), - ); + ButtonStyle? style; + + if (backgroundColor != null || foregroundColor != null) { + // Use custom colors if provided + style = ElevatedButton.styleFrom( + backgroundColor: + backgroundColor ?? Theme.of(context).colorScheme.primary, + foregroundColor: + foregroundColor ?? Theme.of(context).colorScheme.onPrimary, + minimumSize: const Size.fromHeight(48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md)), + ); + } else { + // Use existing logic for primary/secondary + style = isPrimary + ? Theme.of(context).elevatedButtonTheme.style + : ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Theme.of(context).colorScheme.onSecondary, + minimumSize: const Size.fromHeight(48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md)), + ); + } + + // Determine the correct color for the loading indicator + Color loadingColor; + if (foregroundColor != null) { + loadingColor = foregroundColor!; + } else if (backgroundColor != null) { + loadingColor = Theme.of(context).colorScheme.onPrimary; + } else if (isPrimary) { + loadingColor = Theme.of(context).colorScheme.onPrimary; + } else { + loadingColor = Theme.of(context).colorScheme.onSecondary; + } + return ElevatedButton( onPressed: isLoading ? null : onPressed, style: style, child: isLoading - ? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) + ? SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(loadingColor), + ), + ) : Text(label), ); } } - - diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart index a7887bc..85633b8 100644 --- a/lib/features/home/home_screen.dart +++ b/lib/features/home/home_screen.dart @@ -1,12 +1,16 @@ +import 'dart:async'; import 'package:flutter/material.dart'; -// import 'package:mycelmob/mycelmob.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../app/theme/tokens.dart'; +import '../../app/widgets/app_card.dart'; import '../../app/widgets/app_scaffold.dart'; import '../../app/widgets/app_button.dart'; -import '../../app/widgets/app_card.dart'; -import '../../app/theme/tokens.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../models/peer_models.dart' as peer_models; +import '../../services/peers_service.dart'; import '../../state/mycelium_providers.dart'; import '../../services/ffi/mycelium_service.dart'; +import 'widgets/traffic_summary.dart'; +import '../../state/dynamic_traffic_providers.dart'; class HomeScreen extends ConsumerWidget { const HomeScreen({super.key}); @@ -15,6 +19,7 @@ class HomeScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final statusAsync = ref.watch(nodeStatusProvider); final service = ref.read(myceliumServiceProvider); + final peersAsync = ref.watch(peersProvider); final status = statusAsync.asData?.value ?? NodeStatus.disconnected; final buttonColor = Theme.of(context).colorScheme.primary; @@ -45,28 +50,58 @@ class HomeScreen extends ConsumerWidget { ), currentIndex: 0, onTabSelected: null, - child: ListView( - children: [ - const SizedBox(height: AppSpacing.xxl), - _HeaderCard( - status: status, - onConnect: () async { - await service.start([ - 'tcp://185.69.166.7:9651', - 'tcp://188.40.132.242:9651', - 'tcp://209.159.146.190:9651', - 'tcp://5.223.43.251:9651' - ]); - }, - onDisconnect: () async { - await service.stop(); - }, - service: service, - ), - const SizedBox(height: AppSpacing.xxl), - const _StatsRow(), - const SizedBox(height: AppSpacing.xxxl), - ], + child: ListView.builder( + itemCount: 5, + itemBuilder: (context, index) { + switch (index) { + case 0: + return const SizedBox(height: AppSpacing.xxl); + case 1: + return _HeaderCard( + status: status, + onConnect: () async { + final peers = peersAsync.asData?.value ?? []; + if (peers.isNotEmpty) { + await service.start(peers); + } else { + // Fallback to PeersService if no peers available + final peersService = PeersService(); + final fallbackPeers = await peersService.fetchPeers(); + await service.start(fallbackPeers); + } + }, + onDisconnect: () async { + await service.stop(); + }, + service: service, + ); + case 2: + return Column( + children: [ + const SizedBox(height: AppSpacing.xxl), + const _StatsRow(), + const SizedBox(height: AppSpacing.xxl), + ], + ); + case 3: + return Consumer( + builder: (context, ref, child) { + final trafficStats = ref.watch(dynamicTrafficProvider); + + return TrafficSummary( + totalUpload: trafficStats.totalUploadFormatted, + totalDownload: trafficStats.totalDownloadFormatted, + peakUpload: trafficStats.peakUploadFormatted, + peakDownload: trafficStats.peakDownloadFormatted, + ); + }, + ); + case 4: + return const SizedBox(height: AppSpacing.xxxl); + default: + return const SizedBox.shrink(); + } + }, ), ); } @@ -89,23 +124,102 @@ class _HeaderCard extends StatefulWidget { State<_HeaderCard> createState() => _HeaderCardState(); } -class _HeaderCardState extends State<_HeaderCard> { +class _HeaderCardState extends State<_HeaderCard> + with TickerProviderStateMixin { bool _isLoading = false; bool _isSocks5Enabled = false; + late AnimationController _pulseController; + late AnimationController _rotationController; + late Animation _pulseAnimation; + late Animation _rotationAnimation; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + ); + _rotationController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + _pulseAnimation = Tween( + begin: 0.8, + end: 1.2, + ).animate(CurvedAnimation( + parent: _pulseController, + curve: Curves.easeInOut, + )); + _rotationAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _rotationController, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _pulseController.dispose(); + _rotationController.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(_HeaderCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.status != widget.status) { + _updateAnimations(); + } + } + + void _updateAnimations() { + final isConnected = widget.status == NodeStatus.connected; + final isConnecting = _isLoading || widget.status == NodeStatus.connecting; + + if (isConnecting) { + _rotationController.repeat(); + _pulseController.stop(); + } else if (isConnected) { + _rotationController.stop(); + _pulseController.repeat(reverse: true); + } else { + _rotationController.stop(); + _pulseController.stop(); + } + } bool get isRestartVisible => widget.status == NodeStatus.connected && !_isLoading; Future startMycelium() async { setState(() => _isLoading = true); + _updateAnimations(); await widget.onConnect(); setState(() => _isLoading = false); + _updateAnimations(); } Future stopMycelium() async { setState(() => _isLoading = true); + _updateAnimations(); + + // Stop proxy first if it's enabled + if (_isSocks5Enabled) { + try { + await widget.service.proxyDisconnect(); + await widget.service.stopProxyProbe(); + setState(() => _isSocks5Enabled = false); + } catch (e) { + print('Error stopping proxy: $e'); + } + } + await widget.onDisconnect(); setState(() => _isLoading = false); + _updateAnimations(); } @override @@ -117,14 +231,29 @@ class _HeaderCardState extends State<_HeaderCard> { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - CircleAvatar( - radius: 28, - backgroundColor: Theme.of(context).colorScheme.primaryContainer, - child: Icon( - isConnected ? Icons.wifi : Icons.wifi_off, - size: 32, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), + AnimatedBuilder( + animation: + Listenable.merge([_pulseController, _rotationController]), + builder: (context, child) { + return Transform.scale( + scale: isConnected ? _pulseAnimation.value : 1.0, + child: Transform.rotate( + angle: isConnecting + ? _rotationAnimation.value * 2 * 3.14159 + : 0.0, + child: CircleAvatar( + radius: 28, + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + child: Icon( + isConnected ? Icons.wifi : Icons.wifi_off, + size: 32, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ), + ); + }, ), const SizedBox(height: AppSpacing.lg), Text( @@ -149,6 +278,8 @@ class _HeaderCardState extends State<_HeaderCard> { label: isConnected ? 'Stop Mycelium' : 'Start Mycelium', onPressed: isConnected ? stopMycelium : startMycelium, isLoading: isConnecting, + backgroundColor: isConnected ? Colors.red : null, + foregroundColor: isConnected ? Colors.white : null, ), const SizedBox(height: AppSpacing.lg), Visibility( @@ -158,13 +289,15 @@ class _HeaderCardState extends State<_HeaderCard> { height: 48, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).cardColor, - foregroundColor: Theme.of(context).colorScheme.onSurface, + backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + foregroundColor: + Theme.of(context).colorScheme.onSurfaceVariant, padding: const EdgeInsets.symmetric(horizontal: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), textStyle: const TextStyle(fontSize: 16), + elevation: 0, ), child: const Text.rich( TextSpan( @@ -183,41 +316,50 @@ class _HeaderCardState extends State<_HeaderCard> { ), ), ), - const SizedBox(height: AppSpacing.lg), - AppCard( - margin: EdgeInsets.zero, - child: ExpansionTile( - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Flexible( - child: Text('Advanced Options', - overflow: TextOverflow.ellipsis), + if (widget.status == NodeStatus.connected) ...[ + const SizedBox(height: AppSpacing.lg), + AppCard( + margin: EdgeInsets.zero, + child: ExpansionTile( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Flexible( + child: Text('Advanced Options', + overflow: TextOverflow.ellipsis), + ), + Icon(Icons.expand_more), + ], + ), + children: [ + ListTile( + title: const Text('Enable SOCKS5 tunneling as VPN'), + trailing: Switch( + value: _isSocks5Enabled, + onChanged: (value) async { + setState(() => _isSocks5Enabled = value); + if (value) { + // First start proxy probing to discover available proxies + await widget.service.startProxyProbe(); + // Wait a bit for probes to discover proxies + await Future.delayed(Duration(seconds: 10)); + // Then connect to best available proxy + final result = await widget.service.proxyConnect( + '[40a:152c:b85b:9646:5b71:d03a:eb27:2462]:1080'); + debugPrint('Proxy connect result: $result'); + } else { + final result = await widget.service.proxyDisconnect(); + // Stop proxy probing when disabled + await widget.service.stopProxyProbe(); + debugPrint('Proxy disconnect result: $result'); + } + }, + ), ), - Icon(Icons.expand_more), ], ), - children: [ - ListTile( - title: const Text('Enable SOCKS5 tunneling as VPN'), - trailing: Switch( - value: _isSocks5Enabled, - onChanged: (value) async { - setState(() => _isSocks5Enabled = value); - if (value) { - final result = - await widget.service.proxyConnect('127.0.0.1:1080'); - debugPrint('Proxy connect result: $result'); - } else { - final result = await widget.service.proxyDisconnect(); - debugPrint('Proxy disconnect result: $result'); - } - }, - ), - ), - ], ), - ), + ], ], ), ); @@ -229,54 +371,264 @@ class _StatsRow extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final peersAsync = ref.watch(peersProvider); - final peersCount = peersAsync.maybeWhen(data: (list) => list.length, orElse: () => 0); - Widget tileContent(IconData icon, String title, String value) => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(icon), - const SizedBox(height: AppSpacing.sm), - Text(title, style: Theme.of(context).textTheme.labelMedium), - const SizedBox(height: AppSpacing.xs), - Text(value, style: Theme.of(context).textTheme.titleMedium), - ], - ); + final nodeStatusAsync = ref.watch(nodeStatusProvider); + final uptimeNotifier = ref.watch(uptimeProvider.notifier); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), - child: Row( - children: [ - Expanded( - child: AppCard( - margin: EdgeInsets.zero, - padding: const EdgeInsets.all(AppSpacing.md), - child: tileContent(Icons.people, 'Peers', '$peersCount'), + return nodeStatusAsync.when( + loading: () => _buildStatsCards(context, [], uptimeNotifier), + error: (error, stack) => _buildStatsCards(context, [], uptimeNotifier), + data: (status) { + if (status == NodeStatus.connected) { + return _ConnectedStatsRow(uptimeNotifier: uptimeNotifier); + } else { + return _buildStatsCards(context, [], uptimeNotifier); + } + }, + ); + } + + Widget _buildStatsCards(BuildContext context, + List peerStatus, UptimeNotifier uptimeNotifier) { + final networkTraffic = _calculateNetworkTraffic(peerStatus); + + return Row( + children: [ + Expanded( + child: AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Connected Peers', + style: Theme.of(context).textTheme.titleSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.xs), + Text( + '${peerStatus.length}', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], ), ), - const SizedBox(width: AppSpacing.lg), - Expanded( - child: AppCard( - margin: EdgeInsets.zero, - padding: const EdgeInsets.all(AppSpacing.md), - child: tileContent(Icons.podcasts, 'Bandwidth', '2 MB/s'), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Uptime', + style: Theme.of(context).textTheme.titleSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.xs), + Text( + uptimeNotifier.formattedUptime, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], ), ), - const SizedBox(width: AppSpacing.lg), - Expanded( - child: AppCard( - margin: EdgeInsets.zero, - padding: const EdgeInsets.all(AppSpacing.md), - child: tileContent(Icons.access_time, 'Uptime', '2h 34m'), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Traffic', + style: Theme.of(context).textTheme.titleSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.xs), + Text( + networkTraffic['total'] ?? '0 B', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], ), ), - ], - ), + ), + ], ); } + + Map _calculateNetworkTraffic( + List peerStatus) { + int totalRx = 0; + int totalTx = 0; + + for (final peer in peerStatus) { + totalRx += peer.rxBytes; + totalTx += peer.txBytes; + } + final totalTraffic = totalRx + totalTx; + return { + 'rx': peer_models.PeerStats.formatBytes(totalRx), + 'tx': peer_models.PeerStats.formatBytes(totalTx), + 'total': peer_models.PeerStats.formatBytes(totalTraffic), + }; + } } -// TODO: VPN implemented or not and if yes, how to get its data ? -//TODO: How to get num of peers, bandwidth, uptime ?? -//TODO: How to get All data in peers screen ? - \ No newline at end of file +class _ConnectedStatsRow extends ConsumerStatefulWidget { + final UptimeNotifier uptimeNotifier; + + const _ConnectedStatsRow({required this.uptimeNotifier}); + + @override + ConsumerState<_ConnectedStatsRow> createState() => _ConnectedStatsRowState(); +} + +class _ConnectedStatsRowState extends ConsumerState<_ConnectedStatsRow> { + List peerStatus = []; + Timer? _refreshTimer; + + @override + void initState() { + super.initState(); + _fetchPeerStatus(); + _refreshTimer = Timer.periodic(const Duration(seconds: 10), (_) { + _fetchPeerStatus(); + }); + } + + @override + void dispose() { + _refreshTimer?.cancel(); + super.dispose(); + } + + Future _fetchPeerStatus() async { + try { + final service = MyceliumService(); + final status = await service.getPeerStatus(); + if (mounted) { + setState(() { + peerStatus = status; + }); + } + } catch (e) { + if (mounted) { + setState(() { + peerStatus = []; + }); + } + } + } + + int _getConnectedPeersCount() { + return peerStatus + .where((peer) => + peer.connectionState == peer_models.ConnectionState.connected) + .length; + } + + @override + Widget build(BuildContext context) { + final networkTraffic = _calculateNetworkTraffic(); + final connectedCount = _getConnectedPeersCount(); + + return Row( + children: [ + Expanded( + child: AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Connected Peers', + style: Theme.of(context).textTheme.titleSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.xs), + Text( + '$connectedCount', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Uptime', + style: Theme.of(context).textTheme.titleSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.xs), + Text( + widget.uptimeNotifier.formattedUptime, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Traffic', + style: Theme.of(context).textTheme.titleSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.xs), + Text( + networkTraffic['total'] ?? '0 B', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + ); + } + + Map _calculateNetworkTraffic() { + int totalRx = 0; + int totalTx = 0; + + for (final peer in peerStatus) { + totalRx += peer.rxBytes; + totalTx += peer.txBytes; + } + + final totalTraffic = totalRx + totalTx; + return { + 'total': peer_models.PeerStats.formatBytes(totalTraffic), + 'rx': peer_models.PeerStats.formatBytes(totalRx), + 'tx': peer_models.PeerStats.formatBytes(totalTx), + }; + } +} diff --git a/lib/features/home/widgets/traffic_chart.dart b/lib/features/home/widgets/traffic_chart.dart new file mode 100644 index 0000000..9cbb14b --- /dev/null +++ b/lib/features/home/widgets/traffic_chart.dart @@ -0,0 +1,291 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import '../../../app/theme/tokens.dart'; + +class TrafficChart extends StatelessWidget { + final List data; + final String title; + + const TrafficChart({ + super.key, + required this.data, + this.title = 'Traffic Overview (24h)', + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 200, + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.show_chart, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.md), + Expanded( + child: data.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 8), + Text( + 'Loading traffic data...', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ) + : data.length < 3 + ? Center( + child: Text( + 'Collecting data...', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ) + : LineChart( + LineChartData( + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: 2, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Theme.of(context).dividerColor.withOpacity(0.1), + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: 4, + getTitlesWidget: (double value, TitleMeta meta) { + final hour = value.toInt(); + if (hour % 4 == 0) { + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text( + '${hour.toString().padLeft(2, '0')}:00', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 2, + reservedSize: 40, + getTitlesWidget: (double value, TitleMeta meta) { + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text( + _formatBytes(value), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ); + }, + ), + ), + ), + borderData: FlBorderData(show: false), + minX: 0, + maxX: 23, + minY: 0, + maxY: _getMaxY(), + lineBarsData: [ + // Download area + LineChartBarData( + spots: data.asMap().entries.map((entry) { + return FlSpot(entry.key.toDouble(), entry.value.download); + }).toList(), + isCurved: true, + gradient: LinearGradient( + colors: [ + AppColors.success.withOpacity(0.8), + AppColors.success.withOpacity(0.3), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + barWidth: 0, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + AppColors.success.withOpacity(0.4), + AppColors.success.withOpacity(0.1), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + // Upload area (stacked on top) + LineChartBarData( + spots: data.asMap().entries.map((entry) { + return FlSpot(entry.key.toDouble(), entry.value.download + entry.value.upload); + }).toList(), + isCurved: true, + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.primary.withOpacity(0.8), + Theme.of(context).colorScheme.primary.withOpacity(0.3), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + barWidth: 0, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.primary.withOpacity(0.4), + Theme.of(context).colorScheme.primary.withOpacity(0.1), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: AppSpacing.sm), + // Legend + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _LegendItem( + color: AppColors.success, + label: 'Download', + ), + const SizedBox(width: AppSpacing.md), + _LegendItem( + color: Theme.of(context).colorScheme.primary, + label: 'Upload', + ), + ], + ), + ], + ), + ); + } + + double _getMaxY() { + if (data.isEmpty) return 10; + + double maxValue = 0; + for (final point in data) { + final total = point.download + point.upload; + if (total > maxValue) { + maxValue = total; + } + } + + // Add 20% padding to the top + return maxValue * 1.2; + } + + String _formatBytes(double bytes) { + if (bytes < 1024) return '${bytes.toInt()}B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}K'; + if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}M'; + return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}G'; + } +} + +class _LegendItem extends StatelessWidget { + final Color color; + final String label; + + const _LegendItem({ + required this.color, + required this.label, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 4), + Text( + label, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), + ), + ), + ], + ); + } +} + +class TrafficDataPoint { + final double upload; + final double download; + final DateTime timestamp; + + TrafficDataPoint({ + required this.upload, + required this.download, + required this.timestamp, + }); +} diff --git a/lib/features/home/widgets/traffic_summary.dart b/lib/features/home/widgets/traffic_summary.dart new file mode 100644 index 0000000..da5436a --- /dev/null +++ b/lib/features/home/widgets/traffic_summary.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import '../../../app/theme/tokens.dart'; +import '../../../app/widgets/app_card.dart'; + +class TrafficSummary extends StatelessWidget { + final String totalUpload; + final String totalDownload; + final String peakUpload; + final String peakDownload; + + const TrafficSummary({ + super.key, + this.totalUpload = '0 B', + this.totalDownload = '0 B', + this.peakUpload = '0 B', + this.peakDownload = '0 B', + }); + + @override + Widget build(BuildContext context) { + return AppCard( + padding: const EdgeInsets.all(AppSpacing.md), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.analytics_outlined, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'Traffic Summary (24h)', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.md), + Row( + children: [ + Expanded( + child: _TrafficItem( + icon: Icons.upload, + label: 'Total Upload', + value: totalUpload, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: _TrafficItem( + icon: Icons.download, + label: 'Total Download', + value: totalDownload, + color: AppColors.success, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.sm), + Row( + children: [ + Expanded( + child: _TrafficItem( + icon: Icons.trending_up, + label: 'Peak Upload', + value: peakUpload, + color: Theme.of(context).colorScheme.primary.withOpacity(0.7), + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: _TrafficItem( + icon: Icons.trending_down, + label: 'Peak Download', + value: peakDownload, + color: AppColors.success.withOpacity(0.7), + ), + ), + ], + ), + ], + ), + ); + } +} + +class _TrafficItem extends StatelessWidget { + final IconData icon; + final String label; + final String value; + final Color color; + + const _TrafficItem({ + required this.icon, + required this.label, + required this.value, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadii.sm), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 4), + Expanded( + child: Text( + label, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: color, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + value, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/peers/peers_screen.dart b/lib/features/peers/peers_screen.dart index 4108ae6..74ab210 100644 --- a/lib/features/peers/peers_screen.dart +++ b/lib/features/peers/peers_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import '../../app/widgets/app_scaffold.dart'; import '../../app/widgets/app_card.dart'; @@ -7,6 +8,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../state/mycelium_providers.dart'; import 'package:go_router/go_router.dart'; import '../../services/ffi/mycelium_service.dart'; +import '../../models/peer_models.dart' as peer_models; +import '../../services/ping_service.dart'; +import '../../state/geolocation_providers.dart'; class PeersScreen extends ConsumerWidget { const PeersScreen({super.key}); @@ -22,12 +26,10 @@ class PeersScreen extends ConsumerWidget { icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/'), ), - const Spacer(), const Text( 'Peers', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), - const Spacer(), ], ), Divider( @@ -65,41 +67,157 @@ class _PeersDataScreen extends ConsumerStatefulWidget { class _PeersDataScreenState extends ConsumerState<_PeersDataScreen> { String query = ''; - List peerStatus = []; + List peerStatus = []; String? peerStatusError; + Timer? _refreshTimer; + @override void initState() { super.initState(); - _fetchPeerStatus(); + _startConditionalPolling(); + } + + void _startConditionalPolling() { + // Only start polling if mounted and Mycelium is connected + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + + final nodeStatusAsync = ref.read(nodeStatusProvider); + nodeStatusAsync.whenData((status) { + if (mounted && status == NodeStatus.connected) { + _fetchPeerStatus(); + _refreshTimer = Timer.periodic(const Duration(seconds: 10), (_) { + if (mounted) _fetchPeerStatus(); + }); + } + }); + }); + } + + @override + void dispose() { + _refreshTimer?.cancel(); + super.dispose(); } Future _fetchPeerStatus() async { + if (!mounted) return; + try { final service = MyceliumService(); final status = await service.getPeerStatus(); - setState(() { - peerStatus = status; - peerStatusError = null; - }); - print('Peer status: $status'); + if (mounted) { + setState(() { + peerStatus = status; + peerStatusError = null; + }); + } } catch (e) { - setState(() { - peerStatusError = e.toString(); - }); - print('Error getting peer status: $e'); + // Stop polling on errors to prevent spam + _refreshTimer?.cancel(); + _refreshTimer = null; + if (mounted) { + setState(() { + peerStatus = []; + peerStatusError = null; + }); + } + } + } + + // Calculate peer summary statistics + Map _calculatePeerSummary() { + int connected = 0; + int slow = 0; + int down = 0; + + for (final peer in peerStatus) { + switch (peer.connectionState) { + case peer_models.ConnectionState.connected: + connected++; + break; + case peer_models.ConnectionState.connecting: + slow++; + break; + case peer_models.ConnectionState.disconnected: + case peer_models.ConnectionState.failed: + case peer_models.ConnectionState.unknown: + down++; + break; + } } + + return { + 'connected': connected, + 'slow': slow, + 'down': down, + }; + } + + // Calculate total network traffic + Map _calculateNetworkTraffic() { + int totalRx = 0; + int totalTx = 0; + + for (final peer in peerStatus) { + totalRx += peer.rxBytes; + totalTx += peer.txBytes; + } + return { + 'rx': peer_models.PeerStats.formatBytes(totalRx), + 'tx': peer_models.PeerStats.formatBytes(totalTx), + }; + } + + // Find peer status for a given peer address + peer_models.PeerStats? _findPeerStatus(String peerAddress) { + for (final peer in peerStatus) { + // Extract IP from peer address (remove tcp:// and port) + final peerIp = peerAddress.replaceAll('tcp://', '').split(':')[0]; + + // Handle different endpoint formats from Rust + String statusIp = peer.endpoint; + if (statusIp.contains('://')) { + // Format: tcp://ip:port or quic://ip:port + statusIp = statusIp.split('://')[1].split(':')[0]; + } else if (statusIp.contains(':')) { + // Format: ip:port + statusIp = statusIp.split(':')[0]; + } + + // Try exact match first + if (peerIp == statusIp) { + return peer; + } + + // Try matching the full peer address with the endpoint + if (peerAddress == peer.endpoint) { + return peer; + } + } + return null; } @override Widget build(BuildContext context) { final peersNotifier = ref.read(peersProvider.notifier); final userPeers = peersNotifier.userPeers; + final nodeStatusAsync = ref.watch(nodeStatusProvider); + final isMyceliumRunning = nodeStatusAsync.when( + data: (status) => status == NodeStatus.connected, + loading: () => false, + error: (_, __) => false, + ); + final filtered = query.isEmpty ? widget.peers : widget.peers .where((p) => p.toLowerCase().contains(query.toLowerCase())) .toList(); + + final peerSummary = _calculatePeerSummary(); + final networkTraffic = _calculateNetworkTraffic(); return AppScaffold( title: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( @@ -108,12 +226,10 @@ class _PeersDataScreenState extends ConsumerState<_PeersDataScreen> { icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/'), ), - const Spacer(), const Text( 'Peers', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), - const Spacer(), ], ), Divider( @@ -130,6 +246,7 @@ class _PeersDataScreenState extends ConsumerState<_PeersDataScreen> { _SearchAddBar( onChanged: (v) => setState(() => query = v), onAdd: () => _showAddPeerDialog(context, ref), + isDisabled: isMyceliumRunning, ), const SizedBox(height: AppSpacing.md), if (filtered.isEmpty) @@ -144,11 +261,13 @@ class _PeersDataScreenState extends ConsumerState<_PeersDataScreen> { itemBuilder: (_, index) { final p = filtered[index]; final isUserPeer = userPeers.contains(p); + final peerStat = _findPeerStatus(p); return _PeerTile( ip: p, country: "Unknown", - health: 0.8, isUserPeer: isUserPeer, + peerStats: peerStat, + isDisabled: isMyceliumRunning, ); }, ), @@ -170,13 +289,19 @@ class _PeersDataScreenState extends ConsumerState<_PeersDataScreen> { const SizedBox(height: AppSpacing.md), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ + children: [ _SummaryItem( - label: '5', subtitle: 'Usable', color: Colors.green), + label: '${peerSummary['connected']}', + subtitle: 'Connected', + color: Colors.green), _SummaryItem( - label: '2', subtitle: 'Slow', color: Colors.orange), + label: '${peerSummary['slow']}', + subtitle: 'Connecting', + color: Colors.orange), _SummaryItem( - label: '1', subtitle: 'Down', color: Colors.red), + label: '${peerSummary['down']}', + subtitle: 'Down', + color: Colors.red), ], ), ], @@ -184,20 +309,10 @@ class _PeersDataScreenState extends ConsumerState<_PeersDataScreen> { ), const SizedBox(height: AppSpacing.md), if (peerStatusError != null) - Text('Error getting peer status: $peerStatusError', - style: TextStyle(color: Colors.red)), - if (peerStatus.isNotEmpty) AppCard( padding: const EdgeInsets.all(AppSpacing.md), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Peer Status:', - style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: AppSpacing.sm), - ...peerStatus.map((status) => Text(status)).toList(), - ], - ), + child: Text('Error getting peer status: $peerStatusError', + style: TextStyle(color: Colors.red)), ), const SizedBox(height: AppSpacing.md), AppCard( @@ -217,10 +332,13 @@ class _PeersDataScreenState extends ConsumerState<_PeersDataScreen> { const SizedBox(height: AppSpacing.md), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - _SummaryItem(label: '10.5 MB/s', subtitle: 'Total Upload'), + children: [ + _SummaryItem( + label: networkTraffic['rx'] ?? '0 B', + subtitle: 'Total Download'), _SummaryItem( - label: '20.5 MB/s', subtitle: 'Total Download'), + label: networkTraffic['tx'] ?? '0 B', + subtitle: 'Total Upload'), ], ), ], @@ -235,7 +353,9 @@ class _PeersDataScreenState extends ConsumerState<_PeersDataScreen> { class _SearchAddBar extends ConsumerWidget { final VoidCallback onAdd; final ValueChanged? onChanged; - const _SearchAddBar({required this.onAdd, this.onChanged}); + final bool isDisabled; + const _SearchAddBar( + {required this.onAdd, this.onChanged, this.isDisabled = false}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -247,34 +367,160 @@ class _SearchAddBar extends ConsumerWidget { hintText: 'Search peers', prefixIcon: const Icon(Icons.search), ), - onChanged: onChanged, + onChanged: isDisabled ? null : onChanged, + enabled: !isDisabled, ), ), const SizedBox(width: AppSpacing.lg), FilledButton.icon( - onPressed: () => _showAddPeerDialog(context, ref), + onPressed: isDisabled ? null : () => _showAddPeerDialog(context, ref), icon: const Icon(Icons.add), - label: const Text('Add Peer'), + label: Text(isDisabled ? 'Mycelium Running' : 'Add Peer'), ), ], ); } } -class _PeerTile extends StatelessWidget { +class _PeerTile extends ConsumerStatefulWidget { final String ip; final String country; - final double health; final bool isUserPeer; + final peer_models.PeerStats? peerStats; + final bool isDisabled; const _PeerTile( {required this.ip, required this.country, - required this.health, - required this.isUserPeer}); + required this.isUserPeer, + this.peerStats, + this.isDisabled = false}); + + @override + ConsumerState<_PeerTile> createState() => _PeerTileState(); +} + +class _PeerTileState extends ConsumerState<_PeerTile> { + PingResult? _pingResult; + bool _isPinging = false; + Timer? _periodicPingTimer; + + @override + void initState() { + super.initState(); + // Start periodic ping for connected/connecting peers after a short delay + // to ensure widget is fully built + WidgetsBinding.instance.addPostFrameCallback((_) { + _startPeriodicPing(); + }); + } + + @override + void didUpdateWidget(_PeerTile oldWidget) { + super.didUpdateWidget(oldWidget); + // Restart periodic ping if peer status changed + if (oldWidget.peerStats?.connectionState != + widget.peerStats?.connectionState) { + _periodicPingTimer?.cancel(); + _startPeriodicPing(); + } + } + + @override + void dispose() { + _periodicPingTimer?.cancel(); + super.dispose(); + } + + void _startPeriodicPing() { + // Only start periodic ping if peer is connected or connecting + if (widget.peerStats != null && + (widget.peerStats!.connectionState == + peer_models.ConnectionState.connected || + widget.peerStats!.connectionState == + peer_models.ConnectionState.connecting)) { + // Initial ping after 2 seconds + Timer(const Duration(seconds: 2), () { + if (mounted) { + _performPingTest(isAutomatic: true); + } + }); + + // Then ping every 30 seconds + _periodicPingTimer = Timer.periodic(const Duration(seconds: 30), (_) { + if (mounted) { + _performPingTest(isAutomatic: true); + } + }); + } else {} + } + + Future _performPingTest({bool isAutomatic = false}) async { + if (!mounted) return; + + // Don't show loading indicator for automatic pings + if (!isAutomatic) { + setState(() { + _isPinging = true; + _pingResult = null; + }); + } + + try { + final pingService = ref.read(pingServiceProvider); + final pingResult = await pingService.ping(widget.ip); + + if (mounted) { + setState(() { + _pingResult = pingResult; + _isPinging = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _pingResult = PingResult( + host: widget.ip, + latencyMs: null, + success: false, + timestamp: DateTime.now(), + ); + _isPinging = false; + }); + } + } + } @override Widget build(BuildContext context) { + // Determine connection status and color + Color connectionColor = Colors.grey; + String connectionStatus = 'Unknown'; + if (widget.peerStats != null) { + switch (widget.peerStats!.connectionState) { + case peer_models.ConnectionState.connected: + connectionColor = Colors.green; + connectionStatus = 'Connected'; + break; + case peer_models.ConnectionState.connecting: + connectionColor = Colors.orange; + connectionStatus = 'Connecting'; + break; + case peer_models.ConnectionState.disconnected: + connectionColor = Colors.red; + connectionStatus = 'Disconnected'; + break; + case peer_models.ConnectionState.failed: + connectionColor = Colors.red; + connectionStatus = 'Failed'; + break; + case peer_models.ConnectionState.unknown: + connectionColor = Colors.grey; + connectionStatus = 'Unknown'; + break; + } + } + return AppCard( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( @@ -282,68 +528,322 @@ class _PeerTile extends StatelessWidget { children: [ Row( children: [ - CircleAvatar( - radius: 16, - child: Text( - country.isNotEmpty - ? country.substring(0, 2).toUpperCase() - : "??", - ), + // Country flag circle or globe for unknown countries + Consumer( + builder: (context, ref, child) { + final locationAsync = + ref.watch(peerLocationProvider(widget.ip)); + + if (locationAsync != null && + locationAsync.country != 'Unknown') { + final geoService = ref.read(geolocationServiceProvider); + return Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + child: Center( + child: geoService.getFlagWidget(locationAsync.countryCode), + ), + ); + } + + // Show globe icon for unknown countries or while loading + return Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + shape: BoxShape.circle, + ), + child: Icon( + Icons.public, + size: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + }, ), const SizedBox(width: AppSpacing.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - ip, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.ellipsis, + Row( + children: [ + Expanded( + child: Text( + widget.ip + .replaceAll('tcp://', '') + .replaceAll(':9651', ''), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + // Location info row + Consumer( + builder: (context, ref, child) { + final locationAsync = + ref.watch(peerLocationProvider(widget.ip)); + + if (locationAsync != null && + locationAsync.country != 'Unknown') { + return Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + locationAsync.city.isNotEmpty + ? '${locationAsync.country} • ${locationAsync.city}' + : locationAsync.country, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.7), + ), + overflow: TextOverflow.ellipsis, + ), + ); + } + + // Show loading indicator while fetching + if (locationAsync == null) { + return Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Row( + children: [ + const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 1.5), + ), + const SizedBox(width: 8), + Text( + 'Loading location...', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.5), + ), + ), + ], + ), + ); + } + + return const SizedBox.shrink(); + }, ), const SizedBox(height: AppSpacing.xs), - Text( - country, - style: Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, + Row( + children: [ + Text( + connectionStatus, + style: + Theme.of(context).textTheme.bodySmall?.copyWith( + color: connectionColor, + fontWeight: FontWeight.w500, + ), + ), + ], ), ], ), ), - IconButton( - icon: const Icon(Icons.more_horiz), - onPressed: () { - showModalBottomSheet( - context: context, - useSafeArea: true, - isScrollControlled: true, - showDragHandle: true, - builder: (_) => PeerDetailsSheet( - country: country, - ip: ip, - city: "", - latency: "", - status: "", - isUserPeer: isUserPeer, - ), - ); - }, - ), + // Add ping button when Mycelium is running (enabled for connecting and connected peers) + if (widget.peerStats != null && + (widget.peerStats!.connectionState == + peer_models.ConnectionState.connected || + widget.peerStats!.connectionState == + peer_models.ConnectionState.connecting)) ...[ + IconButton( + icon: _isPinging + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Icon( + Icons.speed, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + onPressed: _isPinging ? null : _performPingTest, + tooltip: 'Test ping', + constraints: const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + ], + // Show delete icon only for user-added peers + if (widget.isUserPeer) + IconButton( + icon: Icon( + Icons.delete_outline, + color: Colors.red.shade400, + ), + onPressed: widget.isDisabled + ? null + : () async { + // Show confirmation dialog + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Peer'), + content: Text( + 'Are you sure you want to delete this peer?\n\n${widget.ip.replaceAll('tcp://', '').replaceAll(':9651', '')}', + ), + actions: [ + TextButton( + onPressed: () => + Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => + Navigator.of(context).pop(true), + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('Delete'), + ), + ], + ), + ); + + if (confirmed == true) { + final peersNotifier = + ref.read(peersProvider.notifier); + await peersNotifier.removePeer(widget.ip); + } + }, + tooltip: 'Delete peer', + ), ], ), - const SizedBox(height: AppSpacing.md), - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: LinearProgressIndicator( - minHeight: 6, - value: health, - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, - valueColor: AlwaysStoppedAnimation( - health > 0.7 - ? Colors.green - : (health > 0.3 ? Colors.orange : Colors.red), - ), + if (widget.peerStats != null) ...[ + const SizedBox(height: AppSpacing.md), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'RX: ${widget.peerStats!.formattedRxBytes}', + style: Theme.of(context).textTheme.bodySmall, + ), + Text( + 'TX: ${widget.peerStats!.formattedTxBytes}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Discovered: ${widget.peerStats!.formattedDiscovered}', + style: Theme.of(context).textTheme.bodySmall, + ), + if (widget.peerStats!.lastConnectedSeconds != null) + Text( + 'Last Connected: ${widget.peerStats!.formattedLastConnected}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ], + ), + if (_pingResult != null) ...[ + const SizedBox(height: AppSpacing.sm), + Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: _pingResult!.success + ? (_pingResult!.latencyMs! < 50 + ? AppColors.success.withOpacity(0.1) + : _pingResult!.latencyMs! < 150 + ? Colors.orange.withOpacity(0.1) + : AppColors.error.withOpacity(0.1)) + : AppColors.error.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: _pingResult!.success + ? (_pingResult!.latencyMs! < 50 + ? AppColors.success.withOpacity(0.3) + : _pingResult!.latencyMs! < 150 + ? Colors.orange.withOpacity(0.3) + : AppColors.error.withOpacity(0.3)) + : AppColors.error.withOpacity(0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _pingResult!.success + ? Icons.check_circle + : Icons.error, + size: 14, + color: _pingResult!.success + ? (_pingResult!.latencyMs! < 50 + ? AppColors.success + : _pingResult!.latencyMs! < 150 + ? Colors.orange + : AppColors.error) + : AppColors.error, + ), + const SizedBox(width: AppSpacing.xs), + Text( + _pingResult!.success + ? 'Ping: ${_pingResult!.latencyMs}ms' + : 'Ping: Failed', + style: + Theme.of(context).textTheme.bodySmall?.copyWith( + color: _pingResult!.success + ? (_pingResult!.latencyMs! < 50 + ? AppColors.success + : _pingResult!.latencyMs! < 150 + ? Colors.orange + : AppColors.error) + : AppColors.error, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ], ), - ), + ], ], ), ); @@ -417,34 +917,96 @@ void _showAddPeerDialog(BuildContext context, WidgetRef ref) { showDialog( context: context, - builder: (context) => AlertDialog( - title: const Text('Add Peer'), - content: TextField( - controller: controller, - decoration: const InputDecoration( - hintText: 'Enter peer IP (e.g. 185.69.166.7)', - ), - keyboardType: TextInputType.url, - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: () async { - final ip = controller.text.trim(); - if (ip.isNotEmpty) { - final formattedPeer = - ip.startsWith('tcp://') ? ip : 'tcp://$ip:9651'; - - await peersNotifier.addPeer(formattedPeer); - } - Navigator.of(context).pop(); - }, - child: const Text('Add'), - ), - ], + builder: (context) => StatefulBuilder( + builder: (context, setState) { + bool isValidIP = _isValidIP(controller.text.trim()); + String? errorText = controller.text.trim().isNotEmpty && !isValidIP + ? 'Please enter a valid IP address' + : null; + + return AlertDialog( + title: const Text('Add Peer'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: controller, + decoration: InputDecoration( + hintText: 'Enter peer IP (e.g. 185.69.166.7)', + errorText: errorText, + border: const OutlineInputBorder(), + ), + keyboardType: TextInputType.url, + onChanged: (value) => setState(() {}), + ), + if (errorText == null && controller.text.trim().isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + children: [ + Icon( + Icons.check_circle, + size: 16, + color: Colors.green, + ), + const SizedBox(width: 4), + Text( + 'Valid IP address', + style: TextStyle( + color: Colors.green, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: isValidIP && controller.text.trim().isNotEmpty + ? () async { + final ip = controller.text.trim(); + final formattedPeer = + ip.startsWith('tcp://') ? ip : 'tcp://$ip:9651'; + + await peersNotifier.addPeer(formattedPeer); + Navigator.of(context).pop(); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + ), + child: const Text('Add'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.secondaryContainer, + foregroundColor: + Theme.of(context).colorScheme.onSecondaryContainer, + ), + child: const Text('Cancel'), + ), + ], + ); + }, ), ); } + +bool _isValidIP(String ip) { + if (ip.isEmpty) return false; + + // IPv4 regex + final ipv4Regex = RegExp( + r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'); + + // IPv6 regex (simplified) + final ipv6Regex = + RegExp(r'^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$'); + + return ipv4Regex.hasMatch(ip) || ipv6Regex.hasMatch(ip); +} diff --git a/lib/features/peers/widgets/peer_details_sheet.dart b/lib/features/peers/widgets/peer_details_sheet.dart index bcf36a7..7b45c6c 100644 --- a/lib/features/peers/widgets/peer_details_sheet.dart +++ b/lib/features/peers/widgets/peer_details_sheet.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../app/theme/tokens.dart'; import '../../../state/mycelium_providers.dart'; +import '../../../services/ping_service.dart'; -class PeerDetailsSheet extends ConsumerWidget { +class PeerDetailsSheet extends ConsumerStatefulWidget { final String ip; final String country; final String city; @@ -21,7 +22,46 @@ class PeerDetailsSheet extends ConsumerWidget { this.isUserPeer = false}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _PeerDetailsSheetState(); +} + +class _PeerDetailsSheetState extends ConsumerState { + PingResult? _pingResult; + bool _isPinging = false; + + Future _performPingTest() async { + setState(() { + _isPinging = true; + _pingResult = null; + }); + + try { + final pingService = ref.read(pingServiceProvider); + final result = await pingService.ping(widget.ip); + + if (mounted) { + setState(() { + _pingResult = result; + _isPinging = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _pingResult = PingResult( + host: widget.ip, + latencyMs: null, + success: false, + timestamp: DateTime.now(), + ); + _isPinging = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { final peersNotifier = ref.read(peersProvider.notifier); return Padding( padding: const EdgeInsets.all(AppSpacing.lg), @@ -39,27 +79,74 @@ class PeerDetailsSheet extends ConsumerWidget { const CircleAvatar(radius: 16, child: Text('US')), const SizedBox(width: AppSpacing.lg), Expanded( - child: Text(country, + child: Text(widget.country, style: Theme.of(context).textTheme.titleMedium, overflow: TextOverflow.ellipsis)), const SizedBox(width: AppSpacing.lg), - Text(latency, style: Theme.of(context).textTheme.labelMedium), + Text(_pingResult?.success == true + ? '${_pingResult!.latencyMs}ms' + : widget.latency, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: _pingResult?.success == true + ? (_pingResult!.latencyMs! < 50 + ? AppColors.success + : _pingResult!.latencyMs! < 150 + ? Colors.orange + : AppColors.error) + : null, + )), ]), const SizedBox(height: AppSpacing.lg), + if (_pingResult != null) ...[ + Container( + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: (_pingResult!.success ? AppColors.success : AppColors.error).withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadii.sm), + ), + child: Row( + children: [ + Icon( + _pingResult!.success ? Icons.check_circle : Icons.error, + color: _pingResult!.success ? AppColors.success : AppColors.error, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _pingResult!.success + ? 'Ping successful: ${_pingResult!.latencyMs}ms' + : 'Ping failed: Host unreachable', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: _pingResult!.success ? AppColors.success : AppColors.error, + ), + ), + ), + ], + ), + ), + const SizedBox(height: AppSpacing.lg), + ], Row( children: [ Expanded( child: OutlinedButton( - onPressed: () {}, - child: const Text('Ping Test'), + onPressed: _isPinging ? null : _performPingTest, + child: _isPinging + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Ping Test'), ), ), - if (isUserPeer) ...[ + if (widget.isUserPeer) ...[ const SizedBox(width: AppSpacing.lg), Expanded( child: FilledButton.tonal( onPressed: () async { - await peersNotifier.removePeer(ip); + await peersNotifier.removePeer(widget.ip); if (context.mounted) { Navigator.of(context).pop(); } diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index 33068b1..08094a3 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:myceliumflut/app/theme/tokens.dart'; import 'package:myceliumflut/app/widgets/app_card.dart'; import 'package:myceliumflut/app/widgets/app_scaffold.dart'; import 'package:myceliumflut/state/app_settings.dart'; +import 'package:myceliumflut/state/app_info_providers.dart'; +import 'package:myceliumflut/state/node_address_provider.dart'; class SettingsScreen extends ConsumerWidget { const SettingsScreen({super.key}); @@ -13,6 +16,8 @@ class SettingsScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final settings = ref.watch(appSettingsProvider); final isDark = settings.themeMode == ThemeMode.dark; + final appVersionAsync = ref.watch(fullAppVersionProvider); + final nodeAddressAsync = ref.watch(nodeAddressProvider); return AppScaffold( title: Column( @@ -24,13 +29,10 @@ class SettingsScreen extends ConsumerWidget { icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/'), ), - const Spacer(), const Text( 'Settings', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), - const Spacer(), - const SizedBox(width: 48), ], ), Divider( @@ -65,7 +67,7 @@ class SettingsScreen extends ConsumerWidget { icon: CircleAvatar( radius: 14, backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, + foregroundColor: Theme.of(context).colorScheme.onPrimary, child: const Text('M', style: TextStyle(fontWeight: FontWeight.bold)), ), @@ -74,16 +76,119 @@ class SettingsScreen extends ConsumerWidget { value: 'v0.6.2', ), const SizedBox(height: AppSpacing.lg), - _InfoRowWithSubtitle( - icon: CircleAvatar( - radius: 14, - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, - child: const Icon(Icons.phone_android, size: 16), - ), - title: 'Mobile App', - subtitle: 'Application Build Version', - value: 'v1.0.12', + Consumer( + builder: (context, ref, child) { + return appVersionAsync.when( + data: (version) => _InfoRowWithSubtitle( + icon: CircleAvatar( + radius: 14, + backgroundColor: + Theme.of(context).colorScheme.primary, + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + child: const Icon(Icons.phone_android, size: 16), + ), + title: 'Mobile App', + subtitle: 'Application Build Version', + value: version, + ), + loading: () => _InfoRowWithSubtitle( + icon: CircleAvatar( + radius: 14, + backgroundColor: + Theme.of(context).colorScheme.primary, + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + child: const Icon(Icons.phone_android, size: 16), + ), + title: 'Mobile App', + subtitle: 'Application Build Version', + value: 'Loading...', + ), + error: (error, stack) => _InfoRowWithSubtitle( + icon: CircleAvatar( + radius: 14, + backgroundColor: + Theme.of(context).colorScheme.primary, + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + child: const Icon(Icons.phone_android, size: 16), + ), + title: 'Mobile App', + subtitle: 'Application Build Version', + value: 'Error', + ), + ); + }, + ), + ], + ), + ), + const SizedBox(height: AppSpacing.xxl), + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.language, size: 20), + const SizedBox(width: 8), + Text( + 'Network Information', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + const SizedBox(height: AppSpacing.lg), + Consumer( + builder: (context, ref, child) { + return nodeAddressAsync.when( + data: (nodeAddress) => _InfoRowWithCopy( + icon: CircleAvatar( + radius: 14, + backgroundColor: + Theme.of(context).colorScheme.primary, + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + child: const Icon(Icons.public, size: 16), + ), + title: 'IP Address', + subtitle: 'Your Mycelium Node Address', + value: nodeAddress.isNotEmpty + ? nodeAddress + : 'Not Available', + copyEnabled: nodeAddress.isNotEmpty, + ), + loading: () => _InfoRowWithCopy( + icon: CircleAvatar( + radius: 14, + backgroundColor: + Theme.of(context).colorScheme.primary, + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + child: const Icon(Icons.public, size: 16), + ), + title: 'IP Address', + subtitle: 'Your Mycelium Node Address', + value: 'Loading...', + copyEnabled: false, + ), + error: (error, stack) => _InfoRowWithCopy( + icon: CircleAvatar( + radius: 14, + backgroundColor: + Theme.of(context).colorScheme.primary, + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + child: const Icon(Icons.public, size: 16), + ), + title: 'IP Address', + subtitle: 'Your Mycelium Node Address', + value: 'Error', + copyEnabled: false, + ), + ); + }, ), ], ), @@ -98,7 +203,7 @@ class SettingsScreen extends ConsumerWidget { CircleAvatar( radius: 14, backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, + foregroundColor: Theme.of(context).colorScheme.onPrimary, child: const Icon(Icons.color_lens, size: 16), ), const SizedBox(width: 10), @@ -124,7 +229,11 @@ class SettingsScreen extends ConsumerWidget { style: Theme.of(context) .textTheme .bodySmall - ?.copyWith(color: Colors.grey), + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.6)), ), ], ), @@ -142,20 +251,22 @@ class SettingsScreen extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - '© 2024 Mycelium Network', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: Colors.grey), + ' 2024 Mycelium Network', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.6)), textAlign: TextAlign.center, ), const SizedBox(height: 4), Text( 'Decentralized networking for everyone', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: Colors.grey), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.6)), textAlign: TextAlign.center, ), ], @@ -194,7 +305,11 @@ class _InfoRowWithSubtitle extends StatelessWidget { Text(title, style: textTheme.bodyMedium), Text( subtitle, - style: textTheme.bodySmall?.copyWith(color: Colors.grey), + style: textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.6)), ), ], ), @@ -211,3 +326,98 @@ class _InfoRowWithSubtitle extends StatelessWidget { ); } } + +class _InfoRowWithCopy extends StatelessWidget { + final Widget icon; + final String title; + final String subtitle; + final String value; + final bool copyEnabled; + + const _InfoRowWithCopy({ + required this.icon, + required this.title, + required this.subtitle, + required this.value, + required this.copyEnabled, + }); + + void _copyToClipboard(BuildContext context, String text) { + Clipboard.setData(ClipboardData(text: text)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('IP Address copied to clipboard'), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + } + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + icon, + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: textTheme.bodyMedium), + Text( + subtitle, + style: textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.6)), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + value, + style: textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + ), + overflow: TextOverflow.ellipsis, + ), + ), + ), + if (copyEnabled) ...[ + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.copy, size: 18), + onPressed: () => _copyToClipboard(context, value), + tooltip: 'Copy IP Address', + style: IconButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.primary.withOpacity(0.1), + foregroundColor: Theme.of(context).colorScheme.primary, + minimumSize: const Size(40, 40), + ), + ), + ], + ], + ), + ], + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 1140ece..1a76a6b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:myceliumflut/features/home/home_screen.dart'; import 'app/theme/app_theme.dart'; import 'app/router/app_router.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -13,8 +12,11 @@ import 'package:path_provider/path_provider.dart'; import 'package:logging/logging.dart'; import 'package:flutter_desktop_sleep/flutter_desktop_sleep.dart'; import 'package:flutter_window_close/flutter_window_close.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; import 'myceliumflut_ffi_binding.dart'; +import 'services/peers_service.dart'; final _logger = Logger('Mycelium'); @@ -50,7 +52,8 @@ class MyApp extends ConsumerStatefulWidget { ConsumerState createState() => _MyAppState(); } -class _MyAppState extends ConsumerState { +class _MyAppState extends ConsumerState + with TrayListener, WindowListener, WidgetsBindingObserver { static const platform = MethodChannel("tech.threefold.mycelium/tun"); String _nodeAddr = ''; var privKey = Uint8List(0); @@ -64,6 +67,7 @@ class _MyAppState extends ConsumerState { void initState() { textEditController = TextEditingController(text: ''); super.initState(); + WidgetsBinding.instance.addObserver(this); initPlatformState(); platform.setMethodCallHandler((MethodCall call) async { methodHandler(call.method); @@ -93,15 +97,125 @@ class _MyAppState extends ConsumerState { } }); - // only for windows because macos already has own handler - if (Platform.isWindows) { - FlutterWindowClose.setWindowShouldCloseHandler(() async { - _logger.info("Window close handler"); - if (_isStarted) { - stopMycelium(); + // Initialize desktop lifecycle asynchronously to avoid blocking startup + Future.delayed(const Duration(seconds: 1), () => _initDesktopLifecycle()); + } + + Future _initDesktopLifecycle() async { + if (!(Platform.isMacOS || Platform.isWindows)) { + return; + } + + try { + _logger.info("Initializing desktop lifecycle..."); + + // Initialize window manager + await windowManager.ensureInitialized(); + _logger.info("Window manager ensureInitialized completed"); + + windowManager.addListener(this); + _logger.info("Window manager listener added"); + + await windowManager.setPreventClose(true); + _logger.info("Window manager setPreventClose completed"); + + // Configure window properties + await windowManager.setTitle('Mycelium'); + _logger.info("Window manager setTitle completed"); + + await windowManager.setTitleBarStyle(TitleBarStyle.normal); + _logger.info("Window manager setTitleBarStyle completed"); + + // Skip setSkipTaskbar as it causes crashes on some Windows versions + // The app will show in taskbar by default anyway + _logger.info("Skipping setSkipTaskbar to avoid compatibility issues"); + + _logger.info("Window manager initialized successfully"); + + // Initialize tray manager with careful error handling + try { + _logger.info("Starting tray manager initialization..."); + trayManager.addListener(this); + _logger.info("Tray manager listener added successfully"); + + // Set tray icon with multiple fallback options + bool iconSet = false; + + // Try different icon paths in order of preference + final iconPaths = [ + if (Platform.isWindows) 'assets/images/tray_icon.ico', + // 'assets/images/tray_icon.png', // Smaller, optimized for tray + // 'assets/images/mycelium_top.png', // Alternative smaller icon + 'assets/images/mycelium_icon.png', // Original large icon + ]; + + for (String iconPath in iconPaths) { + try { + await trayManager.setIcon(iconPath); + iconSet = true; + _logger.info("Tray icon set successfully using: $iconPath"); + break; + } catch (e) { + _logger.warning("Failed to set tray icon from $iconPath: $e"); + } } - return true; - }); + + // If all paths failed, try to set a simple system icon + if (!iconSet) { + try { + // Try setting without any icon first to see if tray works + _logger.info("All icon paths failed, trying to initialize tray without icon"); + } catch (e) { + _logger.warning("Failed to initialize tray: $e"); + } + } + + if (iconSet) { + // Set tray tooltip + await trayManager.setToolTip('Mycelium - Click to show/hide window'); + await _updateTrayMenu(); + _logger.info("Tray manager initialized successfully"); + } else { + _logger.warning("Tray icon could not be set, but continuing without tray"); + } + + } catch (e) { + _logger.warning("Failed to initialize tray manager: $e, continuing without tray"); + } + + // Optional: start minimized to tray when app launches on desktop + // await windowManager.hide(); + + } catch (e) { + _logger.severe("Failed to initialize desktop lifecycle: $e"); + // Don't rethrow - allow app to continue without desktop features + } + } + + Future _updateTrayMenu() async { + try { + final statusLabel = _isStarted ? 'Mycelium: Running' : 'Mycelium: Stopped'; + final toggleLabel = _isStarted ? 'Stop Mycelium' : 'Start Mycelium'; + + // Update tray tooltip with current status + final tooltipText = _isStarted + ? 'Mycelium - Running (${_connectedPeers.length} peers connected)' + : 'Mycelium - Stopped'; + await trayManager.setToolTip(tooltipText); + + final items = [ + MenuItem(key: 'status', label: statusLabel, disabled: true), + MenuItem.separator(), + MenuItem(key: 'show', label: 'Show Window'), + MenuItem(key: 'hide', label: 'Hide Window'), + MenuItem.separator(), + MenuItem(key: 'toggle', label: toggleLabel), + MenuItem.separator(), + MenuItem(key: 'quit', label: 'Quit'), + ]; + await trayManager.setContextMenu(Menu(items: items)); + } catch (e) { + _logger.warning("Failed to update tray menu: $e"); } } @@ -151,12 +265,9 @@ class _MyAppState extends ConsumerState { privKey = await loadOrGeneratePrivKey(platform); peers = await loadPeers(); if (peers.isEmpty || (peers.length == 1 && peers[0].isEmpty)) { - peers = [ - 'tcp://185.69.166.7:9651', - 'tcp://188.40.132.242:9651', - 'tcp://209.159.146.190:9651', - 'tcp://5.223.43.251:9651' - ]; + // Use PeersService to fetch peers from GitHub with fallback + final peersService = PeersService(); + peers = await peersService.fetchPeers(); } textEditController = TextEditingController(text: peers.join('\n')); @@ -178,6 +289,15 @@ class _MyAppState extends ConsumerState { setState(() { _nodeAddr = nodeAddr; }); + + // Query native layer for current VPN status to sync UI + if (!isUseDylib()) { + try { + await platform.invokeMethod('queryStatus'); + } catch (e) { + _logger.warning("queryStatus not implemented: $e"); + } + } } // start/stop mycelium button variables @@ -193,6 +313,11 @@ class _MyAppState extends ConsumerState { void dispose() { // Clean up the controller when the widget is disposed. textEditController.dispose(); + if (Platform.isMacOS || Platform.isWindows) { + windowManager.removeListener(this); + trayManager.removeListener(this); + } + WidgetsBinding.instance.removeObserver(this); super.dispose(); } @@ -208,6 +333,17 @@ class _MyAppState extends ConsumerState { ); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + if (!isUseDylib()) { + platform.invokeMethod('queryStatus').catchError((e) { + _logger.warning("queryStatus on resume failed: $e"); + }); + } + } + } + void startMycelium() { if (_isStarted) { _logger.warning("Mycelium already started"); @@ -298,6 +434,7 @@ class _MyAppState extends ConsumerState { _myceliumStatusColor = colorMycelRed; isRestartVisible = false; }); + _updateTrayMenu(); } void setStateStopped() { @@ -310,6 +447,7 @@ class _MyAppState extends ConsumerState { isRestartVisible = false; _connectedPeers.clear(); // Clear connected peers when stopped }); + _updateTrayMenu(); } void setStateStarted() { @@ -321,6 +459,7 @@ class _MyAppState extends ConsumerState { _myceliumStatusColor = colorDarkBlue; isRestartVisible = true; }); + _updateTrayMenu(); // Start periodic peer status updates _startPeerStatusUpdates(); } @@ -358,10 +497,111 @@ class _MyAppState extends ConsumerState { setState(() { _connectedPeers = peerStatus; }); + + // Update tray menu to reflect new peer count + _updateTrayMenu(); } catch (e) { _logger.warning("Failed to get peer status: $e"); } } + + // Window lifecycle handlers + @override + void onWindowClose() async { + // Intercept close to keep app running in tray + _logger.info("Window close intercepted - hiding to tray"); + await windowManager.hide(); + + // Show tray notification that app is still running + try { + await trayManager.popUpContextMenu(); + } catch (e) { + _logger.warning("Failed to show tray context menu: $e"); + } + } + + @override + void onWindowFocus() { + // Called when window gains focus + } + + @override + void onWindowBlur() { + // Called when window loses focus + } + + @override + void onWindowMaximize() { + // Called when window is maximized + } + + @override + void onWindowUnmaximize() { + // Called when window is unmaximized + } + + @override + void onWindowMinimize() { + // Called when window is minimized - hide to tray instead + windowManager.hide(); + } + + @override + void onWindowRestore() { + // Called when window is restored + } + + // Tray handlers + @override + void onTrayIconMouseDown() async { + if (await windowManager.isVisible()) { + await windowManager.hide(); + } else { + await windowManager.show(); + await windowManager.focus(); + } + } + + @override + void onTrayMenuItemClick(MenuItem menuItem) async { + switch (menuItem.key) { + case 'status': + // Status item is disabled, do nothing + break; + case 'show': + await windowManager.show(); + await windowManager.focus(); + break; + case 'hide': + await windowManager.hide(); + break; + case 'toggle': + if (!_isStarted) { + startMycelium(); + } else { + stopMycelium(); + } + break; + case 'quit': + if (_isStarted) { + stopMycelium(); + } + // Give a brief moment for stop to propagate + await Future.delayed(const Duration(milliseconds: 200)); + // Terminate application + if (Platform.isMacOS) { + // Use existing plugin to terminate if available + try { + _flutterDesktopSleepPlugin.terminateApp(); + } catch (_) { + exit(0); + } + } else { + exit(0); + } + break; + } + } } double physicalPxToLogicalPx(BuildContext context, double physicalPx) { diff --git a/lib/models/peer_models.dart b/lib/models/peer_models.dart new file mode 100644 index 0000000..5ef0605 --- /dev/null +++ b/lib/models/peer_models.dart @@ -0,0 +1,225 @@ +import '../services/geolocation_service.dart'; + +/// Enum representing the type of peer connection +enum PeerType { + static, + discovered, + unknown; + + static PeerType fromString(String value) { + switch (value.toLowerCase()) { + case 'static': + return PeerType.static; + case 'discovered': + return PeerType.discovered; + default: + return PeerType.unknown; + } + } + + @override + String toString() { + switch (this) { + case PeerType.static: + return 'Static'; + case PeerType.discovered: + return 'Discovered'; + case PeerType.unknown: + return 'Unknown'; + } + } +} + +/// Enum representing the connection state of a peer +enum ConnectionState { + connected, + connecting, + disconnected, + failed, + unknown; + + static ConnectionState fromString(String value) { + switch (value.toLowerCase()) { + case 'alive': + return ConnectionState.connected; + case 'connecting': + return ConnectionState.connecting; + case 'dead': + return ConnectionState.disconnected; + case 'connected': + return ConnectionState.connected; + case 'disconnected': + return ConnectionState.disconnected; + case 'failed': + return ConnectionState.failed; + default: + return ConnectionState.unknown; + } + } + + @override + String toString() { + switch (this) { + case ConnectionState.connected: + return 'Connected'; + case ConnectionState.connecting: + return 'Connecting'; + case ConnectionState.disconnected: + return 'Disconnected'; + case ConnectionState.failed: + return 'Failed'; + case ConnectionState.unknown: + return 'Unknown'; + } + } +} + +/// Model representing detailed peer statistics and information +class PeerStats { + /// The protocol used by this peer (e.g., "tcp", "quic") + final String protocol; + + /// The socket address of the peer + final String address; + + /// The type of peer (static, discovered, etc.) + final PeerType peerType; + + /// Current connection state + final ConnectionState connectionState; + + /// Total bytes received from this peer + final int rxBytes; + + /// Total bytes transmitted to this peer + final int txBytes; + + /// Time since this peer was discovered (in seconds) + final int discoveredSeconds; + + /// Time since last successful connection (in seconds), null if never connected + final int? lastConnectedSeconds; + + /// Location information for this peer (country, city, etc.) + final LocationInfo? locationInfo; + + const PeerStats({ + required this.protocol, + required this.address, + required this.peerType, + required this.connectionState, + required this.rxBytes, + required this.txBytes, + required this.discoveredSeconds, + this.lastConnectedSeconds, + this.locationInfo, + }); + + /// Create PeerStats from a JSON map + factory PeerStats.fromJson(Map json) { + return PeerStats( + protocol: json['protocol'] as String, + address: json['address'] as String, + peerType: PeerType.fromString(json['peerType'] as String), + connectionState: ConnectionState.fromString(json['connectionState'] as String), + rxBytes: json['rxBytes'] as int, + txBytes: json['txBytes'] as int, + discoveredSeconds: json['discoveredSeconds'] as int, + lastConnectedSeconds: json['lastConnectedSeconds'] as int?, + ); + } + + /// Convert PeerStats to JSON map + Map toJson() { + return { + 'protocol': protocol, + 'address': address, + 'peerType': peerType.toString(), + 'connectionState': connectionState.toString(), + 'rxBytes': rxBytes, + 'txBytes': txBytes, + 'discoveredSeconds': discoveredSeconds, + 'lastConnectedSeconds': lastConnectedSeconds, + // Note: locationInfo is not serialized as it's fetched separately + }; + } + + /// Format bytes in a human-readable way + static String formatBytes(int bytes) { + if (bytes < 1024) return '${bytes}B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)}KB'; + if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(2)}MB'; + return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)}GB'; + } + + /// Format seconds in a human-readable duration + static String formatDuration(int totalSeconds) { + final seconds = totalSeconds % 60; + final minutes = (totalSeconds ~/ 60) % 60; + final hours = (totalSeconds ~/ 3600) % 24; + final days = totalSeconds ~/ 86400; + + if (days > 0) { + return '${days}d ${hours}h ${minutes}m ${seconds}s'; + } else if (hours > 0) { + return '${hours}h ${minutes}m ${seconds}s'; + } else if (minutes > 0) { + return '${minutes}m ${seconds}s'; + } else { + return '${seconds}s'; + } + } + + /// Get formatted receive bytes + String get formattedRxBytes => formatBytes(rxBytes); + + /// Get formatted transmit bytes + String get formattedTxBytes => formatBytes(txBytes); + + /// Get formatted discovered duration + String get formattedDiscovered => formatDuration(discoveredSeconds); + + /// Get formatted last connected duration + String get formattedLastConnected { + if (lastConnectedSeconds == null) return 'Never connected'; + return formatDuration(lastConnectedSeconds!); + } + + /// Get the full endpoint string (protocol + address) + String get endpoint => '$protocol://$address'; + + @override + String toString() { + return 'PeerStats(protocol: $protocol, address: $address, type: $peerType, ' + 'state: $connectionState, rx: $formattedRxBytes, tx: $formattedTxBytes, ' + 'discovered: $formattedDiscovered, lastConnected: $formattedLastConnected)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PeerStats && + other.protocol == protocol && + other.address == address && + other.peerType == peerType && + other.connectionState == connectionState && + other.rxBytes == rxBytes && + other.txBytes == txBytes && + other.discoveredSeconds == discoveredSeconds && + other.lastConnectedSeconds == lastConnectedSeconds; + } + + @override + int get hashCode { + return Object.hash( + protocol, + address, + peerType, + connectionState, + rxBytes, + txBytes, + discoveredSeconds, + lastConnectedSeconds, + ); + } +} diff --git a/lib/myceliumflut_ffi_binding.dart b/lib/myceliumflut_ffi_binding.dart index e0794c0..49422f3 100644 --- a/lib/myceliumflut_ffi_binding.dart +++ b/lib/myceliumflut_ffi_binding.dart @@ -102,18 +102,28 @@ String myFFAddressFromSecretKey(Uint8List data) { } typedef FuncRustStartMycelium = ffi.Void Function( - ffi.Pointer>, - ffi.IntPtr, - ffi.Pointer, - ffi.IntPtr); + ffi.Pointer>, ffi.Size, ffi.Pointer, ffi.Size); typedef FuncDartStartMycelium = void Function( ffi.Pointer>, int, ffi.Pointer, int); -Future myFFStartMycelium(List peers, Uint8List privKey) async { +Future myFFStartMycelium(List peers, Uint8List privKey) async { + // Run the blocking FFI call in a separate isolate to prevent UI freezing + final result = compute(_startMyceliumInIsolate, { + 'peers': peers, + 'privKey': privKey, + }); + return result; +} + +// Function to run in isolate +bool _startMyceliumInIsolate(Map args) { + final List peers = args['peers']; + final Uint8List privKey = args['privKey']; + // Load the dynamic library final dylib = loadDll(); -// Look up the function + // Look up the function final FuncDartStartMycelium startMycelium = dylib .lookup>('ff_start_mycelium') .asFunction(); @@ -131,20 +141,25 @@ Future myFFStartMycelium(List peers, Uint8List privKey) async { final nativePrivKey = privKeyPtr.asTypedList(privKey.length); nativePrivKey.setAll(0, privKey); - // Call the Rust function - startMycelium(peerPtrs, peers.length, privKeyPtr, privKey.length); - - // Free the allocated memory - for (var i = 0; i < peers.length; i++) { - malloc.free(peerPtrs[i]); + try { + // Call the Rust function (this is the blocking call) + startMycelium(peerPtrs, peers.length, privKeyPtr, privKey.length); + return true; + } catch (e) { + print('Error starting mycelium: $e'); + return false; + } finally { + // Free the allocated memory + for (var i = 0; i < peers.length; i++) { + malloc.free(peerPtrs[i]); + } + malloc.free(peerPtrs); + malloc.free(privKeyPtr); } - malloc.free(peerPtrs); - malloc.free(privKeyPtr); - return true; } -typedef FuncRustStopMycelium = ffi.Uint8 Function(); -typedef FuncDartStopMycelium = int Function(); +typedef FuncRustStopMycelium = ffi.Bool Function(); +typedef FuncDartStopMycelium = bool Function(); Future myFFStopMycelium() async { // Load the dynamic library @@ -155,7 +170,7 @@ Future myFFStopMycelium() async { .asFunction(); final result = stopMycelium(); - return result != 0; + return result; } typedef FuncRustGetPeerStatus = ffi.Void Function( @@ -168,80 +183,220 @@ typedef FuncDartFreePeerStatus = void Function( ffi.Pointer>, int); Future> myFFGetPeerStatus() async { - // Load the dynamic library - final dylib = loadDll(); + final outPtr = calloc>>(); + final outLen = calloc(); + + try { + var dylib = loadDll(); + final ffGetPeerStatus = dylib + .lookup>('ff_get_peer_status') + .asFunction(); + + ffGetPeerStatus(outPtr, outLen); + + final length = outLen.value; + final ptr = outPtr.value; + + final List result = []; + for (int i = 0; i < length; i++) { + final stringPtr = ptr.elementAt(i).value; + if (stringPtr != nullptr) { + result.add(stringPtr.cast().toDartString()); + } + } - final FuncDartGetPeerStatus getPeerStatus = dylib - .lookup>('ff_get_peer_status') - .asFunction(); - final FuncDartFreePeerStatus freePeerStatus = dylib - .lookup>('free_peer_status') - .asFunction(); + // Free the memory + final freePeerStatus = dylib + .lookup>('free_peer_status') + .asFunction(); + freePeerStatus(ptr, length); - final outPtr = malloc>>(); - final outLen = malloc(); + return result; + } finally { + calloc.free(outPtr); + calloc.free(outLen); + } +} - getPeerStatus(outPtr, outLen); +Future> myFFProxyConnect(String remote) async { + final outPtr = calloc>>(); + final outLen = calloc(); + final remotePtr = remote.toNativeUtf8(); - final ptr = outPtr.value; - final len = outLen.value; + try { + var dylib = loadDll(); + final ffProxyConnect = dylib + .lookup, Pointer>>, Pointer)>>('ff_proxy_connect') + .asFunction, Pointer>>, Pointer)>(); + + ffProxyConnect(remotePtr, outPtr, outLen); + + final length = outLen.value; + final ptr = outPtr.value; - List peerStatusList = []; - for (int i = 0; i < len; i++) { - final stringPtr = ptr[i]; - if (stringPtr != ffi.nullptr) { - final dartString = stringPtr.cast().toDartString(); - peerStatusList.add(dartString); + final List result = []; + for (int i = 0; i < length; i++) { + final stringPtr = ptr.elementAt(i).value; + if (stringPtr != nullptr) { + result.add(stringPtr.cast().toDartString()); + } } + + // Free the memory + final freePeerStatus = dylib + .lookup>('free_peer_status') + .asFunction(); + freePeerStatus(ptr, length); + + return result; + } finally { + calloc.free(remotePtr); + calloc.free(outPtr); + calloc.free(outLen); } +} - // Free the allocated memory - freePeerStatus(ptr, len); - malloc.free(outPtr); - malloc.free(outLen); +Future> myFFProxyDisconnect() async { + final outPtr = calloc>>(); + final outLen = calloc(); + + try { + var dylib = loadDll(); + final ffProxyDisconnect = dylib + .lookup>('ff_proxy_disconnect') + .asFunction(); - return peerStatusList; + ffProxyDisconnect(outPtr, outLen); + + final length = outLen.value; + final ptr = outPtr.value; + + final List result = []; + for (int i = 0; i < length; i++) { + final stringPtr = ptr.elementAt(i).value; + if (stringPtr != nullptr) { + result.add(stringPtr.cast().toDartString()); + } + } + + // Free the memory + final freePeerStatus = dylib + .lookup>('free_peer_status') + .asFunction(); + freePeerStatus(ptr, length); + + return result; + } finally { + calloc.free(outPtr); + calloc.free(outLen); + } } -final DynamicLibrary _dylib = Platform.isMacOS || Platform.isWindows - ? DynamicLibrary.open('libmycelium.dylib') - : DynamicLibrary.open('libmycelium.so'); +Future> myFFStartProxyProbe() async { + final outPtr = calloc>>(); + final outLen = calloc(); -typedef _ffiProxyConnectC = Pointer Function(Pointer); -typedef _ffiProxyConnectDart = Pointer Function(Pointer); + try { + var dylib = loadDll(); + final ffStartProxyProbe = dylib + .lookup>('ff_start_proxy_probe') + .asFunction(); -typedef _ffiProxyDisconnectC = Pointer Function(); -typedef _ffiProxyDisconnectDart = Pointer Function(); + ffStartProxyProbe(outPtr, outLen); -final _ffiProxyConnect = - _dylib.lookupFunction<_ffiProxyConnectC, _ffiProxyConnectDart>( - 'ffi_proxy_connect'); + final length = outLen.value; + final ptr = outPtr.value; -final _ffiProxyDisconnect = - _dylib.lookupFunction<_ffiProxyDisconnectC, _ffiProxyDisconnectDart>( - 'ffi_proxy_disconnect'); + final List result = []; + for (int i = 0; i < length; i++) { + final stringPtr = ptr.elementAt(i).value; + if (stringPtr != nullptr) { + result.add(stringPtr.cast().toDartString()); + } + } -/// Helper to free strings -typedef _ffiFreeStringC = Void Function(Pointer); -typedef _ffiFreeStringDart = void Function(Pointer); -final _ffiFreeString = _dylib - .lookupFunction<_ffiFreeStringC, _ffiFreeStringDart>('ffi_free_string'); + // Free the memory + final freePeerStatus = dylib + .lookup>('free_peer_status') + .asFunction(); + freePeerStatus(ptr, length); -List myFFProxyConnect(String remote) { - final remotePtr = remote.toNativeUtf8(); - final resultPtr = _ffiProxyConnect(remotePtr); - calloc.free(remotePtr); + return result; + } finally { + calloc.free(outPtr); + calloc.free(outLen); + } +} + +Future> myFFStopProxyProbe() async { + final outPtr = calloc>>(); + final outLen = calloc(); + + try { + var dylib = loadDll(); + final ffStopProxyProbe = dylib + .lookup>('ff_stop_proxy_probe') + .asFunction(); + + ffStopProxyProbe(outPtr, outLen); - final result = resultPtr.toDartString(); - _ffiFreeString(resultPtr); + final length = outLen.value; + final ptr = outPtr.value; - return result.split(','); // convert "a,b,c" -> ["a","b","c"] + final List result = []; + for (int i = 0; i < length; i++) { + final stringPtr = ptr.elementAt(i).value; + if (stringPtr != nullptr) { + result.add(stringPtr.cast().toDartString()); + } + } + + // Free the memory + final freePeerStatus = dylib + .lookup>('free_peer_status') + .asFunction(); + freePeerStatus(ptr, length); + + return result; + } finally { + calloc.free(outPtr); + calloc.free(outLen); + } } -List myFFProxyDisconnect() { - final resultPtr = _ffiProxyDisconnect(); - final result = resultPtr.toDartString(); - _ffiFreeString(resultPtr); +Future> myFFListProxies() async { + final outPtr = calloc>>(); + final outLen = calloc(); + + try { + var dylib = loadDll(); + final ffListProxies = dylib + .lookup>('ff_list_proxies') + .asFunction(); + + ffListProxies(outPtr, outLen); - return result.split(','); + final length = outLen.value; + final ptr = outPtr.value; + + final List result = []; + for (int i = 0; i < length; i++) { + final stringPtr = ptr.elementAt(i).value; + if (stringPtr != nullptr) { + result.add(stringPtr.cast().toDartString()); + } + } + + // Free the memory + final freePeerStatus = dylib + .lookup>('free_peer_status') + .asFunction(); + freePeerStatus(ptr, length); + + return result; + } finally { + calloc.free(outPtr); + calloc.free(outLen); + } } + diff --git a/lib/services/ffi/mycelium_service.dart b/lib/services/ffi/mycelium_service.dart index 9fc443f..0833e1a 100644 --- a/lib/services/ffi/mycelium_service.dart +++ b/lib/services/ffi/mycelium_service.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; import '../../myceliumflut_ffi_binding.dart'; +import '../../models/peer_models.dart'; enum NodeStatus { disconnected, connecting, connected, failed } @@ -85,8 +87,12 @@ class MyceliumService { return null; } - Future start(List peers) async { - print('MyceliumService: Starting with peers: $peers'); + bool _socksEnabled = false; + + Future start(List peers, {bool socksEnabled = false}) async { + print('MyceliumService: Starting with peers: $peers, SOCKS: $socksEnabled'); + print('MyceliumService: Platform check - isUseDylib(): ${isUseDylib()}'); + _socksEnabled = socksEnabled; _status = NodeStatus.connecting; _statusController.add(_status); final cleaned = preprocessPeers(peers); @@ -103,13 +109,22 @@ class MyceliumService { print('MyceliumService: Loaded key, starting VPN...'); try { if (isUseDylib()) { - await myFFStartMycelium(cleaned, key); + print('MyceliumService: Using FFI dylib'); + myFFStartMycelium(cleaned, key); } else { + print('MyceliumService: Using platform channel'); final result = await _platform.invokeMethod('startVpn', { 'peers': cleaned, 'secretKey': key, + 'socksEnabled': socksEnabled, }); print('MyceliumService: startVpn result: $result'); + if (result != true) { + print('MyceliumService: Platform channel returned false'); + _status = NodeStatus.failed; + _statusController.add(_status); + return false; + } } _status = NodeStatus.connected; _statusController.add(_status); @@ -123,6 +138,8 @@ class MyceliumService { } } + bool get socksEnabled => _socksEnabled; + Future stop() async { try { if (isUseDylib()) { @@ -143,33 +160,108 @@ class MyceliumService { } } - Future> getPeerStatus() async { - if (_status != NodeStatus.connected) { - return []; - } + Future> getPeerStatus() async { try { - List peerStatus; + List peerStatusStrings; if (isUseDylib()) { // Windows platform - use FFI - peerStatus = await myFFGetPeerStatus(); + peerStatusStrings = await myFFGetPeerStatus(); } else { // Android/iOS platform - use platform channel final result = await _platform.invokeMethod>('getPeerStatus'); - peerStatus = result?.cast() ?? []; + peerStatusStrings = result?.cast() ?? []; + } + + // Check for error responses first + if (peerStatusStrings.isNotEmpty) { + final firstResponse = peerStatusStrings[0]; + if (firstResponse.startsWith('err_')) { + // Handle error responses like "err_node_timeout" + throw Exception('Mycelium service error: $firstResponse'); + } + + // Filter out the first element if it's "ok" (status indicator) + if (firstResponse == "ok") { + peerStatusStrings = peerStatusStrings.sublist(1); + } } - // Filter out the first element if it's "ok" (status indicator) - if (peerStatus.isNotEmpty && peerStatus[0] == "ok") { - peerStatus = peerStatus.sublist(1); + // Parse JSON strings into PeerStats objects + List peerStats = []; + for (String jsonString in peerStatusStrings) { + try { + // Skip empty or error strings + if (jsonString.trim().isEmpty || jsonString.startsWith('err_')) { + continue; + } + + final Map json = jsonDecode(jsonString); + peerStats.add(PeerStats.fromJson(json)); + } catch (e) { + // Skip malformed entries silently to prevent spam + } } - return peerStatus; + return peerStats; } catch (e) { throw Exception("Failed to get peer status: $e"); } } + /// Get peer status as simple strings (backward compatibility) + Future> getPeerStatusStrings() async { + try { + final peerStats = await getPeerStatus(); + return peerStats.map((peer) => peer.toString()).toList(); + } catch (e) { + throw Exception("Failed to get peer status strings: $e"); + } + } + + /// Get node status as JSON string + Future getStatus() async { + try { + if (isUseDylib()) { + // Use existing peer status method for now + final peerStats = await getPeerStatus(); + final statusMap = { + 'peers': peerStats.map((p) => p.toJson()).toList(), + 'status': _status.toString(), + }; + return jsonEncode(statusMap); + } else { + // For mobile platforms, don't call getPeerStatus if service is not running + if (_status != NodeStatus.connected) { + final statusMap = { + 'peers': >[], + 'status': _status.toString(), + }; + return jsonEncode(statusMap); + } + + try { + final peerStats = await getPeerStatus(); + final statusMap = { + 'peers': peerStats.map((p) => p.toJson()).toList(), + 'status': _status.toString(), + }; + return jsonEncode(statusMap); + } catch (e) { + // Silent fallback for mobile when service is unavailable + final statusMap = { + 'peers': >[], + 'status': _status.toString(), + }; + return jsonEncode(statusMap); + } + } + } catch (e) { + print("Failed to get status: $e"); + return null; + } + } + void dispose() { _statusController.close(); } @@ -177,7 +269,7 @@ class MyceliumService { Future> proxyConnect(String remote) async { try { if (isUseDylib()) { - return myFFProxyConnect(remote); + return await myFFProxyConnect(remote); } else { final result = await _platform.invokeMethod>( 'proxyConnect', @@ -186,21 +278,68 @@ class MyceliumService { return result?.cast() ?? []; } } catch (e) { - throw Exception("Failed to proxyConnect: $e"); + print("Failed to proxyConnect: $e"); + return ['Failed to connect proxy']; } } Future> proxyDisconnect() async { try { if (isUseDylib()) { - return myFFProxyDisconnect(); + return await myFFProxyDisconnect(); } else { final result = await _platform.invokeMethod>('proxyDisconnect'); return result?.cast() ?? []; } } catch (e) { - throw Exception("Failed to proxyDisconnect: $e"); + print("Failed to proxyDisconnect: $e"); + return ['Failed to disconnect proxy']; + } + } + + Future> startProxyProbe() async { + try { + if (isUseDylib()) { + return await myFFStartProxyProbe(); + } else { + final result = + await _platform.invokeMethod>('startProxyProbe'); + return result?.cast() ?? []; + } + } catch (e) { + print("Failed to startProxyProbe: $e"); + return ['Failed to start proxy probe']; + } + } + + Future> stopProxyProbe() async { + try { + if (isUseDylib()) { + return await myFFStopProxyProbe(); + } else { + final result = + await _platform.invokeMethod>('stopProxyProbe'); + return result?.cast() ?? []; + } + } catch (e) { + print("Failed to stopProxyProbe: $e"); + return ['Failed to stop proxy probe']; + } + } + + Future> listProxies() async { + try { + if (isUseDylib()) { + return await myFFListProxies(); + } else { + final result = + await _platform.invokeMethod>('listProxies'); + return result?.cast() ?? []; + } + } catch (e) { + print("Failed to listProxies: $e"); + return ['Failed to list proxies']; } } } diff --git a/lib/services/geolocation_service.dart b/lib/services/geolocation_service.dart new file mode 100644 index 0000000..d8b6dec --- /dev/null +++ b/lib/services/geolocation_service.dart @@ -0,0 +1,122 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:country_flags/country_flags.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +class LocationInfo { + final String country; + final String countryCode; + final String city; + final String region; + final double? latitude; + final double? longitude; + + const LocationInfo({ + required this.country, + required this.countryCode, + required this.city, + required this.region, + this.latitude, + this.longitude, + }); + + factory LocationInfo.fromGridTFJson(Map json) { + return LocationInfo( + country: json['country_name'] ?? 'Unknown', + countryCode: json['country_code'] ?? 'XX', + city: json['city_name'] ?? '', + region: json['subdivision'] ?? '', + latitude: json['latitude']?.toDouble(), + longitude: json['longitude']?.toDouble(), + ); + } + + factory LocationInfo.unknown() { + return const LocationInfo( + country: 'Unknown', + countryCode: 'XX', + city: '', + region: '', + ); + } +} + +class GeolocationService { + static const String _apiUrl = 'https://geoip.grid.tf'; + static final Map _cache = {}; + + Future getLocationForIP(String ip) async { + // Check cache first + if (_cache.containsKey(ip)) { + return _cache[ip]!; + } + + try { + // Extract IP from peer address if it contains protocol + String targetIP = ip; + if (ip.contains('://')) { + final uri = Uri.parse(ip); + targetIP = uri.host; + } else if (ip.contains(':')) { + // Format: ip:port + targetIP = ip.split(':')[0]; + } + + final response = await http.get( + Uri.parse(_apiUrl).replace(queryParameters: {'ip': targetIP}), + headers: { + 'Accept': 'application/json', + }, + ).timeout(const Duration(seconds: 5)); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + final locationInfo = LocationInfo.fromGridTFJson(data); + _cache[ip] = locationInfo; + return locationInfo; + } + } catch (e) { + // Silent error handling for performance + } + + // Return unknown location and cache it + final unknownLocation = LocationInfo.unknown(); + _cache[ip] = unknownLocation; + return unknownLocation; + } + + String getFlagEmoji(String countryCode) { + if (countryCode.length != 2) return '🏳️'; + + final codePoints = countryCode.toUpperCase().codeUnits; + final flag = String.fromCharCode(0x1F1E6 + codePoints[0] - 0x41) + + String.fromCharCode(0x1F1E6 + codePoints[1] - 0x41); + return flag; + } + + void clearCache() { + _cache.clear(); + } + + Widget getFlagWidget(String countryCode) { + if (countryCode.length != 2) { + return Icon( + Icons.public, + size: 18, + ); + } + if (Platform.isAndroid || Platform.isIOS) { + return Text( + getFlagEmoji(countryCode), + style: const TextStyle(fontSize: 18), + ); + } else { + return CountryFlag.fromCountryCode( + countryCode, + height: 12, + width: 18, + ); + } + } +} diff --git a/lib/services/peers_service.dart b/lib/services/peers_service.dart index 0fca4cb..42d0f9e 100644 --- a/lib/services/peers_service.dart +++ b/lib/services/peers_service.dart @@ -1,5 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import '../models/peer_models.dart'; +import 'ffi/mycelium_service.dart'; class PeersService { static const _url = @@ -35,4 +37,25 @@ class PeersService { return _fallbackPeers; } } + + Future> fetchPeerStats() async { + try { + final service = MyceliumService(); + final statusJson = await service.getStatus(); + + if (statusJson == null || statusJson.isEmpty) { + return []; + } + + final statusData = jsonDecode(statusJson); + final peersData = statusData['peers'] as List?; + + if (peersData == null) return []; + + return peersData.map((peerJson) => PeerStats.fromJson(peerJson)).toList(); + } catch (e) { + print("Error fetching peer stats: $e"); + return []; + } + } } diff --git a/lib/services/ping_service.dart b/lib/services/ping_service.dart new file mode 100644 index 0000000..2fe1523 --- /dev/null +++ b/lib/services/ping_service.dart @@ -0,0 +1,128 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class PingResult { + final String host; + final int? latencyMs; + final bool success; + final DateTime timestamp; + + const PingResult({ + required this.host, + this.latencyMs, + required this.success, + required this.timestamp, + }); +} + +class PingService { + static const List _defaultHosts = [ + '8.8.8.8', + '1.1.1.1', + 'google.com', + ]; + + Future ping(String host) async { + try { + // Extract hostname/IP from peer address if it contains protocol + String targetHost = host; + if (host.contains('://')) { + final uri = Uri.parse(host); + targetHost = uri.host; + } + + // Use actual socket connection to test reachability and measure latency + final stopwatch = Stopwatch()..start(); + + final socket = await Socket.connect( + targetHost, + 9651, // Mycelium default port + timeout: const Duration(seconds: 5), + ); + + stopwatch.stop(); + await socket.close(); + + return PingResult( + host: host, + latencyMs: stopwatch.elapsedMilliseconds, + success: true, + timestamp: DateTime.now(), + ); + } catch (e) { + print('Ping failed for $host: $e'); + return PingResult( + host: host, + latencyMs: null, + success: false, + timestamp: DateTime.now(), + ); + } + } + + Future> pingMultiple([List? hosts]) async { + final targetHosts = hosts ?? _defaultHosts; + final futures = targetHosts.map((host) => ping(host)); + return await Future.wait(futures); + } + + Future getAverageLatency([List? hosts]) async { + final results = await pingMultiple(hosts); + final successfulPings = + results.where((r) => r.success && r.latencyMs != null); + + if (successfulPings.isEmpty) return null; + + final totalLatency = successfulPings.fold( + 0, + (sum, result) => sum + (result.latencyMs ?? 0), + ); + + return (totalLatency / successfulPings.length).round(); + } +} + +// Provider for ping service +final pingServiceProvider = Provider((ref) { + return PingService(); +}); + +// Provider for current latency +final latencyProvider = StateNotifierProvider((ref) { + return LatencyNotifier(ref.read(pingServiceProvider)); +}); + +class LatencyNotifier extends StateNotifier { + final PingService _pingService; + Timer? _timer; + + LatencyNotifier(this._pingService) : super(null) { + _startPeriodicPing(); + } + + void _startPeriodicPing() { + // Initial ping + _updateLatency(); + + // Ping every 30 seconds + _timer = Timer.periodic(const Duration(seconds: 30), (_) { + _updateLatency(); + }); + } + + Future _updateLatency() async { + try { + final latency = await _pingService.getAverageLatency(); + state = latency; + } catch (e) { + print('LatencyNotifier: Error updating latency: $e'); + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } +} diff --git a/lib/state/app_info_providers.dart b/lib/state/app_info_providers.dart new file mode 100644 index 0000000..cffbfbd --- /dev/null +++ b/lib/state/app_info_providers.dart @@ -0,0 +1,20 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +/// Provider for app version information +final appVersionProvider = FutureProvider((ref) async { + final packageInfo = await PackageInfo.fromPlatform(); + return 'v${packageInfo.version}'; +}); + +/// Provider for app build number +final appBuildNumberProvider = FutureProvider((ref) async { + final packageInfo = await PackageInfo.fromPlatform(); + return packageInfo.buildNumber; +}); + +/// Provider for full app version (version + build) +final fullAppVersionProvider = FutureProvider((ref) async { + final packageInfo = await PackageInfo.fromPlatform(); + return 'v${packageInfo.version}+${packageInfo.buildNumber}'; +}); diff --git a/lib/state/dynamic_traffic_providers.dart b/lib/state/dynamic_traffic_providers.dart new file mode 100644 index 0000000..d7a06de --- /dev/null +++ b/lib/state/dynamic_traffic_providers.dart @@ -0,0 +1,125 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'mycelium_providers.dart'; +import '../services/ffi/mycelium_service.dart'; + +class TrafficStats { + final int totalUploadBytes; + final int totalDownloadBytes; + final int peakUploadBytesPerSec; + final int peakDownloadBytesPerSec; + final DateTime lastUpdated; + + const TrafficStats({ + required this.totalUploadBytes, + required this.totalDownloadBytes, + required this.peakUploadBytesPerSec, + required this.peakDownloadBytesPerSec, + required this.lastUpdated, + }); + + String get totalUploadFormatted => _formatBytes(totalUploadBytes); + String get totalDownloadFormatted => _formatBytes(totalDownloadBytes); + String get peakUploadFormatted => '${_formatBytes(peakUploadBytesPerSec)}/s'; + String get peakDownloadFormatted => + '${_formatBytes(peakDownloadBytesPerSec)}/s'; + + static String _formatBytes(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; + if (bytes < 1024 * 1024 * 1024) + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; + } +} + +// Provider for dynamic traffic statistics +final dynamicTrafficProvider = + StateNotifierProvider((ref) { + return DynamicTrafficNotifier(ref); +}); + +class DynamicTrafficNotifier extends StateNotifier { + final Ref _ref; + Timer? _timer; + int _previousTotalRx = 0; + int _previousTotalTx = 0; + int _maxRxRate = 0; + int _maxTxRate = 0; + + DynamicTrafficNotifier(this._ref) + : super(TrafficStats( + totalUploadBytes: 0, + totalDownloadBytes: 0, + peakUploadBytesPerSec: 0, + peakDownloadBytesPerSec: 0, + lastUpdated: DateTime.now(), + )) { + _startPeriodicUpdate(); + } + + void _startPeriodicUpdate() { + // Update every 2 seconds + _timer = Timer.periodic(const Duration(seconds: 2), (_) { + _updateTrafficStats(); + }); + } + + Future _updateTrafficStats() async { + try { + // Check if Mycelium is connected before fetching stats + final nodeStatusAsync = _ref.read(nodeStatusProvider); + final nodeStatus = nodeStatusAsync.asData?.value; + + if (nodeStatus != NodeStatus.connected) return; + + // Use MyceliumService directly to get peer stats like the home screen does + final myceliumService = _ref.read(myceliumServiceProvider); + final peerStats = await myceliumService.getPeerStatus(); + + if (peerStats.isEmpty) return; + + int totalRx = 0; + int totalTx = 0; + + // Sum up traffic from all peer stats + for (final peer in peerStats) { + totalRx += peer.rxBytes; + totalTx += peer.txBytes; + } + + // Calculate rates (bytes per second over 2-second interval) + final rxRate = _previousTotalRx > 0 ? ((totalRx - _previousTotalRx) / 2).round() : 0; + final txRate = _previousTotalTx > 0 ? ((totalTx - _previousTotalTx) / 2).round() : 0; + + // Update peak rates + if (rxRate > _maxRxRate) _maxRxRate = rxRate; + if (txRate > _maxTxRate) _maxTxRate = txRate; + + // Accumulate total traffic instead of replacing it + final currentState = state; + final newTotalUpload = currentState.totalUploadBytes + (totalTx - _previousTotalTx).abs(); + final newTotalDownload = currentState.totalDownloadBytes + (totalRx - _previousTotalRx).abs(); + + // Update state with accumulated totals + state = TrafficStats( + totalUploadBytes: _previousTotalTx == 0 ? totalTx : newTotalUpload, + totalDownloadBytes: _previousTotalRx == 0 ? totalRx : newTotalDownload, + peakUploadBytesPerSec: _maxTxRate, + peakDownloadBytesPerSec: _maxRxRate, + lastUpdated: DateTime.now(), + ); + + _previousTotalRx = totalRx; + _previousTotalTx = totalTx; + } catch (e) { + print('DynamicTrafficNotifier: Error updating traffic stats: $e'); + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } +} diff --git a/lib/state/geolocation_providers.dart b/lib/state/geolocation_providers.dart new file mode 100644 index 0000000..0e284a0 --- /dev/null +++ b/lib/state/geolocation_providers.dart @@ -0,0 +1,46 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../services/geolocation_service.dart'; + +// Provider for geolocation service +final geolocationServiceProvider = Provider((ref) { + return GeolocationService(); +}); + +// Provider for peer location cache +final peerLocationProvider = StateNotifierProvider.family((ref, peerAddress) { + return PeerLocationNotifier(ref.read(geolocationServiceProvider), peerAddress); +}); + +class PeerLocationNotifier extends StateNotifier { + final GeolocationService _geolocationService; + final String _peerAddress; + + PeerLocationNotifier(this._geolocationService, this._peerAddress) : super(null) { + _fetchLocation(); + } + + Future _fetchLocation() async { + try { + // Extract IP from address (remove tcp:// prefix if present) + String cleanIP = _peerAddress.replaceAll('tcp://', ''); + if (cleanIP.contains(':')) { + cleanIP = cleanIP.split(':')[0]; // Remove port if present + } + + final location = await _geolocationService.getLocationForIP(cleanIP); + + if (mounted) { + state = location; + } + } catch (e) { + print('Error fetching location for $_peerAddress: $e'); + if (mounted) { + state = LocationInfo.unknown(); + } + } + } + + void refresh() { + _fetchLocation(); + } +} diff --git a/lib/state/mycelium_providers.dart b/lib/state/mycelium_providers.dart index 0064f4c..9958f94 100644 --- a/lib/state/mycelium_providers.dart +++ b/lib/state/mycelium_providers.dart @@ -16,6 +16,65 @@ final nodeStatusProvider = StreamProvider((ref) { return ref.watch(myceliumServiceProvider).statusStream; }); +class UptimeNotifier extends StateNotifier { + Timer? _timer; + + UptimeNotifier() : super(null); + + void startUptime() { + if (state == null) { + state = DateTime.now(); + } + } + + void stopUptime() { + state = null; + } + + Duration? get uptime { + if (state == null) return null; + return DateTime.now().difference(state!); + } + + String get formattedUptime { + final duration = uptime; + if (duration == null) return '0s'; + + if (duration.inDays > 0) { + return '${duration.inDays}d ${duration.inHours % 24}h'; + } else if (duration.inHours > 0) { + return '${duration.inHours}h ${duration.inMinutes % 60}m'; + } else if (duration.inMinutes > 0) { + return '${duration.inMinutes}m ${duration.inSeconds % 60}s'; + } else { + return '${duration.inSeconds}s'; + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } +} + +final uptimeProvider = StateNotifierProvider((ref) { + final notifier = UptimeNotifier(); + + // Listen to node status changes + ref.listen(nodeStatusProvider, (previous, next) { + next.whenData((status) { + if (status == NodeStatus.connected) { + notifier.startUptime(); + } else { + notifier.stopUptime(); + } + }); + }); + + return notifier; +}); + class PeersNotifier extends StateNotifier>> { final PeersService _service; final PeersRepository _repo; diff --git a/lib/state/node_address_provider.dart b/lib/state/node_address_provider.dart new file mode 100644 index 0000000..545dff6 --- /dev/null +++ b/lib/state/node_address_provider.dart @@ -0,0 +1,50 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; +import '../myceliumflut_ffi_binding.dart'; + +/// Provider for the node address +final nodeAddressProvider = FutureProvider((ref) async { + try { + const platform = MethodChannel("tech.threefold.mycelium/tun"); + + // Load the private key + final privKey = await loadOrGeneratePrivKey(platform); + + // Calculate the node address + String nodeAddr; + if (isUseDylib()) { + nodeAddr = myFFAddressFromSecretKey(privKey); + } else { + nodeAddr = (await platform.invokeMethod( + 'addressFromSecretKey', privKey)) as String; + } + + return nodeAddr; + } catch (e) { + // Return empty string if there's an error + return ''; + } +}); + +/// Helper function to load or generate private key (copied from main.dart) +Future loadOrGeneratePrivKey(MethodChannel platform) async { + // get dir + final dir = await getApplicationDocumentsDirectory(); + + final file = File('${dir.path}/priv_key.bin'); + if (file.existsSync()) { + return await file.readAsBytes(); + } + // create new secret key if not exists + Uint8List privKey = Uint8List(0); + if (isUseDylib()) { + privKey = myFFGenerateSecretKey(); + } else { + privKey = (await platform.invokeMethod('generateSecretKey')) + as Uint8List; + } + await file.writeAsBytes(privKey); + return privKey; +} diff --git a/lib/state/traffic_providers.dart b/lib/state/traffic_providers.dart new file mode 100644 index 0000000..1b07b27 --- /dev/null +++ b/lib/state/traffic_providers.dart @@ -0,0 +1,167 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../services/peers_service.dart'; +import 'mycelium_providers.dart'; +import '../features/home/widgets/traffic_chart.dart'; +import '../models/peer_models.dart'; + +// Provider for historical traffic data +final trafficHistoryProvider = StateNotifierProvider>((ref) { + return TrafficHistoryNotifier(); +}); + +class TrafficHistoryNotifier extends StateNotifier> { + Timer? _timer; + + TrafficHistoryNotifier() : super([]) { + // Initialize with minimal data immediately + _initializeMinimalData(); + // Schedule full initialization after UI is ready + Future.microtask(() => _initializeDataAsync()); + } + + void _initializeMinimalData() { + // Add just a few data points to prevent empty chart errors + final now = DateTime.now(); + state = [ + TrafficDataPoint(upload: 0, download: 0, timestamp: now.subtract(const Duration(hours: 2))), + TrafficDataPoint(upload: 0, download: 0, timestamp: now.subtract(const Duration(hours: 1))), + TrafficDataPoint(upload: 0, download: 0, timestamp: now), + ]; + } + + void _initializeDataAsync() async { + try { + // Small delay to ensure UI is rendered first + await Future.delayed(const Duration(milliseconds: 500)); + _initializeData(); + _startPeriodicUpdates(); + } catch (e) { + print('TrafficHistoryNotifier: Error initializing data: $e'); + // Keep minimal data on error + } + } + + void _initializeData() { + // Initialize with 24 hours of sample data (one point per hour) + final now = DateTime.now(); + final List initialData = []; + + for (int i = 23; i >= 0; i--) { + final timestamp = now.subtract(Duration(hours: i)); + // Generate some sample data with realistic patterns + final baseUpload = _generateRealisticTraffic(i, isUpload: true); + final baseDownload = _generateRealisticTraffic(i, isUpload: false); + + initialData.add(TrafficDataPoint( + upload: baseUpload, + download: baseDownload, + timestamp: timestamp, + )); + } + + state = initialData; + } + + double _generateRealisticTraffic(int hourAgo, {required bool isUpload}) { + final random = Random(hourAgo + (isUpload ? 1000 : 0)); + + // Create realistic daily patterns + final hour = (24 - hourAgo) % 24; + double baseActivity = 1.0; + + // Higher activity during day hours (8-22), lower at night + if (hour >= 8 && hour <= 22) { + baseActivity = 0.5 + 0.5 * sin((hour - 8) * pi / 14); + } else { + baseActivity = 0.1 + 0.2 * random.nextDouble(); + } + + // Upload is typically lower than download + final multiplier = isUpload ? 0.3 : 1.0; + + // Generate bytes (in MB range for visibility) + final baseMB = baseActivity * multiplier * (2 + 6 * random.nextDouble()); + return baseMB * 1024 * 1024; // Convert to bytes + } + + void _startPeriodicUpdates() { + _timer = Timer.periodic(const Duration(minutes: 5), (timer) { + _updateWithCurrentTraffic(); + }); + } + + void _updateWithCurrentTraffic() { + // In a real implementation, this would get actual traffic data + // For now, we'll simulate realistic updates + final now = DateTime.now(); + final random = Random(); + + // Remove oldest data point and add new one + final newData = List.from(state); + if (newData.length >= 24) { + newData.removeAt(0); + } + + // Generate new data point with some randomness + final upload = 0.5 + 2.0 * random.nextDouble(); // 0.5-2.5 MB + final download = 1.0 + 4.0 * random.nextDouble(); // 1-5 MB + + newData.add(TrafficDataPoint( + upload: upload * 1024 * 1024, // Convert to bytes + download: download * 1024 * 1024, + timestamp: now, + )); + + state = newData; + } + + void updateWithPeerData(List peerStats) { + // This method can be called to update with real peer traffic data + if (peerStats.isEmpty) return; + + final now = DateTime.now(); + double totalUpload = 0; + double totalDownload = 0; + + for (final peer in peerStats) { + totalUpload += peer.txBytes.toDouble(); + totalDownload += peer.rxBytes.toDouble(); + } + + // Update the most recent data point or add a new one + final newData = List.from(state); + if (newData.isNotEmpty) { + final lastPoint = newData.last; + final timeDiff = now.difference(lastPoint.timestamp); + + if (timeDiff.inMinutes < 30) { + // Update the last point if it's recent + newData[newData.length - 1] = TrafficDataPoint( + upload: totalUpload, + download: totalDownload, + timestamp: now, + ); + } else { + // Add new point + if (newData.length >= 24) { + newData.removeAt(0); + } + newData.add(TrafficDataPoint( + upload: totalUpload, + download: totalDownload, + timestamp: now, + )); + } + } + + state = newData; + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 04ce0d9..f3b15ee 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,20 @@ import Foundation import flutter_desktop_sleep import flutter_window_close +import package_info_plus import path_provider_foundation +import screen_retriever_macos import shared_preferences_foundation +import tray_manager +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterDesktopSleepPlugin.register(with: registry.registrar(forPlugin: "FlutterDesktopSleepPlugin")) FlutterWindowClosePlugin.register(with: registry.registrar(forPlugin: "FlutterWindowClosePlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 14925f8..0ae15f0 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -5,6 +5,81 @@ import flutter_desktop_sleep @main class AppDelegate: FlutterAppDelegate { var _windowManager = FlutterDesktopSleepPlugin() + private var flutterChannel: FlutterMethodChannel? + + override func applicationDidFinishLaunching(_ notification: Notification) { + super.applicationDidFinishLaunching(notification) + + let controller: FlutterViewController = mainFlutterWindow?.contentViewController as! FlutterViewController + flutterChannel = FlutterMethodChannel(name: "tech.threefold.mycelium/tun", + binaryMessenger: controller.engine.binaryMessenger) + flutterChannel!.setMethodCallHandler({ + (call: FlutterMethodCall, result: FlutterResult) -> Void in + switch call.method { + case "generateSecretKey": + let key = generateSecretKey() + result(key) + case "addressFromSecretKey": + if let key = call.arguments as? FlutterStandardTypedData { + let nodeAddr = addressFromSecretKey(data: key.data) + result(nodeAddr) + } else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Expect secret key", details: nil)) + } + case "startVpn": + // macOS doesn't support VPN tunnel like iOS/Android + result(false) + case "stopVpn": + result(true) + case "getPeerStatus": + do { + let peerStatus = getPeerStatus() + result(peerStatus) + } catch { + result(FlutterError(code: "PEER_STATUS_ERROR", message: error.localizedDescription, details: nil)) + } + case "proxyConnect": + do { + let remote = call.arguments as? String ?? "" + let proxyResult = proxyConnect(remote: remote) + result(proxyResult) + } catch { + result(FlutterError(code: "PROXY_CONNECT_ERROR", message: error.localizedDescription, details: nil)) + } + case "proxyDisconnect": + do { + let proxyResult = proxyDisconnect() + result(proxyResult) + } catch { + result(FlutterError(code: "PROXY_DISCONNECT_ERROR", message: error.localizedDescription, details: nil)) + } + case "startProxyProbe": + do { + let proxyResult = startProxyProbe() + result(proxyResult) + } catch { + result(FlutterError(code: "START_PROXY_PROBE_ERROR", message: error.localizedDescription, details: nil)) + } + case "stopProxyProbe": + do { + let proxyResult = stopProxyProbe() + result(proxyResult) + } catch { + result(FlutterError(code: "STOP_PROXY_PROBE_ERROR", message: error.localizedDescription, details: nil)) + } + case "listProxies": + do { + let proxyResult = listProxies() + result(proxyResult) + } catch { + result(FlutterError(code: "LIST_PROXIES_ERROR", message: error.localizedDescription, details: nil)) + } + default: + result(FlutterMethodNotImplemented) + } + }) + } + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f..96d3fee 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "info": { + "version": 1, + "author": "xcode" }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 82b6f9d..6617c13 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 13b35eb..980f69b 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 0a3f5fa..37987c0 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bdb5722..95e6809 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index f083318..f44fe07 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 326c0e7..c80b08d 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 2f1632c..e09fdc1 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 83c9dbc..4a228d7 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -11,7 +11,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile - + AppIcon CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 0ab1ad9..35ed01d 100644 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -61,6 +61,31 @@ class MainFlutterWindow: NSWindow { self.flutterTunnelStatus = .stopped self.stopMycelium() result(true) + case "getPeerStatus": + do { + let peerStatus = getPeerStatus() + result(peerStatus) + } catch { + debuglog("Error getting peer status: \(error.localizedDescription)") + result(FlutterError(code: "PEER_STATUS_ERROR", message: error.localizedDescription, details: nil)) + } + case "proxyConnect": + do { + let remote = call.arguments as? String ?? "" + let proxyResult = proxyConnect(remote: remote) + result(proxyResult) + } catch { + debuglog("Error in proxyConnect: \(error.localizedDescription)") + result(FlutterError(code: "PROXY_CONNECT_ERROR", message: error.localizedDescription, details: nil)) + } + case "proxyDisconnect": + do { + let proxyResult = proxyDisconnect() + result(proxyResult) + } catch { + debuglog("Error in proxyDisconnect: \(error.localizedDescription)") + result(FlutterError(code: "PROXY_DISCONNECT_ERROR", message: error.localizedDescription, details: nil)) + } default: result(FlutterMethodNotImplemented) } diff --git a/macos/Runner/mycelmob.swift b/macos/Runner/mycelmob.swift index ed84c65..66a2fc8 100644 --- a/macos/Runner/mycelmob.swift +++ b/macos/Runner/mycelmob.swift @@ -8,10 +8,10 @@ import Foundation // might be in a separate module, or it might be compiled inline into // this module. This is a bit of light hackery to work with both. #if canImport(mycelmobFFI) - import mycelmobFFI +import mycelmobFFI #endif -private extension RustBuffer { +fileprivate extension RustBuffer { // Allocate a new buffer, copying the contents of a `UInt8` array. init(bytes: [UInt8]) { let rbuf = bytes.withUnsafeBufferPointer { ptr in @@ -21,7 +21,7 @@ private extension RustBuffer { } static func empty() -> RustBuffer { - RustBuffer(capacity: 0, len: 0, data: nil) + RustBuffer(capacity: 0, len:0, data: nil) } static func from(_ ptr: UnsafeBufferPointer) -> RustBuffer { @@ -35,7 +35,7 @@ private extension RustBuffer { } } -private extension ForeignBytes { +fileprivate extension ForeignBytes { init(bufferPointer: UnsafeBufferPointer) { self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) } @@ -48,11 +48,13 @@ private extension ForeignBytes { // Helper classes/extensions that don't change. // Someday, this will be in a library of its own. -private extension Data { +fileprivate extension Data { init(rustBuffer: RustBuffer) { - // TODO: This copies the buffer. Can we read directly from a - // Rust buffer? - self.init(bytes: rustBuffer.data!, count: Int(rustBuffer.len)) + self.init( + bytesNoCopy: rustBuffer.data!, + count: Int(rustBuffer.len), + deallocator: .none + ) } } @@ -70,15 +72,15 @@ private extension Data { // // Instead, the read() method and these helper functions input a tuple of data -private func createReader(data: Data) -> (data: Data, offset: Data.Index) { +fileprivate func createReader(data: Data) -> (data: Data, offset: Data.Index) { (data: data, offset: 0) } // Reads an integer at the current offset, in big-endian order, and advances // the offset on success. Throws if reading the integer would move the // offset past the end of the buffer. -private func readInt(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { - let range = reader.offset ..< reader.offset + MemoryLayout.size +fileprivate func readInt(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { + let range = reader.offset...size guard reader.data.count >= range.upperBound else { throw UniffiInternalError.bufferOverflow } @@ -88,38 +90,38 @@ private func readInt(_ reader: inout (data: Data, offset: return value as! T } var value: T = 0 - let _ = withUnsafeMutableBytes(of: &value) { reader.data.copyBytes(to: $0, from: range) } + let _ = withUnsafeMutableBytes(of: &value, { reader.data.copyBytes(to: $0, from: range)}) reader.offset = range.upperBound return value.bigEndian } // Reads an arbitrary number of bytes, to be used to read // raw bytes, this is useful when lifting strings -private func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> [UInt8] { - let range = reader.offset ..< (reader.offset + count) +fileprivate func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> Array { + let range = reader.offset..<(reader.offset+count) guard reader.data.count >= range.upperBound else { throw UniffiInternalError.bufferOverflow } var value = [UInt8](repeating: 0, count: count) - value.withUnsafeMutableBufferPointer { buffer in + value.withUnsafeMutableBufferPointer({ buffer in reader.data.copyBytes(to: buffer, from: range) - } + }) reader.offset = range.upperBound return value } // Reads a float at the current offset. -private func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { - return try Float(bitPattern: readInt(&reader)) +fileprivate func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { + return Float(bitPattern: try readInt(&reader)) } // Reads a float at the current offset. -private func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { - return try Double(bitPattern: readInt(&reader)) +fileprivate func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { + return Double(bitPattern: try readInt(&reader)) } // Indicates if the offset has reached the end of the buffer. -private func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { +fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { return reader.offset < reader.data.count } @@ -127,11 +129,11 @@ private func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { // struct, but we use standalone functions instead in order to make external // types work. See the above discussion on Readers for details. -private func createWriter() -> [UInt8] { +fileprivate func createWriter() -> [UInt8] { return [] } -private func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { +fileprivate func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { writer.append(contentsOf: byteArr) } @@ -139,22 +141,22 @@ private func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: Seque // // Warning: make sure what you are trying to write // is in the correct type! -private func writeInt(_ writer: inout [UInt8], _ value: T) { +fileprivate func writeInt(_ writer: inout [UInt8], _ value: T) { var value = value.bigEndian withUnsafeBytes(of: &value) { writer.append(contentsOf: $0) } } -private func writeFloat(_ writer: inout [UInt8], _ value: Float) { +fileprivate func writeFloat(_ writer: inout [UInt8], _ value: Float) { writeInt(&writer, value.bitPattern) } -private func writeDouble(_ writer: inout [UInt8], _ value: Double) { +fileprivate func writeDouble(_ writer: inout [UInt8], _ value: Double) { writeInt(&writer, value.bitPattern) } // Protocol for types that transfer other types across the FFI. This is -// analogous go the Rust trait of the same name. -private protocol FfiConverter { +// analogous to the Rust trait of the same name. +fileprivate protocol FfiConverter { associatedtype FfiType associatedtype SwiftType @@ -165,13 +167,19 @@ private protocol FfiConverter { } // Types conforming to `Primitive` pass themselves directly over the FFI. -private protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType {} +fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { } extension FfiConverterPrimitive { +#if swift(>=5.8) + @_documentation(visibility: private) +#endif public static func lift(_ value: FfiType) throws -> SwiftType { return value } +#if swift(>=5.8) + @_documentation(visibility: private) +#endif public static func lower(_ value: SwiftType) -> FfiType { return value } @@ -179,9 +187,12 @@ extension FfiConverterPrimitive { // Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`. // Used for complex types where it's hard to write a custom lift/lower. -private protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} +fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} extension FfiConverterRustBuffer { +#if swift(>=5.8) + @_documentation(visibility: private) +#endif public static func lift(_ buf: RustBuffer) throws -> SwiftType { var reader = createReader(data: Data(rustBuffer: buf)) let value = try read(from: &reader) @@ -192,16 +203,18 @@ extension FfiConverterRustBuffer { return value } +#if swift(>=5.8) + @_documentation(visibility: private) +#endif public static func lower(_ value: SwiftType) -> RustBuffer { - var writer = createWriter() - write(value, into: &writer) - return RustBuffer(bytes: writer) + var writer = createWriter() + write(value, into: &writer) + return RustBuffer(bytes: writer) } } - // An error type for FFI errors. These errors occur at the UniFFI level, not // the library level. -private enum UniffiInternalError: LocalizedError { +fileprivate enum UniffiInternalError: LocalizedError { case bufferOverflow case incompleteData case unexpectedOptionalTag @@ -227,24 +240,24 @@ private enum UniffiInternalError: LocalizedError { } } -private extension NSLock { +fileprivate extension NSLock { func withLock(f: () throws -> T) rethrows -> T { - lock() + self.lock() defer { self.unlock() } return try f() } } -private let CALL_SUCCESS: Int8 = 0 -private let CALL_ERROR: Int8 = 1 -private let CALL_UNEXPECTED_ERROR: Int8 = 2 -private let CALL_CANCELLED: Int8 = 3 +fileprivate let CALL_SUCCESS: Int8 = 0 +fileprivate let CALL_ERROR: Int8 = 1 +fileprivate let CALL_UNEXPECTED_ERROR: Int8 = 2 +fileprivate let CALL_CANCELLED: Int8 = 3 -private extension RustCallStatus { +fileprivate extension RustCallStatus { init() { self.init( code: CALL_SUCCESS, - errorBuf: RustBuffer( + errorBuf: RustBuffer.init( capacity: 0, len: 0, data: nil @@ -254,70 +267,70 @@ private extension RustCallStatus { } private func rustCall(_ callback: (UnsafeMutablePointer) -> T) throws -> T { - try makeRustCall(callback, errorHandler: nil) + let neverThrow: ((RustBuffer) throws -> Never)? = nil + return try makeRustCall(callback, errorHandler: neverThrow) } -private func rustCallWithError( - _ errorHandler: @escaping (RustBuffer) throws -> Error, - _ callback: (UnsafeMutablePointer) -> T -) throws -> T { +private func rustCallWithError( + _ errorHandler: @escaping (RustBuffer) throws -> E, + _ callback: (UnsafeMutablePointer) -> T) throws -> T { try makeRustCall(callback, errorHandler: errorHandler) } -private func makeRustCall( +private func makeRustCall( _ callback: (UnsafeMutablePointer) -> T, - errorHandler: ((RustBuffer) throws -> Error)? + errorHandler: ((RustBuffer) throws -> E)? ) throws -> T { uniffiEnsureInitialized() - var callStatus = RustCallStatus() + var callStatus = RustCallStatus.init() let returnedVal = callback(&callStatus) try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: errorHandler) return returnedVal } -private func uniffiCheckCallStatus( +private func uniffiCheckCallStatus( callStatus: RustCallStatus, - errorHandler: ((RustBuffer) throws -> Error)? + errorHandler: ((RustBuffer) throws -> E)? ) throws { switch callStatus.code { - case CALL_SUCCESS: - return - - case CALL_ERROR: - if let errorHandler = errorHandler { - throw try errorHandler(callStatus.errorBuf) - } else { - callStatus.errorBuf.deallocate() - throw UniffiInternalError.unexpectedRustCallError - } + case CALL_SUCCESS: + return + + case CALL_ERROR: + if let errorHandler = errorHandler { + throw try errorHandler(callStatus.errorBuf) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.unexpectedRustCallError + } - case CALL_UNEXPECTED_ERROR: - // When the rust code sees a panic, it tries to construct a RustBuffer - // with the message. But if that code panics, then it just sends back - // an empty buffer. - if callStatus.errorBuf.len > 0 { - throw try UniffiInternalError.rustPanic(FfiConverterString.lift(callStatus.errorBuf)) - } else { - callStatus.errorBuf.deallocate() - throw UniffiInternalError.rustPanic("Rust panic") - } + case CALL_UNEXPECTED_ERROR: + // When the rust code sees a panic, it tries to construct a RustBuffer + // with the message. But if that code panics, then it just sends back + // an empty buffer. + if callStatus.errorBuf.len > 0 { + throw UniffiInternalError.rustPanic(try FfiConverterString.lift(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } - case CALL_CANCELLED: - fatalError("Cancellation not supported yet") + case CALL_CANCELLED: + fatalError("Cancellation not supported yet") - default: - throw UniffiInternalError.unexpectedRustCallStatusCode + default: + throw UniffiInternalError.unexpectedRustCallStatusCode } } private func uniffiTraitInterfaceCall( callStatus: UnsafeMutablePointer, makeCall: () throws -> T, - writeReturn: (T) -> Void + writeReturn: (T) -> () ) { do { try writeReturn(makeCall()) - } catch { + } catch let error { callStatus.pointee.code = CALL_UNEXPECTED_ERROR callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) } @@ -326,7 +339,7 @@ private func uniffiTraitInterfaceCall( private func uniffiTraitInterfaceCallWithError( callStatus: UnsafeMutablePointer, makeCall: () throws -> T, - writeReturn: (T) -> Void, + writeReturn: (T) -> (), lowerError: (E) -> RustBuffer ) { do { @@ -339,8 +352,7 @@ private func uniffiTraitInterfaceCallWithError( callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) } } - -private class UniffiHandleMap { +fileprivate class UniffiHandleMap { private var map: [UInt64: T] = [:] private let lock = NSLock() private var currentHandle: UInt64 = 1 @@ -354,7 +366,7 @@ private class UniffiHandleMap { } } - func get(handle: UInt64) throws -> T { + func get(handle: UInt64) throws -> T { try lock.withLock { guard let obj = map[handle] else { throw UniffiInternalError.unexpectedStaleHandle @@ -374,13 +386,20 @@ private class UniffiHandleMap { } var count: Int { - map.count + get { + map.count + } } } + // Public interface members begin here. -private struct FfiConverterInt32: FfiConverterPrimitive { + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterInt32: FfiConverterPrimitive { typealias FfiType = Int32 typealias SwiftType = Int32 @@ -393,7 +412,10 @@ private struct FfiConverterInt32: FfiConverterPrimitive { } } -private struct FfiConverterString: FfiConverter { +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterString: FfiConverter { typealias SwiftType = String typealias FfiType = RustBuffer @@ -421,7 +443,7 @@ private struct FfiConverterString: FfiConverter { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String { let len: Int32 = try readInt(&buf) - return try String(bytes: readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! + return String(bytes: try readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! } public static func write(_ value: String, into buf: inout [UInt8]) { @@ -431,12 +453,15 @@ private struct FfiConverterString: FfiConverter { } } -private struct FfiConverterData: FfiConverterRustBuffer { +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterData: FfiConverterRustBuffer { typealias SwiftType = Data public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Data { let len: Int32 = try readInt(&buf) - return try Data(readBytes(&buf, count: Int(len))) + return Data(try readBytes(&buf, count: Int(len))) } public static func write(_ value: Data, into buf: inout [UInt8]) { @@ -446,7 +471,10 @@ private struct FfiConverterData: FfiConverterRustBuffer { } } -private struct FfiConverterSequenceString: FfiConverterRustBuffer { +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceString: FfiConverterRustBuffer { typealias SwiftType = [String] public static func write(_ value: [String], into buf: inout [UInt8]) { @@ -462,51 +490,51 @@ private struct FfiConverterSequenceString: FfiConverterRustBuffer { var seq = [String]() seq.reserveCapacity(Int(len)) for _ in 0 ..< len { - try seq.append(FfiConverterString.read(from: &buf)) + seq.append(try FfiConverterString.read(from: &buf)) } return seq } } - public func addressFromSecretKey(data: Data) -> String { - return try! FfiConverterString.lift(try! rustCall { - uniffi_mycelmob_fn_func_address_from_secret_key( - FfiConverterData.lower(data), $0 - ) - }) + return try! FfiConverterString.lift(try! rustCall() { + uniffi_mycelmob_fn_func_address_from_secret_key( + FfiConverterData.lower(data),$0 + ) +}) } - public func generateSecretKey() -> Data { - return try! FfiConverterData.lift(try! rustCall { - uniffi_mycelmob_fn_func_generate_secret_key($0 - ) - }) + return try! FfiConverterData.lift(try! rustCall() { + uniffi_mycelmob_fn_func_generate_secret_key($0 + ) +}) +} +public func getPeerStatus() -> [String] { + return try! FfiConverterSequenceString.lift(try! rustCall() { + uniffi_mycelmob_fn_func_get_peer_status($0 + ) +}) } - public func helloInt() -> Int32 { - return try! FfiConverterInt32.lift(try! rustCall { - uniffi_mycelmob_fn_func_hello_int($0 - ) - }) + return try! FfiConverterInt32.lift(try! rustCall() { + uniffi_mycelmob_fn_func_hello_int($0 + ) +}) } - public func helloMycelios() -> String { - return try! FfiConverterString.lift(try! rustCall { - uniffi_mycelmob_fn_func_hello_mycelios($0 - ) - }) + return try! FfiConverterString.lift(try! rustCall() { + uniffi_mycelmob_fn_func_hello_mycelios($0 + ) +}) } - -public func startMycelium(peers: [String], tunFd: Int32, secretKey: Data) { try! rustCall { +public func startMycelium(peers: [String], tunFd: Int32, secretKey: Data) {try! rustCall() { uniffi_mycelmob_fn_func_start_mycelium( FfiConverterSequenceString.lower(peers), FfiConverterInt32.lower(tunFd), - FfiConverterData.lower(secretKey), $0 + FfiConverterData.lower(secretKey),$0 ) } } - -public func stopMycelium() { try! rustCall { +public func stopMycelium() {try! rustCall() { uniffi_mycelmob_fn_func_stop_mycelium($0 ) } @@ -517,10 +545,9 @@ private enum InitializationResult { case contractVersionMismatch case apiChecksumMismatch } - -// Use a global variables to perform the versioning checks. Swift ensures that +// Use a global variable to perform the versioning checks. Swift ensures that // the code inside is only computed once. -private var initializationResult: InitializationResult { +private var initializationResult: InitializationResult = { // Get the bindings contract version from our ComponentInterface let bindings_contract_version = 26 // Get the scaffolding contract version by calling the into the dylib @@ -528,27 +555,30 @@ private var initializationResult: InitializationResult { if bindings_contract_version != scaffolding_contract_version { return InitializationResult.contractVersionMismatch } - if uniffi_mycelmob_checksum_func_address_from_secret_key() != 39059 { + if (uniffi_mycelmob_checksum_func_address_from_secret_key() != 39059) { return InitializationResult.apiChecksumMismatch } - if uniffi_mycelmob_checksum_func_generate_secret_key() != 63601 { + if (uniffi_mycelmob_checksum_func_generate_secret_key() != 63601) { return InitializationResult.apiChecksumMismatch } - if uniffi_mycelmob_checksum_func_hello_int() != 31063 { + if (uniffi_mycelmob_checksum_func_get_peer_status() != 1198) { return InitializationResult.apiChecksumMismatch } - if uniffi_mycelmob_checksum_func_hello_mycelios() != 48239 { + if (uniffi_mycelmob_checksum_func_hello_int() != 31063) { return InitializationResult.apiChecksumMismatch } - if uniffi_mycelmob_checksum_func_start_mycelium() != 61012 { + if (uniffi_mycelmob_checksum_func_hello_mycelios() != 48239) { return InitializationResult.apiChecksumMismatch } - if uniffi_mycelmob_checksum_func_stop_mycelium() != 28488 { + if (uniffi_mycelmob_checksum_func_start_mycelium() != 61012) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_mycelmob_checksum_func_stop_mycelium() != 28488) { return InitializationResult.apiChecksumMismatch } return InitializationResult.ok -} +}() private func uniffiEnsureInitialized() { switch initializationResult { @@ -561,4 +591,4 @@ private func uniffiEnsureInitialized() { } } -// swiftlint:enable all +// swiftlint:enable all \ No newline at end of file diff --git a/mycelffi/Cargo.lock b/mycelffi/Cargo.lock index fab9236..e940c6f 100644 --- a/mycelffi/Cargo.lock +++ b/mycelffi/Cargo.lock @@ -52,6 +52,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -82,9 +93,9 @@ checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arc-swap" @@ -104,12 +115,94 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -131,13 +224,33 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bindgen" version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools", @@ -148,14 +261,20 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "bitflags" -version = "2.9.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "blake3" @@ -210,13 +329,24 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.29" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "shlex", ] +[[package]] +name = "cdn-meta" +version = "0.1.0" +source = "git+https://github.com/threefoldtech/mycelium-cdn-registry#2fac04710b60502a5165f1b1d90671730346e977" +dependencies = [ + "aes-gcm", + "bincode", + "serde", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -228,9 +358,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -259,6 +389,20 @@ dependencies = [ "libloading", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -318,9 +462,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.2.0" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373b7c5dbd637569a2cca66e8d66b8c446a1e7bf064ea321d265d7b3dfe7c97e" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", @@ -339,7 +483,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -350,21 +494,32 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.11", ] [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "dlopen2" version = "0.5.0" @@ -382,11 +537,39 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "etherparse" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff83a5facf1a7cbfef93cfb48d6d4fb6a1f42d8ac2341a96b3255acb4d4f860" +checksum = "b119b9796ff800751a220394b8b3613f26dd30c48f254f6837e64c464872d1c7" dependencies = [ "arrayvec", ] @@ -403,9 +586,21 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.3.0" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + +[[package]] +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" @@ -422,6 +617,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.31" @@ -478,7 +682,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -557,7 +761,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] @@ -579,9 +783,28 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] [[package]] name = "hash32" @@ -592,12 +815,27 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "heapless" version = "0.8.0" @@ -608,6 +846,247 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + [[package]] name = "inout" version = "0.1.4" @@ -617,13 +1096,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "io-uring" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -646,6 +1134,27 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "itertools" version = "0.13.0" @@ -663,9 +1172,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -689,20 +1198,32 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-link 0.2.0", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" version = "0.4.13" @@ -715,9 +1236,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "loom" @@ -732,6 +1253,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -740,13 +1270,19 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.5" @@ -762,6 +1298,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -791,11 +1333,13 @@ dependencies = [ [[package]] name = "mobile" version = "0.1.0" -source = "git+http://github.com/threefoldtech/mycelium?tag=v0.6.1#a9ba23d7bd020411be51cd2359775f6f11b159e4" dependencies = [ + "env_logger", + "log", "mycelium", "once_cell", - "thiserror 2.0.12", + "serde_json", + "thiserror 2.0.16", "tokio", "tracing", "tracing-android", @@ -812,14 +1356,16 @@ dependencies = [ [[package]] name = "mycelium" -version = "0.6.1" -source = "git+http://github.com/threefoldtech/mycelium?tag=v0.6.1#a9ba23d7bd020411be51cd2359775f6f11b159e4" +version = "0.6.2" dependencies = [ "aes-gcm", - "ahash", + "ahash 0.8.12", "arc-swap", + "axum", + "axum-extra", "blake3", "bytes", + "cdn-meta", "dashmap", "etherparse", "faster-hex", @@ -835,6 +1381,9 @@ dependencies = [ "quinn", "rand", "rcgen", + "redis", + "reed-solomon-erasure", + "reqwest", "rtnetlink", "rustls", "serde", @@ -852,15 +1401,15 @@ dependencies = [ [[package]] name = "netdev" -version = "0.34.1" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51305a587ceb5e3440e24e02a07f5d3d401d8d7edb71f59f6542ba705bd57483" +checksum = "daa1e3eaf125c54c21e6221df12dd2a0a682784a068782dd564c836c0f281b6d" dependencies = [ "dlopen2", "ipnet", "libc", - "netlink-packet-core", - "netlink-packet-route", + "netlink-packet-core 0.7.0", + "netlink-packet-route 0.22.0", "netlink-sys", "once_cell", "system-configuration", @@ -878,6 +1427,15 @@ dependencies = [ "netlink-packet-utils", ] +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + [[package]] name = "netlink-packet-route" version = "0.22.0" @@ -885,14 +1443,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.9.4", "byteorder", "libc", "log", - "netlink-packet-core", + "netlink-packet-core 0.7.0", "netlink-packet-utils", ] +[[package]] +name = "netlink-packet-route" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" +dependencies = [ + "bitflags 2.9.4", + "libc", + "log", + "netlink-packet-core 0.8.1", +] + [[package]] name = "netlink-packet-utils" version = "0.5.2" @@ -907,16 +1477,16 @@ dependencies = [ [[package]] name = "netlink-proto" -version = "0.11.5" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" dependencies = [ "bytes", "futures", "log", - "netlink-packet-core", + "netlink-packet-core 0.8.1", "netlink-sys", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -938,7 +1508,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", @@ -951,7 +1521,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", @@ -970,21 +1540,21 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] -name = "nu-ansi-term" -version = "0.50.1" +name = "num-bigint" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "windows-sys 0.52.0", + "num-integer", + "num-traits", ] [[package]] @@ -993,6 +1563,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -1020,7 +1608,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -1037,14 +1625,14 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "openssl-src" -version = "300.5.1+3.5.1" +version = "300.5.2+3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "735230c832b28c000e3bc117119e6466a663ec73506bc0a9907ea4187508e42a" +checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" dependencies = [ "cc", ] @@ -1063,10 +1651,15 @@ dependencies = [ ] [[package]] -name = "overload" -version = "0.1.1" +name = "parking_lot" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] [[package]] name = "parking_lot" @@ -1075,7 +1668,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.11", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -1086,7 +1693,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.17", "smallvec", "windows-targets 0.52.6", ] @@ -1107,6 +1714,12 @@ dependencies = [ "serde", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1137,6 +1750,15 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1154,28 +1776,28 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -1185,7 +1807,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -1193,9 +1815,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.3", @@ -1206,7 +1828,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -1214,16 +1836,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1243,9 +1865,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core 0.9.3", @@ -1281,9 +1903,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.13.2" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +checksum = "4c83367ba62b3f1dbd0f086ede4e5ebfb4713fb234dbbc5807772a31245ff46d" dependencies = [ "pem", "ring", @@ -1292,58 +1914,128 @@ dependencies = [ "yasna", ] +[[package]] +name = "redis" +version = "0.32.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] -name = "regex" -version = "1.11.1" +name = "redox_syscall" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "bitflags 2.9.4", ] [[package]] -name = "regex-automata" -version = "0.1.10" +name = "reed-solomon-erasure" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +checksum = "7263373d500d4d4f505d43a2a662d475a894aa94503a1ee28e9188b5f3960d4f" dependencies = [ - "regex-syntax 0.6.29", + "libm", + "lru", + "parking_lot 0.11.2", + "smallvec", + "spin", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] -name = "regex-syntax" -version = "0.8.5" +name = "reqwest" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] [[package]] name = "ring" @@ -1361,15 +2053,14 @@ dependencies = [ [[package]] name = "rtnetlink" -version = "0.16.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb5850b5aa2c9c0ae44f157694bbe85107a2e13d76eb3178d0e3ee96c410f57" +checksum = "08fd15aa4c64c34d0b3178e45ec6dad313a9f02b193376d501668a7950264bb7" dependencies = [ "futures", "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-packet-utils", + "netlink-packet-core 0.8.1", + "netlink-packet-route 0.25.1", "netlink-proto", "netlink-sys", "nix 0.29.0", @@ -1379,9 +2070,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -1406,9 +2097,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.29" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "ring", @@ -1430,9 +2121,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "ring", "rustls-pki-types", @@ -1441,9 +2132,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "scoped-tls" @@ -1459,30 +2156,82 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1500,18 +2249,18 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -1521,14 +2270,20 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1554,22 +2309,42 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "core-foundation", "system-configuration-sys", ] @@ -1584,6 +2359,15 @@ dependencies = [ "libc", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1595,11 +2379,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -1610,18 +2394,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1635,9 +2419,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -1650,25 +2434,35 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -1681,9 +2475,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -1695,7 +2489,7 @@ dependencies = [ "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1706,7 +2500,17 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" +dependencies = [ + "rustls", + "tokio", ] [[package]] @@ -1723,21 +2527,21 @@ dependencies = [ [[package]] name = "tokio-tun" -version = "0.13.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be25a316a2d10d0ddd46de9ddd73cdb4262678e1e52f4a32a31421f1e2b816b4" +checksum = "ef6cadea27ba297ef9124370e49af79913b9b979bffd6511f65702eefbce12fe" dependencies = [ "libc", - "nix 0.29.0", - "thiserror 2.0.12", + "nix 0.30.1", + "thiserror 2.0.16", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -1746,12 +2550,59 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1776,7 +2627,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1806,7 +2657,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd" dependencies = [ - "nu-ansi-term 0.50.1", + "nu-ansi-term", "time", "tracing", "tracing-core", @@ -1823,21 +2674,21 @@ dependencies = [ "cc", "cfg-if", "once_cell", - "parking_lot", + "parking_lot 0.12.4", "tracing-core", "tracing-subscriber", ] [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", - "nu-ansi-term 0.46.0", + "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -1846,6 +2697,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tun" version = "0.6.1" @@ -1871,9 +2728,9 @@ checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "universal-hash" @@ -1891,6 +2748,30 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "valuable" version = "0.1.1" @@ -1909,6 +2790,21 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1917,43 +2813,67 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1961,26 +2881,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -1991,6 +2921,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2007,6 +2946,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2041,6 +2989,47 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2059,6 +3048,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2092,10 +3099,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -2271,13 +3279,16 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "x25519-dalek" @@ -2300,24 +3311,69 @@ dependencies = [ "time", ] +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", + "synstructure", ] [[package]] @@ -2337,5 +3393,38 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] diff --git a/mycelffi/Cargo.toml b/mycelffi/Cargo.toml index cd409e7..d7dca6f 100644 --- a/mycelffi/Cargo.toml +++ b/mycelffi/Cargo.toml @@ -9,4 +9,4 @@ crate-type = ["cdylib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -mobile = { git = "http://github.com/threefoldtech/mycelium", package = "mobile", tag = "v0.6.1" } +mobile = { path = "../../mycelium/mobile" } diff --git a/mycelffi/src/lib.rs b/mycelffi/src/lib.rs index da3ec20..a1ae52c 100644 --- a/mycelffi/src/lib.rs +++ b/mycelffi/src/lib.rs @@ -1,11 +1,11 @@ -use mobile::proxy::{proxy_connect, proxy_disconnect}; +use mobile::{generate_secret_key, address_from_secret_key, start_mycelium, stop_mycelium, get_peer_status}; use std::ffi::{CStr, CString}; use std::os::raw::c_char; #[no_mangle] pub extern "C" fn ff_generate_secret_key(out_ptr: *mut *mut u8, out_len: *mut usize) { - let secret_key = mobile::generate_secret_key(); + let secret_key = generate_secret_key(); let len = secret_key.len(); let ptr = secret_key.as_ptr(); @@ -27,11 +27,12 @@ pub extern "C" fn free_secret_key(ptr: *mut u8, len: usize) { Vec::from_raw_parts(ptr, len, len); } } + #[no_mangle] pub extern "C" fn ff_address_from_secret_key(data: *const u8, len: usize) -> *mut c_char { let slice = unsafe { std::slice::from_raw_parts(data, len) }; let vec = slice.to_vec(); - let address = mobile::address_from_secret_key(vec); + let address = address_from_secret_key(vec); let c_string = CString::new(address).unwrap(); c_string.into_raw() } @@ -65,18 +66,18 @@ pub extern "C" fn ff_start_mycelium( let priv_key: Vec = unsafe { std::slice::from_raw_parts(priv_key_ptr, priv_key_len).to_vec() }; - mobile::start_mycelium(peers, 0, priv_key); + start_mycelium(peers, 0, priv_key); } #[no_mangle] pub extern "C" fn ff_stop_mycelium() -> bool { - let result = mobile::stop_mycelium(); + let result = stop_mycelium(); result == "ok" } #[no_mangle] pub extern "C" fn ff_get_peer_status(out_ptr: *mut *mut *mut c_char, out_len: *mut usize) { - let peer_status = mobile::get_peer_status(); + let peer_status = get_peer_status(); let len = peer_status.len(); // Convert Vec to Vec<*mut c_char> @@ -116,18 +117,5 @@ pub extern "C" fn free_peer_status(ptr: *mut *mut c_char, len: usize) { } } -#[no_mangle] -pub extern "C" fn ff_proxy_connect(remote_str: *const c_char) -> bool { - let c_str = unsafe { CStr::from_ptr(remote_str) }; - let remote_str = c_str.to_string_lossy(); - let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(proxy_connect(&remote_str)); - result == "ok" -} - -#[no_mangle] -pub extern "C" fn ff_proxy_disconnect() -> bool { - let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(proxy_disconnect()); - result == "ok" -} +// Note: Proxy functions are handled through the mobile uniffi bindings instead of FFI +// These functions would require async runtime setup which is complex for FFI diff --git a/mycelmob/Cargo.lock b/mycelmob/Cargo.lock index 8bb12a0..f42af07 100644 --- a/mycelmob/Cargo.lock +++ b/mycelmob/Cargo.lock @@ -52,6 +52,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -82,9 +93,9 @@ checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -112,29 +123,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arc-swap" @@ -177,7 +188,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -195,12 +206,94 @@ dependencies = [ "nom", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -240,13 +333,33 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bindgen" version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools", @@ -257,14 +370,20 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "bitflags" -version = "2.9.1" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "blake3" @@ -319,11 +438,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.10" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" +checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -351,13 +470,24 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.29" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "shlex", ] +[[package]] +name = "cdn-meta" +version = "0.1.0" +source = "git+https://github.com/threefoldtech/mycelium-cdn-registry#2fac04710b60502a5165f1b1d90671730346e977" +dependencies = [ + "aes-gcm", + "bincode 2.0.1", + "serde", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -369,9 +499,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -402,9 +532,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", "clap_derive", @@ -412,9 +542,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstream", "anstyle", @@ -424,14 +554,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -446,6 +576,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -505,9 +649,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.2.0" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373b7c5dbd637569a2cca66e8d66b8c446a1e7bf064ea321d265d7b3dfe7c97e" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", @@ -526,7 +670,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -537,21 +681,32 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.11", ] [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "dlopen2" version = "0.5.0" @@ -569,11 +724,26 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "etherparse" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff83a5facf1a7cbfef93cfb48d6d4fb6a1f42d8ac2341a96b3255acb4d4f860" +checksum = "b119b9796ff800751a220394b8b3613f26dd30c48f254f6837e64c464872d1c7" dependencies = [ "arrayvec", ] @@ -590,9 +760,21 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.3.0" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" @@ -609,6 +791,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fs-err" version = "2.11.0" @@ -674,7 +865,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -753,7 +944,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] @@ -775,9 +966,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "goblin" @@ -790,6 +981,25 @@ dependencies = [ "scroll", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hash32" version = "0.3.1" @@ -799,12 +1009,27 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "heapless" version = "0.8.0" @@ -821,6 +1046,235 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + [[package]] name = "inout" version = "0.1.4" @@ -830,13 +1284,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "io-uring" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -859,6 +1322,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -882,9 +1355,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -908,9 +1381,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" @@ -919,9 +1392,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" version = "0.4.13" @@ -934,9 +1419,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "loom" @@ -951,6 +1436,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -959,13 +1453,19 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.5" @@ -1026,11 +1526,11 @@ dependencies = [ [[package]] name = "mobile" version = "0.1.0" -source = "git+http://github.com/threefoldtech/mycelium?tag=v0.6.1#a9ba23d7bd020411be51cd2359775f6f11b159e4" dependencies = [ "mycelium", "once_cell", - "thiserror 2.0.12", + "serde_json", + "thiserror 2.0.16", "tokio", "tracing", "tracing-android", @@ -1040,14 +1540,16 @@ dependencies = [ [[package]] name = "mycelium" -version = "0.6.1" -source = "git+http://github.com/threefoldtech/mycelium?tag=v0.6.1#a9ba23d7bd020411be51cd2359775f6f11b159e4" +version = "0.6.2" dependencies = [ "aes-gcm", - "ahash", + "ahash 0.8.12", "arc-swap", + "axum", + "axum-extra", "blake3", "bytes", + "cdn-meta", "dashmap", "etherparse", "faster-hex", @@ -1063,6 +1565,9 @@ dependencies = [ "quinn", "rand", "rcgen", + "redis", + "reed-solomon-erasure", + "reqwest", "rtnetlink", "rustls", "serde", @@ -1083,20 +1588,22 @@ name = "mycelmob" version = "0.1.0" dependencies = [ "mobile", + "once_cell", + "tokio", "uniffi", ] [[package]] name = "netdev" -version = "0.34.1" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51305a587ceb5e3440e24e02a07f5d3d401d8d7edb71f59f6542ba705bd57483" +checksum = "daa1e3eaf125c54c21e6221df12dd2a0a682784a068782dd564c836c0f281b6d" dependencies = [ "dlopen2", "ipnet", "libc", - "netlink-packet-core", - "netlink-packet-route", + "netlink-packet-core 0.7.0", + "netlink-packet-route 0.22.0", "netlink-sys", "once_cell", "system-configuration", @@ -1114,6 +1621,15 @@ dependencies = [ "netlink-packet-utils", ] +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + [[package]] name = "netlink-packet-route" version = "0.22.0" @@ -1121,14 +1637,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.9.4", "byteorder", "libc", "log", - "netlink-packet-core", + "netlink-packet-core 0.7.0", "netlink-packet-utils", ] +[[package]] +name = "netlink-packet-route" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" +dependencies = [ + "bitflags 2.9.4", + "libc", + "log", + "netlink-packet-core 0.8.1", +] + [[package]] name = "netlink-packet-utils" version = "0.5.2" @@ -1143,16 +1671,16 @@ dependencies = [ [[package]] name = "netlink-proto" -version = "0.11.5" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" dependencies = [ "bytes", "futures", "log", - "netlink-packet-core", + "netlink-packet-core 0.8.1", "netlink-sys", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -1174,7 +1702,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", @@ -1187,7 +1715,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", @@ -1206,21 +1734,21 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] -name = "nu-ansi-term" -version = "0.50.1" +name = "num-bigint" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "windows-sys 0.52.0", + "num-integer", + "num-traits", ] [[package]] @@ -1229,6 +1757,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -1262,7 +1808,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -1279,14 +1825,14 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "openssl-src" -version = "300.5.1+3.5.1" +version = "300.5.2+3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "735230c832b28c000e3bc117119e6466a663ec73506bc0a9907ea4187508e42a" +checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" dependencies = [ "cc", ] @@ -1305,10 +1851,15 @@ dependencies = [ ] [[package]] -name = "overload" -version = "0.1.1" +name = "parking_lot" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] [[package]] name = "parking_lot" @@ -1317,7 +1868,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.11", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -1328,7 +1893,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.17", "smallvec", "windows-targets 0.52.6", ] @@ -1349,6 +1914,12 @@ dependencies = [ "serde", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1385,6 +1956,15 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1402,28 +1982,28 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -1433,7 +2013,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -1441,9 +2021,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.3", @@ -1454,7 +2034,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -1462,16 +2042,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1491,9 +2071,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core 0.9.3", @@ -1529,9 +2109,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.13.2" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +checksum = "4c83367ba62b3f1dbd0f086ede4e5ebfb4713fb234dbbc5807772a31245ff46d" dependencies = [ "pem", "ring", @@ -1540,58 +2120,128 @@ dependencies = [ "yasna", ] +[[package]] +name = "redis" +version = "0.32.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "reed-solomon-erasure" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "7263373d500d4d4f505d43a2a662d475a894aa94503a1ee28e9188b5f3960d4f" dependencies = [ - "bitflags", + "libm", + "lru", + "parking_lot 0.11.2", + "smallvec", + "spin", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] -name = "regex-syntax" -version = "0.8.5" +name = "reqwest" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] [[package]] name = "ring" @@ -1609,15 +2259,14 @@ dependencies = [ [[package]] name = "rtnetlink" -version = "0.16.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb5850b5aa2c9c0ae44f157694bbe85107a2e13d76eb3178d0e3ee96c410f57" +checksum = "08fd15aa4c64c34d0b3178e45ec6dad313a9f02b193376d501668a7950264bb7" dependencies = [ "futures", "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-packet-utils", + "netlink-packet-core 0.8.1", + "netlink-packet-route 0.25.1", "netlink-proto", "netlink-sys", "nix 0.29.0", @@ -1627,9 +2276,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -1654,9 +2303,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.29" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "ring", @@ -1678,9 +2327,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "ring", "rustls-pki-types", @@ -1689,9 +2338,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1728,50 +2377,91 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1789,9 +2479,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -1804,9 +2494,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -1822,14 +2512,20 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1867,22 +2563,42 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "core-foundation", "system-configuration-sys", ] @@ -1917,11 +2633,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -1932,18 +2648,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1957,9 +2673,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -1972,25 +2688,35 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -2003,9 +2729,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -2017,7 +2743,7 @@ dependencies = [ "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2028,7 +2754,17 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" +dependencies = [ + "rustls", + "tokio", ] [[package]] @@ -2045,21 +2781,21 @@ dependencies = [ [[package]] name = "tokio-tun" -version = "0.13.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be25a316a2d10d0ddd46de9ddd73cdb4262678e1e52f4a32a31421f1e2b816b4" +checksum = "ef6cadea27ba297ef9124370e49af79913b9b979bffd6511f65702eefbce12fe" dependencies = [ "libc", - "nix 0.29.0", - "thiserror 2.0.12", + "nix 0.30.1", + "thiserror 2.0.16", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -2077,12 +2813,59 @@ dependencies = [ "serde", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2107,7 +2890,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2137,7 +2920,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd" dependencies = [ - "nu-ansi-term 0.50.1", + "nu-ansi-term", "time", "tracing", "tracing-core", @@ -2154,21 +2937,21 @@ dependencies = [ "cc", "cfg-if", "once_cell", - "parking_lot", + "parking_lot 0.12.4", "tracing-core", "tracing-subscriber", ] [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", - "nu-ansi-term 0.46.0", + "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -2177,6 +2960,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tun" version = "0.6.1" @@ -2208,9 +2997,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "uniffi" @@ -2269,7 +3058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2292,14 +3081,14 @@ version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b" dependencies = [ - "bincode", + "bincode 1.3.3", "camino", "fs-err", "once_cell", "proc-macro2", "quote", "serde", - "syn 2.0.104", + "syn 2.0.106", "toml", "uniffi_meta", ] @@ -2358,6 +3147,30 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2382,6 +3195,21 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2390,43 +3218,67 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2434,26 +3286,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -2464,6 +3326,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weedle2" version = "5.0.0" @@ -2523,6 +3394,41 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2541,6 +3447,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2574,10 +3489,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -2753,13 +3669,16 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "x25519-dalek" @@ -2782,24 +3701,69 @@ dependencies = [ "time", ] +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", + "synstructure", ] [[package]] @@ -2819,5 +3783,38 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] diff --git a/mycelmob/Cargo.toml b/mycelmob/Cargo.toml index 835cd03..a7cedcb 100644 --- a/mycelmob/Cargo.toml +++ b/mycelmob/Cargo.toml @@ -11,13 +11,12 @@ name = "mycelmob" [dependencies] uniffi = { version = "0.28.2", features = ["cli"] } -mobile = { git = "http://github.com/threefoldtech/mycelium", package = "mobile", tag = "v0.6.2", features = [ +mobile = { path = "../../mycelium/mobile", features = [ "mactunfd", ] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] } once_cell = "1.19" -#mobile = { path = "../../mycelium/mobile" } [build-dependencies] uniffi = { version = "0.28.2", features = ["build"] } diff --git a/mycelmob/src/lib.rs b/mycelmob/src/lib.rs index 0e53943..a919288 100644 --- a/mycelmob/src/lib.rs +++ b/mycelmob/src/lib.rs @@ -86,6 +86,30 @@ pub extern "C" fn ffi_proxy_disconnect() -> *mut c_char { CString::new(joined).unwrap().into_raw() } +#[no_mangle] +pub extern "C" fn ffi_start_proxy_probe() -> *mut c_char { + let result = start_proxy_probe(); + let joined = result.join(","); + + CString::new(joined).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn ffi_stop_proxy_probe() -> *mut c_char { + let result = stop_proxy_probe(); + let joined = result.join(","); + + CString::new(joined).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn ffi_list_proxies() -> *mut c_char { + let result = list_proxies(); + let joined = result.join(","); + + CString::new(joined).unwrap().into_raw() +} + /// Helper to free strings allocated by Rust #[no_mangle] pub extern "C" fn ffi_free_string(s: *mut c_char) { diff --git a/pubspec.lock b/pubspec.lock index f75e38f..a986de8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -81,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + country_flags: + dependency: "direct main" + description: + name: country_flags + sha256: "66726c7070d60c2f90c4a1d58980e9188fa04335d6287e98aef835461019c3c2" + url: "https://pub.dev" + source: hosted + version: "3.2.0" crypto: dependency: "direct main" description: @@ -105,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -129,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb + url: "https://pub.dev" + source: hosted + version: "0.68.0" flutter: dependency: "direct main" description: flutter @@ -147,6 +187,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" flutter_lints: dependency: "direct dev" description: @@ -210,6 +258,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.5" + http: + dependency: "direct main" + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" image: dependency: transitive description: @@ -223,6 +287,30 @@ packages: description: flutter source: sdk version: "0.0.0" + jovial_misc: + dependency: transitive + description: + name: jovial_misc + sha256: "4301011027d87b8b919cb862db84071a34448eadbb32cc8d40fe505424dfe69a" + url: "https://pub.dev" + source: hosted + version: "0.9.2" + jovial_svg: + dependency: transitive + description: + name: jovial_svg + sha256: "08dd24b800d48796c9c0227acb96eb00c6cacccb1d7de58d79fc924090049868" + url: "https://pub.dev" + source: hosted + version: "1.1.28" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -279,6 +367,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.1" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.dev" + source: hosted + version: "0.1.1" meta: dependency: transitive description: @@ -287,6 +383,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: "direct main" description: @@ -383,6 +495,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" shared_preferences: dependency: "direct main" description: @@ -439,6 +591,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.dev" + source: hosted + version: "0.1.2" sky_engine: dependency: transitive description: flutter @@ -508,6 +668,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.3" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: bdc3ac6c36f3d12d871459e4a9822705ce5a1165a17fa837103bc842719bf3f7 + url: "https://pub.dev" + source: hosted + version: "0.2.4" typed_data: dependency: transitive description: @@ -556,6 +724,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.4" + win32: + dependency: transitive + description: + name: win32 + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + url: "https://pub.dev" + source: hosted + version: "5.10.1" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" + url: "https://pub.dev" + source: hosted + version: "0.4.3" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d432f00..e52095f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,20 +32,25 @@ dependencies: sdk: flutter convert: ^3.0.1 crypto: ^3.0.1 - go_router: ^14.2.7 - flutter_riverpod: ^2.5.1 + go_router: ^14.8.1 + flutter_riverpod: ^2.6.1 shared_preferences: ^2.3.2 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 - path_provider: ^2.0.2 - logging: ^1.0.1 + path_provider: ^2.1.4 + logging: ^1.2.0 flutter_desktop_sleep: ^0.0.2 path: 1.9.0 - ffi: 2.1.3 + ffi: ^2.1.3 flutter_window_close: ^1.2.0 http: ^1.5.0 + fl_chart: ^0.68.0 + country_flags: ^3.2.0 + tray_manager: ^0.2.3 + window_manager: ^0.4.2 + package_info_plus: ^8.0.0 dev_dependencies: flutter_test: @@ -61,7 +66,7 @@ dev_dependencies: sdk: flutter change_app_package_name: ^1.1.0 - #flutter_launcher_icons: ^0.9.2 + flutter_launcher_icons: ^0.14.1 flutter_native_splash: ^2.4.0 # For information on the generic Dart part of this file, see the @@ -80,6 +85,8 @@ flutter: - assets/images/mycelium_top.svg - assets/images/mycelium_top.png - assets/dll/winmycelium.dll + - assets/images/mycelium_icon.png + - assets/images/tray_icon.ico # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see @@ -110,9 +117,15 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages -flutter_icons: +flutter_launcher_icons: android: true ios: true + windows: + generate: true + image_path: "assets/images/mycelium_icon.png" + macos: + generate: true + image_path: "assets/images/mycelium_icon.png" image_path: "assets/images/mycelium_icon.png" remove_alpha_ios: true diff --git a/test/peer_status_test.dart b/test/peer_status_test.dart new file mode 100644 index 0000000..f1c7f25 --- /dev/null +++ b/test/peer_status_test.dart @@ -0,0 +1,120 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:myceliumflut/models/peer_models.dart'; +import 'dart:convert'; + +void main() { + group('PeerStats', () { + test('should parse JSON correctly', () { + final jsonString = ''' + { + "protocol": "tcp", + "address": "185.69.166.7:9651", + "peerType": "Static", + "connectionState": "Connected", + "rxBytes": 1048576, + "txBytes": 2097152, + "discoveredSeconds": 3600, + "lastConnectedSeconds": 1800 + } + '''; + + final json = jsonDecode(jsonString); + final peerStats = PeerStats.fromJson(json); + + expect(peerStats.protocol, equals('tcp')); + expect(peerStats.address, equals('185.69.166.7:9651')); + expect(peerStats.peerType, equals(PeerType.static)); + expect(peerStats.connectionState, equals(ConnectionState.connected)); + expect(peerStats.rxBytes, equals(1048576)); + expect(peerStats.txBytes, equals(2097152)); + expect(peerStats.discoveredSeconds, equals(3600)); + expect(peerStats.lastConnectedSeconds, equals(1800)); + }); + + test('should format bytes correctly', () { + expect(PeerStats.formatBytes(1024), equals('1.00KB')); + expect(PeerStats.formatBytes(1048576), equals('1.00MB')); + expect(PeerStats.formatBytes(1073741824), equals('1.00GB')); + expect(PeerStats.formatBytes(512), equals('512B')); + }); + + test('should format duration correctly', () { + expect(PeerStats.formatDuration(30), equals('30s')); + expect(PeerStats.formatDuration(90), equals('1m 30s')); + expect(PeerStats.formatDuration(3661), equals('1h 1m 1s')); + expect(PeerStats.formatDuration(90061), equals('1d 1h 1m 1s')); + }); + + test('should handle null lastConnectedSeconds', () { + final jsonString = ''' + { + "protocol": "tcp", + "address": "185.69.166.7:9651", + "peerType": "Discovered", + "connectionState": "Disconnected", + "rxBytes": 0, + "txBytes": 0, + "discoveredSeconds": 60, + "lastConnectedSeconds": null + } + '''; + + final json = jsonDecode(jsonString); + final peerStats = PeerStats.fromJson(json); + + expect(peerStats.lastConnectedSeconds, isNull); + expect(peerStats.formattedLastConnected, equals('Never connected')); + }); + + test('should create endpoint correctly', () { + final peerStats = PeerStats( + protocol: 'tcp', + address: '185.69.166.7:9651', + peerType: PeerType.static, + connectionState: ConnectionState.connected, + rxBytes: 1024, + txBytes: 2048, + discoveredSeconds: 3600, + lastConnectedSeconds: 1800, + ); + + expect(peerStats.endpoint, equals('tcp://185.69.166.7:9651')); + }); + }); + + group('PeerType', () { + test('should parse from string correctly', () { + expect(PeerType.fromString('static'), equals(PeerType.static)); + expect(PeerType.fromString('Static'), equals(PeerType.static)); + expect(PeerType.fromString('STATIC'), equals(PeerType.static)); + expect(PeerType.fromString('discovered'), equals(PeerType.discovered)); + expect(PeerType.fromString('unknown_type'), equals(PeerType.unknown)); + }); + + test('should convert to string correctly', () { + expect(PeerType.static.toString(), equals('Static')); + expect(PeerType.discovered.toString(), equals('Discovered')); + expect(PeerType.unknown.toString(), equals('Unknown')); + }); + }); + + group('ConnectionState', () { + test('should parse from string correctly', () { + expect(ConnectionState.fromString('connected'), equals(ConnectionState.connected)); + expect(ConnectionState.fromString('Connected'), equals(ConnectionState.connected)); + expect(ConnectionState.fromString('CONNECTED'), equals(ConnectionState.connected)); + expect(ConnectionState.fromString('connecting'), equals(ConnectionState.connecting)); + expect(ConnectionState.fromString('disconnected'), equals(ConnectionState.disconnected)); + expect(ConnectionState.fromString('failed'), equals(ConnectionState.failed)); + expect(ConnectionState.fromString('unknown_state'), equals(ConnectionState.unknown)); + }); + + test('should convert to string correctly', () { + expect(ConnectionState.connected.toString(), equals('Connected')); + expect(ConnectionState.connecting.toString(), equals('Connecting')); + expect(ConnectionState.disconnected.toString(), equals('Disconnected')); + expect(ConnectionState.failed.toString(), equals('Failed')); + expect(ConnectionState.unknown.toString(), equals('Unknown')); + }); + }); +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f7b19c3..cfd9569 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,19 @@ #include #include +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterDesktopSleepPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterDesktopSleepPluginCApi")); FlutterWindowClosePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWindowClosePlugin")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + TrayManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TrayManagerPlugin")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4f6d1f5..ffa7675 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_desktop_sleep flutter_window_close + screen_retriever_windows + tray_manager + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20c..cdfed1a 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/resources/app_icon_copy.ico b/windows/runner/resources/app_icon_copy.ico new file mode 100644 index 0000000..2841987 Binary files /dev/null and b/windows/runner/resources/app_icon_copy.ico differ