Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.network.ForwardingCookieHandler
import com.facebook.react.modules.network.ReactCookieJarContainer
import com.mattermost.networkclient.helpers.KeyStoreHelper
import com.mattermost.networkclient.sessionattributes.SessionAttributesEngine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import okhttp3.Call
Expand Down Expand Up @@ -132,6 +133,7 @@ class ApiClientModuleImpl(appContext: Context) {

init {
setCtx(appContext)
SessionAttributesEngine.init(appContext)
migrateSharedPreferences(appContext)
setCookieJar(appContext)
}
Expand Down Expand Up @@ -360,6 +362,34 @@ class ApiClientModuleImpl(appContext: Context) {
}
}

fun setSessionAttributesEnabled(serverUrl: String, enabled: Boolean) {
SessionAttributesEngine.setEnabled(serverUrl, enabled)
}

fun removeSessionAttributesServer(serverUrl: String) {
SessionAttributesEngine.removeServer(serverUrl)
}

fun setSessionAttributesManifest(serverUrl: String, manifest: String) {
SessionAttributesEngine.setManifest(serverUrl, manifest)
}

fun upsertSessionAttributesField(serverUrl: String, field: String) {
SessionAttributesEngine.upsertManifestField(serverUrl, field)
}

fun removeSessionAttributesField(serverUrl: String, name: String) {
SessionAttributesEngine.removeManifestField(serverUrl, name)
}

fun setSessionAttributesStableValues(values: String) {
SessionAttributesEngine.setStableValues(values)
}

fun getSessionAttributesHeader(serverUrl: String): String? {
return SessionAttributesEngine.getOutboundHeader(serverUrl)
}

