diff options
author | Samuel Holland <samuel@sholland.org> | 2018-09-05 20:17:14 -0500 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2018-12-08 02:39:41 +0100 |
commit | d1e85633fbe8d871355d2b9feb51e2c9983d8a21 (patch) | |
tree | d95ad1ae84d02fc3e18a211aa1e1ef8150d8fa35 /app/src/main/java/com/wireguard/android | |
parent | a264f7ab36bf1335999d53cb4a0d753c54b231d0 (diff) |
Remodel the Model
- The configuration and crypto model is now entirely independent
of Android classes other than Nullable and TextUtils.
- Model classes are immutable and use builders that enforce the
appropriate optional/required attributes.
- The Android config proxies (for Parcelable and databinding) are
moved to the Android side of the codebase, and are designed to be
safe for two-way databinding. This allows proper observability in
TunnelDetailFragment.
- Various robustness fixes and documentation updates to helper classes.
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'app/src/main/java/com/wireguard/android')
24 files changed, 830 insertions, 175 deletions
diff --git a/app/src/main/java/com/wireguard/android/backend/GoBackend.java b/app/src/main/java/com/wireguard/android/backend/GoBackend.java index 295df9d0..97cf0f8e 100644 --- a/app/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/app/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -22,13 +22,10 @@ import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.SharedLibraryLoader; import com.wireguard.config.Config; import com.wireguard.config.InetNetwork; -import com.wireguard.config.Interface; import com.wireguard.config.Peer; -import com.wireguard.crypto.KeyEncoding; import java.net.InetAddress; import java.util.Collections; -import java.util.Formatter; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -146,29 +143,7 @@ public final class GoBackend implements Backend { } // Build config - final Interface iface = config.getInterface(); - final String goConfig; - try (final Formatter fmt = new Formatter(new StringBuilder())) { - fmt.format("replace_peers=true\n"); - if (iface.getPrivateKey() != null) - fmt.format("private_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(iface.getPrivateKey()))); - if (iface.getListenPort() != 0) - fmt.format("listen_port=%d\n", config.getInterface().getListenPort()); - for (final Peer peer : config.getPeers()) { - if (peer.getPublicKey() != null) - fmt.format("public_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPublicKey()))); - if (peer.getPreSharedKey() != null) - fmt.format("preshared_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPreSharedKey()))); - if (peer.getEndpoint() != null) - fmt.format("endpoint=%s\n", peer.getResolvedEndpointString()); - if (peer.getPersistentKeepalive() != 0) - fmt.format("persistent_keepalive_interval=%d\n", peer.getPersistentKeepalive()); - for (final InetNetwork addr : peer.getAllowedIPs()) { - fmt.format("allowed_ip=%s\n", addr.toString()); - } - } - goConfig = fmt.toString(); - } + final String goConfig = config.toWgUserspaceString(); // Create the vpn tunnel with android API final VpnService.Builder builder = service.getBuilder(); @@ -184,18 +159,15 @@ public final class GoBackend implements Backend { for (final InetNetwork addr : config.getInterface().getAddresses()) builder.addAddress(addr.getAddress(), addr.getMask()); - for (final InetAddress addr : config.getInterface().getDnses()) + for (final InetAddress addr : config.getInterface().getDnsServers()) builder.addDnsServer(addr.getHostAddress()); for (final Peer peer : config.getPeers()) { - for (final InetNetwork addr : peer.getAllowedIPs()) + for (final InetNetwork addr : peer.getAllowedIps()) builder.addRoute(addr.getAddress(), addr.getMask()); } - int mtu = config.getInterface().getMtu(); - if (mtu == 0) - mtu = 1280; - builder.setMtu(mtu); + builder.setMtu(config.getInterface().getMtu().orElse(1280)); builder.setBlocking(true); try (final ParcelFileDescriptor tun = builder.establish()) { diff --git a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java index bfc363a4..68799057 100644 --- a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java +++ b/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java @@ -114,7 +114,7 @@ public final class WgQuickBackend implements Backend { final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf"); try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) { - stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); + stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8)); } String command = String.format("wg-quick %s '%s'", state.toString().toLowerCase(), tempFile.getAbsolutePath()); diff --git a/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java b/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java index 0e66dab8..654cb48f 100644 --- a/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java +++ b/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java @@ -9,6 +9,7 @@ import android.content.Context; import android.util.Log; import com.wireguard.config.Config; +import com.wireguard.config.ParseException; import java.io.File; import java.io.FileInputStream; @@ -41,7 +42,7 @@ public final class FileConfigStore implements ConfigStore { if (!file.createNewFile()) throw new IOException("Configuration file " + file.getName() + " already exists"); try (final FileOutputStream stream = new FileOutputStream(file, false)) { - stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); + stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8)); } return config; } @@ -67,9 +68,9 @@ public final class FileConfigStore implements ConfigStore { } @Override - public Config load(final String name) throws IOException { + public Config load(final String name) throws IOException, ParseException { try (final FileInputStream stream = new FileInputStream(fileFor(name))) { - return Config.from(stream); + return Config.parse(stream); } } @@ -94,7 +95,7 @@ public final class FileConfigStore implements ConfigStore { if (!file.isFile()) throw new FileNotFoundException("Configuration file " + file.getName() + " not found"); try (final FileOutputStream stream = new FileOutputStream(file, false)) { - stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); + stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8)); } return config; } diff --git a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java b/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java index 629f99e5..fe01bf10 100644 --- a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java +++ b/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java @@ -6,21 +6,30 @@ package com.wireguard.android.databinding; import android.databinding.BindingAdapter; +import android.databinding.DataBindingUtil; import android.databinding.ObservableList; +import android.databinding.ViewDataBinding; import android.databinding.adapters.ListenerUtil; +import android.support.annotation.Nullable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.InputFilter; +import android.view.LayoutInflater; import android.widget.LinearLayout; import android.widget.TextView; +import com.wireguard.android.BR; import com.wireguard.android.R; import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler; import com.wireguard.android.util.ObservableKeyedList; import com.wireguard.android.widget.ToggleSwitch; import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener; +import com.wireguard.config.Attribute; +import com.wireguard.config.InetNetwork; import com.wireguard.util.Keyed; +import java9.util.Optional; + /** * Static methods for use by generated code in the Android data binding library. */ @@ -42,9 +51,10 @@ public final class BindingAdapters { } @BindingAdapter({"items", "layout"}) - public static <E> void setItems(final LinearLayout view, - final ObservableList<E> oldList, final int oldLayoutId, - final ObservableList<E> newList, final int newLayoutId) { + public static <E> + void setItems(final LinearLayout view, + @Nullable final ObservableList<E> oldList, final int oldLayoutId, + @Nullable final ObservableList<E> newList, final int newLayoutId) { if (oldList == newList && oldLayoutId == newLayoutId) return; ItemChangeListener<E> listener = ListenerUtil.getListener(view, R.id.item_change_listener); @@ -66,11 +76,34 @@ public final class BindingAdapters { listener.setList(newList); } + @BindingAdapter({"items", "layout"}) + public static <E> + void setItems(final LinearLayout view, + @Nullable final Iterable<E> oldList, final int oldLayoutId, + @Nullable final Iterable<E> newList, final int newLayoutId) { + if (oldList == newList && oldLayoutId == newLayoutId) + return; + view.removeAllViews(); + if (newList == null) + return; + final LayoutInflater layoutInflater = LayoutInflater.from(view.getContext()); + for (final E item : newList) { + final ViewDataBinding binding = + DataBindingUtil.inflate(layoutInflater, newLayoutId, view, false); + binding.setVariable(BR.collection, newList); + binding.setVariable(BR.item, item); + binding.executePendingBindings(); + view.addView(binding.getRoot()); + } + } + @BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"}) public static <K, E extends Keyed<? extends K>> void setItems(final RecyclerView view, - final ObservableKeyedList<K, E> oldList, final int oldLayoutId, final RowConfigurationHandler oldRowConfigurationHandler, - final ObservableKeyedList<K, E> newList, final int newLayoutId, final RowConfigurationHandler newRowConfigurationHandler) { + @Nullable final ObservableKeyedList<K, E> oldList, final int oldLayoutId, + final RowConfigurationHandler oldRowConfigurationHandler, + @Nullable final ObservableKeyedList<K, E> newList, final int newLayoutId, + final RowConfigurationHandler newRowConfigurationHandler) { if (view.getLayoutManager() == null) view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false)); @@ -103,4 +136,13 @@ public final class BindingAdapters { view.setOnBeforeCheckedChangeListener(listener); } + @BindingAdapter("android:text") + public static void setText(final TextView view, final Optional<?> text) { + view.setText(text.map(Object::toString).orElse("")); + } + + @BindingAdapter("android:text") + public static void setText(final TextView view, @Nullable final Iterable<InetNetwork> networks) { + view.setText(networks != null ? Attribute.join(networks) : ""); + } } diff --git a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java index 26e7687f..5bfa64f1 100644 --- a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java +++ b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java @@ -73,7 +73,7 @@ public class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> holder.binding.executePendingBindings(); if (rowConfigurationHandler != null) { - E item = getItem(position); + final E item = getItem(position); if (item != null) { rowConfigurationHandler.onConfigureRow(holder.binding, item, position); } diff --git a/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java b/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java index 8bf5a22d..20633c3e 100644 --- a/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java @@ -27,19 +27,21 @@ import com.wireguard.android.util.ObservableKeyedArrayList; import com.wireguard.android.util.ObservableKeyedList; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; +import java9.util.Comparators; + public class AppListDialogFragment extends DialogFragment { private static final String KEY_EXCLUDED_APPS = "excludedApps"; private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>(); - private List<String> currentlyExcludedApps; + @Nullable private List<String> currentlyExcludedApps; - public static <T extends Fragment & AppExclusionListener> AppListDialogFragment newInstance(final String[] excludedApps, final T target) { + public static <T extends Fragment & AppExclusionListener> + AppListDialogFragment newInstance(final ArrayList<String> excludedApps, final T target) { final Bundle extras = new Bundle(); - extras.putStringArray(KEY_EXCLUDED_APPS, excludedApps); + extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps); final AppListDialogFragment fragment = new AppListDialogFragment(); fragment.setTargetFragment(target, 0); fragment.setArguments(extras); @@ -64,7 +66,7 @@ public class AppListDialogFragment extends DialogFragment { appData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName))); } - Collections.sort(appData, (lhs, rhs) -> lhs.getName().toLowerCase().compareTo(rhs.getName().toLowerCase())); + Collections.sort(appData, Comparators.comparing(ApplicationData::getName, String.CASE_INSENSITIVE_ORDER)); return appData; }).whenComplete(((data, throwable) -> { if (data != null) { @@ -82,12 +84,11 @@ public class AppListDialogFragment extends DialogFragment { @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - currentlyExcludedApps = Arrays.asList(getArguments().getStringArray(KEY_EXCLUDED_APPS)); + currentlyExcludedApps = getArguments().getStringArrayList(KEY_EXCLUDED_APPS); } @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { + public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); alertDialogBuilder.setTitle(R.string.excluded_applications); diff --git a/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java b/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java index 83799818..0931868e 100644 --- a/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java @@ -19,8 +19,11 @@ import com.wireguard.android.Application; import com.wireguard.android.R; import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding; import com.wireguard.config.Config; +import com.wireguard.config.ParseException; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Objects; public class ConfigNamingDialogFragment extends DialogFragment { @@ -63,8 +66,8 @@ public class ConfigNamingDialogFragment extends DialogFragment { super.onCreate(savedInstanceState); try { - config = Config.from(getArguments().getString(KEY_CONFIG_TEXT)); - } catch (final IOException exception) { + config = Config.parse(new ByteArrayInputStream(getArguments().getString(KEY_CONFIG_TEXT).getBytes(StandardCharsets.UTF_8))); + } catch (final IOException | ParseException exception) { throw new RuntimeException("Invalid config passed to " + getClass().getSimpleName(), exception); } } diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java index fcc601f3..b4e7202f 100644 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java @@ -16,7 +16,6 @@ import android.view.ViewGroup; import com.wireguard.android.R; import com.wireguard.android.databinding.TunnelDetailFragmentBinding; import com.wireguard.android.model.Tunnel; -import com.wireguard.config.Config; /** * Fragment that shows details about a specific tunnel. @@ -25,12 +24,6 @@ import com.wireguard.config.Config; public class TunnelDetailFragment extends BaseFragment { @Nullable private TunnelDetailFragmentBinding binding; - private void onConfigLoaded(final String name, final Config config) { - if (binding != null) { - binding.setConfig(new Config.Observable(config, name)); - } - } - @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -65,7 +58,7 @@ public class TunnelDetailFragment extends BaseFragment { if (newTunnel == null) binding.setConfig(null); else - newTunnel.getConfigAsync().thenAccept(a -> onConfigLoaded(newTunnel.getName(), a)); + newTunnel.getConfigAsync().thenAccept(binding::setConfig); } @Override diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java index 8f319e1e..f1250e64 100644 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java @@ -7,7 +7,6 @@ package com.wireguard.android.fragment; import android.app.Activity; import android.content.Context; -import android.databinding.Observable; import android.databinding.ObservableList; import android.os.Bundle; import android.support.annotation.Nullable; @@ -24,19 +23,16 @@ import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import com.wireguard.android.Application; -import com.wireguard.android.BR; import com.wireguard.android.R; import com.wireguard.android.databinding.TunnelEditorFragmentBinding; import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener; import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.TunnelManager; import com.wireguard.android.util.ExceptionLoggers; -import com.wireguard.config.Attribute; +import com.wireguard.android.viewmodel.ConfigProxy; import com.wireguard.config.Config; -import com.wireguard.config.Peer; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Objects; @@ -48,64 +44,13 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi private static final String KEY_LOCAL_CONFIG = "local_config"; private static final String KEY_ORIGINAL_NAME = "original_name"; private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName(); - private final Collection<Object> breakObjectOrientedLayeringHandlerReceivers = new ArrayList<>(); - @Nullable private TunnelEditorFragmentBinding binding; - private final Observable.OnPropertyChangedCallback breakObjectOrientedLayeringHandler = new Observable.OnPropertyChangedCallback() { - @Override - public void onPropertyChanged(final Observable sender, final int propertyId) { - if (binding == null) - return; - final Config.Observable config = binding.getConfig(); - if (config == null) - return; - if (propertyId == BR.config) { - config.addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler); - breakObjectOrientedLayeringHandlerReceivers.add(config); - config.getInterfaceSection().addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler); - breakObjectOrientedLayeringHandlerReceivers.add(config.getInterfaceSection()); - config.getPeers().addOnListChangedCallback(breakObjectListOrientedLayeringHandler); - breakObjectOrientedLayeringHandlerReceivers.add(config.getPeers()); - } else if (propertyId == BR.dnses || propertyId == BR.peers) - ; - else - return; - final int numSiblings = config.getPeers().size() - 1; - for (final Peer.Observable peer : config.getPeers()) { - peer.setInterfaceDNSRoutes(config.getInterfaceSection().getDnses()); - peer.setNumSiblings(numSiblings); - } - } - }; - private final ObservableList.OnListChangedCallback<? extends ObservableList<Peer.Observable>> breakObjectListOrientedLayeringHandler = new ObservableList.OnListChangedCallback<ObservableList<Peer.Observable>>() { - @Override - public void onChanged(final ObservableList<Peer.Observable> sender) { - } - - @Override - public void onItemRangeChanged(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) { - } - - @Override - public void onItemRangeInserted(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) { - if (binding != null) - breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers); - } - @Override - public void onItemRangeMoved(final ObservableList<Peer.Observable> sender, final int fromPosition, final int toPosition, final int itemCount) { - } - - @Override - public void onItemRangeRemoved(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) { - if (binding != null) - breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers); - } - }; + @Nullable private TunnelEditorFragmentBinding binding; @Nullable private Tunnel tunnel; - private void onConfigLoaded(final String name, final Config config) { + private void onConfigLoaded(final Config config) { if (binding != null) { - binding.setConfig(new Config.Observable(config, name)); + binding.setConfig(new ConfigProxy(config)); } } @@ -143,29 +88,23 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi @Nullable final Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); binding = TunnelEditorFragmentBinding.inflate(inflater, container, false); - binding.addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler); - breakObjectOrientedLayeringHandlerReceivers.add(binding); binding.executePendingBindings(); return binding.getRoot(); } - @SuppressWarnings("unchecked") @Override public void onDestroyView() { binding = null; - for (final Object o : breakObjectOrientedLayeringHandlerReceivers) { - if (o instanceof Observable) - ((Observable) o).removeOnPropertyChangedCallback(breakObjectOrientedLayeringHandler); - else if (o instanceof ObservableList) - ((ObservableList) o).removeOnListChangedCallback(breakObjectListOrientedLayeringHandler); - } super.onDestroyView(); } @Override public void onExcludedAppsSelected(final List<String> excludedApps) { Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded"); - binding.getConfig().getInterfaceSection().setExcludedApplications(Attribute.iterableToString(excludedApps)); + final ObservableList<String> excludedApplications = + binding.getConfig().getInterface().getExcludedApplications(); + excludedApplications.clear(); + excludedApplications.addAll(excludedApps); } private void onFinished() { @@ -195,25 +134,27 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_action_save: - final Config newConfig = new Config(); + if (binding == null) + return false; + final Config newConfig; try { - binding.getConfig().commitData(newConfig); + newConfig = binding.getConfig().resolve(); } catch (final Exception e) { final String error = ExceptionLoggers.unwrapMessage(e); - final String tunnelName = tunnel == null ? binding.getConfig().getName() : tunnel.getName(); + final String tunnelName = tunnel == null ? binding.getName() : tunnel.getName(); final String message = getString(R.string.config_save_error, tunnelName, error); Log.e(TAG, message, e); Snackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show(); return false; } if (tunnel == null) { - Log.d(TAG, "Attempting to create new tunnel " + binding.getConfig().getName()); + Log.d(TAG, "Attempting to create new tunnel " + binding.getName()); final TunnelManager manager = Application.getTunnelManager(); - manager.create(binding.getConfig().getName(), newConfig) + manager.create(binding.getName(), newConfig) .whenComplete(this::onTunnelCreated); - } else if (!tunnel.getName().equals(binding.getConfig().getName())) { - Log.d(TAG, "Attempting to rename tunnel to " + binding.getConfig().getName()); - tunnel.setName(binding.getConfig().getName()) + } else if (!tunnel.getName().equals(binding.getName())) { + Log.d(TAG, "Attempting to rename tunnel to " + binding.getName()); + tunnel.setName(binding.getName()) .whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b)); } else { Log.d(TAG, "Attempting to save config of " + tunnel.getName()); @@ -229,7 +170,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) { final FragmentManager fragmentManager = getFragmentManager(); if (fragmentManager != null && binding != null) { - final String[] excludedApps = Attribute.stringToList(binding.getConfig().getInterfaceSection().getExcludedApplications()); + final ArrayList<String> excludedApps = new ArrayList<>(binding.getConfig().getInterface().getExcludedApplications()); final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this); fragment.show(fragmentManager, null); } @@ -237,19 +178,25 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi @Override public void onSaveInstanceState(final Bundle outState) { - outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig()); + if (binding != null) + outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig()); outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName()); super.onSaveInstanceState(outState); } @Override - public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) { + public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, + @Nullable final Tunnel newTunnel) { tunnel = newTunnel; if (binding == null) return; - binding.setConfig(new Config.Observable(null, null)); - if (tunnel != null) - tunnel.getConfigAsync().thenAccept(a -> onConfigLoaded(tunnel.getName(), a)); + binding.setConfig(new ConfigProxy()); + if (tunnel != null) { + binding.setName(tunnel.getName()); + tunnel.getConfigAsync().thenAccept(this::onConfigLoaded); + } else { + binding.setName(""); + } } private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) { @@ -301,7 +248,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi onSelectedTunnelChanged(null, getSelectedTunnel()); } else { tunnel = getSelectedTunnel(); - final Config.Observable config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG); + final ConfigProxy config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG); final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME); if (tunnel != null && !tunnel.getName().equals(originalName)) onSelectedTunnelChanged(null, tunnel); diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java index 7509e40c..783c0a29 100644 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java @@ -41,9 +41,10 @@ import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.widget.MultiselectableRelativeLayout; import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener; import com.wireguard.config.Config; +import com.wireguard.config.ParseException; -import java.io.BufferedReader; -import java.io.InputStreamReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; @@ -79,13 +80,13 @@ public class TunnelListFragment extends BaseFragment { private void importTunnel(@NonNull final String configText) { try { // Ensure the config text is parseable before proceeding… - Config.from(configText); + Config.parse(new ByteArrayInputStream(configText.getBytes(StandardCharsets.UTF_8))); // Config text is valid, now create the tunnel… final FragmentManager fragmentManager = getFragmentManager(); if (fragmentManager != null) ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null); - } catch (final Exception exception) { + } catch (final IllegalArgumentException | IOException | ParseException exception) { onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception)); } } @@ -122,7 +123,6 @@ public class TunnelListFragment extends BaseFragment { if (isZip) { try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri))) { - BufferedReader reader = new BufferedReader(new InputStreamReader(zip, StandardCharsets.UTF_8)); ZipEntry entry; while ((entry = zip.getNextEntry()) != null) { if (entry.isDirectory()) @@ -140,7 +140,7 @@ public class TunnelListFragment extends BaseFragment { continue; Config config = null; try { - config = Config.from(reader); + config = Config.parse(zip); } catch (Exception e) { throwables.add(e); } @@ -150,7 +150,7 @@ public class TunnelListFragment extends BaseFragment { } } else { futureTunnels.add(Application.getTunnelManager().create(name, - Config.from(contentResolver.openInputStream(uri))).toCompletableFuture()); + Config.parse(contentResolver.openInputStream(uri))).toCompletableFuture()); } if (futureTunnels.isEmpty()) { diff --git a/app/src/main/java/com/wireguard/android/model/ApplicationData.java b/app/src/main/java/com/wireguard/android/model/ApplicationData.java index efe1ef87..f7c335de 100644 --- a/app/src/main/java/com/wireguard/android/model/ApplicationData.java +++ b/app/src/main/java/com/wireguard/android/model/ApplicationData.java @@ -13,7 +13,6 @@ import com.wireguard.android.BR; import com.wireguard.util.Keyed; public class ApplicationData extends BaseObservable implements Keyed<String> { - private final Drawable icon; private final String name; private final String packageName; diff --git a/app/src/main/java/com/wireguard/android/model/Tunnel.java b/app/src/main/java/com/wireguard/android/model/Tunnel.java index 6d37e009..9092b288 100644 --- a/app/src/main/java/com/wireguard/android/model/Tunnel.java +++ b/app/src/main/java/com/wireguard/android/model/Tunnel.java @@ -49,7 +49,8 @@ public class Tunnel extends BaseObservable implements Keyed<String> { return manager.delete(this); } - @Bindable @Nullable + @Bindable + @Nullable public Config getConfig() { if (config == null) manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E); @@ -81,7 +82,8 @@ public class Tunnel extends BaseObservable implements Keyed<String> { return TunnelManager.getTunnelState(this); } - @Bindable @Nullable + @Bindable + @Nullable public Statistics getStatistics() { // FIXME: Check age of statistics. if (statistics == null) diff --git a/app/src/main/java/com/wireguard/android/model/TunnelManager.java b/app/src/main/java/com/wireguard/android/model/TunnelManager.java index 3fd7bfc0..83df3595 100644 --- a/app/src/main/java/com/wireguard/android/model/TunnelManager.java +++ b/app/src/main/java/com/wireguard/android/model/TunnelManager.java @@ -44,6 +44,7 @@ public final class TunnelManager extends BaseObservable { 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, Tunnel>> completableTunnels = new CompletableFuture<>(); private final ConfigStore configStore; private final Context context = Application.get(); @@ -111,7 +112,8 @@ public final class TunnelManager extends BaseObservable { }); } - @Bindable @Nullable + @Bindable + @Nullable public Tunnel getLastUsedTunnel() { return lastUsedTunnel; } diff --git a/app/src/main/java/com/wireguard/android/preference/VersionPreference.java b/app/src/main/java/com/wireguard/android/preference/VersionPreference.java index 228facc7..1f3f5aa8 100644 --- a/app/src/main/java/com/wireguard/android/preference/VersionPreference.java +++ b/app/src/main/java/com/wireguard/android/preference/VersionPreference.java @@ -34,7 +34,8 @@ public class VersionPreference extends Preference { }); } - @Override @Nullable + @Nullable + @Override public CharSequence getSummary() { return versionSummary; } diff --git a/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java b/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java index a32e77a4..199b1fbd 100644 --- a/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java +++ b/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java @@ -5,9 +5,15 @@ package com.wireguard.android.util; +import android.content.res.Resources; import android.support.annotation.Nullable; import android.util.Log; +import com.wireguard.android.Application; +import com.wireguard.android.R; +import com.wireguard.config.ParseException; +import com.wireguard.crypto.Key; + import java9.util.concurrent.CompletionException; import java9.util.function.BiConsumer; @@ -34,12 +40,35 @@ public enum ExceptionLoggers implements BiConsumer<Object, Throwable> { return throwable; } - public static String unwrapMessage(Throwable throwable) { - throwable = unwrap(throwable); - final String message = throwable.getMessage(); - if (message != null) - return message; - return throwable.getClass().getSimpleName(); + public static String unwrapMessage(final Throwable throwable) { + final Throwable innerThrowable = unwrap(throwable); + final Resources resources = Application.get().getResources(); + String message; + if (innerThrowable instanceof ParseException) { + final ParseException parseException = (ParseException) innerThrowable; + message = resources.getString(R.string.parse_error, parseException.getText(), parseException.getContext()); + if (parseException.getMessage() != null) + message += ": " + parseException.getMessage(); + } else if (innerThrowable instanceof Key.KeyFormatException) { + final Key.KeyFormatException keyFormatException = (Key.KeyFormatException) innerThrowable; + switch (keyFormatException.getFormat()) { + case BASE64: + message = resources.getString(R.string.key_length_base64_exception_message); + break; + case BINARY: + message = resources.getString(R.string.key_length_exception_message); + break; + case HEX: + message = resources.getString(R.string.key_length_hex_exception_message); + break; + default: + // Will never happen, as getFormat is not nullable. + message = null; + } + } else { + message = throwable.getMessage(); + } + return message != null ? message : innerThrowable.getClass().getSimpleName(); } @Override diff --git a/app/src/main/java/com/wireguard/android/util/FragmentUtils.java b/app/src/main/java/com/wireguard/android/util/FragmentUtils.java index d5838a95..b7fdd095 100644 --- a/app/src/main/java/com/wireguard/android/util/FragmentUtils.java +++ b/app/src/main/java/com/wireguard/android/util/FragmentUtils.java @@ -11,7 +11,6 @@ import android.view.ContextThemeWrapper; import com.wireguard.android.activity.SettingsActivity; public final class FragmentUtils { - private FragmentUtils() { // Prevent instantiation } diff --git a/app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java b/app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java index 2ba87535..7af829fb 100644 --- a/app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java +++ b/app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java @@ -64,13 +64,15 @@ public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>> return indexOfKey(key) >= 0; } - @Override @Nullable + @Nullable + @Override public E get(final K key) { final int index = indexOfKey(key); return index >= 0 ? get(index) : null; } - @Override @Nullable + @Nullable + @Override public E getLast(final K key) { final int index = lastIndexOfKey(key); return index >= 0 ? get(index) : null; diff --git a/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java b/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java index 7ef94106..d287d33d 100644 --- a/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java +++ b/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java @@ -28,8 +28,7 @@ import java.util.Spliterator; public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>> extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> { - @Nullable - private final Comparator<? super K> comparator; + @Nullable private final Comparator<? super K> comparator; private final transient KeyList<K, E> keyList = new KeyList<>(this); @SuppressWarnings("WeakerAccess") diff --git a/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java new file mode 100644 index 00000000..abe8cbcf --- /dev/null +++ b/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java @@ -0,0 +1,93 @@ +/* + * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.viewmodel; + +import android.databinding.ObservableArrayList; +import android.databinding.ObservableList; +import android.os.Parcel; +import android.os.Parcelable; + +import com.wireguard.config.Config; +import com.wireguard.config.ParseException; +import com.wireguard.config.Peer; + +import java.util.ArrayList; +import java.util.Collection; + +public class ConfigProxy implements Parcelable { + public static final Parcelable.Creator<ConfigProxy> CREATOR = new ConfigProxyCreator(); + + private final InterfaceProxy interfaze; + private final ObservableList<PeerProxy> peers = new ObservableArrayList<>(); + + private ConfigProxy(final Parcel in) { + interfaze = in.readParcelable(InterfaceProxy.class.getClassLoader()); + in.readTypedList(peers, PeerProxy.CREATOR); + for (final PeerProxy proxy : peers) + proxy.bind(this); + } + + public ConfigProxy(final Config other) { + interfaze = new InterfaceProxy(other.getInterface()); + for (final Peer peer : other.getPeers()) { + final PeerProxy proxy = new PeerProxy(peer); + peers.add(proxy); + proxy.bind(this); + } + } + + public ConfigProxy() { + interfaze = new InterfaceProxy(); + } + + public PeerProxy addPeer() { + final PeerProxy proxy = new PeerProxy(); + peers.add(proxy); + proxy.bind(this); + return proxy; + } + + @Override + public int describeContents() { + return 0; + } + + public InterfaceProxy getInterface() { + return interfaze; + } + + public ObservableList<PeerProxy> getPeers() { + return peers; + } + + public Config resolve() throws ParseException { + final Collection<Peer> resolvedPeers = new ArrayList<>(); + for (final PeerProxy proxy : peers) + resolvedPeers.add(proxy.resolve()); + return new Config.Builder() + .setInterface(interfaze.resolve()) + .addPeers(resolvedPeers) + .build(); + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeParcelable(interfaze, flags); + dest.writeTypedList(peers); + } + + private static class ConfigProxyCreator implements Parcelable.Creator<ConfigProxy> { + @Override + public ConfigProxy createFromParcel(final Parcel in) { + return new ConfigProxy(in); + } + + @Override + public ConfigProxy[] newArray(final int size) { + return new ConfigProxy[size]; + } + } +} diff --git a/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java new file mode 100644 index 00000000..63d82042 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java @@ -0,0 +1,189 @@ +/* + * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.viewmodel; + +import android.databinding.BaseObservable; +import android.databinding.Bindable; +import android.databinding.ObservableArrayList; +import android.databinding.ObservableList; +import android.os.Parcel; +import android.os.Parcelable; + +import com.wireguard.android.BR; +import com.wireguard.config.Attribute; +import com.wireguard.config.Interface; +import com.wireguard.config.ParseException; +import com.wireguard.crypto.Key; +import com.wireguard.crypto.KeyPair; + +import java.net.InetAddress; +import java.util.List; + +import java9.util.stream.Collectors; +import java9.util.stream.StreamSupport; + +public class InterfaceProxy extends BaseObservable implements Parcelable { + public static final Parcelable.Creator<InterfaceProxy> CREATOR = new InterfaceProxyCreator(); + + private final ObservableList<String> excludedApplications = new ObservableArrayList<>(); + private String addresses; + private String dnsServers; + private String listenPort; + private String mtu; + private String privateKey; + private String publicKey; + + private InterfaceProxy(final Parcel in) { + addresses = in.readString(); + dnsServers = in.readString(); + in.readStringList(excludedApplications); + listenPort = in.readString(); + mtu = in.readString(); + privateKey = in.readString(); + publicKey = in.readString(); + } + + public InterfaceProxy(final Interface other) { + addresses = Attribute.join(other.getAddresses()); + final List<String> dnsServerStrings = StreamSupport.stream(other.getDnsServers()) + .map(InetAddress::getHostAddress) + .collect(Collectors.toUnmodifiableList()); + dnsServers = Attribute.join(dnsServerStrings); + excludedApplications.addAll(other.getExcludedApplications()); + listenPort = other.getListenPort().map(String::valueOf).orElse(""); + mtu = other.getMtu().map(String::valueOf).orElse(""); + final KeyPair keyPair = other.getKeyPair(); + privateKey = keyPair.getPrivateKey().toBase64(); + publicKey = keyPair.getPublicKey().toBase64(); + } + + public InterfaceProxy() { + addresses = ""; + dnsServers = ""; + listenPort = ""; + mtu = ""; + privateKey = ""; + publicKey = ""; + } + + @Override + public int describeContents() { + return 0; + } + + public void generateKeyPair() { + final KeyPair keyPair = new KeyPair(); + privateKey = keyPair.getPrivateKey().toBase64(); + publicKey = keyPair.getPublicKey().toBase64(); + notifyPropertyChanged(BR.privateKey); + notifyPropertyChanged(BR.publicKey); + } + + @Bindable + public String getAddresses() { + return addresses; + } + + @Bindable + public String getDnsServers() { + return dnsServers; + } + + public ObservableList<String> getExcludedApplications() { + return excludedApplications; + } + + @Bindable + public String getListenPort() { + return listenPort; + } + + @Bindable + public String getMtu() { + return mtu; + } + + @Bindable + public String getPrivateKey() { + return privateKey; + } + + @Bindable + public String getPublicKey() { + return publicKey; + } + + public Interface resolve() throws ParseException { + final Interface.Builder builder = new Interface.Builder(); + if (!addresses.isEmpty()) + builder.parseAddresses(addresses); + if (!dnsServers.isEmpty()) + builder.parseDnsServers(dnsServers); + if (!excludedApplications.isEmpty()) + builder.excludeApplications(excludedApplications); + if (!listenPort.isEmpty()) + builder.parseListenPort(listenPort); + if (!mtu.isEmpty()) + builder.parseMtu(mtu); + if (!privateKey.isEmpty()) + builder.parsePrivateKey(privateKey); + return builder.build(); + } + + public void setAddresses(final String addresses) { + this.addresses = addresses; + notifyPropertyChanged(BR.addresses); + } + + public void setDnsServers(final String dnsServers) { + this.dnsServers = dnsServers; + notifyPropertyChanged(BR.dnsServers); + } + + public void setListenPort(final String listenPort) { + this.listenPort = listenPort; + notifyPropertyChanged(BR.listenPort); + } + + public void setMtu(final String mtu) { + this.mtu = mtu; + notifyPropertyChanged(BR.mtu); + } + + public void setPrivateKey(final String privateKey) { + this.privateKey = privateKey; + try { + publicKey = new KeyPair(Key.fromBase64(privateKey)).getPublicKey().toBase64(); + } catch (final Key.KeyFormatException ignored) { + publicKey = ""; + } + notifyPropertyChanged(BR.privateKey); + notifyPropertyChanged(BR.publicKey); + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeString(addresses); + dest.writeString(dnsServers); + dest.writeStringList(excludedApplications); + dest.writeString(listenPort); + dest.writeString(mtu); + dest.writeString(privateKey); + dest.writeString(publicKey); + } + + private static class InterfaceProxyCreator implements Parcelable.Creator<InterfaceProxy> { + @Override + public InterfaceProxy createFromParcel(final Parcel in) { + return new InterfaceProxy(in); + } + + @Override + public InterfaceProxy[] newArray(final int size) { + return new InterfaceProxy[size]; + } + } +} diff --git a/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java new file mode 100644 index 00000000..822a4278 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java @@ -0,0 +1,379 @@ +/* + * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.viewmodel; + +import android.databinding.BaseObservable; +import android.databinding.Bindable; +import android.databinding.Observable; +import android.databinding.ObservableList; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; + +import com.wireguard.android.BR; +import com.wireguard.config.Attribute; +import com.wireguard.config.InetEndpoint; +import com.wireguard.config.ParseException; +import com.wireguard.config.Peer; +import com.wireguard.crypto.Key; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import java9.util.Lists; +import java9.util.Sets; +import java9.util.stream.Collectors; +import java9.util.stream.Stream; + +public class PeerProxy extends BaseObservable implements Parcelable { + public static final Parcelable.Creator<PeerProxy> CREATOR = new PeerProxyCreator(); + private static final Set<String> IPV4_PUBLIC_NETWORKS = new LinkedHashSet<>(Lists.of( + "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", + "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", + "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", + "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", + "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", + "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4" + )); + private static final Set<String> IPV4_WILDCARD = Sets.of("0.0.0.0/0"); + + private final List<String> dnsRoutes = new ArrayList<>(); + private String allowedIps; + private AllowedIpsState allowedIpsState = AllowedIpsState.INVALID; + private String endpoint; + @Nullable private InterfaceDnsListener interfaceDnsListener; + @Nullable private ConfigProxy owner; + @Nullable private PeerListListener peerListListener; + private String persistentKeepalive; + private String preSharedKey; + private String publicKey; + private int totalPeers; + + private PeerProxy(final Parcel in) { + allowedIps = in.readString(); + endpoint = in.readString(); + persistentKeepalive = in.readString(); + preSharedKey = in.readString(); + publicKey = in.readString(); + } + + public PeerProxy(final Peer other) { + allowedIps = Attribute.join(other.getAllowedIps()); + endpoint = other.getEndpoint().map(InetEndpoint::toString).orElse(""); + persistentKeepalive = other.getPersistentKeepalive().map(String::valueOf).orElse(""); + preSharedKey = other.getPreSharedKey().map(Key::toBase64).orElse(""); + publicKey = other.getPublicKey().toBase64(); + } + + public PeerProxy() { + allowedIps = ""; + endpoint = ""; + persistentKeepalive = ""; + preSharedKey = ""; + publicKey = ""; + } + + public void bind(final ConfigProxy owner) { + final InterfaceProxy interfaze = owner.getInterface(); + final ObservableList<PeerProxy> peers = owner.getPeers(); + if (interfaceDnsListener == null) + interfaceDnsListener = new InterfaceDnsListener(this); + interfaze.addOnPropertyChangedCallback(interfaceDnsListener); + setInterfaceDns(interfaze.getDnsServers()); + if (peerListListener == null) + peerListListener = new PeerListListener(this); + peers.addOnListChangedCallback(peerListListener); + setTotalPeers(peers.size()); + this.owner = owner; + } + + private void calculateAllowedIpsState() { + final AllowedIpsState newState; + if (totalPeers == 1) { + // String comparison works because we only care if allowedIps is a superset of one of + // the above sets of (valid) *networks*. We are not checking for a superset based on + // the individual addresses in each set. + final Collection<String> networkStrings = getAllowedIpsSet(); + // If allowedIps contains both the wildcard and the public networks, then private + // networks aren't excluded! + if (networkStrings.containsAll(IPV4_WILDCARD)) + newState = AllowedIpsState.CONTAINS_IPV4_WILDCARD; + else if (networkStrings.containsAll(IPV4_PUBLIC_NETWORKS)) + newState = AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS; + else + newState = AllowedIpsState.OTHER; + } else { + newState = AllowedIpsState.INVALID; + } + if (newState != allowedIpsState) { + allowedIpsState = newState; + notifyPropertyChanged(BR.ableToExcludePrivateIps); + notifyPropertyChanged(BR.excludingPrivateIps); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Bindable + public String getAllowedIps() { + return allowedIps; + } + + private Set<String> getAllowedIpsSet() { + return new LinkedHashSet<>(Lists.of(Attribute.split(allowedIps))); + } + + @Bindable + public String getEndpoint() { + return endpoint; + } + + @Bindable + public String getPersistentKeepalive() { + return persistentKeepalive; + } + + @Bindable + public String getPreSharedKey() { + return preSharedKey; + } + + @Bindable + public String getPublicKey() { + return publicKey; + } + + @Bindable + public boolean isAbleToExcludePrivateIps() { + return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS + || allowedIpsState == AllowedIpsState.CONTAINS_IPV4_WILDCARD; + } + + @Bindable + public boolean isExcludingPrivateIps() { + return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS; + } + + public Peer resolve() throws ParseException { + final Peer.Builder builder = new Peer.Builder(); + if (!allowedIps.isEmpty()) + builder.parseAllowedIPs(allowedIps); + if (!endpoint.isEmpty()) + builder.parseEndpoint(endpoint); + if (!persistentKeepalive.isEmpty()) + builder.parsePersistentKeepalive(persistentKeepalive); + if (!preSharedKey.isEmpty()) + builder.parsePreSharedKey(preSharedKey); + if (!publicKey.isEmpty()) + builder.parsePublicKey(publicKey); + return builder.build(); + } + + public void setAllowedIps(final String allowedIps) { + this.allowedIps = allowedIps; + notifyPropertyChanged(BR.allowedIps); + calculateAllowedIpsState(); + } + + public void setEndpoint(final String endpoint) { + this.endpoint = endpoint; + notifyPropertyChanged(BR.endpoint); + } + + public void setExcludingPrivateIps(final boolean excludingPrivateIps) { + if (!isAbleToExcludePrivateIps() || isExcludingPrivateIps() == excludingPrivateIps) + return; + final Set<String> oldNetworks = excludingPrivateIps ? IPV4_WILDCARD : IPV4_PUBLIC_NETWORKS; + final Set<String> newNetworks = excludingPrivateIps ? IPV4_PUBLIC_NETWORKS : IPV4_WILDCARD; + final Collection<String> input = getAllowedIpsSet(); + final int outputSize = input.size() - oldNetworks.size() + newNetworks.size(); + final Collection<String> output = new LinkedHashSet<>(outputSize); + boolean replaced = false; + // Replace the first instance of the wildcard with the public network list, or vice versa. + for (final String network : input) { + if (oldNetworks.contains(network)) { + if (!replaced) { + for (final String replacement : newNetworks) + if (!output.contains(replacement)) + output.add(replacement); + replaced = true; + } + } else if (!output.contains(network)) { + output.add(network); + } + } + // DNS servers only need to handled specially when we're excluding private IPs. + if (excludingPrivateIps) + output.addAll(dnsRoutes); + else + output.removeAll(dnsRoutes); + allowedIps = Attribute.join(output); + allowedIpsState = excludingPrivateIps ? + AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS : AllowedIpsState.CONTAINS_IPV4_WILDCARD; + notifyPropertyChanged(BR.allowedIps); + notifyPropertyChanged(BR.excludingPrivateIps); + } + + private void setInterfaceDns(final CharSequence dnsServers) { + final List<String> newDnsRoutes = Stream.of(Attribute.split(dnsServers)) + .map(server -> server + "/32") + .collect(Collectors.toUnmodifiableList()); + if (allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS) { + final Collection<String> input = getAllowedIpsSet(); + final Collection<String> output = new LinkedHashSet<>(input.size() + 1); + // Yes, this is quadratic in the number of DNS servers, but most users have 1 or 2. + for (final String network : input) + if (!dnsRoutes.contains(network) || newDnsRoutes.contains(network)) + output.add(network); + // Since output is a Set, this does the Right Thing™ (it does not duplicate networks). + output.addAll(newDnsRoutes); + // None of the public networks are /32s, so this cannot change the AllowedIPs state. + allowedIps = Attribute.join(output); + notifyPropertyChanged(BR.allowedIps); + } + dnsRoutes.clear(); + dnsRoutes.addAll(newDnsRoutes); + } + + public void setPersistentKeepalive(final String persistentKeepalive) { + this.persistentKeepalive = persistentKeepalive; + notifyPropertyChanged(BR.persistentKeepalive); + } + + public void setPreSharedKey(final String preSharedKey) { + this.preSharedKey = preSharedKey; + notifyPropertyChanged(BR.preSharedKey); + } + + public void setPublicKey(final String publicKey) { + this.publicKey = publicKey; + notifyPropertyChanged(BR.publicKey); + } + + private void setTotalPeers(final int totalPeers) { + if (this.totalPeers == totalPeers) + return; + this.totalPeers = totalPeers; + calculateAllowedIpsState(); + } + + public void unbind() { + if (owner == null) + return; + final InterfaceProxy interfaze = owner.getInterface(); + final ObservableList<PeerProxy> peers = owner.getPeers(); + if (interfaceDnsListener != null) + interfaze.removeOnPropertyChangedCallback(interfaceDnsListener); + if (peerListListener != null) + peers.removeOnListChangedCallback(peerListListener); + peers.remove(this); + setInterfaceDns(""); + setTotalPeers(0); + owner = null; + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeString(allowedIps); + dest.writeString(endpoint); + dest.writeString(persistentKeepalive); + dest.writeString(preSharedKey); + dest.writeString(publicKey); + } + + private enum AllowedIpsState { + CONTAINS_IPV4_PUBLIC_NETWORKS, + CONTAINS_IPV4_WILDCARD, + INVALID, + OTHER + } + + private static final class InterfaceDnsListener extends Observable.OnPropertyChangedCallback { + private final WeakReference<PeerProxy> weakPeerProxy; + + private InterfaceDnsListener(final PeerProxy peerProxy) { + weakPeerProxy = new WeakReference<>(peerProxy); + } + + @Override + public void onPropertyChanged(final Observable sender, final int propertyId) { + @Nullable final PeerProxy peerProxy = weakPeerProxy.get(); + if (peerProxy == null) { + sender.removeOnPropertyChangedCallback(this); + return; + } + // This shouldn't be possible, but try to avoid a ClassCastException anyway. + if (!(sender instanceof InterfaceProxy)) + return; + if (!(propertyId == BR._all || propertyId == BR.dnsServers)) + return; + peerProxy.setInterfaceDns(((InterfaceProxy) sender).getDnsServers()); + } + } + + private static final class PeerListListener + extends ObservableList.OnListChangedCallback<ObservableList<PeerProxy>> { + private final WeakReference<PeerProxy> weakPeerProxy; + + private PeerListListener(final PeerProxy peerProxy) { + weakPeerProxy = new WeakReference<>(peerProxy); + } + + @Override + public void onChanged(final ObservableList<PeerProxy> sender) { + @Nullable final PeerProxy peerProxy = weakPeerProxy.get(); + if (peerProxy == null) { + sender.removeOnListChangedCallback(this); + return; + } + peerProxy.setTotalPeers(sender.size()); + } + + @Override + public void onItemRangeChanged(final ObservableList<PeerProxy> sender, + final int positionStart, final int itemCount) { + // Do nothing. + } + + @Override + public void onItemRangeInserted(final ObservableList<PeerProxy> sender, + final int positionStart, final int itemCount) { + onChanged(sender); + } + + @Override + public void onItemRangeMoved(final ObservableList<PeerProxy> sender, + final int fromPosition, final int toPosition, + final int itemCount) { + // Do nothing. + } + + @Override + public void onItemRangeRemoved(final ObservableList<PeerProxy> sender, + final int positionStart, final int itemCount) { + onChanged(sender); + } + } + + private static class PeerProxyCreator implements Parcelable.Creator<PeerProxy> { + @Override + public PeerProxy createFromParcel(final Parcel in) { + return new PeerProxy(in); + } + + @Override + public PeerProxy[] newArray(final int size) { + return new PeerProxy[size]; + } + } +} diff --git a/app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java b/app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java index b6cdada7..6332b856 100644 --- a/app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java +++ b/app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java @@ -10,7 +10,7 @@ import android.text.InputFilter; import android.text.SpannableStringBuilder; import android.text.Spanned; -import com.wireguard.crypto.KeyEncoding; +import com.wireguard.crypto.Key; /** * InputFilter for entering WireGuard private/public keys encoded with base64. @@ -25,7 +25,8 @@ public class KeyInputFilter implements InputFilter { return new KeyInputFilter(); } - @Override @Nullable + @Nullable + @Override public CharSequence filter(final CharSequence source, final int sStart, final int sEnd, final Spanned dest, @@ -38,9 +39,9 @@ public class KeyInputFilter implements InputFilter { final int dIndex = dStart + (sIndex - sStart); // Restrict characters to the base64 character set. // Ensure adding this character does not push the length over the limit. - if (((dIndex + 1 < KeyEncoding.KEY_LENGTH_BASE64 && isAllowed(c)) || - (dIndex + 1 == KeyEncoding.KEY_LENGTH_BASE64 && c == '=')) && - dLength + (sIndex - sStart) < KeyEncoding.KEY_LENGTH_BASE64) { + if (((dIndex + 1 < Key.Format.BASE64.getLength() && isAllowed(c)) || + (dIndex + 1 == Key.Format.BASE64.getLength() && c == '=')) && + dLength + (sIndex - sStart) < Key.Format.BASE64.getLength()) { ++rIndex; } else { if (replacement == null) diff --git a/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java b/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java index db5336d0..2352630e 100644 --- a/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java +++ b/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java @@ -25,7 +25,8 @@ public class NameInputFilter implements InputFilter { return new NameInputFilter(); } - @Override @Nullable + @Nullable + @Override public CharSequence filter(final CharSequence source, final int sStart, final int sEnd, final Spanned dest, diff --git a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java index ed838914..7f5b67e6 100644 --- a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java +++ b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java @@ -539,7 +539,7 @@ public class FloatingActionsMenu extends ViewGroup { return new SavedState[size]; } }; - public boolean mExpanded; + private boolean mExpanded; public SavedState(final Parcelable parcel) { super(parcel); |