TunnelManager: convert to kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
b2bbaf050c
commit
37949ba1ec
@ -72,7 +72,7 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
|
|||||||
}
|
}
|
||||||
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
|
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
|
||||||
tunnelManager.onCreate()
|
tunnelManager.onCreate()
|
||||||
asyncWorker.supplyAsync { getBackend() }.thenAccept { futureBackend.complete(it) }
|
asyncWorker.supplyAsync(Companion::getBackend).thenAccept { futureBackend.complete(it) }
|
||||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,8 +24,9 @@ class FileConfigStore(private val context: Context) : ConfigStore {
|
|||||||
override fun create(name: String, config: Config): Config {
|
override fun create(name: String, config: Config): Config {
|
||||||
Log.d(TAG, "Creating configuration for tunnel $name")
|
Log.d(TAG, "Creating configuration for tunnel $name")
|
||||||
val file = fileFor(name)
|
val file = fileFor(name)
|
||||||
if (!file.createNewFile()) throw IOException(context.getString(R.string.config_file_exists_error, file.name))
|
if (!file.createNewFile())
|
||||||
FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
|
throw IOException(context.getString(R.string.config_file_exists_error, file.name))
|
||||||
|
FileOutputStream(file, false).use { it.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +34,8 @@ class FileConfigStore(private val context: Context) : ConfigStore {
|
|||||||
override fun delete(name: String) {
|
override fun delete(name: String) {
|
||||||
Log.d(TAG, "Deleting configuration for tunnel $name")
|
Log.d(TAG, "Deleting configuration for tunnel $name")
|
||||||
val file = fileFor(name)
|
val file = fileFor(name)
|
||||||
if (!file.delete()) throw IOException(context.getString(R.string.config_delete_error, file.name))
|
if (!file.delete())
|
||||||
|
throw IOException(context.getString(R.string.config_delete_error, file.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun enumerate(): Set<String> {
|
override fun enumerate(): Set<String> {
|
||||||
@ -68,7 +70,8 @@ class FileConfigStore(private val context: Context) : ConfigStore {
|
|||||||
override fun save(name: String, config: Config): Config {
|
override fun save(name: String, config: Config): Config {
|
||||||
Log.d(TAG, "Saving configuration for tunnel $name")
|
Log.d(TAG, "Saving configuration for tunnel $name")
|
||||||
val file = fileFor(name)
|
val file = fileFor(name)
|
||||||
if (!file.isFile) throw FileNotFoundException(context.getString(R.string.config_not_found_error, file.name))
|
if (!file.isFile)
|
||||||
|
throw FileNotFoundException(context.getString(R.string.config_not_found_error, file.name))
|
||||||
FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
|
FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
@ -125,13 +125,11 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
|||||||
tunnel == null -> {
|
tunnel == null -> {
|
||||||
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
||||||
val manager = Application.getTunnelManager()
|
val manager = Application.getTunnelManager()
|
||||||
manager.create(binding!!.name!!, newConfig)
|
manager.create(binding!!.name!!, newConfig).whenComplete(this::onTunnelCreated)
|
||||||
.whenComplete(this::onTunnelCreated)
|
|
||||||
}
|
}
|
||||||
tunnel!!.name != binding!!.name -> {
|
tunnel!!.name != binding!!.name -> {
|
||||||
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
||||||
tunnel!!.setNameAsync(binding!!.name!!)
|
tunnel!!.setNameAsync(binding!!.name!!).whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
|
||||||
.whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
||||||
|
@ -77,7 +77,7 @@ class ObservableTunnel internal constructor(
|
|||||||
else
|
else
|
||||||
CompletableFuture.completedFuture(config)
|
CompletableFuture.completedFuture(config)
|
||||||
|
|
||||||
fun setConfigAsync(config: Config): CompletionStage<Config?> = if (config != this.config)
|
fun setConfigAsync(config: Config): CompletionStage<Config> = if (config != this.config)
|
||||||
manager.setTunnelConfig(this, config)
|
manager.setTunnelConfig(this, config)
|
||||||
else
|
else
|
||||||
CompletableFuture.completedFuture(this.config)
|
CompletableFuture.completedFuture(this.config)
|
||||||
@ -93,13 +93,13 @@ class ObservableTunnel internal constructor(
|
|||||||
var statistics: Statistics? = null
|
var statistics: Statistics? = null
|
||||||
get() {
|
get() {
|
||||||
if (field == null || field!!.isStale)
|
if (field == null || field!!.isStale)
|
||||||
TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E)
|
manager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E)
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val statisticsAsync: CompletionStage<Statistics> = if (statistics == null || statistics!!.isStale)
|
val statisticsAsync: CompletionStage<Statistics> = if (statistics == null || statistics!!.isStale)
|
||||||
TunnelManager.getTunnelStatistics(this)
|
manager.getTunnelStatistics(this)
|
||||||
else
|
else
|
||||||
CompletableFuture.completedFuture(statistics)
|
CompletableFuture.completedFuture(statistics)
|
||||||
|
|
||||||
|
@ -1,305 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.wireguard.android.model;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import com.wireguard.android.Application;
|
|
||||||
import com.wireguard.android.BR;
|
|
||||||
import com.wireguard.android.R;
|
|
||||||
import com.wireguard.android.backend.Statistics;
|
|
||||||
import com.wireguard.android.backend.Tunnel;
|
|
||||||
import com.wireguard.android.backend.Tunnel.State;
|
|
||||||
import com.wireguard.android.configStore.ConfigStore;
|
|
||||||
import com.wireguard.android.util.ExceptionLoggers;
|
|
||||||
import com.wireguard.android.util.ObservableSortedKeyedArrayList;
|
|
||||||
import com.wireguard.android.util.ObservableSortedKeyedList;
|
|
||||||
import com.wireguard.config.Config;
|
|
||||||
import com.wireguard.util.NonNullForAll;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.databinding.BaseObservable;
|
|
||||||
import androidx.databinding.Bindable;
|
|
||||||
import java9.util.Comparators;
|
|
||||||
import java9.util.concurrent.CompletableFuture;
|
|
||||||
import java9.util.concurrent.CompletionStage;
|
|
||||||
import java9.util.stream.Collectors;
|
|
||||||
import java9.util.stream.StreamSupport;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
|
||||||
*/
|
|
||||||
|
|
||||||
@NonNullForAll
|
|
||||||
public final class TunnelManager extends BaseObservable {
|
|
||||||
private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing(
|
|
||||||
String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder());
|
|
||||||
private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel";
|
|
||||||
private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
|
|
||||||
private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
|
|
||||||
|
|
||||||
private final CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> completableTunnels = new CompletableFuture<>();
|
|
||||||
private final ConfigStore configStore;
|
|
||||||
private final Context context = Application.get();
|
|
||||||
private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>();
|
|
||||||
private final ObservableSortedKeyedList<String, ObservableTunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
|
|
||||||
private boolean haveLoaded;
|
|
||||||
@Nullable private ObservableTunnel lastUsedTunnel;
|
|
||||||
|
|
||||||
public TunnelManager(final ConfigStore configStore) {
|
|
||||||
this.configStore = configStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
static CompletionStage<State> getTunnelState(final ObservableTunnel tunnel) {
|
|
||||||
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
|
|
||||||
.thenApply(tunnel::onStateChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
static CompletionStage<Statistics> getTunnelStatistics(final ObservableTunnel tunnel) {
|
|
||||||
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
|
|
||||||
.thenApply(tunnel::onStatisticsChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ObservableTunnel addToList(final String name, @Nullable final Config config, final State state) {
|
|
||||||
final ObservableTunnel tunnel = new ObservableTunnel(this, name, config, state);
|
|
||||||
tunnels.add(tunnel);
|
|
||||||
return tunnel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletionStage<ObservableTunnel> create(final String name, @Nullable final Config config) {
|
|
||||||
if (Tunnel.isNameInvalid(name))
|
|
||||||
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
|
|
||||||
if (tunnels.containsKey(name)) {
|
|
||||||
final String message = context.getString(R.string.tunnel_error_already_exists, name);
|
|
||||||
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
|
|
||||||
}
|
|
||||||
return Application.getAsyncWorker().supplyAsync(() -> configStore.create(name, config))
|
|
||||||
.thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN));
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletionStage<Void> delete(final ObservableTunnel tunnel) {
|
|
||||||
final State originalState = tunnel.getState();
|
|
||||||
final boolean wasLastUsed = tunnel == lastUsedTunnel;
|
|
||||||
// Make sure nothing touches the tunnel.
|
|
||||||
if (wasLastUsed)
|
|
||||||
setLastUsedTunnel(null);
|
|
||||||
tunnels.remove(tunnel);
|
|
||||||
return Application.getAsyncWorker().runAsync(() -> {
|
|
||||||
if (originalState == State.UP)
|
|
||||||
Application.getBackend().setState(tunnel, State.DOWN, null);
|
|
||||||
try {
|
|
||||||
configStore.delete(tunnel.getName());
|
|
||||||
} catch (final Exception e) {
|
|
||||||
if (originalState == State.UP)
|
|
||||||
Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig());
|
|
||||||
// Re-throw the exception to fail the completion.
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}).whenComplete((x, e) -> {
|
|
||||||
if (e == null)
|
|
||||||
return;
|
|
||||||
// Failure, put the tunnel back.
|
|
||||||
tunnels.add(tunnel);
|
|
||||||
if (wasLastUsed)
|
|
||||||
setLastUsedTunnel(tunnel);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bindable
|
|
||||||
@Nullable
|
|
||||||
public ObservableTunnel getLastUsedTunnel() {
|
|
||||||
return lastUsedTunnel;
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletionStage<Config> getTunnelConfig(final ObservableTunnel tunnel) {
|
|
||||||
return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName()))
|
|
||||||
.thenApply(tunnel::onConfigChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> getTunnels() {
|
|
||||||
return completableTunnels;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCreate() {
|
|
||||||
Application.getAsyncWorker().supplyAsync(configStore::enumerate)
|
|
||||||
.thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames()), this::onTunnelsLoaded)
|
|
||||||
.whenComplete(ExceptionLoggers.E);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private void onTunnelsLoaded(final Iterable<String> present, final Collection<String> running) {
|
|
||||||
for (final String name : present)
|
|
||||||
addToList(name, null, running.contains(name) ? State.UP : State.DOWN);
|
|
||||||
final String lastUsedName = Application.getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null);
|
|
||||||
if (lastUsedName != null)
|
|
||||||
setLastUsedTunnel(tunnels.get(lastUsedName));
|
|
||||||
final CompletableFuture<Void>[] toComplete;
|
|
||||||
synchronized (delayedLoadRestoreTunnels) {
|
|
||||||
haveLoaded = true;
|
|
||||||
toComplete = delayedLoadRestoreTunnels.toArray(new CompletableFuture[delayedLoadRestoreTunnels.size()]);
|
|
||||||
delayedLoadRestoreTunnels.clear();
|
|
||||||
}
|
|
||||||
restoreState(true).whenComplete((v, t) -> {
|
|
||||||
for (final CompletableFuture<Void> f : toComplete) {
|
|
||||||
if (t == null)
|
|
||||||
f.complete(v);
|
|
||||||
else
|
|
||||||
f.completeExceptionally(t);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
completableTunnels.complete(tunnels);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshTunnelStates() {
|
|
||||||
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames())
|
|
||||||
.thenAccept(running -> {
|
|
||||||
for (final ObservableTunnel tunnel : tunnels)
|
|
||||||
tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN);
|
|
||||||
})
|
|
||||||
.whenComplete(ExceptionLoggers.E);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletionStage<Void> restoreState(final boolean force) {
|
|
||||||
if (!force && !Application.getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
synchronized (delayedLoadRestoreTunnels) {
|
|
||||||
if (!haveLoaded) {
|
|
||||||
final CompletableFuture<Void> f = new CompletableFuture<>();
|
|
||||||
delayedLoadRestoreTunnels.add(f);
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final Set<String> previouslyRunning = Application.getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null);
|
|
||||||
if (previouslyRunning == null)
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
return CompletableFuture.allOf(StreamSupport.stream(tunnels)
|
|
||||||
.filter(tunnel -> previouslyRunning.contains(tunnel.getName()))
|
|
||||||
.map(tunnel -> setTunnelState(tunnel, State.UP))
|
|
||||||
.toArray(CompletableFuture[]::new));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ApplySharedPref")
|
|
||||||
public void saveState() {
|
|
||||||
final Set<String> runningTunnels = StreamSupport.stream(tunnels)
|
|
||||||
.filter(tunnel -> tunnel.getState() == State.UP)
|
|
||||||
.map(ObservableTunnel::getName)
|
|
||||||
.collect(Collectors.toUnmodifiableSet());
|
|
||||||
Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ApplySharedPref")
|
|
||||||
private void setLastUsedTunnel(@Nullable final ObservableTunnel tunnel) {
|
|
||||||
if (tunnel == lastUsedTunnel)
|
|
||||||
return;
|
|
||||||
lastUsedTunnel = tunnel;
|
|
||||||
notifyPropertyChanged(BR.lastUsedTunnel);
|
|
||||||
if (tunnel != null)
|
|
||||||
Application.getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, tunnel.getName()).commit();
|
|
||||||
else
|
|
||||||
Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletionStage<Config> setTunnelConfig(final ObservableTunnel tunnel, final Config config) {
|
|
||||||
return Application.getAsyncWorker().supplyAsync(() -> {
|
|
||||||
Application.getBackend().setState(tunnel, tunnel.getState(), config);
|
|
||||||
return configStore.save(tunnel.getName(), config);
|
|
||||||
}).thenApply(tunnel::onConfigChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletionStage<String> setTunnelName(final ObservableTunnel tunnel, final String name) {
|
|
||||||
if (Tunnel.isNameInvalid(name))
|
|
||||||
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
|
|
||||||
if (tunnels.containsKey(name)) {
|
|
||||||
final String message = context.getString(R.string.tunnel_error_already_exists, name);
|
|
||||||
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
|
|
||||||
}
|
|
||||||
final State originalState = tunnel.getState();
|
|
||||||
final boolean wasLastUsed = tunnel == lastUsedTunnel;
|
|
||||||
// Make sure nothing touches the tunnel.
|
|
||||||
if (wasLastUsed)
|
|
||||||
setLastUsedTunnel(null);
|
|
||||||
tunnels.remove(tunnel);
|
|
||||||
return Application.getAsyncWorker().supplyAsync(() -> {
|
|
||||||
if (originalState == State.UP)
|
|
||||||
Application.getBackend().setState(tunnel, State.DOWN, null);
|
|
||||||
configStore.rename(tunnel.getName(), name);
|
|
||||||
final String newName = tunnel.onNameChanged(name);
|
|
||||||
if (originalState == State.UP)
|
|
||||||
Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig());
|
|
||||||
return newName;
|
|
||||||
}).whenComplete((newName, e) -> {
|
|
||||||
// On failure, we don't know what state the tunnel might be in. Fix that.
|
|
||||||
if (e != null)
|
|
||||||
getTunnelState(tunnel);
|
|
||||||
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
|
||||||
tunnels.add(tunnel);
|
|
||||||
if (wasLastUsed)
|
|
||||||
setLastUsedTunnel(tunnel);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletionStage<State> setTunnelState(final ObservableTunnel tunnel, final State state) {
|
|
||||||
// Ensure the configuration is loaded before trying to use it.
|
|
||||||
return tunnel.getConfigAsync().thenCompose(config ->
|
|
||||||
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state, config))
|
|
||||||
).whenComplete((newState, e) -> {
|
|
||||||
// Ensure onStateChanged is always called (failure or not), and with the correct state.
|
|
||||||
tunnel.onStateChanged(e == null ? newState : tunnel.getState());
|
|
||||||
if (e == null && newState == State.UP)
|
|
||||||
setLastUsedTunnel(tunnel);
|
|
||||||
saveState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class IntentReceiver extends BroadcastReceiver {
|
|
||||||
@Override
|
|
||||||
public void onReceive(final Context context, @Nullable final Intent intent) {
|
|
||||||
final TunnelManager manager = Application.getTunnelManager();
|
|
||||||
if (intent == null)
|
|
||||||
return;
|
|
||||||
final String action = intent.getAction();
|
|
||||||
if (action == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES".equals(action)) {
|
|
||||||
manager.refreshTunnelStates();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
|
|
||||||
!Application.getSharedPreferences().getBoolean("allow_remote_control_intents", false))
|
|
||||||
return;
|
|
||||||
|
|
||||||
final State state;
|
|
||||||
if ("com.wireguard.android.action.SET_TUNNEL_UP".equals(action))
|
|
||||||
state = State.UP;
|
|
||||||
else if ("com.wireguard.android.action.SET_TUNNEL_DOWN".equals(action))
|
|
||||||
state = State.DOWN;
|
|
||||||
else
|
|
||||||
return;
|
|
||||||
|
|
||||||
final String tunnelName = intent.getStringExtra("tunnel");
|
|
||||||
if (tunnelName == null)
|
|
||||||
return;
|
|
||||||
manager.getTunnels().thenAccept(tunnels -> {
|
|
||||||
final ObservableTunnel tunnel = tunnels.get(tunnelName);
|
|
||||||
if (tunnel == null)
|
|
||||||
return;
|
|
||||||
manager.setTunnelState(tunnel, state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
239
ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
Normal file
239
ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.model
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.databinding.BaseObservable
|
||||||
|
import androidx.databinding.Bindable
|
||||||
|
import com.wireguard.android.Application.Companion.get
|
||||||
|
import com.wireguard.android.Application.Companion.getAsyncWorker
|
||||||
|
import com.wireguard.android.Application.Companion.getBackend
|
||||||
|
import com.wireguard.android.Application.Companion.getSharedPreferences
|
||||||
|
import com.wireguard.android.Application.Companion.getTunnelManager
|
||||||
|
import com.wireguard.android.BR
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.configStore.ConfigStore
|
||||||
|
import com.wireguard.android.util.ExceptionLoggers
|
||||||
|
import com.wireguard.android.util.ObservableSortedKeyedArrayList
|
||||||
|
import com.wireguard.android.util.ObservableSortedKeyedList
|
||||||
|
import com.wireguard.config.Config
|
||||||
|
import java9.util.Comparators
|
||||||
|
import java9.util.concurrent.CompletableFuture
|
||||||
|
import java9.util.concurrent.CompletionStage
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
||||||
|
*/
|
||||||
|
class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||||
|
val tunnels = CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>>()
|
||||||
|
private val context: Context = get()
|
||||||
|
private val delayedLoadRestoreTunnels = ArrayList<CompletableFuture<Void>>()
|
||||||
|
private val tunnelMap: ObservableSortedKeyedList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(COMPARATOR)
|
||||||
|
private var haveLoaded = false
|
||||||
|
|
||||||
|
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel? {
|
||||||
|
val tunnel = ObservableTunnel(this, name, config, state)
|
||||||
|
tunnelMap.add(tunnel)
|
||||||
|
return tunnel
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(name: String, config: Config?): CompletionStage<ObservableTunnel> {
|
||||||
|
if (Tunnel.isNameInvalid(name))
|
||||||
|
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
|
||||||
|
if (tunnelMap.containsKey(name))
|
||||||
|
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
|
||||||
|
return getAsyncWorker().supplyAsync { configStore.create(name, config!!) }.thenApply { addToList(name, it, Tunnel.State.DOWN) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(tunnel: ObservableTunnel): CompletionStage<Void> {
|
||||||
|
val originalState = tunnel.state
|
||||||
|
val wasLastUsed = tunnel == lastUsedTunnel
|
||||||
|
// Make sure nothing touches the tunnel.
|
||||||
|
if (wasLastUsed)
|
||||||
|
lastUsedTunnel = null
|
||||||
|
tunnelMap.remove(tunnel)
|
||||||
|
return getAsyncWorker().runAsync {
|
||||||
|
if (originalState == Tunnel.State.UP)
|
||||||
|
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
|
||||||
|
try {
|
||||||
|
configStore.delete(tunnel.name)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (originalState == Tunnel.State.UP)
|
||||||
|
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}.whenComplete { _, e ->
|
||||||
|
if (e == null)
|
||||||
|
return@whenComplete
|
||||||
|
// Failure, put the tunnel back.
|
||||||
|
tunnelMap.add(tunnel)
|
||||||
|
if (wasLastUsed)
|
||||||
|
lastUsedTunnel = tunnel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Bindable
|
||||||
|
@SuppressLint("ApplySharedPref")
|
||||||
|
var lastUsedTunnel: ObservableTunnel? = null
|
||||||
|
private set(value) {
|
||||||
|
if (value == field) return
|
||||||
|
field = value
|
||||||
|
notifyPropertyChanged(BR.lastUsedTunnel)
|
||||||
|
if (value != null)
|
||||||
|
getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, value.name).commit()
|
||||||
|
else
|
||||||
|
getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTunnelConfig(tunnel: ObservableTunnel): CompletionStage<Config> = getAsyncWorker()
|
||||||
|
.supplyAsync { configStore.load(tunnel.name) }.thenApply(tunnel::onConfigChanged)
|
||||||
|
|
||||||
|
|
||||||
|
fun onCreate() {
|
||||||
|
getAsyncWorker().supplyAsync { configStore.enumerate() }
|
||||||
|
.thenAcceptBoth(getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }, this::onTunnelsLoaded)
|
||||||
|
.whenComplete(ExceptionLoggers.E)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
|
||||||
|
for (name in present)
|
||||||
|
addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN)
|
||||||
|
val lastUsedName = getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null)
|
||||||
|
if (lastUsedName != null)
|
||||||
|
lastUsedTunnel = tunnelMap[lastUsedName]
|
||||||
|
var toComplete: Array<CompletableFuture<Void>>
|
||||||
|
synchronized(delayedLoadRestoreTunnels) {
|
||||||
|
haveLoaded = true
|
||||||
|
toComplete = delayedLoadRestoreTunnels.toTypedArray()
|
||||||
|
delayedLoadRestoreTunnels.clear()
|
||||||
|
}
|
||||||
|
restoreState(true).whenComplete { v: Void?, t: Throwable? ->
|
||||||
|
for (f in toComplete) {
|
||||||
|
if (t == null)
|
||||||
|
f.complete(v)
|
||||||
|
else
|
||||||
|
f.completeExceptionally(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tunnels.complete(tunnelMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshTunnelStates() {
|
||||||
|
getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }
|
||||||
|
.thenAccept { running: Set<String> -> for (tunnel in tunnelMap) tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN) }
|
||||||
|
.whenComplete(ExceptionLoggers.E)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreState(force: Boolean): CompletionStage<Void> {
|
||||||
|
if (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
|
||||||
|
return CompletableFuture.completedFuture(null)
|
||||||
|
synchronized(delayedLoadRestoreTunnels) {
|
||||||
|
if (!haveLoaded) {
|
||||||
|
val f = CompletableFuture<Void>()
|
||||||
|
delayedLoadRestoreTunnels.add(f)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val previouslyRunning = getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null)
|
||||||
|
?: return CompletableFuture.completedFuture(null)
|
||||||
|
return CompletableFuture.allOf(*tunnelMap.filter { previouslyRunning.contains(it.name) }.map { setTunnelState(it, Tunnel.State.UP).toCompletableFuture() }.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ApplySharedPref")
|
||||||
|
fun saveState() {
|
||||||
|
getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet()).commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): CompletionStage<Config> = getAsyncWorker().supplyAsync {
|
||||||
|
getBackend().setState(tunnel, tunnel.state, config)
|
||||||
|
configStore.save(tunnel.name, config)
|
||||||
|
}.thenApply { tunnel.onConfigChanged(it) }
|
||||||
|
|
||||||
|
fun setTunnelName(tunnel: ObservableTunnel, name: String): CompletionStage<String> {
|
||||||
|
if (Tunnel.isNameInvalid(name))
|
||||||
|
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
|
||||||
|
if (tunnelMap.containsKey(name)) {
|
||||||
|
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
|
||||||
|
}
|
||||||
|
val originalState = tunnel.state
|
||||||
|
val wasLastUsed = tunnel == lastUsedTunnel
|
||||||
|
// Make sure nothing touches the tunnel.
|
||||||
|
if (wasLastUsed)
|
||||||
|
lastUsedTunnel = null
|
||||||
|
tunnelMap.remove(tunnel)
|
||||||
|
return getAsyncWorker().supplyAsync {
|
||||||
|
if (originalState == Tunnel.State.UP)
|
||||||
|
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
|
||||||
|
configStore.rename(tunnel.name, name)
|
||||||
|
val newName = tunnel.onNameChanged(name)
|
||||||
|
if (originalState == Tunnel.State.UP)
|
||||||
|
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
|
||||||
|
newName
|
||||||
|
}.whenComplete { _, e ->
|
||||||
|
// On failure, we don't know what state the tunnel might be in. Fix that.
|
||||||
|
if (e != null)
|
||||||
|
getTunnelState(tunnel)
|
||||||
|
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
||||||
|
tunnelMap.add(tunnel)
|
||||||
|
if (wasLastUsed)
|
||||||
|
lastUsedTunnel = tunnel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): CompletionStage<Tunnel.State> = tunnel.configAsync
|
||||||
|
.thenCompose { getAsyncWorker().supplyAsync { getBackend().setState(tunnel, state, it) } }
|
||||||
|
.whenComplete { newState, e ->
|
||||||
|
// Ensure onStateChanged is always called (failure or not), and with the correct state.
|
||||||
|
tunnel.onStateChanged(if (e == null) newState else tunnel.state)
|
||||||
|
if (e == null && newState == Tunnel.State.UP)
|
||||||
|
lastUsedTunnel = tunnel
|
||||||
|
saveState()
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntentReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
val manager = getTunnelManager()
|
||||||
|
if (intent == null) return
|
||||||
|
val action = intent.action ?: return
|
||||||
|
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
|
||||||
|
manager.refreshTunnelStates()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !getSharedPreferences().getBoolean("allow_remote_control_intents", false))
|
||||||
|
return
|
||||||
|
val state: Tunnel.State
|
||||||
|
state = when (action) {
|
||||||
|
"com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
|
||||||
|
"com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
val tunnelName = intent.getStringExtra("tunnel") ?: return
|
||||||
|
manager.tunnels.thenAccept {
|
||||||
|
val tunnel = it[tunnelName] ?: return@thenAccept
|
||||||
|
manager.setTunnelState(tunnel, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTunnelState(tunnel: ObservableTunnel): CompletionStage<Tunnel.State> = getAsyncWorker()
|
||||||
|
.supplyAsync { getBackend().getState(tunnel) }.thenApply(tunnel::onStateChanged)
|
||||||
|
|
||||||
|
fun getTunnelStatistics(tunnel: ObservableTunnel): CompletionStage<Statistics> = getAsyncWorker()
|
||||||
|
.supplyAsync { getBackend().getStatistics(tunnel) }.thenApply(tunnel::onStatisticsChanged)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val COMPARATOR = Comparators.thenComparing(java.lang.String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder())
|
||||||
|
private const val KEY_LAST_USED_TUNNEL = "last_used_tunnel"
|
||||||
|
private const val KEY_RESTORE_ON_BOOT = "restore_on_boot"
|
||||||
|
private const val KEY_RUNNING_TUNNELS = "enabled_configs"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user