Add custom HTTP header support to mobile app (#1939)

This commit is contained in:
Leendert de Borst
2026-04-27 16:03:29 +02:00
parent cf7350b210
commit 1dd0df6c8d
6 changed files with 176 additions and 10 deletions

View File

@@ -937,6 +937,33 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
}
}
/**
* Set the custom proxy headers (JSON-encoded array of {name, value} pairs).
*/
@ReactMethod
override fun setCustomProxyHeaders(headersJson: String, promise: Promise) {
try {
webApiService.setCustomProxyHeaders(headersJson)
promise.resolve(null)
} catch (e: Exception) {
Log.e(TAG, "Error setting custom proxy headers", e)
promise.reject("ERR_SET_CUSTOM_PROXY_HEADERS", "Failed to set custom proxy headers: ${e.message}", e)
}
}
/**
* Get the custom proxy headers as a JSON string.
*/
@ReactMethod
override fun getCustomProxyHeaders(promise: Promise) {
try {
promise.resolve(webApiService.getCustomProxyHeadersJson())
} catch (e: Exception) {
Log.e(TAG, "Error getting custom proxy headers", e)
promise.reject("ERR_GET_CUSTOM_PROXY_HEADERS", "Failed to get custom proxy headers: ${e.message}", e)
}
}
// MARK: - WebAPI Token Management
/**

View File

@@ -5,6 +5,7 @@ import android.content.pm.PackageManager
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStreamReader
@@ -35,6 +36,7 @@ class WebApiService(private val context: Context) {
private const val ACCESS_TOKEN_KEY = "accessToken"
private const val REFRESH_TOKEN_KEY = "refreshToken"
private const val APP_INSTANCE_ID_KEY = "appInstanceId"
private const val CUSTOM_PROXY_HEADERS_KEY = "customProxyHeaders"
private const val DEFAULT_API_URL = "https://app.aliasvault.net/api"
private const val SHARED_PREFS_NAME = "aliasvault"
}
@@ -73,6 +75,62 @@ class WebApiService(private val context: Context) {
return sharedPreferences.getString(API_URL_KEY, DEFAULT_API_URL) ?: DEFAULT_API_URL
}
/**
* Set the custom proxy headers (JSON-encoded array of {name, value} pairs).
*/
fun setCustomProxyHeaders(json: String) {
sharedPreferences.edit().putString(CUSTOM_PROXY_HEADERS_KEY, json).apply()
}
/**
* Get the custom proxy headers as a raw JSON string. Returns "[]" when none configured.
*/
fun getCustomProxyHeadersJson(): String {
return sharedPreferences.getString(CUSTOM_PROXY_HEADERS_KEY, "[]") ?: "[]"
}
/**
* Parse the stored custom proxy headers into a name->value map.
* Headers conflicting with built-in AliasVault headers are ignored.
*/
private fun getCustomProxyHeaders(): Map<String, String> {
if (getApiUrl() == DEFAULT_API_URL) {
return emptyMap()
}
return try {
val array = JSONArray(getCustomProxyHeadersJson())
(0 until array.length())
.mapNotNull { parseProxyHeaderEntry(array.optJSONObject(it)) }
.toMap()
} catch (e: Exception) {
Log.w(TAG, "Failed to parse custom proxy headers", e)
emptyMap()
}
}
/**
* Parse and validate a single proxy-header JSON entry. Returns null if the entry is missing,
* empty, or conflicts with a built-in AliasVault header.
*/
private fun parseProxyHeaderEntry(entry: JSONObject?): Pair<String, String>? {
if (entry == null) {
return null
}
val name = entry.optString("name", "").trim()
val value = entry.optString("value", "").trim()
if (name.isEmpty() || value.isEmpty()) {
return null
}
val lower = name.lowercase()
if (lower == "authorization" || lower.startsWith("x-aliasvault-")) {
return null
}
return name to value
}
/**
* Get the base URL with /v1/ appended.
*/
@@ -202,8 +260,12 @@ class WebApiService(private val context: Context) {
connection.readTimeout = 30000 // 30 seconds
connection.doInput = true
// Add any custom proxy headers
val finalHeaders = getCustomProxyHeaders().toMutableMap()
finalHeaders.putAll(headers)
// Set headers
for ((key, value) in headers) {
for ((key, value) in finalHeaders) {
connection.setRequestProperty(key, value)
}

View File

@@ -185,6 +185,14 @@
[vaultManager getApiUrl:resolve rejecter:reject];
}
- (void)setCustomProxyHeaders:(NSString *)headersJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[vaultManager setCustomProxyHeaders:headersJson resolver:resolve rejecter:reject];
}
- (void)getCustomProxyHeaders:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[vaultManager getCustomProxyHeaders:resolve rejecter:reject];
}
// MARK: - WebAPI Token Management
- (void)setAuthTokens:(NSString *)accessToken refreshToken:(NSString *)refreshToken resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {

View File

@@ -492,6 +492,20 @@ public class VaultManager: NSObject {
resolve(apiUrl)
}
@objc
func setCustomProxyHeaders(_ headersJson: String,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
webApiService.setCustomProxyHeaders(headersJson)
resolve(nil)
}
@objc
func getCustomProxyHeaders(_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
resolve(webApiService.getCustomProxyHeadersJson())
}
// MARK: - WebAPI Token Management
@objc

View File

@@ -11,6 +11,7 @@ public class WebApiService {
private let apiUrlKey = "apiUrl"
private let accessTokenKey = "accessToken"
private let refreshTokenKey = "refreshToken"
private let customProxyHeadersKey = "customProxyHeaders"
// Default API URL
private let defaultApiUrl = "https://app.aliasvault.net/api"
@@ -38,6 +39,54 @@ public class WebApiService {
return userDefaults.string(forKey: apiUrlKey) ?? defaultApiUrl
}
/**
* Set the custom proxy headers (JSON-encoded array of {name, value} pairs).
*/
public func setCustomProxyHeaders(_ json: String) {
userDefaults.set(json, forKey: customProxyHeadersKey)
userDefaults.synchronize()
}
/**
* Get the custom proxy headers as a raw JSON string. Returns "[]" when none configured.
*/
public func getCustomProxyHeadersJson() -> String {
return userDefaults.string(forKey: customProxyHeadersKey) ?? "[]"
}
/**
* Parse the stored custom proxy headers into a name->value dictionary.
* Headers whose name conflicts with built-in AliasVault headers are ignored.
*/
private func getCustomProxyHeaders() -> [String: String] {
if getApiUrl() == defaultApiUrl {
return [:]
}
let json = getCustomProxyHeadersJson()
guard let data = json.data(using: .utf8),
let array = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
return [:]
}
return array
.compactMap(parseProxyHeaderEntry)
.reduce(into: [String: String]()) { result, pair in result[pair.0] = pair.1 }
}
/**
* Parse and validate a single proxy-header entry. Returns nil if the entry is missing,
* empty, or conflicts with a built-in AliasVault header.
*/
private func parseProxyHeaderEntry(_ entry: [String: Any]) -> (String, String)? {
guard let name = entry["name"] as? String,
let value = entry["value"] as? String else { return nil }
let trimmedName = name.trimmingCharacters(in: .whitespaces)
let trimmedValue = value.trimmingCharacters(in: .whitespaces)
if trimmedName.isEmpty || trimmedValue.isEmpty { return nil }
let lower = trimmedName.lowercased()
if lower == "authorization" || lower.hasPrefix("x-aliasvault-") { return nil }
return (trimmedName, trimmedValue)
}
/**
* Get the base URL with /v1/ appended
*/
@@ -161,8 +210,14 @@ public class WebApiService {
var request = URLRequest(url: url)
request.httpMethod = method.uppercased()
// Set headers
// Add any custom proxy headers
var finalHeaders = getCustomProxyHeaders()
for (key, value) in headers {
finalHeaders[key] = value
}
// Set headers
for (key, value) in finalHeaders {
request.setValue(value, forHTTPHeaderField: key)
}

View File

@@ -11,6 +11,9 @@ export interface Spec extends TurboModule {
getAccessToken(): Promise<string | null>;
clearAuthTokens(): Promise<void>;
revokeTokens(): Promise<void>;
// Custom proxy headers added to every outgoing API request
setCustomProxyHeaders(headersJson: string): Promise<void>;
getCustomProxyHeaders(): Promise<string>;
// WebAPI request execution
executeWebApiRequest(method: string, endpoint: string, body: string | null, headers: string, requiresAuth: boolean): Promise<string>;
@@ -22,18 +25,16 @@ export interface Spec extends TurboModule {
clearSession(): Promise<void>; // Clears session only, preserves vault for potential RPO recovery
clearVault(): Promise<void>; // Clears everything including vault data
// Vault sync - single method handles all sync logic including merge
// Returns detailed result about what action was taken
// Vault sync
syncVaultWithServer(): Promise<{ success: boolean; action: 'uploaded' | 'downloaded' | 'merged' | 'already_in_sync' | 'error'; newRevision: number; wasOffline: boolean; error: string | null }>;
// Quick check if sync is needed without doing the actual sync
// Used to show appropriate UI indicator before starting sync
// Quick check if sync is needed
checkSyncStatus(): Promise<{ success: boolean; hasNewerVault: boolean; hasDirtyChanges: boolean; isOffline: boolean; requiresLogout: boolean; errorKey: string | null }>;
// Sync state management (kept for local mutation tracking)
// Sync state management
getSyncState(): Promise<{isDirty: boolean; mutationSequence: number; serverRevision: number; isSyncing: boolean}>;
markVaultClean(mutationSeqAtStart: number, newServerRevision: number): Promise<boolean>;
clearEncryptedVaultForFreshDownload(): Promise<void>; // Deletes corrupted vault and resets sync state to force fresh download
clearEncryptedVaultForFreshDownload(): Promise<void>;
// Vault SQL operations
executeQuery(query: string, params: (string | number | null)[]): Promise<string[]>;
@@ -42,8 +43,7 @@ export interface Spec extends TurboModule {
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;
// Persist the in-memory database to encrypted storage and mark as dirty.
// Used after migrations where SQL handles its own transactions but we need to persist and sync.
// Persist the in-memory database to encrypted storage and mark as dirty
persistAndMarkDirty(): Promise<void>;
// Cryptography operations