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>
This commit is contained in:
Samuel Holland
2018-12-08 02:39:41 +01:00
committed by Jason A. Donenfeld
parent a264f7ab36
commit d1e85633fb
47 changed files with 2082 additions and 1340 deletions
+144 -121
View File
@@ -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 final class Config {
private final Interface interfaze;
private final List<Peer> peers;
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)));
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);
}
/**
* 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();
}
@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;
@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;
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));
}
public Builder addPeer(final Peer peer) {
peers.add(peer);
return this;
}
private Observable(final Parcel in) {
name = in.readString();
observableInterface = in.readParcelable(Interface.Observable.class.getClassLoader());
observablePeers = new ObservableArrayList<>();
in.readTypedList(observablePeers, Peer.Observable.CREATOR);
public Builder addPeers(final Collection<Peer> peers) {
this.peers.addAll(peers);
return this;
}
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();
public Config build() {
return new Config(this);
}
@Override
public int describeContents() {
return 0;
public Builder parseInterface(final Iterable<? extends CharSequence> lines) throws ParseException {
return setInterface(Interface.parse(lines));
}
@Bindable
public Interface.Observable getInterfaceSection() {
return observableInterface;
public Builder parsePeer(final Iterable<? extends CharSequence> lines) throws ParseException {
return addPeer(Peer.parse(lines));
}
@Bindable
public String getName() {
return name == null ? "" : name;
}
@Bindable
public ObservableList<Peer.Observable> getPeers() {
return observablePeers;
}
public void setName(final String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
@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;
}
}
}