You've already forked joplin
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:
@@ -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
3
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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_ = () => {
|
||||
|
||||
9
packages/app-mobile/package-lock.json
generated
9
packages/app-mobile/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
9
packages/app-mobile/utils/TlsUtils.ts
Normal file
9
packages/app-mobile/utils/TlsUtils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user