[NEW] Support client certificates for SSL (two-way authentication) (#1125)
This commit is contained in:
parent
48cdd3b511
commit
2515295fc6
|
@ -23,10 +23,11 @@ export function selectServerFailure() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serverRequest(server) {
|
export function serverRequest(server, certificate = null) {
|
||||||
return {
|
return {
|
||||||
type: SERVER.REQUEST,
|
type: SERVER.REQUEST,
|
||||||
server
|
server,
|
||||||
|
certificate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@ export default {
|
||||||
and: 'and',
|
and: 'and',
|
||||||
announcement: 'announcement',
|
announcement: 'announcement',
|
||||||
Announcement: 'Announcement',
|
Announcement: 'Announcement',
|
||||||
|
Apply_Your_Certificate: 'Apply Your Certificate',
|
||||||
ARCHIVE: 'ARCHIVE',
|
ARCHIVE: 'ARCHIVE',
|
||||||
archive: 'archive',
|
archive: 'archive',
|
||||||
are_typing: 'are typing',
|
are_typing: 'are typing',
|
||||||
|
@ -137,6 +138,8 @@ export default {
|
||||||
Copied_to_clipboard: 'Copied to clipboard!',
|
Copied_to_clipboard: 'Copied to clipboard!',
|
||||||
Copy: 'Copy',
|
Copy: 'Copy',
|
||||||
Permalink: 'Permalink',
|
Permalink: 'Permalink',
|
||||||
|
Certificate_password: 'Certificate Password',
|
||||||
|
Whats_the_password_for_your_certificate: 'What\'s the password for your certificate?',
|
||||||
Create_account: 'Create an account',
|
Create_account: 'Create an account',
|
||||||
Create_Channel: 'Create Channel',
|
Create_Channel: 'Create Channel',
|
||||||
Created_snippet: 'Created a snippet',
|
Created_snippet: 'Created a snippet',
|
||||||
|
@ -155,6 +158,7 @@ export default {
|
||||||
Disable_notifications: 'Disable notifications',
|
Disable_notifications: 'Disable notifications',
|
||||||
Discussions: 'Discussions',
|
Discussions: 'Discussions',
|
||||||
Dont_Have_An_Account: 'Don\'t have an account?',
|
Dont_Have_An_Account: 'Don\'t have an account?',
|
||||||
|
Do_you_have_a_certificate: 'Do you have a certificate?',
|
||||||
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
|
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
|
||||||
edit: 'edit',
|
edit: 'edit',
|
||||||
edited: 'edited',
|
edited: 'edited',
|
||||||
|
@ -427,6 +431,7 @@ export default {
|
||||||
you: 'you',
|
you: 'you',
|
||||||
You: 'You',
|
You: 'You',
|
||||||
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'You need to access at least one Rocket.Chat server to share something.',
|
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'You need to access at least one Rocket.Chat server to share something.',
|
||||||
|
Your_certificate: 'Your Certificate',
|
||||||
Version_no: 'Version: {{version}}',
|
Version_no: 'Version: {{version}}',
|
||||||
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
|
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
|
||||||
Change_Language: 'Change Language',
|
Change_Language: 'Change Language',
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { AsyncStorage, InteractionManager } from 'react-native';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
|
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
|
||||||
import RNUserDefaults from 'rn-user-defaults';
|
import RNUserDefaults from 'rn-user-defaults';
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
|
||||||
import reduxStore from './createStore';
|
import reduxStore from './createStore';
|
||||||
import defaultSettings from '../constants/settings';
|
import defaultSettings from '../constants/settings';
|
||||||
|
@ -9,6 +10,7 @@ import messagesStatus from '../constants/messagesStatus';
|
||||||
import database from './realm';
|
import database from './realm';
|
||||||
import log from '../utils/log';
|
import log from '../utils/log';
|
||||||
import { isIOS, getBundleId } from '../utils/deviceInfo';
|
import { isIOS, getBundleId } from '../utils/deviceInfo';
|
||||||
|
import { extractHostname } from '../utils/server';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
setUser, setLoginServices, loginRequest, loginFailure, logout
|
setUser, setLoginServices, loginRequest, loginFailure, logout
|
||||||
|
@ -364,6 +366,12 @@ const RocketChat = {
|
||||||
try {
|
try {
|
||||||
const servers = await RNUserDefaults.objectForKey(SERVERS);
|
const servers = await RNUserDefaults.objectForKey(SERVERS);
|
||||||
await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
|
await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
|
||||||
|
// clear certificate for server - SSL Pinning
|
||||||
|
const certificate = await RNUserDefaults.objectForKey(extractHostname(server));
|
||||||
|
if (certificate && certificate.path) {
|
||||||
|
await RNUserDefaults.clear(extractHostname(server));
|
||||||
|
await FileSystem.deleteAsync(certificate.path);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('logout_rn_user_defaults', error);
|
console.log('logout_rn_user_defaults', error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { setUser } from '../actions/login';
|
||||||
import RocketChat from '../lib/rocketchat';
|
import RocketChat from '../lib/rocketchat';
|
||||||
import database from '../lib/realm';
|
import database from '../lib/realm';
|
||||||
import log from '../utils/log';
|
import log from '../utils/log';
|
||||||
|
import { extractHostname } from '../utils/server';
|
||||||
import I18n from '../i18n';
|
import I18n from '../i18n';
|
||||||
import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults';
|
import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults';
|
||||||
|
|
||||||
|
@ -77,8 +78,12 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleServerRequest = function* handleServerRequest({ server }) {
|
const handleServerRequest = function* handleServerRequest({ server, certificate }) {
|
||||||
try {
|
try {
|
||||||
|
if (certificate) {
|
||||||
|
yield RNUserDefaults.setObjectForKey(extractHostname(server), certificate);
|
||||||
|
}
|
||||||
|
|
||||||
const serverInfo = yield getServerInfo({ server });
|
const serverInfo = yield getServerInfo({ server });
|
||||||
|
|
||||||
const loginServicesLength = yield RocketChat.getLoginServices(server);
|
const loginServicesLength = yield RocketChat.getLoginServices(server);
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
Extract hostname from url
|
||||||
|
url = 'https://open.rocket.chat/method'
|
||||||
|
hostname = 'open.rocket.chat'
|
||||||
|
*/
|
||||||
|
export const extractHostname = (url) => {
|
||||||
|
let hostname;
|
||||||
|
|
||||||
|
if (url.indexOf('//') > -1) {
|
||||||
|
[,, hostname] = url.split('/');
|
||||||
|
} else {
|
||||||
|
[hostname] = url.split('/');
|
||||||
|
}
|
||||||
|
[hostname] = hostname.split(':');
|
||||||
|
[hostname] = hostname.split('?');
|
||||||
|
|
||||||
|
return hostname;
|
||||||
|
};
|
|
@ -1,10 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity
|
Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity, View, Alert, LayoutAnimation
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { SafeAreaView } from 'react-navigation';
|
import { SafeAreaView } from 'react-navigation';
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
import DocumentPicker from 'react-native-document-picker';
|
||||||
|
import ActionSheet from 'react-native-action-sheet';
|
||||||
|
import isEqual from 'deep-equal';
|
||||||
|
|
||||||
import { serverRequest } from '../actions/server';
|
import { serverRequest } from '../actions/server';
|
||||||
import sharedStyles from './Styles';
|
import sharedStyles from './Styles';
|
||||||
|
@ -18,6 +22,7 @@ import { isIOS, isNotch } from '../utils/deviceInfo';
|
||||||
import { CustomIcon } from '../lib/Icons';
|
import { CustomIcon } from '../lib/Icons';
|
||||||
import StatusBar from '../containers/StatusBar';
|
import StatusBar from '../containers/StatusBar';
|
||||||
import { COLOR_PRIMARY } from '../constants/colors';
|
import { COLOR_PRIMARY } from '../constants/colors';
|
||||||
|
import log from '../utils/log';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
image: {
|
image: {
|
||||||
|
@ -41,6 +46,22 @@ const styles = StyleSheet.create({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
paddingHorizontal: 9,
|
paddingHorizontal: 9,
|
||||||
left: 15
|
left: 15
|
||||||
|
},
|
||||||
|
certificatePicker: {
|
||||||
|
flex: 1,
|
||||||
|
marginTop: 40,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
chooseCertificateTitle: {
|
||||||
|
fontSize: 15,
|
||||||
|
...sharedStyles.textRegular,
|
||||||
|
...sharedStyles.textColorDescription
|
||||||
|
},
|
||||||
|
chooseCertificate: {
|
||||||
|
fontSize: 15,
|
||||||
|
...sharedStyles.textSemibold,
|
||||||
|
...sharedStyles.textColorHeaderBack
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -61,9 +82,19 @@ class NewServerView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const server = props.navigation.getParam('server');
|
const server = props.navigation.getParam('server');
|
||||||
|
|
||||||
|
// Cancel
|
||||||
|
this.options = [I18n.t('Cancel')];
|
||||||
|
this.CANCEL_INDEX = 0;
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
this.options.push(I18n.t('Delete'));
|
||||||
|
this.DELETE_INDEX = 1;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
text: server || '',
|
text: server || '',
|
||||||
autoFocus: !server
|
autoFocus: !server,
|
||||||
|
certificate: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,11 +107,14 @@ class NewServerView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { text } = this.state;
|
const { text, certificate } = this.state;
|
||||||
const { connecting } = this.props;
|
const { connecting } = this.props;
|
||||||
if (nextState.text !== text) {
|
if (nextState.text !== text) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!isEqual(nextState.certificate, certificate)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (nextProps.connecting !== connecting) {
|
if (nextProps.connecting !== connecting) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -91,13 +125,51 @@ class NewServerView extends React.Component {
|
||||||
this.setState({ text });
|
this.setState({ text });
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = () => {
|
submit = async() => {
|
||||||
const { text } = this.state;
|
const { text, certificate } = this.state;
|
||||||
const { connectServer } = this.props;
|
const { connectServer } = this.props;
|
||||||
|
let cert = null;
|
||||||
|
|
||||||
|
if (certificate) {
|
||||||
|
const certificatePath = `${ FileSystem.documentDirectory }/${ certificate.name }`;
|
||||||
|
try {
|
||||||
|
await FileSystem.copyAsync({ from: certificate.path, to: certificatePath });
|
||||||
|
} catch (error) {
|
||||||
|
log('err_save_certificate', error);
|
||||||
|
}
|
||||||
|
cert = {
|
||||||
|
path: this.uriToPath(certificatePath), // file:// isn't allowed by obj-C
|
||||||
|
password: certificate.password
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
connectServer(this.completeUrl(text));
|
connectServer(this.completeUrl(text), cert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseCertificate = async() => {
|
||||||
|
try {
|
||||||
|
const res = await DocumentPicker.pick({
|
||||||
|
type: ['com.rsa.pkcs-12']
|
||||||
|
});
|
||||||
|
const { uri: path, name } = res;
|
||||||
|
Alert.prompt(
|
||||||
|
I18n.t('Certificate_password'),
|
||||||
|
I18n.t('Whats_the_password_for_your_certificate'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'OK',
|
||||||
|
onPress: password => this.saveCertificate({ path, name, password })
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'secure-text',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (!DocumentPicker.isCancel(error)) {
|
||||||
|
log('err_choose_certificate', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +192,25 @@ class NewServerView extends React.Component {
|
||||||
return url.replace(/\/+$/, '');
|
return url.replace(/\/+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uriToPath = uri => uri.replace('file://', '');
|
||||||
|
|
||||||
|
saveCertificate = (certificate) => {
|
||||||
|
LayoutAnimation.easeInEaseOut();
|
||||||
|
this.setState({ certificate });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDelete = () => this.setState({ certificate: null }); // We not need delete file from DocumentPicker because it is a temp file
|
||||||
|
|
||||||
|
showActionSheet = () => {
|
||||||
|
ActionSheet.showActionSheetWithOptions({
|
||||||
|
options: this.options,
|
||||||
|
cancelButtonIndex: this.CANCEL_INDEX,
|
||||||
|
destructiveButtonIndex: this.DELETE_INDEX
|
||||||
|
}, (actionIndex) => {
|
||||||
|
if (actionIndex === this.DELETE_INDEX) { this.handleDelete(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderBack = () => {
|
renderBack = () => {
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
|
|
||||||
|
@ -142,6 +233,18 @@ class NewServerView extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCertificatePicker = () => {
|
||||||
|
const { certificate } = this.state;
|
||||||
|
return (
|
||||||
|
<View style={styles.certificatePicker}>
|
||||||
|
<Text style={styles.chooseCertificateTitle}>{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}</Text>
|
||||||
|
<TouchableOpacity onPress={certificate ? this.showActionSheet : this.chooseCertificate} testID='new-server-choose-certificate'>
|
||||||
|
<Text style={styles.chooseCertificate}>{certificate ? certificate.name : I18n.t('Apply_Your_Certificate')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { connecting } = this.props;
|
const { connecting } = this.props;
|
||||||
const { text, autoFocus } = this.state;
|
const { text, autoFocus } = this.state;
|
||||||
|
@ -175,6 +278,7 @@ class NewServerView extends React.Component {
|
||||||
loading={connecting}
|
loading={connecting}
|
||||||
testID='new-server-view-button'
|
testID='new-server-view-button'
|
||||||
/>
|
/>
|
||||||
|
{ isIOS ? this.renderCertificatePicker() : null }
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{this.renderBack()}
|
{this.renderBack()}
|
||||||
|
@ -188,7 +292,7 @@ const mapStateToProps = state => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
connectServer: server => dispatch(serverRequest(server))
|
connectServer: (server, certificate) => dispatch(serverRequest(server, certificate))
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(NewServerView);
|
export default connect(mapStateToProps, mapDispatchToProps)(NewServerView);
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"commonmark-react-renderer": "git+https://github.com/RocketChat/commonmark-react-renderer.git",
|
"commonmark-react-renderer": "git+https://github.com/RocketChat/commonmark-react-renderer.git",
|
||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"ejson": "2.2.0",
|
"ejson": "2.2.0",
|
||||||
|
"expo-file-system": "^6.0.2",
|
||||||
"expo-av": "^6.0.0",
|
"expo-av": "^6.0.0",
|
||||||
"expo-haptics": "6.0.0",
|
"expo-haptics": "6.0.0",
|
||||||
"expo-web-browser": "^6.0.0",
|
"expo-web-browser": "^6.0.0",
|
||||||
|
|
|
@ -1,8 +1,140 @@
|
||||||
|
diff --git a/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm b/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm
|
||||||
|
index 76131fa..59804aa 100644
|
||||||
|
--- a/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm
|
||||||
|
+++ b/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm
|
||||||
|
@@ -55,6 +55,59 @@ - (BOOL)canHandleRequest:(NSURLRequest *)request
|
||||||
|
return [schemes containsObject:request.URL.scheme.lowercaseString];
|
||||||
|
}
|
||||||
|
|
||||||
|
+-(NSURLCredential *)getUrlCredential:(NSURLAuthenticationChallenge *)challenge path:(NSString *)path password:(NSString *)password
|
||||||
|
+{
|
||||||
|
+ NSString *authMethod = [[challenge protectionSpace] authenticationMethod];
|
||||||
|
+ SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
|
||||||
|
+
|
||||||
|
+ if ([authMethod isEqualToString:NSURLAuthenticationMethodServerTrust] || path == nil || password == nil) {
|
||||||
|
+ return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
|
||||||
|
+ } else if (path && password) {
|
||||||
|
+ NSMutableArray *policies = [NSMutableArray array];
|
||||||
|
+ [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host)];
|
||||||
|
+ SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
|
||||||
|
+
|
||||||
|
+ SecTrustResultType result;
|
||||||
|
+ SecTrustEvaluate(serverTrust, &result);
|
||||||
|
+
|
||||||
|
+ if (![[NSFileManager defaultManager] fileExistsAtPath:path])
|
||||||
|
+ {
|
||||||
|
+ return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ NSData *p12data = [NSData dataWithContentsOfFile:path];
|
||||||
|
+ NSDictionary* options = @{ (id)kSecImportExportPassphrase:password };
|
||||||
|
+ CFArrayRef rawItems = NULL;
|
||||||
|
+ OSStatus status = SecPKCS12Import((__bridge CFDataRef)p12data,
|
||||||
|
+ (__bridge CFDictionaryRef)options,
|
||||||
|
+ &rawItems);
|
||||||
|
+
|
||||||
|
+ if (status != noErr) {
|
||||||
|
+ return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ NSArray* items = (NSArray*)CFBridgingRelease(rawItems);
|
||||||
|
+ NSDictionary* firstItem = nil;
|
||||||
|
+ if ((status == errSecSuccess) && ([items count]>0)) {
|
||||||
|
+ firstItem = items[0];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ SecIdentityRef identity = (SecIdentityRef)CFBridgingRetain(firstItem[(id)kSecImportItemIdentity]);
|
||||||
|
+ SecCertificateRef certificate = NULL;
|
||||||
|
+ if (identity) {
|
||||||
|
+ SecIdentityCopyCertificate(identity, &certificate);
|
||||||
|
+ if (certificate) { CFRelease(certificate); }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ NSMutableArray *certificates = [[NSMutableArray alloc] init];
|
||||||
|
+ [certificates addObject:CFBridgingRelease(certificate)];
|
||||||
|
+
|
||||||
|
+ return [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceNone];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
- (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request
|
||||||
|
withDelegate:(id<RCTURLRequestDelegate>)delegate
|
||||||
|
{
|
||||||
|
@@ -177,4 +230,21 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp
|
||||||
|
[delegate URLRequest:task didCompleteWithError:error];
|
||||||
|
}
|
||||||
|
|
||||||
|
+-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
|
||||||
|
+
|
||||||
|
+ NSString *host = challenge.protectionSpace.host;
|
||||||
|
+ NSDictionary *clientSSL = [[[NSUserDefaults alloc] initWithSuiteName:@"group.ios.chat.rocket"] objectForKey:host];
|
||||||
|
+
|
||||||
|
+ NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
|
||||||
|
+
|
||||||
|
+ if (clientSSL != (id)[NSNull null]) {
|
||||||
|
+ NSString *path = [clientSSL objectForKey:@"path"];
|
||||||
|
+ NSString *password = [clientSSL objectForKey:@"password"];
|
||||||
|
+ credential = [self getUrlCredential:challenge path:path password:password];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+
|
||||||
|
@end
|
||||||
diff --git a/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m b/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m
|
diff --git a/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m b/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m
|
||||||
index 6f1e5e8..b835657 100644
|
index 6f1e5e8..f268d93 100644
|
||||||
--- a/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m
|
--- a/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m
|
||||||
+++ b/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m
|
+++ b/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m
|
||||||
@@ -595,6 +595,7 @@ - (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
|
@@ -479,6 +479,29 @@ - (void)didConnect
|
||||||
|
[self _readHTTPHeader];
|
||||||
|
}
|
||||||
|
|
||||||
|
+- (void)setClientSSL:(NSString *)path password:(NSString *)password options:(NSMutableDictionary *)options;
|
||||||
|
+{
|
||||||
|
+ if ([[NSFileManager defaultManager] fileExistsAtPath:path])
|
||||||
|
+ {
|
||||||
|
+ NSData *pkcs12data = [[NSData alloc] initWithContentsOfFile:path];
|
||||||
|
+ NSDictionary* certOptions = @{ (id)kSecImportExportPassphrase:password };
|
||||||
|
+ CFArrayRef keyref = NULL;
|
||||||
|
+ OSStatus sanityChesk = SecPKCS12Import((__bridge CFDataRef)pkcs12data,
|
||||||
|
+ (__bridge CFDictionaryRef)certOptions,
|
||||||
|
+ &keyref);
|
||||||
|
+ if (sanityChesk == noErr) {
|
||||||
|
+ CFDictionaryRef identityDict = CFArrayGetValueAtIndex(keyref, 0);
|
||||||
|
+ SecIdentityRef identityRef = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
|
||||||
|
+ SecCertificateRef cert = NULL;
|
||||||
|
+ OSStatus status = SecIdentityCopyCertificate(identityRef, &cert);
|
||||||
|
+ if (!status) {
|
||||||
|
+ NSArray *certificates = [[NSArray alloc] initWithObjects:(__bridge id)identityRef, (__bridge id)cert, nil];
|
||||||
|
+ [options setObject:certificates forKey:(NSString *)kCFStreamSSLCertificates];
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
- (void)_initializeStreams;
|
||||||
|
{
|
||||||
|
assert(_url.port.unsignedIntValue <= UINT32_MAX);
|
||||||
|
@@ -516,6 +539,15 @@ - (void)_initializeStreams;
|
||||||
|
RCTLogInfo(@"SocketRocket: In debug mode. Allowing connection to any root cert");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+ // SSL Pinning
|
||||||
|
+ NSDictionary *clientSSL = [[[NSUserDefaults alloc] initWithSuiteName:@"group.ios.chat.rocket"] objectForKey:host];
|
||||||
|
+ if (clientSSL != (id)[NSNull null]) {
|
||||||
|
+ NSString *path = [clientSSL objectForKey:@"path"];
|
||||||
|
+ NSString *password = [clientSSL objectForKey:@"password"];
|
||||||
|
+
|
||||||
|
+ [self setClientSSL:path password:password options:SSLOptions];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
[_outputStream setProperty:SSLOptions
|
||||||
|
forKey:(__bridge id)kCFStreamPropertySSLSettings];
|
||||||
|
}
|
||||||
|
@@ -595,6 +627,7 @@ - (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,3 +155,16 @@ index 7ed4900..bb85402 100644
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java
|
||||||
|
index cf5ca40..262f22a 100644
|
||||||
|
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java
|
||||||
|
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java
|
||||||
|
@@ -91,7 +91,7 @@ public class AndroidInfoModule extends ReactContextBaseJavaModule {
|
||||||
|
|
||||||
|
private Boolean isRunningScreenshotTest() {
|
||||||
|
try {
|
||||||
|
- Class.forName("android.support.test.rule.ActivityTestRule");
|
||||||
|
+ Class.forName("androidx.test.rule.ActivityTestRule");
|
||||||
|
return true;
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
return false;
|
||||||
|
|
|
@ -4433,7 +4433,7 @@ expo-constants@~6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ua-parser-js "^0.7.19"
|
ua-parser-js "^0.7.19"
|
||||||
|
|
||||||
expo-file-system@~6.0.1:
|
expo-file-system@^6.0.2, expo-file-system@~6.0.1:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-6.0.2.tgz#e65f30eb6a7213e07933df9688116eaf4e25bbf8"
|
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-6.0.2.tgz#e65f30eb6a7213e07933df9688116eaf4e25bbf8"
|
||||||
integrity sha512-s+6oQpLhcT7MQp7fcoj1E+zttMr0WX6c0FrddzqB4dUfhIggV+nb35nQMASIiTHAj8VPUanTFliY5rETHRMHRA==
|
integrity sha512-s+6oQpLhcT7MQp7fcoj1E+zttMr0WX6c0FrddzqB4dUfhIggV+nb35nQMASIiTHAj8VPUanTFliY5rETHRMHRA==
|
||||||
|
|
Loading…
Reference in New Issue