From d09ded83ab80fc73b0f526e30f8c424c8b095ab6 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Fri, 9 Aug 2019 20:15:31 -0300 Subject: [PATCH 01/13] WebSocket & Fetch - SSL Pinning --- app/i18n/locales/en.js | 2 + app/sagas/selectServer.js | 16 +++ app/views/NewServerView.js | 42 +++++++- patches/react-native+0.60.4.patch | 157 ++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 1 deletion(-) diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 547f7db3316..70a80a67921 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -93,6 +93,7 @@ export default { and: 'and', announcement: 'announcement', Announcement: 'Announcement', + Apply_Your_Certificate: 'Apply Your Certificate', ARCHIVE: 'ARCHIVE', archive: 'archive', are_typing: 'are typing', @@ -151,6 +152,7 @@ export default { Disable_notifications: 'Disable notifications', Discussions: 'Discussions', 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?', edit: 'edit', edited: 'edited', diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index f6793f998db..baf720b4fea 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -77,8 +77,24 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch } }; +const extractHostname = (url) => { + let hostname; + + if (url.indexOf('//') > -1) { + [,, hostname] = url.split('/'); + } else { + [hostname] = url.split('/'); + } + [hostname] = hostname.split(':'); + [hostname] = hostname.split('?'); + + return hostname; +}; + const handleServerRequest = function* handleServerRequest({ server }) { try { + yield RNUserDefaults.setObjectForKey(extractHostname(server), { path: 'CER_NAME', password: 'PASSWORD' }); + const serverInfo = yield getServerInfo({ server }); const loginServicesLength = yield RocketChat.getLoginServices(server); diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index 463aeb4c7c5..ac8c8748091 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -1,10 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity + Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity, View } from 'react-native'; import { connect } from 'react-redux'; import { SafeAreaView } from 'react-navigation'; +import DocumentPicker from 'react-native-document-picker'; import { serverRequest } from '../actions/server'; import sharedStyles from './Styles'; @@ -18,6 +19,7 @@ import { isIOS, isNotch } from '../utils/deviceInfo'; import { CustomIcon } from '../lib/Icons'; import StatusBar from '../containers/StatusBar'; import { COLOR_PRIMARY } from '../constants/colors'; +import log from '../utils/log'; const styles = StyleSheet.create({ image: { @@ -41,6 +43,21 @@ const styles = StyleSheet.create({ position: 'absolute', paddingHorizontal: 9, left: 15 + }, + certificatePicker: { + flex: 1, + alignItems: 'center', + justifyContent: 'center' + }, + chooseCertificateTitle: { + fontSize: 15, + ...sharedStyles.textRegular, + ...sharedStyles.textColorDescription + }, + chooseCertificate: { + fontSize: 15, + ...sharedStyles.textSemibold, + ...sharedStyles.textColorHeaderBack } }); @@ -101,6 +118,19 @@ class NewServerView extends React.Component { } } + chooseCertificate = async() => { + try { + const res = await DocumentPicker.pick({ + type: ['com.rsa.pkcs-12'] + }); + console.log(res); + } catch (error) { + if (!DocumentPicker.isCancel(error)) { + log('chooseCertificate', error); + } + } + } + completeUrl = (url) => { url = url && url.trim(); @@ -142,6 +172,15 @@ class NewServerView extends React.Component { ); } + renderCertificatePicker = () => ( + + {I18n.t('Do_you_have_a_certificate')} + + {I18n.t('Apply_Your_Certificate')} + + + ); + render() { const { connecting } = this.props; const { text, autoFocus } = this.state; @@ -175,6 +214,7 @@ class NewServerView extends React.Component { loading={connecting} testID='new-server-view-button' /> + {isIOS ? this.renderCertificatePicker() : null} {this.renderBack()} diff --git a/patches/react-native+0.60.4.patch b/patches/react-native+0.60.4.patch index 16cddd454da..536c64ae700 100644 --- a/patches/react-native+0.60.4.patch +++ b/patches/react-native+0.60.4.patch @@ -1,3 +1,147 @@ +diff --git a/node_modules/react-native/.DS_Store b/node_modules/react-native/.DS_Store +new file mode 100644 +index 0000000..2f45b44 +Binary files /dev/null and b/node_modules/react-native/.DS_Store differ +diff --git a/node_modules/react-native/Libraries/.DS_Store b/node_modules/react-native/Libraries/.DS_Store +new file mode 100644 +index 0000000..45d32b5 +Binary files /dev/null and b/node_modules/react-native/Libraries/.DS_Store differ +diff --git a/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm b/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm +index 76131fa..65128b9 100644 +--- a/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm ++++ b/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm +@@ -55,6 +55,61 @@ - (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); ++ ++ NSString *pfile = [[NSBundle mainBundle] pathForResource:path ofType:@"p12" ]; ++ ++ if (![[NSFileManager defaultManager] fileExistsAtPath:pfile]) ++ { ++ return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; ++ } ++ ++ NSData *p12data = [NSData dataWithContentsOfFile:pfile]; ++ 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)delegate + { +@@ -177,4 +232,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 +index 6f1e5e8..dc3c110 100644 +--- a/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m ++++ b/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m +@@ -479,6 +479,27 @@ - (void)didConnect + [self _readHTTPHeader]; + } + ++- (void)setClientSSL:(NSString *)path password:(NSString *)password options:(NSMutableDictionary *)options; ++{ ++ NSString *filepath = [[NSBundle mainBundle] pathForResource:path ofType:@"p12"]; ++ NSData *pkcs12data = [[NSData alloc] initWithContentsOfFile:filepath]; ++ 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 +537,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]; + } diff --git a/node_modules/react-native/React/Modules/RCTRedBox.m b/node_modules/react-native/React/Modules/RCTRedBox.m index 7ed4900..bb85402 100644 --- a/node_modules/react-native/React/Modules/RCTRedBox.m @@ -11,3 +155,16 @@ index 7ed4900..bb85402 100644 } else { 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; From d32f99aaeb24e596be2ed4e54349a083315283cd Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Fri, 9 Aug 2019 20:38:06 -0300 Subject: [PATCH 02/13] Fix access file --- patches/react-native+0.60.4.patch | 34 ++++++++++++++++--------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/patches/react-native+0.60.4.patch b/patches/react-native+0.60.4.patch index 536c64ae700..d328aa17904 100644 --- a/patches/react-native+0.60.4.patch +++ b/patches/react-native+0.60.4.patch @@ -1,11 +1,9 @@ diff --git a/node_modules/react-native/.DS_Store b/node_modules/react-native/.DS_Store new file mode 100644 -index 0000000..2f45b44 -Binary files /dev/null and b/node_modules/react-native/.DS_Store differ +index 0000000..e69de29 diff --git a/node_modules/react-native/Libraries/.DS_Store b/node_modules/react-native/Libraries/.DS_Store new file mode 100644 -index 0000000..45d32b5 -Binary files /dev/null and b/node_modules/react-native/Libraries/.DS_Store differ +index 0000000..e69de29 diff --git a/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm b/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm index 76131fa..65128b9 100644 --- a/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm @@ -95,38 +93,42 @@ index 76131fa..65128b9 100644 + @end diff --git a/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m b/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m -index 6f1e5e8..dc3c110 100644 +index 6f1e5e8..7e9c877 100644 --- a/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m +++ b/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m -@@ -479,6 +479,27 @@ - (void)didConnect +@@ -479,6 +479,31 @@ - (void)didConnect [self _readHTTPHeader]; } +- (void)setClientSSL:(NSString *)path password:(NSString *)password options:(NSMutableDictionary *)options; +{ + NSString *filepath = [[NSBundle mainBundle] pathForResource:path ofType:@"p12"]; -+ NSData *pkcs12data = [[NSData alloc] initWithContentsOfFile:filepath]; -+ NSDictionary* certOptions = @{ (id)kSecImportExportPassphrase:password }; -+ CFArrayRef keyref = NULL; -+ OSStatus sanityChesk = SecPKCS12Import((__bridge CFDataRef)pkcs12data, -+ (__bridge CFDictionaryRef)certOptions, -+ &keyref); -+ if (sanityChesk == noErr) { ++ ++ if ([[NSFileManager defaultManager] fileExistsAtPath:filepath]) ++ { ++ NSData *pkcs12data = [[NSData alloc] initWithContentsOfFile:filepath]; ++ 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]; ++ 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 +537,15 @@ - (void)_initializeStreams; +@@ -516,6 +541,15 @@ - (void)_initializeStreams; RCTLogInfo(@"SocketRocket: In debug mode. Allowing connection to any root cert"); #endif From 1aea7471527e7ec977eb3884a9640888c687e148 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Fri, 9 Aug 2019 21:02:38 -0300 Subject: [PATCH 03/13] Add certificate by uri-path --- app/actions/server.js | 5 ++-- app/sagas/selectServer.js | 4 ++-- app/views/NewServerView.js | 38 +++++++++++++++++++------------ patches/react-native+0.60.4.patch | 26 +++++++++------------ 4 files changed, 40 insertions(+), 33 deletions(-) diff --git a/app/actions/server.js b/app/actions/server.js index a5441c7ba1d..a3356d89a5f 100644 --- a/app/actions/server.js +++ b/app/actions/server.js @@ -23,10 +23,11 @@ export function selectServerFailure() { }; } -export function serverRequest(server) { +export function serverRequest(server, certificate = null) { return { type: SERVER.REQUEST, - server + server, + certificate }; } diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index baf720b4fea..59fd533340f 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -91,9 +91,9 @@ const extractHostname = (url) => { return hostname; }; -const handleServerRequest = function* handleServerRequest({ server }) { +const handleServerRequest = function* handleServerRequest({ server, certificate }) { try { - yield RNUserDefaults.setObjectForKey(extractHostname(server), { path: 'CER_NAME', password: 'PASSWORD' }); + yield RNUserDefaults.setObjectForKey(extractHostname(server), certificate); const serverInfo = yield getServerInfo({ server }); diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index ac8c8748091..acfba7828f5 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -80,7 +80,9 @@ class NewServerView extends React.Component { const server = props.navigation.getParam('server'); this.state = { text: server || '', - autoFocus: !server + autoFocus: !server, + path: null, + name: null }; } @@ -93,11 +95,14 @@ class NewServerView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const { text } = this.state; + const { text, name } = this.state; const { connecting } = this.props; if (nextState.text !== text) { return true; } + if (nextState.name !== name) { + return true; + } if (nextProps.connecting !== connecting) { return true; } @@ -109,12 +114,14 @@ class NewServerView extends React.Component { } submit = () => { - const { text } = this.state; + const { text, path } = this.state; const { connectServer } = this.props; + const certificate = { path, password: 'PASSWORD' }; + if (text) { Keyboard.dismiss(); - connectServer(this.completeUrl(text)); + connectServer(this.completeUrl(text), certificate); } } @@ -123,7 +130,7 @@ class NewServerView extends React.Component { const res = await DocumentPicker.pick({ type: ['com.rsa.pkcs-12'] }); - console.log(res); + this.setState({ path: res.uri.replace('file://', ''), name: res.name }); } catch (error) { if (!DocumentPicker.isCancel(error)) { log('chooseCertificate', error); @@ -172,14 +179,17 @@ class NewServerView extends React.Component { ); } - renderCertificatePicker = () => ( - - {I18n.t('Do_you_have_a_certificate')} - - {I18n.t('Apply_Your_Certificate')} - - - ); + renderCertificatePicker = () => { + const { name } = this.state; + return ( + + {I18n.t('Do_you_have_a_certificate')} + + {name || I18n.t('Apply_Your_Certificate')} + + + ); + } render() { const { connecting } = this.props; @@ -228,7 +238,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ - connectServer: server => dispatch(serverRequest(server)) + connectServer: (server, certificate) => dispatch(serverRequest(server, certificate)) }); export default connect(mapStateToProps, mapDispatchToProps)(NewServerView); diff --git a/patches/react-native+0.60.4.patch b/patches/react-native+0.60.4.patch index d328aa17904..1844cdf715c 100644 --- a/patches/react-native+0.60.4.patch +++ b/patches/react-native+0.60.4.patch @@ -5,10 +5,10 @@ diff --git a/node_modules/react-native/Libraries/.DS_Store b/node_modules/react- new file mode 100644 index 0000000..e69de29 diff --git a/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm b/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm -index 76131fa..65128b9 100644 +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,61 @@ - (BOOL)canHandleRequest:(NSURLRequest *)request +@@ -55,6 +55,59 @@ - (BOOL)canHandleRequest:(NSURLRequest *)request return [schemes containsObject:request.URL.scheme.lowercaseString]; } @@ -27,14 +27,12 @@ index 76131fa..65128b9 100644 + SecTrustResultType result; + SecTrustEvaluate(serverTrust, &result); + -+ NSString *pfile = [[NSBundle mainBundle] pathForResource:path ofType:@"p12" ]; -+ -+ if (![[NSFileManager defaultManager] fileExistsAtPath:pfile]) ++ if (![[NSFileManager defaultManager] fileExistsAtPath:path]) + { + return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; + } + -+ NSData *p12data = [NSData dataWithContentsOfFile:pfile]; ++ NSData *p12data = [NSData dataWithContentsOfFile:path]; + NSDictionary* options = @{ (id)kSecImportExportPassphrase:password }; + CFArrayRef rawItems = NULL; + OSStatus status = SecPKCS12Import((__bridge CFDataRef)p12data, @@ -70,7 +68,7 @@ index 76131fa..65128b9 100644 - (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request withDelegate:(id)delegate { -@@ -177,4 +232,21 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp +@@ -177,4 +230,21 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp [delegate URLRequest:task didCompleteWithError:error]; } @@ -93,20 +91,18 @@ index 76131fa..65128b9 100644 + @end diff --git a/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m b/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m -index 6f1e5e8..7e9c877 100644 +index 6f1e5e8..91f79a9 100644 --- a/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m +++ b/node_modules/react-native/Libraries/WebSocket/RCTSRWebSocket.m -@@ -479,6 +479,31 @@ - (void)didConnect +@@ -479,6 +479,29 @@ - (void)didConnect [self _readHTTPHeader]; } +- (void)setClientSSL:(NSString *)path password:(NSString *)password options:(NSMutableDictionary *)options; -+{ -+ NSString *filepath = [[NSBundle mainBundle] pathForResource:path ofType:@"p12"]; -+ -+ if ([[NSFileManager defaultManager] fileExistsAtPath:filepath]) ++{ ++ if ([[NSFileManager defaultManager] fileExistsAtPath:path]) + { -+ NSData *pkcs12data = [[NSData alloc] initWithContentsOfFile:filepath]; ++ NSData *pkcs12data = [[NSData alloc] initWithContentsOfFile:path]; + NSDictionary* certOptions = @{ (id)kSecImportExportPassphrase:password }; + CFArrayRef keyref = NULL; + OSStatus sanityChesk = SecPKCS12Import((__bridge CFDataRef)pkcs12data, @@ -128,7 +124,7 @@ index 6f1e5e8..7e9c877 100644 - (void)_initializeStreams; { assert(_url.port.unsignedIntValue <= UINT32_MAX); -@@ -516,6 +541,15 @@ - (void)_initializeStreams; +@@ -516,6 +539,15 @@ - (void)_initializeStreams; RCTLogInfo(@"SocketRocket: In debug mode. Allowing connection to any root cert"); #endif From 606b3cf808d9f010481984e1465c9d48d794969b Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Sat, 10 Aug 2019 11:21:43 -0300 Subject: [PATCH 04/13] Create password alert to certificate --- app/i18n/locales/en.js | 3 +++ app/views/NewServerView.js | 53 +++++++++++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 70a80a67921..7c5482c92ec 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -136,6 +136,8 @@ export default { Copied_to_clipboard: 'Copied to clipboard!', Copy: 'Copy', 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_Channel: 'Create Channel', Created_snippet: 'Created a snippet', @@ -406,6 +408,7 @@ export default { 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.', + Your_certificate: 'Your Certificate', Version_no: 'Version: {{version}}', You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!', Change_Language: 'Change Language', diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index acfba7828f5..7f5bd042ba1 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -1,11 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity, View + Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity, View, Platform } from 'react-native'; import { connect } from 'react-redux'; import { SafeAreaView } from 'react-navigation'; import DocumentPicker from 'react-native-document-picker'; +import Dialog from 'react-native-dialog'; import { serverRequest } from '../actions/server'; import sharedStyles from './Styles'; @@ -58,7 +59,16 @@ const styles = StyleSheet.create({ fontSize: 15, ...sharedStyles.textSemibold, ...sharedStyles.textColorHeaderBack - } + }, + dialogInput: Platform.select({ + ios: {}, + android: { + borderRadius: 4, + borderColor: 'rgba(0,0,0,.15)', + borderWidth: 2, + paddingHorizontal: 10 + } + }) }); const defaultServer = 'https://open.rocket.chat'; @@ -81,7 +91,9 @@ class NewServerView extends React.Component { this.state = { text: server || '', autoFocus: !server, + showPasswordAlert: false, path: null, + password: null, name: null }; } @@ -95,7 +107,7 @@ class NewServerView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const { text, name } = this.state; + const { text, name, showPasswordAlert } = this.state; const { connecting } = this.props; if (nextState.text !== text) { return true; @@ -103,6 +115,9 @@ class NewServerView extends React.Component { if (nextState.name !== name) { return true; } + if (nextState.showPasswordAlert !== showPasswordAlert) { + return true; + } if (nextProps.connecting !== connecting) { return true; } @@ -114,10 +129,10 @@ class NewServerView extends React.Component { } submit = () => { - const { text, path } = this.state; + const { text, path, password } = this.state; const { connectServer } = this.props; - const certificate = { path, password: 'PASSWORD' }; + const certificate = { path, password }; if (text) { Keyboard.dismiss(); @@ -130,7 +145,7 @@ class NewServerView extends React.Component { const res = await DocumentPicker.pick({ type: ['com.rsa.pkcs-12'] }); - this.setState({ path: res.uri.replace('file://', ''), name: res.name }); + this.setState({ path: res.uri.replace('file://', ''), name: res.name, showPasswordAlert: true }); } catch (error) { if (!DocumentPicker.isCancel(error)) { log('chooseCertificate', error); @@ -179,11 +194,34 @@ class NewServerView extends React.Component { ); } + saveCertificate = () => this.setState({ showPasswordAlert: false }); + + renderCertificatePassword = () => { + const { showPasswordAlert } = this.state; + return ( + + + {I18n.t('Certificate_password')} + + + {I18n.t('Whats_the_password_for_your_certificate')} + + this.setState({ password: value })} + secureTextEntry + testID='certificate-password-input' + style={styles.dialogInput} + /> + + + ); + } + renderCertificatePicker = () => { const { name } = this.state; return ( - {I18n.t('Do_you_have_a_certificate')} + {name ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')} {name || I18n.t('Apply_Your_Certificate')} @@ -225,6 +263,7 @@ class NewServerView extends React.Component { testID='new-server-view-button' /> {isIOS ? this.renderCertificatePicker() : null} + {isIOS ? this.renderCertificatePassword() : null} {this.renderBack()} From 1d3b7c86474a395b9e393e0fef403049e3a352a0 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Mon, 12 Aug 2019 15:12:54 -0300 Subject: [PATCH 05/13] Copy file from tmp to documents dir --- app/views/NewServerView.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index 7f5bd042ba1..a54e8ecfb81 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -7,6 +7,7 @@ import { connect } from 'react-redux'; import { SafeAreaView } from 'react-navigation'; import DocumentPicker from 'react-native-document-picker'; import Dialog from 'react-native-dialog'; +import RNFetchBlob from 'rn-fetch-blob'; import { serverRequest } from '../actions/server'; import sharedStyles from './Styles'; @@ -145,7 +146,7 @@ class NewServerView extends React.Component { const res = await DocumentPicker.pick({ type: ['com.rsa.pkcs-12'] }); - this.setState({ path: res.uri.replace('file://', ''), name: res.name, showPasswordAlert: true }); + this.setState({ path: this.uriToPath(res.uri), name: res.name, showPasswordAlert: true }); } catch (error) { if (!DocumentPicker.isCancel(error)) { log('chooseCertificate', error); @@ -194,7 +195,15 @@ class NewServerView extends React.Component { ); } - saveCertificate = () => this.setState({ showPasswordAlert: false }); + uriToPath = uri => uri.replace('file://', ''); + + saveCertificate = async() => { + const { name, path } = this.state; + const documents = RNFetchBlob.fs.dirs.DocumentDir; + const certificatePath = `${ documents }/${ name }`; + await RNFetchBlob.fs.cp(this.uriToPath(path), certificatePath); // copy from tmp to documents dir + this.setState({ showPasswordAlert: false, path: certificatePath }); + } renderCertificatePassword = () => { const { showPasswordAlert } = this.state; From 570ce69121e6c58035c99797f5367e1f444fe4d3 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Mon, 12 Aug 2019 20:45:26 -0300 Subject: [PATCH 06/13] Remove certificate when logout from server --- app/lib/rocketchat.js | 8 ++++++++ app/sagas/selectServer.js | 15 +-------------- app/utils/server.js | 13 +++++++++++++ app/views/NewServerView.js | 6 ++---- 4 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 app/utils/server.js diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index d3efaa4b3d0..a94b14994c7 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -2,6 +2,7 @@ import { AsyncStorage, InteractionManager } from 'react-native'; import semver from 'semver'; import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; import RNUserDefaults from 'rn-user-defaults'; +import RNFetchBlob from 'rn-fetch-blob'; import reduxStore from './createStore'; import defaultSettings from '../constants/settings'; @@ -9,6 +10,7 @@ import messagesStatus from '../constants/messagesStatus'; import database from './realm'; import log from '../utils/log'; import { isIOS, getBundleId } from '../utils/deviceInfo'; +import { extractHostname } from '../utils/server'; import { setUser, setLoginServices, loginRequest, loginFailure, logout @@ -358,6 +360,12 @@ const RocketChat = { try { const servers = await RNUserDefaults.objectForKey(SERVERS); 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 RNFetchBlob.fs.unlink(certificate.path); + } } catch (error) { console.log('logout_rn_user_defaults', error); } diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index 59fd533340f..232c983c123 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -14,6 +14,7 @@ import { setUser } from '../actions/login'; import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; import log from '../utils/log'; +import { extractHostname } from '../utils/server'; import I18n from '../i18n'; import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults'; @@ -77,20 +78,6 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch } }; -const extractHostname = (url) => { - let hostname; - - if (url.indexOf('//') > -1) { - [,, hostname] = url.split('/'); - } else { - [hostname] = url.split('/'); - } - [hostname] = hostname.split(':'); - [hostname] = hostname.split('?'); - - return hostname; -}; - const handleServerRequest = function* handleServerRequest({ server, certificate }) { try { yield RNUserDefaults.setObjectForKey(extractHostname(server), certificate); diff --git a/app/utils/server.js b/app/utils/server.js new file mode 100644 index 00000000000..7ed471dc93c --- /dev/null +++ b/app/utils/server.js @@ -0,0 +1,13 @@ +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; +}; diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index a54e8ecfb81..2395816158d 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -48,6 +48,7 @@ const styles = StyleSheet.create({ }, certificatePicker: { flex: 1, + marginTop: 40, alignItems: 'center', justifyContent: 'center' }, @@ -108,14 +109,11 @@ class NewServerView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const { text, name, showPasswordAlert } = this.state; + const { text, showPasswordAlert } = this.state; const { connecting } = this.props; if (nextState.text !== text) { return true; } - if (nextState.name !== name) { - return true; - } if (nextState.showPasswordAlert !== showPasswordAlert) { return true; } From c29c49147ccb9ceacd2abf4c0ec6f89f3b4c8052 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Mon, 12 Aug 2019 21:06:25 -0300 Subject: [PATCH 07/13] Can remove certificate after pick --- app/views/NewServerView.js | 59 +++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index 2395816158d..bcfaf58f4a5 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -8,6 +8,7 @@ import { SafeAreaView } from 'react-navigation'; import DocumentPicker from 'react-native-document-picker'; import Dialog from 'react-native-dialog'; import RNFetchBlob from 'rn-fetch-blob'; +import ActionSheet from 'react-native-action-sheet'; import { serverRequest } from '../actions/server'; import sharedStyles from './Styles'; @@ -90,6 +91,15 @@ class NewServerView extends React.Component { constructor(props) { super(props); 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 = { text: server || '', autoFocus: !server, @@ -109,7 +119,7 @@ class NewServerView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const { text, showPasswordAlert } = this.state; + const { text, showPasswordAlert, name } = this.state; const { connecting } = this.props; if (nextState.text !== text) { return true; @@ -117,6 +127,9 @@ class NewServerView extends React.Component { if (nextState.showPasswordAlert !== showPasswordAlert) { return true; } + if (nextState.name !== name) { + return true; + } if (nextProps.connecting !== connecting) { return true; } @@ -147,7 +160,7 @@ class NewServerView extends React.Component { this.setState({ path: this.uriToPath(res.uri), name: res.name, showPasswordAlert: true }); } catch (error) { if (!DocumentPicker.isCancel(error)) { - log('chooseCertificate', error); + log('err_choose_certificate', error); } } } @@ -199,10 +212,48 @@ class NewServerView extends React.Component { const { name, path } = this.state; const documents = RNFetchBlob.fs.dirs.DocumentDir; const certificatePath = `${ documents }/${ name }`; - await RNFetchBlob.fs.cp(this.uriToPath(path), certificatePath); // copy from tmp to documents dir + try { + await RNFetchBlob.fs.cp(this.uriToPath(path), certificatePath); // copy from tmp to documents dir + } catch (error) { + log('err_save_certificate', error); + } this.setState({ showPasswordAlert: false, path: certificatePath }); } + handleDelete = async() => { + const { name } = this.state; + const documents = RNFetchBlob.fs.dirs.DocumentDir; + const certificatePath = `${ documents }/${ name }`; + try { + await RNFetchBlob.fs.unlink(certificatePath); + } catch (error) { + log('err_remove_certificate', error); + } + this.setState({ name: null, path: null, password: null }); + } + + handleActionPress = (actionIndex) => { + if (actionIndex) { + switch (actionIndex) { + case this.DELETE_INDEX: + this.handleDelete(); + break; + default: + break; + } + } + } + + showActionSheet = () => { + ActionSheet.showActionSheetWithOptions({ + options: this.options, + cancelButtonIndex: this.CANCEL_INDEX, + destructiveButtonIndex: this.DELETE_INDEX + }, (actionIndex) => { + this.handleActionPress(actionIndex); + }); + } + renderCertificatePassword = () => { const { showPasswordAlert } = this.state; return ( @@ -229,7 +280,7 @@ class NewServerView extends React.Component { return ( {name ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')} - + {name || I18n.t('Apply_Your_Certificate')} From c5be1a909fde145a71f8722d57dfe33b0488566c Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Tue, 13 Aug 2019 20:51:20 -0300 Subject: [PATCH 08/13] Refactor --- app/views/NewServerView.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index bcfaf58f4a5..d178703a4ef 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -23,6 +23,7 @@ import { CustomIcon } from '../lib/Icons'; import StatusBar from '../containers/StatusBar'; import { COLOR_PRIMARY } from '../constants/colors'; import log from '../utils/log'; +import random from '../utils/random'; const styles = StyleSheet.create({ image: { @@ -157,7 +158,7 @@ class NewServerView extends React.Component { const res = await DocumentPicker.pick({ type: ['com.rsa.pkcs-12'] }); - this.setState({ path: this.uriToPath(res.uri), name: res.name, showPasswordAlert: true }); + this.setState({ path: res.uri, name: res.name, showPasswordAlert: true }); } catch (error) { if (!DocumentPicker.isCancel(error)) { log('err_choose_certificate', error); @@ -210,10 +211,9 @@ class NewServerView extends React.Component { saveCertificate = async() => { const { name, path } = this.state; - const documents = RNFetchBlob.fs.dirs.DocumentDir; - const certificatePath = `${ documents }/${ name }`; + const certificatePath = `${ RNFetchBlob.fs.dirs.DocumentDir }/${ name }${ random(4) }`; try { - await RNFetchBlob.fs.cp(this.uriToPath(path), certificatePath); // copy from tmp to documents dir + await RNFetchBlob.fs.cp(this.uriToPath(path), certificatePath); } catch (error) { log('err_save_certificate', error); } @@ -222,8 +222,7 @@ class NewServerView extends React.Component { handleDelete = async() => { const { name } = this.state; - const documents = RNFetchBlob.fs.dirs.DocumentDir; - const certificatePath = `${ documents }/${ name }`; + const certificatePath = `${ RNFetchBlob.fs.dirs.DocumentDir }/${ name }`; try { await RNFetchBlob.fs.unlink(certificatePath); } catch (error) { @@ -320,8 +319,16 @@ class NewServerView extends React.Component { loading={connecting} testID='new-server-view-button' /> - {isIOS ? this.renderCertificatePicker() : null} - {isIOS ? this.renderCertificatePassword() : null} + { + isIOS + ? ( + <> + {this.renderCertificatePicker()} + {this.renderCertificatePassword()} + + ) + : null + } {this.renderBack()} From ba4b9842e4a940ce085cda38eefb3fa09b0ee79a Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Tue, 13 Aug 2019 21:00:16 -0300 Subject: [PATCH 09/13] Reorder and fix delete --- app/views/NewServerView.js | 49 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index d178703a4ef..eaedefaa07f 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -185,28 +185,6 @@ class NewServerView extends React.Component { return url.replace(/\/+$/, ''); } - renderBack = () => { - const { navigation } = this.props; - - let top = 15; - if (isIOS) { - top = isNotch ? 45 : 30; - } - - return ( - navigation.pop()} - > - - - ); - } - uriToPath = uri => uri.replace('file://', ''); saveCertificate = async() => { @@ -221,10 +199,9 @@ class NewServerView extends React.Component { } handleDelete = async() => { - const { name } = this.state; - const certificatePath = `${ RNFetchBlob.fs.dirs.DocumentDir }/${ name }`; + const { path } = this.state; try { - await RNFetchBlob.fs.unlink(certificatePath); + await RNFetchBlob.fs.unlink(path); } catch (error) { log('err_remove_certificate', error); } @@ -253,6 +230,28 @@ class NewServerView extends React.Component { }); } + renderBack = () => { + const { navigation } = this.props; + + let top = 15; + if (isIOS) { + top = isNotch ? 45 : 30; + } + + return ( + navigation.pop()} + > + + + ); + } + renderCertificatePassword = () => { const { showPasswordAlert } = this.state; return ( From 385cf179b59478ced045838071f054587f24e02f Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Tue, 13 Aug 2019 21:44:42 -0300 Subject: [PATCH 10/13] Refactor state --- app/sagas/selectServer.js | 4 ++- app/views/NewServerView.js | 68 ++++++++++++++++++++++---------------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index 232c983c123..69d64f3f700 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -80,7 +80,9 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch const handleServerRequest = function* handleServerRequest({ server, certificate }) { try { - yield RNUserDefaults.setObjectForKey(extractHostname(server), certificate); + if (certificate) { + yield RNUserDefaults.setObjectForKey(extractHostname(server), certificate); + } const serverInfo = yield getServerInfo({ server }); diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index eaedefaa07f..7523bc2cd4c 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -9,6 +9,7 @@ import DocumentPicker from 'react-native-document-picker'; import Dialog from 'react-native-dialog'; import RNFetchBlob from 'rn-fetch-blob'; import ActionSheet from 'react-native-action-sheet'; +import isEqual from 'deep-equal'; import { serverRequest } from '../actions/server'; import sharedStyles from './Styles'; @@ -23,7 +24,6 @@ import { CustomIcon } from '../lib/Icons'; import StatusBar from '../containers/StatusBar'; import { COLOR_PRIMARY } from '../constants/colors'; import log from '../utils/log'; -import random from '../utils/random'; const styles = StyleSheet.create({ image: { @@ -105,9 +105,7 @@ class NewServerView extends React.Component { text: server || '', autoFocus: !server, showPasswordAlert: false, - path: null, - password: null, - name: null + certificate: null }; } @@ -120,7 +118,7 @@ class NewServerView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const { text, showPasswordAlert, name } = this.state; + const { text, showPasswordAlert, certificate } = this.state; const { connecting } = this.props; if (nextState.text !== text) { return true; @@ -128,7 +126,7 @@ class NewServerView extends React.Component { if (nextState.showPasswordAlert !== showPasswordAlert) { return true; } - if (nextState.name !== name) { + if (isEqual(nextState.certificate, certificate)) { return true; } if (nextProps.connecting !== connecting) { @@ -141,15 +139,24 @@ class NewServerView extends React.Component { this.setState({ text }); } - submit = () => { - const { text, path, password } = this.state; + submit = async() => { + const { text, certificate } = this.state; const { connectServer } = this.props; - - const certificate = { path, password }; + let cert = null; + + if (certificate) { + const certificatePath = `${ RNFetchBlob.fs.dirs.DocumentDir }/${ certificate.name }`; + try { + await RNFetchBlob.fs.cp(this.uriToPath(certificate.path), certificatePath); + } catch (error) { + log('err_save_certificate', error); + } + cert = { path: certificatePath, password: certificate.password }; + } if (text) { Keyboard.dismiss(); - connectServer(this.completeUrl(text), certificate); + connectServer(this.completeUrl(text), cert); } } @@ -158,7 +165,14 @@ class NewServerView extends React.Component { const res = await DocumentPicker.pick({ type: ['com.rsa.pkcs-12'] }); - this.setState({ path: res.uri, name: res.name, showPasswordAlert: true }); + const { uri: path, name } = res; + this.setState({ + certificate: { + path, + name + }, + showPasswordAlert: true + }); } catch (error) { if (!DocumentPicker.isCancel(error)) { log('err_choose_certificate', error); @@ -187,25 +201,21 @@ class NewServerView extends React.Component { uriToPath = uri => uri.replace('file://', ''); - saveCertificate = async() => { - const { name, path } = this.state; - const certificatePath = `${ RNFetchBlob.fs.dirs.DocumentDir }/${ name }${ random(4) }`; - try { - await RNFetchBlob.fs.cp(this.uriToPath(path), certificatePath); - } catch (error) { - log('err_save_certificate', error); - } - this.setState({ showPasswordAlert: false, path: certificatePath }); + saveCertificate = () => { + const { certificate } = this.state; + const { path } = certificate; + this.setState({ showPasswordAlert: false, certificate: { path: this.uriToPath(path), ...certificate } }); } handleDelete = async() => { - const { path } = this.state; + const { certificate } = this.state; + const { path } = certificate; try { await RNFetchBlob.fs.unlink(path); } catch (error) { log('err_remove_certificate', error); } - this.setState({ name: null, path: null, password: null }); + this.setState({ certificate: null }); } handleActionPress = (actionIndex) => { @@ -253,7 +263,7 @@ class NewServerView extends React.Component { } renderCertificatePassword = () => { - const { showPasswordAlert } = this.state; + const { showPasswordAlert, certificate } = this.state; return ( @@ -263,7 +273,7 @@ class NewServerView extends React.Component { {I18n.t('Whats_the_password_for_your_certificate')} this.setState({ password: value })} + onChangeText={value => this.setState({ certificate: { ...certificate, password: value } })} secureTextEntry testID='certificate-password-input' style={styles.dialogInput} @@ -274,12 +284,12 @@ class NewServerView extends React.Component { } renderCertificatePicker = () => { - const { name } = this.state; + const { certificate } = this.state; return ( - {name ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')} - - {name || I18n.t('Apply_Your_Certificate')} + {certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')} + + {certificate ? certificate.name : I18n.t('Apply_Your_Certificate')} ); From f05371c95c5eaeb338d80b5ab487b472f6224506 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Wed, 14 Aug 2019 10:14:04 -0300 Subject: [PATCH 11/13] Switch rn-fetch-blob -> expo-file-system --- app/lib/rocketchat.js | 4 ++-- app/views/NewServerView.js | 15 +++++++++------ package.json | 1 + yarn.lock | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 8de5fba4c95..ec3c84f2dde 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -2,7 +2,7 @@ import { AsyncStorage, InteractionManager } from 'react-native'; import semver from 'semver'; import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; import RNUserDefaults from 'rn-user-defaults'; -import RNFetchBlob from 'rn-fetch-blob'; +import * as FileSystem from 'expo-file-system'; import reduxStore from './createStore'; import defaultSettings from '../constants/settings'; @@ -364,7 +364,7 @@ const RocketChat = { const certificate = await RNUserDefaults.objectForKey(extractHostname(server)); if (certificate && certificate.path) { await RNUserDefaults.clear(extractHostname(server)); - await RNFetchBlob.fs.unlink(certificate.path); + await FileSystem.deleteAsync(certificate.path); } } catch (error) { console.log('logout_rn_user_defaults', error); diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index 7523bc2cd4c..0391935ebb1 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -5,9 +5,9 @@ import { } from 'react-native'; import { connect } from 'react-redux'; import { SafeAreaView } from 'react-navigation'; +import * as FileSystem from 'expo-file-system'; import DocumentPicker from 'react-native-document-picker'; import Dialog from 'react-native-dialog'; -import RNFetchBlob from 'rn-fetch-blob'; import ActionSheet from 'react-native-action-sheet'; import isEqual from 'deep-equal'; @@ -145,13 +145,16 @@ class NewServerView extends React.Component { let cert = null; if (certificate) { - const certificatePath = `${ RNFetchBlob.fs.dirs.DocumentDir }/${ certificate.name }`; + const certificatePath = `${ FileSystem.documentDirectory }/${ certificate.name }`; try { - await RNFetchBlob.fs.cp(this.uriToPath(certificate.path), certificatePath); + await FileSystem.copyAsync({ from: certificate.path, to: certificatePath }); } catch (error) { log('err_save_certificate', error); } - cert = { path: certificatePath, password: certificate.password }; + cert = { + path: this.uriToPath(certificatePath), // file:// isn't allowed by obj-C + password: certificate.password + }; } if (text) { @@ -204,14 +207,14 @@ class NewServerView extends React.Component { saveCertificate = () => { const { certificate } = this.state; const { path } = certificate; - this.setState({ showPasswordAlert: false, certificate: { path: this.uriToPath(path), ...certificate } }); + this.setState({ showPasswordAlert: false, certificate: { path, ...certificate } }); } handleDelete = async() => { const { certificate } = this.state; const { path } = certificate; try { - await RNFetchBlob.fs.unlink(path); + await FileSystem.deleteAsync(path); } catch (error) { log('err_remove_certificate', error); } diff --git a/package.json b/package.json index 806bf49c2ad..706a43b8125 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@rocket.chat/sdk": "1.0.0-alpha.28", "deep-equal": "^1.0.1", "ejson": "2.2.0", + "expo-file-system": "^6.0.2", "expo-haptics": "^6.0.0", "expo-web-browser": "^6.0.0", "i18n-js": "^3.3.0", diff --git a/yarn.lock b/yarn.lock index 3c1660f73db..9bab2880138 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4428,7 +4428,7 @@ expo-constants@~6.0.0: dependencies: 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" resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-6.0.2.tgz#e65f30eb6a7213e07933df9688116eaf4e25bbf8" integrity sha512-s+6oQpLhcT7MQp7fcoj1E+zttMr0WX6c0FrddzqB4dUfhIggV+nb35nQMASIiTHAj8VPUanTFliY5rETHRMHRA== From f799db604d9ba37021151df8a16b358c6ee8e820 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Fri, 16 Aug 2019 18:19:14 -0300 Subject: [PATCH 12/13] Review --- app/views/NewServerView.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index 0391935ebb1..254b14b1eae 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -221,25 +221,13 @@ class NewServerView extends React.Component { this.setState({ certificate: null }); } - handleActionPress = (actionIndex) => { - if (actionIndex) { - switch (actionIndex) { - case this.DELETE_INDEX: - this.handleDelete(); - break; - default: - break; - } - } - } - showActionSheet = () => { ActionSheet.showActionSheetWithOptions({ options: this.options, cancelButtonIndex: this.CANCEL_INDEX, destructiveButtonIndex: this.DELETE_INDEX }, (actionIndex) => { - this.handleActionPress(actionIndex); + if (actionIndex === this.DELETE_INDEX) { this.handleDelete(); } }); } @@ -265,8 +253,13 @@ class NewServerView extends React.Component { ); } + onChangePassword = (value) => { + const { certificate } = this.state; + this.setState({ certificate: { ...certificate, password: value } }); + } + renderCertificatePassword = () => { - const { showPasswordAlert, certificate } = this.state; + const { showPasswordAlert } = this.state; return ( @@ -276,7 +269,7 @@ class NewServerView extends React.Component { {I18n.t('Whats_the_password_for_your_certificate')} this.setState({ certificate: { ...certificate, password: value } })} + onChangeText={this.onChangePassword} secureTextEntry testID='certificate-password-input' style={styles.dialogInput} From fe598f12ca7c8dedb76889cda5e3a79966a4bca6 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Mon, 19 Aug 2019 20:30:34 -0300 Subject: [PATCH 13/13] Update code --- app/utils/server.js | 5 ++ app/views/NewServerView.js | 95 ++++++++------------------------------ 2 files changed, 25 insertions(+), 75 deletions(-) diff --git a/app/utils/server.js b/app/utils/server.js index 7ed471dc93c..0dcc105465b 100644 --- a/app/utils/server.js +++ b/app/utils/server.js @@ -1,3 +1,8 @@ +/* + Extract hostname from url + url = 'https://open.rocket.chat/method' + hostname = 'open.rocket.chat' +*/ export const extractHostname = (url) => { let hostname; diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index 254b14b1eae..5d03cb0ed37 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -1,13 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity, View, Platform + Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity, View, Alert, LayoutAnimation } from 'react-native'; import { connect } from 'react-redux'; import { SafeAreaView } from 'react-navigation'; import * as FileSystem from 'expo-file-system'; import DocumentPicker from 'react-native-document-picker'; -import Dialog from 'react-native-dialog'; import ActionSheet from 'react-native-action-sheet'; import isEqual from 'deep-equal'; @@ -63,16 +62,7 @@ const styles = StyleSheet.create({ fontSize: 15, ...sharedStyles.textSemibold, ...sharedStyles.textColorHeaderBack - }, - dialogInput: Platform.select({ - ios: {}, - android: { - borderRadius: 4, - borderColor: 'rgba(0,0,0,.15)', - borderWidth: 2, - paddingHorizontal: 10 - } - }) + } }); const defaultServer = 'https://open.rocket.chat'; @@ -104,7 +94,6 @@ class NewServerView extends React.Component { this.state = { text: server || '', autoFocus: !server, - showPasswordAlert: false, certificate: null }; } @@ -118,15 +107,12 @@ class NewServerView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const { text, showPasswordAlert, certificate } = this.state; + const { text, certificate } = this.state; const { connecting } = this.props; if (nextState.text !== text) { return true; } - if (nextState.showPasswordAlert !== showPasswordAlert) { - return true; - } - if (isEqual(nextState.certificate, certificate)) { + if (!isEqual(nextState.certificate, certificate)) { return true; } if (nextProps.connecting !== connecting) { @@ -169,13 +155,17 @@ class NewServerView extends React.Component { type: ['com.rsa.pkcs-12'] }); const { uri: path, name } = res; - this.setState({ - certificate: { - path, - name - }, - showPasswordAlert: true - }); + 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); @@ -204,22 +194,12 @@ class NewServerView extends React.Component { uriToPath = uri => uri.replace('file://', ''); - saveCertificate = () => { - const { certificate } = this.state; - const { path } = certificate; - this.setState({ showPasswordAlert: false, certificate: { path, ...certificate } }); + saveCertificate = (certificate) => { + LayoutAnimation.easeInEaseOut(); + this.setState({ certificate }); } - handleDelete = async() => { - const { certificate } = this.state; - const { path } = certificate; - try { - await FileSystem.deleteAsync(path); - } catch (error) { - log('err_remove_certificate', error); - } - this.setState({ certificate: null }); - } + handleDelete = () => this.setState({ certificate: null }); // We not need delete file from DocumentPicker because it is a temp file showActionSheet = () => { ActionSheet.showActionSheetWithOptions({ @@ -253,32 +233,6 @@ class NewServerView extends React.Component { ); } - onChangePassword = (value) => { - const { certificate } = this.state; - this.setState({ certificate: { ...certificate, password: value } }); - } - - renderCertificatePassword = () => { - const { showPasswordAlert } = this.state; - return ( - - - {I18n.t('Certificate_password')} - - - {I18n.t('Whats_the_password_for_your_certificate')} - - - - - ); - } - renderCertificatePicker = () => { const { certificate } = this.state; return ( @@ -324,16 +278,7 @@ class NewServerView extends React.Component { loading={connecting} testID='new-server-view-button' /> - { - isIOS - ? ( - <> - {this.renderCertificatePicker()} - {this.renderCertificatePassword()} - - ) - : null - } + { isIOS ? this.renderCertificatePicker() : null } {this.renderBack()}