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.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.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
import java9.util.function.Function;
import java9.util.stream.Collectors;
import java9.util.stream.IntStream;
import java9.util.stream.StreamSupport;
@ -68,7 +73,10 @@ public class TunnelListFragment extends BaseFragment {
if (activity == null)
return;
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};
String name = null;
try (Cursor cursor = contentResolver.query(uri, columns, null, null, null)) {
@ -77,17 +85,71 @@ public class TunnelListFragment extends BaseFragment {
}
if (name == null)
name = Uri.decode(uri.getLastPathSegment());
if (name.indexOf('/') >= 0)
name = name.substring(name.lastIndexOf('/') + 1);
if (name.endsWith(".conf"))
int idx = name.lastIndexOf('/');
if (idx >= 0) {
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());
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
@ -164,16 +226,25 @@ public class TunnelListFragment extends BaseFragment {
}
}
private void onTunnelImportFinished(final Tunnel tunnel, final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.import_success, tunnel.getName());
} else {
private void onTunnelImportFinished(final List<Tunnel> tunnels, final List<Throwable> throwables) {
String message = null;
for (final Throwable throwable : throwables) {
final String error = ExceptionLoggers.unwrap(throwable).getMessage();
message = getString(R.string.import_error, error);
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;
Snackbar.make(container, message, Snackbar.LENGTH_LONG).show();
}

View File

@ -97,11 +97,12 @@ public class Config implements Parcelable {
in.readTypedList(peers, Peer.CREATOR);
}
public static Config from(final InputStream stream)
throws IOException {
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();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(stream, StandardCharsets.UTF_8))) {
Peer currentPeer = null;
String line;
boolean inInterfaceSection = false;
@ -126,7 +127,6 @@ public class Config implements Parcelable {
if (!inInterfaceSection && currentPeer == null) {
throw new IllegalArgumentException("Could not find any config information");
}
}
return config;
}

View File

@ -34,7 +34,9 @@
<string name="hint_optional">(optional)</string>
<string name="hint_random">(random)</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="listen_port">Listen port</string>
<string name="mtu">MTU</string>