Rework module validation and setup

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/27/head
androidacy-user 3 years ago
parent e17e839f2d
commit fc3406ce08

@ -9,7 +9,6 @@ import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@ -136,39 +135,14 @@ public class AppUpdateManager {
}
public void checkUpdateCompat() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Checking compatibility flags");
if (this.compatFile.exists()) {
long lastUpdate = this.compatFile.lastModified();
if (lastUpdate <= System.currentTimeMillis() && lastUpdate + 600_000L > System.currentTimeMillis()) {
return; // Skip update
}
}
try {
if (BuildConfig.DEBUG)
Log.d(TAG, "Downloading compatibility flags");
JSONObject object = new JSONObject(new String(Http.doHttpGet(COMPAT_API_URL, false), StandardCharsets.UTF_8));
if (object.isNull("body")) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Compatibility flags not found");
compatDataId.clear();
try {
Files.write(compatFile, new byte[0]);
return;
}
if (BuildConfig.DEBUG)
Log.d(TAG, "Parsing compatibility flags");
byte[] rawData = object.getString("body").getBytes(StandardCharsets.UTF_8);
this.parseCompatibilityFlags(new ByteArrayInputStream(rawData));
Files.write(compatFile, rawData);
if (BuildConfig.DEBUG)
Log.d(TAG, "Compatibility flags update finishing");
return;
} catch (
Exception e) {
Log.e("AppUpdateManager", "Failed to update compat list", e);
} catch (IOException e) {
e.printStackTrace();
}
if (BuildConfig.DEBUG)
Log.d(TAG, "Compatibility flags updated");
// There once lived an implementation that used a GitHub API to get the compatibility flags. It was removed because it was too slow and the API was rate limited.
Log.w(TAG, "Remote compatibility data flags are not implemented.");
}
public boolean peekShouldUpdate() {

@ -27,7 +27,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.PopupMenu;
import androidx.appcompat.widget.SearchView;
import androidx.cardview.widget.CardView;
import androidx.core.app.ActivityCompat;
@ -775,55 +774,48 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
// Setup popup dialogue for the setup_theme_button
MaterialButton themeButton = view.findViewById(R.id.setup_theme_button);
themeButton.setOnClickListener(v -> {
// Create a new popup menu
PopupMenu popupMenu = new PopupMenu(this, themeButton);
// Inflate the menu
popupMenu.getMenuInflater().inflate(R.menu.theme_menu, popupMenu.getMenu());
// Create a new dialog for the theme picker
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.setup_theme_title);
// Create a new array of theme names (system, light, dark, black, transparent light)
String[] themeNames = new String[]{getString(R.string.theme_system), getString(R.string.theme_light), getString(R.string.theme_dark), getString(R.string.theme_black), getString(R.string.theme_transparent_light)};
// Create a new array of theme values (system, light, dark, black, transparent_light)
String[] themeValues = new String[]{"system", "light", "dark", "black", "transparent_light"};
// if pref_theme is set, check the relevant theme_* menu item, otherwise check the default (theme_system)
String prefTheme = prefs.getString("pref_theme", "system");
if (BuildConfig.DEBUG)
Log.i("SetupWizard", "pref_theme: " + prefTheme);
int checkedItem = 0;
switch (prefTheme) {
case "system":
break;
case "light":
popupMenu.getMenu().findItem(R.id.theme_light).setChecked(true);
checkedItem = 1;
break;
case "dark":
popupMenu.getMenu().findItem(R.id.theme_dark).setChecked(true);
checkedItem = 2;
break;
case "system":
popupMenu.getMenu().findItem(R.id.theme_system).setChecked(true);
break;
// Black and transparent_light
case "black":
popupMenu.getMenu().findItem(R.id.theme_black).setChecked(true);
checkedItem = 3;
break;
case "transparent_light":
popupMenu.getMenu().findItem(R.id.theme_transparent_light).setChecked(true);
checkedItem = 4;
break;
}
// Set the on click listener
popupMenu.setOnMenuItemClickListener(item -> {
if (item == null) {
return false;
}
// Make sure it.s an actual item, not the overflow menu. Actual items have an id of theme_* (see theme_menu.xml)
// Check if item id contains theme_ and return false if it doesn't
String itemId = getResources().getResourceEntryName(item.getItemId());
if (!itemId.contains("theme_")) {
return false;
}
// Save the theme. ID is theme_* so we need to remove the first 6 characters
// Possible values are light, dark, system, transparent_light, and black
prefs.edit().putString("pref_theme", item.getItemId() == R.id.theme_light ? "light" : item.getItemId() == R.id.theme_dark ? "dark" : item.getItemId() == R.id.theme_system ? "system" : item.getItemId() == R.id.theme_transparent_light ? "transparent_light" : "black").commit();
builder.setCancelable(true);
// Create the dialog
builder.setSingleChoiceItems(themeNames, checkedItem, (dialog, which) -> {
// Set the theme
prefs.edit().putString("pref_theme", themeValues[which]).commit();
// Set the theme button text to the selected theme
themeButton.setText(themeNames[which]);
// Dismiss the dialog
dialog.dismiss();
// Set the theme
UiThreadHandler.handler.postDelayed(() -> {
MainApplication.getINSTANCE().updateTheme();
FoxActivity.getFoxActivity(this).setThemeRecreate(MainApplication.getINSTANCE().getManagerThemeResId());
}, 1);
return true;
});
// Show the popup menu
popupMenu.show();
builder.show();
});
// Set up the buttons
// Cancel button

@ -16,9 +16,12 @@ import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
interface NotificationTypeCst {
@ -151,11 +154,34 @@ public enum NotificationType implements NotificationTypeCst {
public static boolean needPatch(File target) throws IOException {
try (ZipFile zipFile = new ZipFile(target)) {
return zipFile.getEntry("module.prop") == null &&
zipFile.getEntry("anykernel.sh") == null &&
zipFile.getEntry("META-INF/com/google/android/magisk/module.prop") == null;
boolean validEntries = zipFile.getEntry("module.prop") == null && zipFile.getEntry("anykernel.sh") == null && zipFile.getEntry("META-INF/com/google/android/magisk/module.prop") == null;
if (validEntries) {
// Ensure id of module is not empty and matches ^[a-zA-Z][a-zA-Z0-9._-]+$ regex
// We need to get the module.prop and parse the id= line
ZipEntry moduleProp = zipFile.getEntry("module.prop");
// Parse the module.prop
if (moduleProp != null) {
// Find the line with id=, and check if it matches the regex
try (BufferedReader reader = new BufferedReader(new InputStreamReader(zipFile.getInputStream(moduleProp)))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("id=")) {
String id = line.substring(3);
return id.isEmpty() || !id.matches("^[a-zA-Z][a-zA-Z0-9._-]+$");
}
}
}
} else {
return true;
}
} else {
return true;
}
} catch (IOException e) {
return true;
}
return false;
}
@StringRes
public final int textId;

@ -292,9 +292,7 @@ public final class AndroidacyRepoData extends RepoData {
for (int i = 0; i < len; i++) {
jsonObject = jsonArray.getJSONObject(i);
String moduleId = jsonObject.getString("codename");
// Deny remote modules ids shorter than 3 chars or containing null char or space
if (moduleId.length() < 3 || moduleId.indexOf('\0') != -1 || moduleId.indexOf(' ') != -1 || "ak3-helper".equals(moduleId))
continue;
// Normally, we'd validate the module id here, but we don't need to because the server does it for us
long lastUpdate = jsonObject.getLong("updated_at") * 1000;
lastLastUpdate = Math.max(lastLastUpdate, lastUpdate);
RepoModule repoModule = this.moduleHashMap.get(moduleId);

@ -199,6 +199,18 @@ public enum ActionButtonType {
public void doAction(Chip button, ModuleHolder moduleHolder) {
IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate);
}
}, WARNING() {
@Override
public void update(Chip button, ModuleHolder moduleHolder) {
button.setChipIcon(button.getContext().getDrawable(R.drawable.ic_baseline_warning_24));
button.setText(R.string.warning);
}
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.warning).setMessage(R.string.warning_message).setPositiveButton(R.string.understand, (v, i) -> {
}).create().show();
}
};
@DrawableRes

@ -172,6 +172,10 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
public void getButtons(Context context, List<ActionButtonType> buttonTypeList, boolean showcaseMode) {
if (!this.isModuleHolder()) return;
LocalModuleInfo localModuleInfo = this.moduleInfo;
// Add warning button if module id begins with a dot - this is a hidden module which could indicate malware
if (this.moduleId.startsWith(".") || !this.moduleId.matches("^[a-zA-Z][a-zA-Z0-9._-]+$")) {
buttonTypeList.add(ActionButtonType.WARNING);
}
if (localModuleInfo != null && !showcaseMode) {
buttonTypeList.add(ActionButtonType.UNINSTALL);
}

@ -104,9 +104,14 @@ public class RepoData extends XRepo {
for (int i = 0; i < len; i++) {
JSONObject module = array.getJSONObject(i);
String moduleId = module.getString("id");
// Deny remote modules ids shorter than 3 chars or containing null char or space
if (moduleId.length() < 3 || moduleId.indexOf('\0') != -1 || moduleId.indexOf(' ') != -1 || "ak3-helper".equals(moduleId))
// module IDs must match the regex ^[a-zA-Z][a-zA-Z0-9._-]+$ and cannot be empty or null or equal ak3-helper
if (moduleId.isEmpty() || moduleId.equals("ak3-helper") || !moduleId.matches("^[a-zA-Z][a-zA-Z0-9._-]+$")) {
continue;
}
// If module id start with a dot, warn user
if (moduleId.charAt(0) == '.') {
Log.w("MMM", "Module ID " + moduleId + " in repo " + this.url + " start with a dot, this is not recommended and may indicate an attempt to hide the module");
}
long moduleLastUpdate = module.getLong("last_update");
String moduleNotesUrl = module.getString("notes_url");
String modulePropsUrl = module.getString("prop_url");

@ -0,0 +1,56 @@
package com.fox2code.mmm.utils;
// Original written by tsuharesu
// Adapted to create a "drop it in and watch it work" approach by Nikhil Jha.
// Just add your package statement and drop it in the folder with all your other classes.
import android.content.Context;
import androidx.annotation.NonNull;
import com.fox2code.mmm.MainApplication;
import java.io.IOException;
import java.util.HashSet;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* This interceptor put all the Cookies in Preferences in the Request.
* Your implementation on how to get the Preferences may ary, but this will work 99% of the time.
*/
public class AddCookiesInterceptor implements Interceptor {
public static final String PREF_COOKIES = "PREF_COOKIES";
// We're storing our stuff in a database made just for cookies called PREF_COOKIES.
// I reccomend you do this, and don't change this default value.
private final Context context;
public AddCookiesInterceptor(Context context) {
this.context = context;
}
@NonNull
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request.Builder builder = chain.request().newBuilder();
HashSet<String> preferences = (HashSet<String>) MainApplication.getSharedPreferences().getStringSet(PREF_COOKIES, new HashSet<>());
// Use the following if you need everything in one line.
// Some APIs die if you do it differently.
StringBuilder cookiestring = new StringBuilder();
for (String cookie : preferences) {
String[] parser = cookie.split(";");
cookiestring.append(parser[0]).append("; ");
}
builder.addHeader("Cookie", cookiestring.toString());
for (String cookie : preferences) {
builder.addHeader("Cookie", cookie);
}
return chain.proceed(builder.build());
}
}

@ -117,7 +117,9 @@ public class Http {
RuntimeException e) {
Log.e(TAG, "Failed to init DoH", e);
}
httpclientBuilder.cookieJar(CookieJar.NO_COOKIES);
// Add cookie support.
httpclientBuilder.addInterceptor(new AddCookiesInterceptor(MainApplication.getINSTANCE().getApplicationContext())); // VERY VERY IMPORTANT
httpclientBuilder.addInterceptor(new ReceivedCookiesInterceptor(MainApplication.getINSTANCE().getApplicationContext())); // VERY VERY IMPORTANT
// User-Agent format was agreed on telegram
if (hasWebView) {
androidacyUA = WebSettings.getDefaultUserAgent(mainApplication).replace("wv", "") + " FoxMMM/" + BuildConfig.VERSION_CODE;
@ -224,10 +226,17 @@ public class Http {
@SuppressLint("RestrictedApi")
@SuppressWarnings("resource")
public static byte[] doHttpGet(String url, boolean allowCache) throws IOException {
if (BuildConfig.DEBUG) {
// Log, but set all query parameters values to "****" while keeping the keys
Log.d(TAG, "doHttpGet: " + url.replaceAll("=[^&]*", "=****"));
}
Response response = (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).get().build()).execute();
if (BuildConfig.DEBUG) {
Log.d(TAG, "doHttpGet: request executed");
}
// 200/204 == success, 304 == cache valid
if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) {
Log.e(TAG, "Failed to fetch " + url + ", code: " + response.code());
Log.e(TAG, "Failed to fetch " + url.replaceAll("=[^&]*", "=****") + " with code " + response.code());
checkNeedCaptchaAndroidacy(url, response.code());
// If it's a 401, and an androidacy link, it's probably an invalid token
if (response.code() == 401 && AndroidacyUtil.isAndroidacyLink(url)) {
@ -236,6 +245,9 @@ public class Http {
}
throw new HttpException(response.code());
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "doHttpGet: " + url.replaceAll("=[^&]*", "=****") + " succeeded");
}
ResponseBody responseBody = response.body();
// Use cache api if used cached response
if (response.code() == 304) {
@ -243,6 +255,9 @@ public class Http {
if (response != null)
responseBody = response.body();
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "doHttpGet: returning " + responseBody.contentLength() + " bytes");
}
return responseBody.bytes();
}

@ -0,0 +1,44 @@
package com.fox2code.mmm.utils;
// Original written by tsuharesu
// Adapted to create a "drop it in and watch it work" approach by Nikhil Jha.
// Just add your package statement and drop it in the folder with all your other classes.
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import com.fox2code.mmm.MainApplication;
import java.io.IOException;
import java.util.HashSet;
import okhttp3.Interceptor;
import okhttp3.Response;
public class ReceivedCookiesInterceptor implements Interceptor {
private final Context context;
public ReceivedCookiesInterceptor(Context context) {
this.context = context;
} // AddCookiesInterceptor()
@NonNull
@SuppressLint({"MutatingSharedPrefs", "ApplySharedPref"})
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
if (!originalResponse.headers("Set-Cookie").isEmpty()) {
HashSet<String> cookies = (HashSet<String>) MainApplication.getSharedPreferences().getStringSet("PREF_COOKIES", new HashSet<>());
cookies.addAll(originalResponse.headers("Set-Cookie"));
SharedPreferences.Editor memes = MainApplication.getSharedPreferences().edit();
memes.putStringSet("PREF_COOKIES", cookies).apply();
memes.commit();
}
return originalResponse;
}
}

@ -38,19 +38,11 @@
android:text="@string/setup_message" />
<!-- Theme radio select. Options are system, light, dark, black, transparent_light -->
<!-- First, choose theme header -->
<TextView
android:id="@+id/setup_theme_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/setup_theme_header"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" />
<!-- Button to trigger theme selection -->
<com.google.android.material.button.MaterialButton
android:id="@+id/setup_theme_button"
android:layout_width="320dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:backgroundTint="@color/gray_900"
@ -74,7 +66,7 @@
android:text="@string/repos"
android:textAppearance="@android:style/TextAppearance.Material.Headline" /><com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/setup_androidacy_repo"
android:layout_width="320dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:checked="false"
@ -94,7 +86,7 @@
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/setup_magisk_alt_repo"
android:layout_width="320dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:checked="false"
@ -128,7 +120,7 @@
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/setup_crash_reporting"
android:layout_width="320dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:checked="false"
@ -147,7 +139,7 @@
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/setup_background_update_check"
android:layout_width="320dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:checked="false"

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Options for system, dark, light, black, transparent_light -->
<!-- Only one of these can be selected at a time -->
<!-- Radio buttons -->
<!-- Themes are taken from theme_values_names string array -->
<item
android:id="@+id/theme"
android:icon="@drawable/ic_baseline_design_services_24"
android:title="@string/theme"
app:showAsAction="never">
<menu>
<group android:checkableBehavior="single">
<!-- System is default selected -->
<item
android:id="@+id/theme_system"
android:title="@string/theme_system"
app:showAsAction="never" />
<item
android:id="@+id/theme_dark"
android:title="@string/theme_dark"
app:showAsAction="never" />
<item
android:id="@+id/theme_black"
android:title="@string/theme_black"
app:showAsAction="never" />
<item
android:id="@+id/theme_transparent_light"
android:title="@string/theme_transparent_light"
app:showAsAction="never" />
<item
android:id="@+id/theme_light"
android:title="@string/theme_light"
app:showAsAction="never" />
</group>
</menu>
</item>
</menu>

@ -276,5 +276,5 @@
<string name="androidacy_thanks">Developed in part by Androidacy</string>
<string name="androidacy_thanks_desc">Huge shoutout to Androidacy for their integration and contributions to the app.</string>
<string name="contributors">And of course, thanks to all of our contributors, whether it\'s translations, code, or just being fun to hang out with! We love you all.</string>
<string name="zip_unpacking">Inspecting module…</string>
<string name="zip_unpacking">Inspecting module…</string><string name="warning_message">Warning!This module has indicators it may have been installed without your knowledge, or may be attempting to hide itself. Uninstall is strongly recommended.</string><string name="understand">I understand</string><string name="setup_theme_title">Choose theme</string>
</resources>

Loading…
Cancel
Save