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>
272 lines
9.5 KiB
Java
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();
|
|
}
|
|
}
|
|
}
|
|
}
|