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/config/Config.java | |
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/config/Config.java')
-rw-r--r-- | app/src/main/java/com/wireguard/config/Config.java | 267 |
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; } } } |