wireguard-android/app/src/main/java/com/wireguard/android/ProfileService.java
Samuel Holland 5d5cdf54fa ProfileService: Rework profile updating
This should help discourage editing a Profile without making a copy
first, since the caller has to keep track of the old Profile as well.

ProfileAdder has been updated to prevent adding duplicate profiles. It
checks once in the constructor, so the caller can catch the exception
and pass the error back to the UI. It checks again in the worker thread
to prevent any race from happening if a profile is added twice quickly.
Either the file exists, or it doesn't.

Additionally, this change solves the race condition when the old
profile is removed before it is updated; previously this would lead
to the profile being re-added. Now, ProfileRemover will fail and the
profile will stay removed.

Finally, updating a profile's name should now work correctly. There were
previously multiple bugs with that (the old profile wasn't removed, the
new one could duplicate a name, the new one could overwrite some random
other one, etc.).

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-01 01:38:39 -05:00

272 lines
9.5 KiB
Java

package com.wireguard.android;
import android.app.Service;
import android.content.Intent;
import android.databinding.ObservableArrayList;
import android.databinding.ObservableList;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import com.wireguard.config.Profile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* Service that handles profile state coordination and all background processing for the app.
*/
public class ProfileService extends Service {
private static final String TAG = "ProfileService";
private final IBinder binder = new ProfileServiceBinder();
private final ObservableList<Profile> profiles = new ObservableArrayList<>();
private RootShell rootShell;
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void onCreate() {
rootShell = new RootShell(this);
new ProfileLoader().execute(getFilesDir().listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".conf");
}
}));
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
private class ProfileAdder extends AsyncTask<Void, Void, Boolean> {
private final Profile profile;
private final boolean shouldConnect;
private ProfileAdder(Profile profile, boolean shouldConnect) {
super();
for (Profile p : profiles)
if (p.getName().equals(profile.getName()))
throw new IllegalStateException("Profile already exists: " + profile.getName());
this.profile = profile;
this.shouldConnect = shouldConnect;
}
@Override
protected Boolean doInBackground(Void... voids) {
Log.i(TAG, "Adding profile " + profile.getName());
try {
final String configFile = profile.getName() + ".conf";
if (new File(getFilesDir(), configFile).exists())
throw new IOException("Refusing to overwrite existing profile configuration");
final FileOutputStream stream = openFileOutput(configFile, MODE_PRIVATE);
stream.write(profile.toString().getBytes(StandardCharsets.UTF_8));
stream.close();
return true;
} catch (IOException e) {
Log.e(TAG, "Could not create profile " + profile.getName(), e);
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
if (!result)
return;
profile.setIsConnected(false);
profiles.add(profile);
if (shouldConnect)
new ProfileConnecter(profile).execute();
}
}
private class ProfileConnecter extends AsyncTask<Void, Void, Boolean> {
private final Profile profile;
private ProfileConnecter(Profile profile) {
super();
this.profile = profile;
}
@Override
protected Boolean doInBackground(Void... voids) {
Log.i(TAG, "Running wg-quick up for profile " + profile.getName());
final File configFile = new File(getFilesDir(), profile.getName() + ".conf");
return rootShell.run(null, "wg-quick up '" + configFile.getPath() + "'") == 0;
}
@Override
protected void onPostExecute(Boolean result) {
if (!result)
return;
profile.setIsConnected(true);
}
}
private class ProfileDisconnecter extends AsyncTask<Void, Void, Boolean> {
private final Profile profile;
private ProfileDisconnecter(Profile profile) {
super();
this.profile = profile;
}
@Override
protected Boolean doInBackground(Void... voids) {
Log.i(TAG, "Running wg-quick down for profile " + profile.getName());
final File configFile = new File(getFilesDir(), profile.getName() + ".conf");
return rootShell.run(null, "wg-quick down '" + configFile.getPath() + "'") == 0;
}
@Override
protected void onPostExecute(Boolean result) {
if (!result)
return;
profile.setIsConnected(false);
}
}
private class ProfileLoader extends AsyncTask<File, Void, List<Profile>> {
@Override
protected List<Profile> doInBackground(File... files) {
final List<String> interfaceNames = new LinkedList<>();
final List<Profile> loadedProfiles = new LinkedList<>();
final String command = "wg show interfaces";
if (rootShell.run(interfaceNames, command) == 0 && interfaceNames.size() == 1) {
// wg puts all interface names on the same line. Split them into separate elements.
final String nameList = interfaceNames.get(0);
Collections.addAll(interfaceNames, nameList.split(" "));
interfaceNames.remove(0);
} else {
interfaceNames.clear();
Log.w(TAG, "Can't enumerate network interfaces. All profiles will appear down.");
}
for (File file : files) {
if (isCancelled())
return null;
final String fileName = file.getName();
final String profileName = fileName.substring(0, fileName.length() - 5);
final Profile profile = new Profile(profileName);
Log.v(TAG, "Attempting to load profile " + profileName);
try {
profile.parseFrom(openFileInput(fileName));
profile.setIsConnected(interfaceNames.contains(profileName));
loadedProfiles.add(profile);
} catch (IOException | IndexOutOfBoundsException e) {
Log.w(TAG, "Failed to load profile from " + fileName, e);
}
}
return loadedProfiles;
}
@Override
protected void onPostExecute(List<Profile> loadedProfiles) {
if (loadedProfiles == null)
return;
profiles.addAll(loadedProfiles);
}
}
private class ProfileRemover extends AsyncTask<Void, Void, Boolean> {
private final Profile profile;
private final Profile replaceWith;
private final boolean shouldConnect;
private ProfileRemover(Profile profile, Profile replaceWith, Boolean shouldConnect) {
super();
this.profile = profile;
this.replaceWith = replaceWith;
this.shouldConnect = shouldConnect != null ? shouldConnect : false;
}
@Override
protected Boolean doInBackground(Void... voids) {
Log.i(TAG, "Removing profile " + profile.getName());
final File configFile = new File(getFilesDir(), profile.getName() + ".conf");
if (configFile.delete()) {
return true;
} else {
Log.e(TAG, "Could not delete configuration for profile " + profile.getName());
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
if (!result)
return;
profiles.remove(profile);
if (replaceWith != null)
new ProfileAdder(replaceWith, shouldConnect).execute();
}
}
private class ProfileServiceBinder extends Binder implements ProfileServiceInterface {
@Override
public void connectProfile(Profile profile) {
if (!profiles.contains(profile))
return;
if (profile.getIsConnected())
return;
new ProfileConnecter(profile).execute();
}
@Override
public Profile copyProfileForEditing(Profile profile) {
if (!profiles.contains(profile))
return null;
return profile.copy();
}
@Override
public void disconnectProfile(Profile profile) {
if (!profiles.contains(profile))
return;
if (!profile.getIsConnected())
return;
new ProfileDisconnecter(profile).execute();
}
@Override
public ObservableList<Profile> getProfiles() {
return profiles;
}
@Override
public void removeProfile(Profile profile) {
if (!profiles.contains(profile))
return;
if (profile.getIsConnected())
new ProfileDisconnecter(profile).execute();
new ProfileRemover(profile, null, null).execute();
}
@Override
public void saveProfile(Profile oldProfile, Profile newProfile) {
if (oldProfile != null) {
if (!profiles.contains(oldProfile))
return;
final boolean wasConnected = oldProfile.getIsConnected();
if (wasConnected)
new ProfileDisconnecter(oldProfile).execute();
new ProfileRemover(oldProfile, newProfile, wasConnected).execute();
} else {
new ProfileAdder(newProfile, false).execute();
}
}
}
}