summaryrefslogtreecommitdiffhomepage
path: root/app/src/main/java/com/wireguard/config/Config.java
diff options
context:
space:
mode:
authorSamuel Holland <samuel@sholland.org>2018-09-05 20:17:14 -0500
committerJason A. Donenfeld <Jason@zx2c4.com>2018-12-08 02:39:41 +0100
commitd1e85633fbe8d871355d2b9feb51e2c9983d8a21 (patch)
treed95ad1ae84d02fc3e18a211aa1e1ef8150d8fa35 /app/src/main/java/com/wireguard/config/Config.java
parenta264f7ab36bf1335999d53cb4a0d753c54b231d0 (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/config/Config.java')
-rw-r--r--app/src/main/java/com/wireguard/config/Config.java267
1 files changed, 145 insertions, 122 deletions
diff --git a/app/src/main/java/com/wireguard/config/Config.java b/app/src/main/java/com/wireguard/config/Config.java
index 61e31838..7645583d 100644
--- a/app/src/main/java/com/wireguard/config/Config.java
+++ b/app/src/main/java/com/wireguard/config/Config.java
@@ -5,170 +5,193 @@
package com.wireguard.config;
-import android.content.Context;
-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 android.support.annotation.Nullable;
-import com.android.databinding.library.baseAdapters.BR;
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.io.StringReader;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Set;
/**
- * Represents a wg-quick configuration file, its name, and its connection state.
+ * Represents the contents of a wg-quick configuration file, made up of one or more "Interface"
+ * sections (combined together), and zero or more "Peer" sections (treated individually).
+ * <p>
+ * Instances of this class are immutable.
*/
-
-public class Config {
- private final Interface interfaceSection = new Interface();
- private List<Peer> peers = new ArrayList<>();
-
- public static Config from(final String string) throws IOException {
- return from(new BufferedReader(new StringReader(string)));
+public final class Config {
+ private final Interface interfaze;
+ private final List<Peer> peers;
+
+ private Config(final Builder builder) {
+ interfaze = Objects.requireNonNull(builder.interfaze, "An [Interface] section is required");
+ // Defensively copy to ensure immutability even if the Builder is reused.
+ peers = Collections.unmodifiableList(new ArrayList<>(builder.peers));
}
- public static Config from(final InputStream stream) throws IOException {
- return from(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
- }
-
- public static Config from(final BufferedReader reader) throws IOException {
- final Config config = new Config();
- final Context context = Application.get();
- Peer currentPeer = null;
- String line;
- boolean inInterfaceSection = false;
- while ((line = reader.readLine()) != null) {
- final int commentIndex = line.indexOf('#');
- if (commentIndex != -1)
- line = line.substring(0, commentIndex);
- line = line.trim();
- if (line.isEmpty())
- continue;
- if ("[Interface]".toLowerCase().equals(line.toLowerCase())) {
- currentPeer = null;
- inInterfaceSection = true;
- } else if ("[Peer]".toLowerCase().equals(line.toLowerCase())) {
- currentPeer = new Peer();
- config.peers.add(currentPeer);
- inInterfaceSection = false;
- } else if (inInterfaceSection) {
- config.interfaceSection.parse(line);
- } else if (currentPeer != null) {
- currentPeer.parse(line);
- } else {
- throw new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_config_line, line));
+ /**
+ * Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
+ * {@link ParseException} if the input is not well-formed or contains unparseable sections.
+ *
+ * @param stream a stream of UTF-8 text that is interpreted as a WireGuard configuration file
+ * @return a {@code Config} instance representing the supplied configuration
+ */
+ public static Config parse(final InputStream stream) throws IOException, ParseException {
+ final Builder builder = new Builder();
+ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
+ final Collection<String> interfaceLines = new ArrayList<>();
+ final Collection<String> peerLines = new ArrayList<>();
+ boolean inInterfaceSection = false;
+ boolean inPeerSection = false;
+ @Nullable String line;
+ while ((line = reader.readLine()) != null) {
+ final int commentIndex = line.indexOf('#');
+ if (commentIndex != -1)
+ line = line.substring(0, commentIndex);
+ line = line.trim();
+ if (line.isEmpty())
+ continue;
+ if (line.startsWith("[")) {
+ // Consume all [Peer] lines read so far.
+ if (inPeerSection) {
+ builder.parsePeer(peerLines);
+ peerLines.clear();
+ }
+ if ("[Interface]".equalsIgnoreCase(line)) {
+ inInterfaceSection = true;
+ inPeerSection = false;
+ } else if ("[Peer]".equalsIgnoreCase(line)) {
+ inInterfaceSection = false;
+ inPeerSection = true;
+ } else {
+ throw new ParseException("top level", line, "Unknown section name");
+ }
+ } else if (inInterfaceSection) {
+ interfaceLines.add(line);
+ } else if (inPeerSection) {
+ peerLines.add(line);
+ } else {
+ throw new ParseException("top level", line, "Expected [Interface] or [Peer]");
+ }
}
+ if (inPeerSection)
+ builder.parsePeer(peerLines);
+ else if (!inInterfaceSection)
+ throw new ParseException("top level", "", "Empty configuration");
+ // Combine all [Interface] sections in the file.
+ builder.parseInterface(interfaceLines);
}
- if (!inInterfaceSection && currentPeer == null) {
- throw new IllegalArgumentException(context.getString(R.string.tunnel_error_no_config_information));
- }
- return config;
+ return builder.build();
}
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof Config))
+ return false;
+ final Config other = (Config) obj;
+ return interfaze.equals(other.interfaze) && peers.equals(other.peers);
+ }
+
+ /**
+ * Returns the interface section of the configuration.
+ *
+ * @return the interface configuration
+ */
public Interface getInterface() {
- return interfaceSection;
+ return interfaze;
}
+ /**
+ * Returns a list of the configuration's peer sections.
+ *
+ * @return a list of {@link Peer}s
+ */
public List<Peer> getPeers() {
return peers;
}
@Override
+ public int hashCode() {
+ return 31 * interfaze.hashCode() + peers.hashCode();
+ }
+
+ /**
+ * Converts the {@code Config} into a string suitable for debugging purposes. The {@code Config}
+ * is identified by its interface's public key and the number of peers it has.
+ *
+ * @return a concise single-line identifier for the {@code Config}
+ */
+ @Override
public String toString() {
- final StringBuilder sb = new StringBuilder().append(interfaceSection);
+ return "(Config " + interfaze + " (" + peers.size() + " peers))";
+ }
+
+ /**
+ * Converts the {@code Config} into a string suitable for use as a {@code wg-quick}
+ * configuration file.
+ *
+ * @return the {@code Config} represented as one [Interface] and zero or more [Peer] sections
+ */
+ public String toWgQuickString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("[Interface]\n").append(interfaze.toWgQuickString());
for (final Peer peer : peers)
- sb.append('\n').append(peer);
+ sb.append("\n[Peer]\n").append(peer.toWgQuickString());
return sb.toString();
}
- public static class Observable extends BaseObservable implements Parcelable {
- public static final Creator<Observable> CREATOR = new Creator<Observable>() {
- @Override
- public Observable createFromParcel(final Parcel in) {
- return new Observable(in);
- }
-
- @Override
- public Observable[] newArray(final int size) {
- return new Observable[size];
- }
- };
- private final Interface.Observable observableInterface;
- private final ObservableList<Peer.Observable> observablePeers;
- @Nullable private String name;
-
- public Observable(@Nullable final Config parent, @Nullable final String name) {
- this.name = name;
-
- observableInterface = new Interface.Observable(parent == null ? null : parent.interfaceSection);
- observablePeers = new ObservableArrayList<>();
- if (parent != null) {
- for (final Peer peer : parent.getPeers())
- observablePeers.add(new Peer.Observable(peer));
- }
- }
-
- private Observable(final Parcel in) {
- name = in.readString();
- observableInterface = in.readParcelable(Interface.Observable.class.getClassLoader());
- observablePeers = new ObservableArrayList<>();
- in.readTypedList(observablePeers, Peer.Observable.CREATOR);
- }
+ /**
+ * Serializes the {@code Config} for use with the WireGuard cross-platform userspace API.
+ *
+ * @return the {@code Config} represented as a series of "key=value" lines
+ */
+ public String toWgUserspaceString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(interfaze.toWgUserspaceString());
+ sb.append("replace_peers=true\n");
+ for (final Peer peer : peers)
+ sb.append(peer.toWgUserspaceString());
+ return sb.toString();
+ }
- public void commitData(final Config parent) {
- observableInterface.commitData(parent.interfaceSection);
- final List<Peer> newPeers = new ArrayList<>(observablePeers.size());
- for (final Peer.Observable observablePeer : observablePeers) {
- final Peer peer = new Peer();
- observablePeer.commitData(peer);
- newPeers.add(peer);
- }
- parent.peers = newPeers;
- notifyChange();
- }
+ @SuppressWarnings("UnusedReturnValue")
+ public static final class Builder {
+ // Defaults to an empty set.
+ private final Set<Peer> peers = new LinkedHashSet<>();
+ // No default; must be provided before building.
+ @Nullable private Interface interfaze;
- @Override
- public int describeContents() {
- return 0;
+ public Builder addPeer(final Peer peer) {
+ peers.add(peer);
+ return this;
}
- @Bindable
- public Interface.Observable getInterfaceSection() {
- return observableInterface;
+ public Builder addPeers(final Collection<Peer> peers) {
+ this.peers.addAll(peers);
+ return this;
}
- @Bindable
- public String getName() {
- return name == null ? "" : name;
+ public Config build() {
+ return new Config(this);
}
- @Bindable
- public ObservableList<Peer.Observable> getPeers() {
- return observablePeers;
+ public Builder parseInterface(final Iterable<? extends CharSequence> lines) throws ParseException {
+ return setInterface(Interface.parse(lines));
}
- public void setName(final String name) {
- this.name = name;
- notifyPropertyChanged(BR.name);
+ public Builder parsePeer(final Iterable<? extends CharSequence> lines) throws ParseException {
+ return addPeer(Peer.parse(lines));
}
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeString(name);
- dest.writeParcelable(observableInterface, flags);
- dest.writeTypedList(observablePeers);
+ public Builder setInterface(final Interface interfaze) {
+ this.interfaze = interfaze;
+ return this;
}
}
}