1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-26 22:41:17 +02:00

Android: Fixes #4439: Add option to disable TLS validation and allow self-signed certificates for WebDAV/NextCloud (#4742)

This commit is contained in:
Roman Musin
2021-04-25 09:50:52 +01:00
committed by GitHub
parent 3235f58f5a
commit a4854fcde8
17 changed files with 186 additions and 9 deletions

View File

@@ -754,6 +754,9 @@ packages/app-mobile/utils/ShareExtension.js.map
packages/app-mobile/utils/ShareUtils.d.ts
packages/app-mobile/utils/ShareUtils.js
packages/app-mobile/utils/ShareUtils.js.map
packages/app-mobile/utils/TlsUtils.d.ts
packages/app-mobile/utils/TlsUtils.js
packages/app-mobile/utils/TlsUtils.js.map
packages/app-mobile/utils/checkPermissions.d.ts
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/checkPermissions.js.map

3
.gitignore vendored
View File

@@ -741,6 +741,9 @@ packages/app-mobile/utils/ShareExtension.js.map
packages/app-mobile/utils/ShareUtils.d.ts
packages/app-mobile/utils/ShareUtils.js
packages/app-mobile/utils/ShareUtils.js.map
packages/app-mobile/utils/TlsUtils.d.ts
packages/app-mobile/utils/TlsUtils.js
packages/app-mobile/utils/TlsUtils.js.map
packages/app-mobile/utils/checkPermissions.d.ts
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/checkPermissions.js.map

View File

@@ -3,18 +3,23 @@ package net.cozic.joplin;
import android.app.Application;
import android.content.Context;
import android.database.CursorWindow;
import android.webkit.WebView;
import androidx.multidex.MultiDex;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
import net.cozic.joplin.share.SharePackage;
import net.cozic.joplin.ssl.SslPackage;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import net.cozic.joplin.share.SharePackage;
import android.webkit.WebView;
public class MainApplication extends Application implements ReactApplication {
@@ -38,6 +43,7 @@ public class MainApplication extends Application implements ReactApplication {
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
packages.add(new SharePackage());
packages.add(new SslPackage());
return packages;
}

View File

@@ -0,0 +1,47 @@
package net.cozic.joplin.ssl;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.modules.network.NetworkingModule;
import java.util.concurrent.atomic.AtomicBoolean;
import static net.cozic.joplin.ssl.SslUtils.TRUST_ALL_CERTS;
import static net.cozic.joplin.ssl.SslUtils.getTrustySocketFactory;
public class SslModule extends ReactContextBaseJavaModule {
private final AtomicBoolean current = new AtomicBoolean(false);
@NonNull
@Override
public String getName() {
return "SslModule";
}
@ReactMethod
public void setIgnoreTlsErrors(boolean isIgnoreTlsErrors, Promise promise) {
Log.d("JOPLIN", "Set ignore TLS errors: " + isIgnoreTlsErrors);
try {
boolean prev = current.getAndSet(isIgnoreTlsErrors);
if (isIgnoreTlsErrors) {
NetworkingModule.setCustomClientBuilder(
builder -> {
builder.sslSocketFactory(getTrustySocketFactory(), TRUST_ALL_CERTS);
builder.hostnameVerifier((hostname, session) -> true);
});
} else {
NetworkingModule.setCustomClientBuilder(null);
}
promise.resolve(prev);
} catch (Exception e) {
Log.e("JOPLIN", "Error disabling TLS validation", e);
promise.reject(e);
}
}
}

View File

@@ -0,0 +1,25 @@
package net.cozic.joplin.ssl;
import androidx.annotation.NonNull;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Collections;
import java.util.List;
public class SslPackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
return Collections.singletonList(new SslModule());
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,38 @@
package net.cozic.joplin.ssl;
import android.annotation.SuppressLint;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class SslUtils {
@SuppressLint("TrustAllX509TrustManager")
static final X509TrustManager TRUST_ALL_CERTS = new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[]{};
}
};
static SSLSocketFactory getTrustySocketFactory() {
try {
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new TrustManager[]{TRUST_ALL_CERTS}, new java.security.SecureRandom());
return sslContext.getSocketFactory();
} catch (Exception e) {
throw new RuntimeException("Could not create socket factory", e);
}
}
}

