Allow importing from zip file

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2018-04-28 18:35:12 +02:00
parent 217ab5e17f
commit f4e462fabd
3 changed files with 119 additions and 46 deletions

View File

@ -39,11 +39,16 @@ import com.wireguard.android.util.AsyncWorker;
import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java9.util.concurrent.CompletableFuture; import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
import java9.util.function.Function;
import java9.util.stream.Collectors; import java9.util.stream.Collectors;
import java9.util.stream.IntStream; import java9.util.stream.IntStream;
import java9.util.stream.StreamSupport; import java9.util.stream.StreamSupport;
@ -68,7 +73,10 @@ public class TunnelListFragment extends BaseFragment {
if (activity == null) if (activity == null)
return; return;
final ContentResolver contentResolver = activity.getContentResolver(); final ContentResolver contentResolver = activity.getContentResolver();
final CompletionStage<String> nameStage = asyncWorker.supplyAsync(() -> {
final List<CompletableFuture<Tunnel>> futureTunnels = new ArrayList<>();
final List<Throwable> throwables = new ArrayList<>();
asyncWorker.supplyAsync(() -> {
final String[] columns = {OpenableColumns.DISPLAY_NAME}; final String[] columns = {OpenableColumns.DISPLAY_NAME};
String name = null; String name = null;
try (Cursor cursor = contentResolver.query(uri, columns, null, null, null)) { try (Cursor cursor = contentResolver.query(uri, columns, null, null, null)) {
@ -77,17 +85,71 @@ public class TunnelListFragment extends BaseFragment {
} }
if (name == null) if (name == null)
name = Uri.decode(uri.getLastPathSegment()); name = Uri.decode(uri.getLastPathSegment());
if (name.indexOf('/') >= 0) int idx = name.lastIndexOf('/');
name = name.substring(name.lastIndexOf('/') + 1); if (idx >= 0) {
if (name.endsWith(".conf")) if (idx >= name.length() - 1)
throw new IllegalArgumentException("Illegal file name: " + name);
name = name.substring(idx + 1);
}
boolean isZip = name.toLowerCase().endsWith(".zip");
if (name.toLowerCase().endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length()); name = name.substring(0, name.length() - ".conf".length());
Log.d(TAG, "Import mapped URI " + uri + " to tunnel name " + name);
return name; if (isZip) {
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())
continue;
name = entry.getName();
idx = name.lastIndexOf('/');
if (idx >= 0) {
if (idx >= name.length() - 1)
continue;
name = name.substring(name.lastIndexOf('/') + 1);
}
if (name.toLowerCase().endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length());
else
continue;
Config config = null;
try {
config = Config.from(reader);
} catch (Exception e) {
throwables.add(e);
}
if (config != null)
futureTunnels.add(tunnelManager.create(name, config).toCompletableFuture());
}
} else {
futureTunnels.add(tunnelManager.create(name, Config.from(contentResolver.openInputStream(uri))).toCompletableFuture());
}
if (futureTunnels.isEmpty() && throwables.size() == 1)
throw throwables.get(0);
return CompletableFuture.allOf(futureTunnels.toArray(new CompletableFuture[futureTunnels.size()]));
}).whenComplete((future, exception) -> {
if (exception != null) {
this.onTunnelImportFinished(Arrays.asList(), Arrays.asList(exception));
} else {
future.whenComplete((ignored1, ignored2) -> {
ArrayList<Tunnel> tunnels = new ArrayList<>(futureTunnels.size());
for (CompletableFuture<Tunnel> futureTunnel : futureTunnels) {
Tunnel tunnel = null;
try {
tunnel = futureTunnel.getNow(null);
} catch (Exception e) {
throwables.add(e);
}
if (tunnel != null)
tunnels.add(tunnel);
}
onTunnelImportFinished(tunnels, throwables);
});
}
}); });
asyncWorker.supplyAsync(() -> Config.from(contentResolver.openInputStream(uri)))
.thenCombine(nameStage, (config, name) -> tunnelManager.create(name, config))
.thenCompose(Function.identity())
.whenComplete(this::onTunnelImportFinished);
} }
@Override @Override
@ -164,16 +226,25 @@ public class TunnelListFragment extends BaseFragment {
} }
} }
private void onTunnelImportFinished(final Tunnel tunnel, final Throwable throwable) { private void onTunnelImportFinished(final List<Tunnel> tunnels, final List<Throwable> throwables) {
final String message; String message = null;
if (throwable == null) {
message = getString(R.string.import_success, tunnel.getName()); for (final Throwable throwable : throwables) {
} else {
final String error = ExceptionLoggers.unwrap(throwable).getMessage(); final String error = ExceptionLoggers.unwrap(throwable).getMessage();
message = getString(R.string.import_error, error); message = getString(R.string.import_error, error);
Log.e(TAG, message, throwable); Log.e(TAG, message, throwable);
} }
if (binding != null) {
if (tunnels.size() == 1 && throwables.isEmpty())
message = getString(R.string.import_success, tunnels.get(0).getName());
else if (tunnels.isEmpty() && throwables.size() == 1)
/* Use the exception message from above. */;
else if (throwables.isEmpty())
message = getString(R.string.import_total_success, tunnels.size());
else if (!throwables.isEmpty())
message = getString(R.string.import_partial_success, tunnels.size(), tunnels.size() + throwables.size());
if (binding != null && message != null) {
final CoordinatorLayout container = binding.mainContainer; final CoordinatorLayout container = binding.mainContainer;
Snackbar.make(container, message, Snackbar.LENGTH_LONG).show(); Snackbar.make(container, message, Snackbar.LENGTH_LONG).show();
} }

View File

@ -97,36 +97,36 @@ public class Config implements Parcelable {
in.readTypedList(peers, Peer.CREATOR); in.readTypedList(peers, Peer.CREATOR);
} }
public static Config from(final InputStream stream) public static Config from(final InputStream stream) throws IOException {
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 Config config = new Config();
try (BufferedReader reader = new BufferedReader( Peer currentPeer = null;
new InputStreamReader(stream, StandardCharsets.UTF_8))) { String line;
Peer currentPeer = null; boolean inInterfaceSection = false;
String line; while ((line = reader.readLine()) != null) {
boolean inInterfaceSection = false; if (line.isEmpty() || line.startsWith("#"))
while ((line = reader.readLine()) != null) { continue;
if (line.isEmpty() || line.startsWith("#")) if ("[Interface]".equals(line)) {
continue; currentPeer = null;
if ("[Interface]".equals(line)) { inInterfaceSection = true;
currentPeer = null; } else if ("[Peer]".equals(line)) {
inInterfaceSection = true; currentPeer = new Peer();
} else if ("[Peer]".equals(line)) { config.peers.add(currentPeer);
currentPeer = new Peer(); inInterfaceSection = false;
config.peers.add(currentPeer); } else if (inInterfaceSection) {
inInterfaceSection = false; config.interfaceSection.parse(line);
} else if (inInterfaceSection) { } else if (currentPeer != null) {
config.interfaceSection.parse(line); currentPeer.parse(line);
} else if (currentPeer != null) { } else {
currentPeer.parse(line); throw new IllegalArgumentException("Invalid configuration line: " + line);
} else {
throw new IllegalArgumentException("Invalid configuration line: " + line);
}
}
if (!inInterfaceSection && currentPeer == null) {
throw new IllegalArgumentException("Could not find any config information");
} }
} }
if (!inInterfaceSection && currentPeer == null) {
throw new IllegalArgumentException("Could not find any config information");
}
return config; return config;
} }

View File

@ -34,7 +34,9 @@
<string name="hint_optional">(optional)</string> <string name="hint_optional">(optional)</string>
<string name="hint_random">(random)</string> <string name="hint_random">(random)</string>
<string name="import_error">Unable to import tunnel: %s</string> <string name="import_error">Unable to import tunnel: %s</string>
<string name="import_success">Successfully imported “%s”</string> <string name="import_success">Imported “%s”</string>
<string name="import_total_success">Imported %d tunnels</string>
<string name="import_partial_success">Imported %d of %d tunnels</string>
<string name="interface_title">Interface</string> <string name="interface_title">Interface</string>
<string name="listen_port">Listen port</string> <string name="listen_port">Listen port</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>