// Methods to use with native implementations
fun hasClientFor(url: HttpUrl): Boolean {
return clients.containsKey(url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ internal class NetworkClient(private val context: Context, private val baseUrl:
builder.addInterceptor(bearerTokenInterceptor)
}

val sessionAttributesInterceptor = getSessionAttributesInterceptor(options)
if (sessionAttributesInterceptor != null) {
// Added after BearerTokenInterceptor so the Authorization header is present when the
// guard is evaluated. Also covers adaptRCTRequest() since RCT requests are executed
// through this same client's okHttpClient.
builder.addInterceptor(sessionAttributesInterceptor)
}

applyClientSslConfiguration(options)
configureSsl()

Expand Down Expand Up @@ -496,6 +504,19 @@ internal class NetworkClient(private val context: Context, private val baseUrl:
return null
}

private fun getSessionAttributesInterceptor(options: ReadableMap?): SessionAttributesInterceptor? {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

if (options != null && options.hasKey("requestAdapterConfiguration")) {
val requestAdapterConfiguration = options.getMap("requestAdapterConfiguration")!!
if (requestAdapterConfiguration.hasKey("enableSessionAttributes") &&
requestAdapterConfiguration.getBoolean("enableSessionAttributes")
) {
return SessionAttributesInterceptor(baseUrlString)
}
}

return null
}

@SuppressLint("CustomX509TrustManager")
private fun getTrustManager(defaultTrustManager: X509TrustManager): X509TrustManager {
return object : X509TrustManager {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.mattermost.networkclient.interceptors

import com.mattermost.networkclient.sessionattributes.SessionAttributesConstants
import com.mattermost.networkclient.sessionattributes.SessionAttributesEngine
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException

class SessionAttributesInterceptor(private val serverUrl: String) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()

val hasAuthorization = request.header("Authorization") != null
val hasSessionAttributes = request.header(SessionAttributesConstants.HEADER_NAME) != null

if (!hasAuthorization || hasSessionAttributes) {
return chain.proceed(request)
}

val header = SessionAttributesEngine.getOutboundHeader(serverUrl)
?: return chain.proceed(request)

val newRequest = request.newBuilder()
.header(SessionAttributesConstants.HEADER_NAME, header)
.build()

return chain.proceed(newRequest)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.mattermost.networkclient.sessionattributes

import android.content.Context

/**
* React-free entry point for native code outside this library (e.g. app
* background handlers, standalone OkHttp usage) to resolve the outbound
* X-MM-Session-Attributes header for a server.
*
* Callers that reach this outside the React module (e.g. WorkManager jobs) must
* call [init] from their Application.onCreate, which runs before any component
* in the process.
*/
object SessionAttributes {
@JvmStatic
fun init(context: Context) {
SessionAttributesEngine.init(context)
}

@JvmStatic
fun getOutboundHeader(serverUrl: String): String? {
return SessionAttributesEngine.getOutboundHeader(serverUrl)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.mattermost.networkclient.sessionattributes

import android.app.admin.DevicePolicyManager
import android.content.Context
import android.content.RestrictionsManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Build
import android.util.Log
import java.net.Inet4Address
import java.net.NetworkInterface
import java.util.concurrent.ConcurrentHashMap

data class NetworkSnapshot(
val interfaceType: String,
val ipAddress: String,
val vpnActive: Boolean,
val ssid: String,
)

class SessionAttributesCollector(
private val context: Context,
private val store: SessionAttributesStore,
) {
private val fqdnCache = ConcurrentHashMap<String, String>()

fun collect(name: String, serverUrl: String): String {
store.getStableValue(name)?.let { return it }

val snapshot = currentNetworkSnapshot()
return when (name) {
SessionAttributesConstants.AttributeKey.CLIENT_IP_ADDRESS -> snapshot.ipAddress
SessionAttributesConstants.AttributeKey.NETWORK_INTERFACE_TYPE -> snapshot.interfaceType
SessionAttributesConstants.AttributeKey.VPN_ACTIVE -> if (snapshot.vpnActive) "true" else "false"
SessionAttributesConstants.AttributeKey.SSID -> snapshot.ssid
SessionAttributesConstants.AttributeKey.MDM_ENROLLED -> if (isMdmEnrolled()) "true" else "false"
SessionAttributesConstants.AttributeKey.SERVER_FQDN -> getServerFqdn(serverUrl)
else -> ""
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private fun getServerFqdn(serverUrl: String): String {
return fqdnCache.computeIfAbsent(serverUrl) { url ->
try {
Uri.parse(url).host ?: ""
} catch (e: Exception) {
Log.w("NetworkClient", "Failed to resolve server FQDN: ${e.message}")
""
}
}
}

private fun isMdmEnrolled(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as? DevicePolicyManager
if (devicePolicyManager?.isOrganizationOwnedDeviceWithManagedProfile == true) {
return true
}
}

val restrictionsManager = context.getSystemService(Context.RESTRICTIONS_SERVICE) as? RestrictionsManager
val restrictions = restrictionsManager?.applicationRestrictions
if (restrictions != null) {
if (isTruthyManagedFlag(restrictions.get(SessionAttributesConstants.IS_DEVICE_MANAGED_KEY)) ||
isTruthyManagedFlag(restrictions.get(SessionAttributesConstants.IS_SUPERVISED_KEY))
) {
return true
}
}
return false
}

private fun isTruthyManagedFlag(value: Any?): Boolean {
return when (value) {
is Boolean -> value
is Int -> value != 0
is String -> {
val normalized = value.trim().lowercase()
normalized == "true" || normalized == "1" || normalized == "yes"
}
else -> false
}
}

fun currentNetworkSnapshot(): NetworkSnapshot {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = connectivityManager.activeNetwork
val capabilities = activeNetwork?.let { connectivityManager.getNetworkCapabilities(it) }

val vpnActive = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
val interfaceType = when {
vpnActive -> "vpn"
capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> "wifi"
capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "cellular"
capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true -> "ethernet"
activeNetwork == null -> ""
else -> "other"
}

val ipAddress = resolveIpAddress()
val ssid = if (interfaceType == "wifi") resolveSsid() else ""

return NetworkSnapshot(interfaceType, ipAddress, vpnActive, ssid)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private fun resolveIpAddress(): String {
return try {
val interfaces = NetworkInterface.getNetworkInterfaces()
while (interfaces.hasMoreElements()) {
val networkInterface = interfaces.nextElement()
val addresses = networkInterface.inetAddresses
while (addresses.hasMoreElements()) {
val address = addresses.nextElement()
if (!address.isLoopbackAddress && address is Inet4Address) {
return address.hostAddress ?: ""
}
}
}
""
} catch (e: Exception) {
Log.w("NetworkClient", "Failed to resolve client IP address: ${e.message}")
""
Comment thread
devinbinnie marked this conversation as resolved.
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Suppress("DEPRECATION")
private fun resolveSsid(): String {
return try {
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val info = wifiManager.connectionInfo
val ssid = info?.ssid?.replace("\"", "") ?: ""
if (ssid == "<unknown ssid>" || ssid == "0x" || ssid == "Wi-Fi" || ssid == "WLAN") {
""
} else {
ssid
}
} catch (e: Exception) {
Log.w("NetworkClient", "Failed to resolve SSID: ${e.message}")
""
Comment thread
devinbinnie marked this conversation as resolved.
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.mattermost.networkclient.sessionattributes

object SessionAttributesConstants {
const val HEADER_NAME = "X-MM-Session-Attributes"
const val STORE_PREFIX = "sa_"
const val STATE_ALIAS_SUFFIX = "SA_STATE"
const val STABLE_VALUES_ALIAS = "SA_STABLE_VALUES"
const val IS_DEVICE_MANAGED_KEY = "isDeviceManaged"
const val IS_SUPERVISED_KEY = "isSupervised"

object AttributeKey {
const val VPN_ACTIVE = "vpn_active"
const val CLIENT_DEVICE_ID = "client_device_id"
const val CLIENT_IP_ADDRESS = "client_ip_address"
const val CLIENT_VERSION = "client_version"
const val JAILBREAK_DETECTED = "jailbreak_detected"
const val MDM_ENROLLED = "mdm_enrolled"
const val NETWORK_INTERFACE_TYPE = "network_interface_type"
const val OS_PLATFORM = "os_platform"
const val OS_VERSION = "os_version"
const val SERVER_FQDN = "server_fqdn"
const val SSID = "ssid"
}
}
Loading
Loading