View File

@@ -19,6 +19,7 @@ const shim = require('@joplin/lib/shim').default;
const SearchEngine = require('@joplin/lib/services/searchengine/SearchEngine').default;
const RNFS = require('react-native-fs');
const checkPermissions = require('../../utils/checkPermissions.js').default;
import setIgnoreTlsErrors from '../../utils/TlsUtils';
class ConfigScreenComponent extends BaseScreenComponent {
static navigationOptions() {
@@ -38,7 +39,13 @@ class ConfigScreenComponent extends BaseScreenComponent {
shared.init(this);
this.checkSyncConfig_ = async () => {
await shared.checkSyncConfig(this, this.state.settings);
// to ignore TLS erros we need to chage the global state of the app, if the check fails we need to restore the original state
// this call sets the new value and returns the previous one which we can use later to revert the change
const prevIgnoreTlsErrors = await setIgnoreTlsErrors(this.state.settings['net.ignoreTlsErrors']);
const result = await shared.checkSyncConfig(this, this.state.settings);
if (!result || !result.ok) {
await setIgnoreTlsErrors(prevIgnoreTlsErrors);
}
};
this.e2eeConfig_ = () => {
@@ -50,7 +57,15 @@ class ConfigScreenComponent extends BaseScreenComponent {
Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.'));
// Save settings anyway, even if permission has not been granted
}
return shared.saveSettings(this);
// changedSettingKeys is cleared in shared.saveSettings so reading it now
const setIgnoreTlsErrors = this.state.changedSettingKeys.includes('net.ignoreTlsErrors');
await shared.saveSettings(this);
if (setIgnoreTlsErrors) {
await setIgnoreTlsErrors(Setting.value('net.ignoreTlsErrors'));
}
};
this.syncStatusButtonPress_ = () => {

View File

@@ -1616,6 +1616,15 @@
"csstype": "^3.0.2"
}
},
"@types/react-native": {
"version": "0.64.4",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.64.4.tgz",
"integrity": "sha512-VqnlmadGkD5usREvnuyVpWDS1W8f6cCz6MP5fZdgONsaZ9/Ijfb9Iq9MZ5O3bnW1OyJixDX9HtSp3COsFSLD8Q==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/yargs": {
"version": "15.0.8",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.8.tgz",

View File

@@ -65,6 +65,7 @@
"@joplin/tools": "^1.0.9",
"@types/node": "^14.14.6",
"@types/react": "^16.9.55",
"@types/react-native": "^0.64.4",
"execa": "^4.0.0",
"fs-extra": "^8.1.0",
"gulp": "^4.0.2",

View File

@@ -27,7 +27,7 @@ import { setLocale, closestSupportedLocale, defaultLocale } from '@joplin/lib/lo
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar, Linking } = require('react-native');
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar, Linking, Platform } = require('react-native');
import NetInfo from '@react-native-community/netinfo';
const DropdownAlert = require('react-native-dropdownalert').default;
@@ -96,6 +96,7 @@ import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import MigrationService from '@joplin/lib/services/MigrationService';
import { clearSharedFilesCache } from './utils/ShareUtils';
import setIgnoreTlsErrors from './utils/TlsUtils';
let storeDispatch = function(_action: any) {};
@@ -509,6 +510,13 @@ async function initialize(dispatch: Function) {
setLocale(Setting.value('locale'));
if (Platform.OS === 'android') {
const ignoreTlsErrors = Setting.value('net.ignoreTlsErrors');
if (ignoreTlsErrors) {
await setIgnoreTlsErrors(ignoreTlsErrors);
}
}
// ----------------------------------------------------------------
// E2EE SETUP
// ----------------------------------------------------------------

View File

@@ -0,0 +1,9 @@
import { Platform, NativeModules } from 'react-native';
export default async function setIgnoreTlsErrors(ignore: boolean): Promise<boolean> {
if (Platform.OS === 'android') {
return await NativeModules.SslModule.setIgnoreTlsErrors(ignore);
} else {
return false;
}
}

View File

@@ -88,6 +88,7 @@ function shimInit() {
const doFetchBlob = () => {
return RNFetchBlob.config({
path: localFilePath,
trusty: options.ignoreTlsErrors,
}).fetch(method, url, headers);
};
@@ -123,7 +124,9 @@ function shimInit() {
const method = options.method ? options.method : 'POST';
try {
const response = await RNFetchBlob.fetch(method, url, headers, RNFetchBlob.wrap(options.path));
const response = await RNFetchBlob.config({
trusty: options.ignoreTlsErrors,
}).fetch(method, url, headers, RNFetchBlob.wrap(options.path));
// Returns an object that's roughtly compatible with a standard Response object
return {

View File

@@ -38,6 +38,7 @@ class SyncTargetNextcloud extends BaseSyncTarget {
path: () => Setting.value('sync.5.path'),
username: () => Setting.value('sync.5.username'),
password: () => Setting.value('sync.5.password'),
ignoreTlsErrors: () => Setting.value('net.ignoreTlsErrors'),
});
fileApi.setLogger(this.logger());

View File

@@ -32,6 +32,7 @@ class SyncTargetWebDAV extends BaseSyncTarget {
baseUrl: () => options.path(),
username: () => options.username(),
password: () => options.password(),
ignoreTlsErrors: () => options.ignoreTlsErrors(),
};
const api = new WebDavApi(apiOptions);
@@ -67,6 +68,7 @@ class SyncTargetWebDAV extends BaseSyncTarget {
path: () => Setting.value('sync.6.path'),
username: () => Setting.value('sync.6.username'),
password: () => Setting.value('sync.6.password'),
ignoreTlsErrors: () => Setting.value('net.ignoreTlsErrors'),
});
fileApi.setLogger(this.logger());

View File

@@ -369,6 +369,7 @@ class WebDavApi {
fetchOptions.method = method;
if (options.path) fetchOptions.path = options.path;
if (body) fetchOptions.body = body;
fetchOptions.ignoreTlsErrors = this.options_.ignoreTlsErrors();
const url = `${this.baseUrl()}/${path}`;
if (shim.httpAgent(url)) fetchOptions.agent = shim.httpAgent(url);

View File

@@ -25,7 +25,11 @@ shared.advancedSettingsButton_click = (comp) => {
shared.checkSyncConfig = async function(comp, settings) {
const syncTargetId = settings['sync.target'];
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId);
const options = Setting.subValues(`sync.${syncTargetId}`, settings);
const options = Object.assign({},
Setting.subValues(`sync.${syncTargetId}`, settings),
Setting.subValues('net', settings));
comp.setState({ checkSyncConfigResult: 'checking' });
const result = await SyncTargetClass.checkConfig(ObjectUtils.convertValuesToFunctions(options));
comp.setState({ checkSyncConfigResult: result });
@@ -35,6 +39,7 @@ shared.checkSyncConfig = async function(comp, settings) {
// Users often expect config to be auto-saved at this point, if the config check was successful
shared.saveSettings(comp);
}
return result;
};
shared.checkSyncConfigMessages = function(comp) {

View File

@@ -1025,10 +1025,11 @@ class Setting extends BaseModel {
advanced: true,
section: 'sync',
show: (settings: any) => {
return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav'), SyncTargetRegistry.nameToId('joplinServer')].indexOf(settings['sync.target']) >= 0;
return (shim.isNode() || shim.mobilePlatform() === 'android') &&
[SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav'), SyncTargetRegistry.nameToId('joplinServer')].indexOf(settings['sync.target']) >= 0;
},
public: true,
appTypes: ['desktop', 'cli'],
appTypes: ['desktop', 'cli', 'mobile'],
label: () => _('Ignore TLS certificate errors'),
storage: SettingStorage.File,
},