mattermost-mobile/app/managers/network_manager.ts
Felipe Martin f50056f57b
MM-65085: Support Pre Shared Password on server connect (#9082)
* feat: add shared server password to server setup

* feat: allow editing the sever

* refactor: changed password -> secret, styling and tests

* e2e: draft e2e tests

* chore: lint fix

* feat: also send preauth secret header when using native share

* fix: removed unused server database migration

credentials are being stored in the keychain

* i18n: added missing english translations

* test(e2e): simplified connection tests

* test(e2e): rework

* refactor: remove setBearerToken

* chore: restore migrations the way it was

* chore: reverted file to original state

* chore: removed unneeded test and renamed password to secret

* chore: function version

* chore: updated forms i18n keys

* chore: remove if from test

* chore: unneeded variable

* fix: add missing key on object list

* refactor: swift keychain access to retrieve all credentials in one call

* revert: edit server screen

* refactor: credentials use getGenericCredential

* fix: objc code calling old method

* fix: added scroll to login screen

* chore: variable names

* fix: avoid inline styles

* fix: Improved appVersion positioning

* Update app/screens/server/form.tsx

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* feat: show error message on 403

* Revert "feat: show error message on 403"

This reverts commit f41630c767e10211adf1885321ceefd8a0931e32.

---------

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
2025-09-01 11:24:15 +02:00

180 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Emm from '@mattermost/react-native-emm';
import {
type APIClientErrorEvent,
type APIClientErrorEventHandler,
getOrCreateAPIClient,
RetryTypes,
type APIClientConfiguration,
} from '@mattermost/react-native-network-client';
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application';
import {modelName, osName, osVersion} from 'expo-device';
import {defineMessages} from 'react-intl';
import {Alert, DeviceEventEmitter} from 'react-native';
import urlParse from 'url-parse';
import LocalConfig from '@assets/config.json';
import {Client} from '@client/rest';
import * as ClientConstants from '@client/rest/constants';
import ClientError from '@client/rest/error';
import {CERTIFICATE_ERRORS} from '@constants/network';
import ManagedApp from '@init/managed_app';
import {toMilliseconds} from '@utils/datetime';
import {getIntlShape} from '@utils/general';
import {logDebug, logError} from '@utils/log';
import {getCSRFFromCookie} from '@utils/security';
const CLIENT_CERTIFICATE_IMPORT_ERROR_CODES = [-103, -104, -105, -108];
const CLIENT_CERTIFICATE_MISSING_ERROR_CODE = -200;
const SERVER_CERTIFICATE_INVALID = -299;
const SERVER_TRUST_EVALUATION_FAILED = -298;
let showingServerTrustAlert = false;
const messages = defineMessages({
invalidSslTitle: {
id: 'server.invalid.certificate.title',
defaultMessage: 'Invalid SSL certificate',
},
invalidSslDescription: {
id: 'server.invalid.certificate.description',
defaultMessage: 'The certificate for this server is invalid.\nYou might be connecting to a server that is pretending to be “{hostname}” which could put your confidential information at risk.',
},
invalidPinningTitle: {
id: 'server.invalid.pinning.title',
defaultMessage: 'Invalid pinned SSL certificate',
},
});
class NetworkManagerSingleton {
private clients: Record<string, Client> = {};
private intl = getIntlShape();
private DEFAULT_CONFIG: APIClientConfiguration = {
headers: {
'X-Requested-With': 'XMLHttpRequest',
...LocalConfig.CustomRequestHeaders,
},
sessionConfiguration: {
allowsCellularAccess: true,
waitsForConnectivity: false,
httpMaximumConnectionsPerHost: 100,
cancelRequestsOnUnauthorized: true,
collectMetrics: false,
},
retryPolicyConfiguration: {
type: RetryTypes.EXPONENTIAL_RETRY,
retryLimit: 3,
exponentialBackoffBase: 2,
exponentialBackoffScale: 0.5,
},
requestAdapterConfiguration: {
bearerAuthTokenResponseHeader: 'token',
},
};
public init = async (serverCredentials: ServerCredential[]) => {
for await (const {serverUrl, token, preauthSecret} of serverCredentials) {
try {
await this.createClient(serverUrl, token, preauthSecret);
} catch (error) {
logError('NetworkManager init error', error);
}
}
};
public invalidateClient = (serverUrl: string) => {
this.clients[serverUrl]?.invalidate();
delete this.clients[serverUrl];
};
public getClient = (serverUrl: string) => {
const client = this.clients[serverUrl];
if (!client) {
throw new Error(`${serverUrl} client not found`);
}
return client;
};
public createClient = async (serverUrl: string, bearerToken?: string, preauthSecret?: string) => {
const config = await this.buildConfig(preauthSecret);
try {
const {client} = await getOrCreateAPIClient(serverUrl, config, this.clientErrorEventHandler);
const csrfToken = await getCSRFFromCookie(serverUrl);
this.clients[serverUrl] = new Client(client, serverUrl, bearerToken, csrfToken, preauthSecret);
} catch (error) {
throw new ClientError(serverUrl, {
message: 'Cant find this server. Check spelling and URL format.',
intl: {
id: 'apps.error.network.no_server',
defaultMessage: 'Cant find this server. Check spelling and URL format.',
},
url: serverUrl,
details: error,
});
}
return this.clients[serverUrl];
};
private buildConfig = async (preauthSecret?: string) => {
const userAgent = `Mattermost Mobile/${nativeApplicationVersion}+${nativeBuildVersion} (${osName}; ${osVersion}; ${modelName})`;
const managedConfig = ManagedApp.enabled ? Emm.getManagedConfig<ManagedConfig>() : undefined;
const headers: Record<string, string> = {
[ClientConstants.HEADER_USER_AGENT]: userAgent,
...(preauthSecret ? {[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]: preauthSecret} : {}),
...this.DEFAULT_CONFIG.headers,
};
const config = {
...this.DEFAULT_CONFIG,
sessionConfiguration: {
...this.DEFAULT_CONFIG.sessionConfiguration,
timeoutIntervalForRequest: managedConfig?.timeout ? parseInt(managedConfig.timeout, 10) : this.DEFAULT_CONFIG.sessionConfiguration?.timeoutIntervalForRequest,
timeoutIntervalForResource: managedConfig?.timeoutVPN ? parseInt(managedConfig.timeoutVPN, 10) : this.DEFAULT_CONFIG.sessionConfiguration?.timeoutIntervalForResource,
waitsForConnectivity: managedConfig?.useVPN === 'true',
collectMetrics: LocalConfig.CollectNetworkMetrics,
},
headers,
};
return config;
};
private clientErrorEventHandler: APIClientErrorEventHandler = (event: APIClientErrorEvent) => {
if (CLIENT_CERTIFICATE_IMPORT_ERROR_CODES.includes(event.errorCode)) {
DeviceEventEmitter.emit(CERTIFICATE_ERRORS.CLIENT_CERTIFICATE_IMPORT_ERROR, event.serverUrl);
} else if (CLIENT_CERTIFICATE_MISSING_ERROR_CODE === event.errorCode) {
DeviceEventEmitter.emit(CERTIFICATE_ERRORS.CLIENT_CERTIFICATE_MISSING, event.serverUrl);
} else if (SERVER_CERTIFICATE_INVALID === event.errorCode) {
logDebug('Invalid SSL certificate:', event.errorDescription);
const parsed = urlParse(event.serverUrl);
Alert.alert(
this.intl.formatMessage(messages.invalidSslTitle),
this.intl.formatMessage(messages.invalidSslDescription, {hostname: parsed.hostname}),
);
} else if (SERVER_TRUST_EVALUATION_FAILED === event.errorCode && !showingServerTrustAlert) {
logDebug('Invalid SSL Pinning:', event.errorDescription);
showingServerTrustAlert = true;
Alert.alert(
this.intl.formatMessage(messages.invalidPinningTitle),
event.errorDescription,
[{
text: this.intl.formatMessage({id: 'server_upgrade.dismiss', defaultMessage: 'Dismiss'}),
onPress: () => {
setTimeout(() => {
showingServerTrustAlert = false;
}, toMilliseconds({hours: 23}));
},
}],
);
}
};
}
const NetworkManager = new NetworkManagerSingleton();
export default NetworkManager;