462 lines
16 KiB
C#
462 lines
16 KiB
C#
|
|
/*
|
||
|
|
* Copyright 2025 Google LLC
|
||
|
|
*
|
||
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
|
* you may not use this file except in compliance with the License.
|
||
|
|
* You may obtain a copy of the License at
|
||
|
|
*
|
||
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
*
|
||
|
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
|
* See the License for the specific language governing permissions and
|
||
|
|
* limitations under the License.
|
||
|
|
*/
|
||
|
|
|
||
|
|
using System;
|
||
|
|
using System.Net.Http;
|
||
|
|
using System.Net.WebSockets;
|
||
|
|
using System.Reflection;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
|
||
|
|
namespace Firebase.Internal
|
||
|
|
{
|
||
|
|
// Contains internal helper methods for interacting with other Firebase libraries.
|
||
|
|
internal static class FirebaseInterops
|
||
|
|
{
|
||
|
|
// The cached fields for FirebaseApp reflection.
|
||
|
|
private static PropertyInfo _dataCollectionProperty = null;
|
||
|
|
|
||
|
|
// The various App Check types needed to retrieve the token, cached via reflection on startup.
|
||
|
|
private static Type _appCheckType;
|
||
|
|
private static MethodInfo _appCheckGetInstanceMethod;
|
||
|
|
private static MethodInfo _appCheckGetTokenMethod;
|
||
|
|
private static MethodInfo _appCheckGetLimitedUseTokenMethod;
|
||
|
|
private static PropertyInfo _appCheckTokenResultProperty;
|
||
|
|
private static PropertyInfo _appCheckTokenTokenProperty;
|
||
|
|
// Used to determine if the App Check reflection initialized successfully, and should work.
|
||
|
|
private static bool _appCheckReflectionInitialized = false;
|
||
|
|
// The header used by the AppCheck token.
|
||
|
|
private const string appCheckHeader = "X-Firebase-AppCheck";
|
||
|
|
|
||
|
|
// The various Auth types needed to retrieve the token, cached via reflection on startup.
|
||
|
|
private static Type _authType;
|
||
|
|
private static MethodInfo _authGetAuthMethod;
|
||
|
|
private static PropertyInfo _authCurrentUserProperty;
|
||
|
|
private static MethodInfo _userTokenAsyncMethod;
|
||
|
|
private static PropertyInfo _userTokenTaskResultProperty;
|
||
|
|
// Used to determine if the Auth reflection initialized successfully, and should work.
|
||
|
|
private static bool _authReflectionInitialized = false;
|
||
|
|
// The header used by the AppCheck token.
|
||
|
|
private const string authHeader = "Authorization";
|
||
|
|
|
||
|
|
static FirebaseInterops()
|
||
|
|
{
|
||
|
|
InitializeAppReflection();
|
||
|
|
InitializeAppCheckReflection();
|
||
|
|
InitializeAuthReflection();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void LogError(string message)
|
||
|
|
{
|
||
|
|
#if FIREBASEAI_DEBUG_LOGGING
|
||
|
|
UnityEngine.Debug.LogError(message);
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cache the methods needed for FirebaseApp reflection.
|
||
|
|
private static void InitializeAppReflection()
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_dataCollectionProperty = typeof(FirebaseApp).GetProperty(
|
||
|
|
"IsDataCollectionDefaultEnabled",
|
||
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||
|
|
if (_dataCollectionProperty == null)
|
||
|
|
{
|
||
|
|
LogError("Could not find FirebaseApp.IsDataCollectionDefaultEnabled property via reflection.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (_dataCollectionProperty.PropertyType != typeof(bool))
|
||
|
|
{
|
||
|
|
LogError("FirebaseApp.IsDataCollectionDefaultEnabled is not a bool, " +
|
||
|
|
$"but is {_dataCollectionProperty.PropertyType}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
LogError($"Failed to initialize FirebaseApp reflection: {e}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Gets the property FirebaseApp.IsDataCollectionDefaultEnabled.
|
||
|
|
public static bool GetIsDataCollectionDefaultEnabled(FirebaseApp firebaseApp)
|
||
|
|
{
|
||
|
|
if (firebaseApp == null || _dataCollectionProperty == null)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
return (bool)_dataCollectionProperty.GetValue(firebaseApp);
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
LogError($"Error accessing 'IsDataCollectionDefaultEnabled': {e}");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// SDK version to use if unable to find it.
|
||
|
|
private const string _unknownSdkVersion = "unknown";
|
||
|
|
private static readonly Lazy<string> _sdkVersionFetcher = new(() =>
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// Get the type Firebase.VersionInfo from the assembly that defines FirebaseApp.
|
||
|
|
Type versionInfoType = typeof(FirebaseApp).Assembly.GetType("Firebase.VersionInfo");
|
||
|
|
if (versionInfoType == null)
|
||
|
|
{
|
||
|
|
LogError("Firebase.VersionInfo type not found via reflection");
|
||
|
|
return _unknownSdkVersion;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Firebase.VersionInfo.SdkVersion
|
||
|
|
PropertyInfo sdkVersionProperty = versionInfoType.GetProperty(
|
||
|
|
"SdkVersion",
|
||
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
||
|
|
if (sdkVersionProperty == null)
|
||
|
|
{
|
||
|
|
LogError("Firebase.VersionInfo.SdkVersion property not found via reflection.");
|
||
|
|
return _unknownSdkVersion;
|
||
|
|
}
|
||
|
|
|
||
|
|
return sdkVersionProperty.GetValue(null) as string ?? _unknownSdkVersion;
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
LogError($"Error accessing SdkVersion via reflection: {e}");
|
||
|
|
return _unknownSdkVersion;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Gets the internal property Firebase.VersionInfo.SdkVersion
|
||
|
|
internal static string GetVersionInfoSdkVersion()
|
||
|
|
{
|
||
|
|
return _sdkVersionFetcher.Value;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cache the various types and methods needed for AppCheck token retrieval.
|
||
|
|
private static void InitializeAppCheckReflection()
|
||
|
|
{
|
||
|
|
const string firebaseAppCheckTypeName = "Firebase.AppCheck.FirebaseAppCheck, Firebase.AppCheck";
|
||
|
|
const string getAppCheckTokenMethodName = "GetAppCheckTokenAsync";
|
||
|
|
const string getLimitedUseAppCheckTokenMethodName = "GetLimitedUseAppCheckTokenAsync";
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// Set this to false, to allow easy failing out via return.
|
||
|
|
_appCheckReflectionInitialized = false;
|
||
|
|
|
||
|
|
_appCheckType = Type.GetType(firebaseAppCheckTypeName);
|
||
|
|
if (_appCheckType == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the static method GetInstance(FirebaseApp app)
|
||
|
|
_appCheckGetInstanceMethod = _appCheckType.GetMethod(
|
||
|
|
"GetInstance", BindingFlags.Static | BindingFlags.Public, null,
|
||
|
|
new Type[] { typeof(FirebaseApp) }, null);
|
||
|
|
if (_appCheckGetInstanceMethod == null)
|
||
|
|
{
|
||
|
|
LogError("Could not find FirebaseAppCheck.GetInstance method via reflection.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the instance method GetAppCheckTokenAsync(bool forceRefresh)
|
||
|
|
_appCheckGetTokenMethod = _appCheckType.GetMethod(
|
||
|
|
getAppCheckTokenMethodName, BindingFlags.Instance | BindingFlags.Public, null,
|
||
|
|
new Type[] { typeof(bool) }, null);
|
||
|
|
if (_appCheckGetTokenMethod == null)
|
||
|
|
{
|
||
|
|
LogError($"Could not find {getAppCheckTokenMethodName} method via reflection.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the instance method GetLimitedUseAppCheckTokenAsync()
|
||
|
|
_appCheckGetLimitedUseTokenMethod = _appCheckType.GetMethod(
|
||
|
|
getLimitedUseAppCheckTokenMethodName, BindingFlags.Instance | BindingFlags.Public, null,
|
||
|
|
Type.EmptyTypes, null);
|
||
|
|
if (_appCheckGetLimitedUseTokenMethod == null)
|
||
|
|
{
|
||
|
|
LogError($"Could not find {getLimitedUseAppCheckTokenMethodName} method via reflection.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Should be Task<AppCheckToken>
|
||
|
|
Type appCheckTokenTaskType = _appCheckGetTokenMethod.ReturnType;
|
||
|
|
|
||
|
|
// Get the Result property from the Task<AppCheckToken>
|
||
|
|
_appCheckTokenResultProperty = appCheckTokenTaskType.GetProperty("Result");
|
||
|
|
if (_appCheckTokenResultProperty == null)
|
||
|
|
{
|
||
|
|
LogError("Could not find Result property on App Check token Task.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Should be AppCheckToken
|
||
|
|
Type appCheckTokenType = _appCheckTokenResultProperty.PropertyType;
|
||
|
|
|
||
|
|
_appCheckTokenTokenProperty = appCheckTokenType.GetProperty("Token");
|
||
|
|
if (_appCheckTokenTokenProperty == null)
|
||
|
|
{
|
||
|
|
LogError($"Could not find Token property on AppCheckToken.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
_appCheckReflectionInitialized = true;
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
LogError($"Exception during static initialization of FirebaseInterops: {e}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Gets the AppCheck Token, assuming there is one. Otherwise, returns null.
|
||
|
|
internal static async Task<string> GetAppCheckTokenAsync(FirebaseApp firebaseApp, bool limitedUse = false)
|
||
|
|
{
|
||
|
|
// If AppCheck reflection failed for any reason, nothing to do.
|
||
|
|
if (!_appCheckReflectionInitialized)
|
||
|
|
{
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// Get the FirebaseAppCheck instance for the current FirebaseApp
|
||
|
|
object appCheckInstance = _appCheckGetInstanceMethod.Invoke(null, new object[] { firebaseApp });
|
||
|
|
if (appCheckInstance == null)
|
||
|
|
{
|
||
|
|
LogError("Failed to get FirebaseAppCheck instance via reflection.");
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
object taskObject;
|
||
|
|
if (limitedUse)
|
||
|
|
{
|
||
|
|
taskObject = _appCheckGetLimitedUseTokenMethod.Invoke(appCheckInstance, null);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// Invoke GetAppCheckTokenAsync(false) - returns a Task<AppCheckToken>
|
||
|
|
taskObject = _appCheckGetTokenMethod.Invoke(appCheckInstance, new object[] { false });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (taskObject is not Task appCheckTokenTask)
|
||
|
|
{
|
||
|
|
LogError($"Invoking GetToken did not return a Task.");
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Await the task to get the AppCheckToken result
|
||
|
|
await appCheckTokenTask;
|
||
|
|
|
||
|
|
// Check for exceptions in the task
|
||
|
|
if (appCheckTokenTask.IsFaulted)
|
||
|
|
{
|
||
|
|
LogError($"Error getting App Check token: {appCheckTokenTask.Exception}");
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the Result property from the Task<AppCheckToken>
|
||
|
|
object tokenResult = _appCheckTokenResultProperty.GetValue(appCheckTokenTask); // This is the AppCheckToken struct
|
||
|
|
if (tokenResult == null)
|
||
|
|
{
|
||
|
|
LogError("App Check token result was null.");
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the Token property from the AppCheckToken struct
|
||
|
|
string finalToken = _appCheckTokenTokenProperty.GetValue(tokenResult) as string;
|
||
|
|
return finalToken;
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
// Log any exceptions during the reflection/invocation process
|
||
|
|
LogError($"An error occurred while trying to fetch App Check token: {e}");
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cache the various types and methods needed for Auth token retrieval.
|
||
|
|
private static void InitializeAuthReflection()
|
||
|
|
{
|
||
|
|
const string firebaseAuthTypeName = "Firebase.Auth.FirebaseAuth, Firebase.Auth";
|
||
|
|
const string getTokenMethodName = "TokenAsync";
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// Set this to false, to allow easy failing out via return.
|
||
|
|
_authReflectionInitialized = false;
|
||
|
|
|
||
|
|
_authType = Type.GetType(firebaseAuthTypeName);
|
||
|
|
if (_authType == null)
|
||
|
|
{
|
||
|
|
// Auth assembly likely not present, fine to skip
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the static method GetAuth(FirebaseApp app):
|
||
|
|
_authGetAuthMethod = _authType.GetMethod(
|
||
|
|
"GetAuth", BindingFlags.Static | BindingFlags.Public, null,
|
||
|
|
new Type[] { typeof(FirebaseApp) }, null);
|
||
|
|
if (_authGetAuthMethod == null)
|
||
|
|
{
|
||
|
|
LogError("Could not find FirebaseAuth.GetAuth method via reflection.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the CurrentUser property from FirebaseAuth instance
|
||
|
|
_authCurrentUserProperty = _authType.GetProperty("CurrentUser", BindingFlags.Instance | BindingFlags.Public);
|
||
|
|
if (_authCurrentUserProperty == null)
|
||
|
|
{
|
||
|
|
LogError("Could not find FirebaseAuth.CurrentUser property via reflection.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// This should be FirebaseUser type
|
||
|
|
Type userType = _authCurrentUserProperty.PropertyType;
|
||
|
|
|
||
|
|
// Get the TokenAsync(bool) method from FirebaseUser
|
||
|
|
_userTokenAsyncMethod = userType.GetMethod(
|
||
|
|
getTokenMethodName, BindingFlags.Instance | BindingFlags.Public, null,
|
||
|
|
new Type[] { typeof(bool) }, null);
|
||
|
|
if (_userTokenAsyncMethod == null)
|
||
|
|
{
|
||
|
|
LogError($"Could not find FirebaseUser.{getTokenMethodName}(bool) method via reflection.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The return type is Task<string>
|
||
|
|
Type tokenTaskType = _userTokenAsyncMethod.ReturnType;
|
||
|
|
|
||
|
|
// Get the Result property from Task<string>
|
||
|
|
_userTokenTaskResultProperty = tokenTaskType.GetProperty("Result");
|
||
|
|
if (_userTokenTaskResultProperty == null)
|
||
|
|
{
|
||
|
|
LogError("Could not find Result property on Auth token Task.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if Result property is actually a string
|
||
|
|
if (_userTokenTaskResultProperty.PropertyType != typeof(string))
|
||
|
|
{
|
||
|
|
LogError("Auth token Task's Result property is not a string, " +
|
||
|
|
$"but is {_userTokenTaskResultProperty.PropertyType}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
_authReflectionInitialized = true;
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
LogError($"Exception during static initialization of Auth reflection in FirebaseInterops: {e}");
|
||
|
|
_authReflectionInitialized = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Gets the Auth Token, assuming there is one. Otherwise, returns null.
|
||
|
|
internal static async Task<string> GetAuthTokenAsync(FirebaseApp firebaseApp)
|
||
|
|
{
|
||
|
|
// If Auth reflection failed for any reason, nothing to do.
|
||
|
|
if (!_authReflectionInitialized)
|
||
|
|
{
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// Get the FirebaseAuth instance for the given FirebaseApp.
|
||
|
|
object authInstance = _authGetAuthMethod.Invoke(null, new object[] { firebaseApp });
|
||
|
|
if (authInstance == null)
|
||
|
|
{
|
||
|
|
LogError("Failed to get FirebaseAuth instance via reflection.");
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the CurrentUser property
|
||
|
|
object currentUser = _authCurrentUserProperty.GetValue(authInstance);
|
||
|
|
if (currentUser == null)
|
||
|
|
{
|
||
|
|
// No user logged in, so no token
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Invoke TokenAsync(false) - returns a Task<string>
|
||
|
|
object taskObject = _userTokenAsyncMethod.Invoke(currentUser, new object[] { false });
|
||
|
|
if (taskObject is not Task tokenTask)
|
||
|
|
{
|
||
|
|
LogError("Invoking TokenAsync did not return a Task.");
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Await the task to get the token result
|
||
|
|
await tokenTask;
|
||
|
|
|
||
|
|
// Check for exceptions in the task
|
||
|
|
if (tokenTask.IsFaulted)
|
||
|
|
{
|
||
|
|
LogError($"Error getting Auth token: {tokenTask.Exception}");
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the Result property (which is the string token)
|
||
|
|
return _userTokenTaskResultProperty.GetValue(tokenTask) as string;
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
// Log any exceptions during the reflection/invocation process
|
||
|
|
LogError($"An error occurred while trying to fetch Auth token: {e}");
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Adds the other Firebase tokens to the HttpRequest, as available.
|
||
|
|
internal static async Task AddFirebaseTokensAsync(HttpRequestMessage request, FirebaseApp firebaseApp, string authTokenPrefix = "Firebase", bool limitedUseAppCheckTokens = false)
|
||
|
|
{
|
||
|
|
string appCheckToken = await GetAppCheckTokenAsync(firebaseApp, limitedUseAppCheckTokens);
|
||
|
|
if (!string.IsNullOrEmpty(appCheckToken))
|
||
|
|
{
|
||
|
|
request.Headers.Add(appCheckHeader, appCheckToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
string authToken = await GetAuthTokenAsync(firebaseApp);
|
||
|
|
if (!string.IsNullOrEmpty(authToken))
|
||
|
|
{
|
||
|
|
request.Headers.Add(authHeader, $"{authTokenPrefix} {authToken}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Adds the other Firebase tokens to the WebSocket, as available.
|
||
|
|
internal static async Task AddFirebaseTokensAsync(ClientWebSocket socket, FirebaseApp firebaseApp, string authTokenPrefix = "Firebase", bool limitedUseAppCheckTokens = false)
|
||
|
|
{
|
||
|
|
string appCheckToken = await GetAppCheckTokenAsync(firebaseApp, limitedUseAppCheckTokens);
|
||
|
|
if (!string.IsNullOrEmpty(appCheckToken))
|
||
|
|
{
|
||
|
|
socket.Options.SetRequestHeader(appCheckHeader, appCheckToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
string authToken = await GetAuthTokenAsync(firebaseApp);
|
||
|
|
if (!string.IsNullOrEmpty(authToken))
|
||
|
|
{
|
||
|
|
socket.Options.SetRequestHeader(authHeader, $"{authTokenPrefix} {authToken}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|