From fc3406ce0859f3a5f264281fcb5d986f1df1621f Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Tue, 3 Jan 2023 13:23:35 -0500 Subject: [PATCH] Rework module validation and setup Signed-off-by: androidacy-user --- .../com/fox2code/mmm/AppUpdateManager.java | 38 ++----------- .../java/com/fox2code/mmm/MainActivity.java | 56 ++++++++----------- .../com/fox2code/mmm/NotificationType.java | 32 ++++++++++- .../mmm/androidacy/AndroidacyRepoData.java | 4 +- .../fox2code/mmm/module/ActionButtonType.java | 12 ++++ .../com/fox2code/mmm/module/ModuleHolder.java | 4 ++ .../java/com/fox2code/mmm/repo/RepoData.java | 9 ++- .../mmm/utils/AddCookiesInterceptor.java | 56 +++++++++++++++++++ .../java/com/fox2code/mmm/utils/Http.java | 19 ++++++- .../mmm/utils/ReceivedCookiesInterceptor.java | 44 +++++++++++++++ app/src/main/res/layout/setup_box.xml | 18 ++---- app/src/main/res/menu/theme_menu.xml | 38 ------------- app/src/main/res/values/strings.xml | 2 +- 13 files changed, 206 insertions(+), 126 deletions(-) create mode 100644 app/src/main/java/com/fox2code/mmm/utils/AddCookiesInterceptor.java create mode 100644 app/src/main/java/com/fox2code/mmm/utils/ReceivedCookiesInterceptor.java delete mode 100644 app/src/main/res/menu/theme_menu.xml diff --git a/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java b/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java index cf8bc47..760bff2 100644 --- a/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java +++ b/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java @@ -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 - } - } + compatDataId.clear(); 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(); - 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); + Files.write(compatFile, new byte[0]); + } 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() { diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index e1a3512..9e51a73 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -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); - break; - case "system": - popupMenu.getMenu().findItem(R.id.theme_system).setChecked(true); + checkedItem = 2; 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 diff --git a/app/src/main/java/com/fox2code/mmm/NotificationType.java b/app/src/main/java/com/fox2code/mmm/NotificationType.java index be73c83..b057a90 100644 --- a/app/src/main/java/com/fox2code/mmm/NotificationType.java +++ b/app/src/main/java/com/fox2code/mmm/NotificationType.java @@ -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,10 +154,33 @@ 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 diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java index 2116f09..4ee8862 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java @@ -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); diff --git a/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java b/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java index 43e0436..df81b3e 100644 --- a/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java +++ b/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java @@ -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 diff --git a/app/src/main/java/com/fox2code/mmm/module/ModuleHolder.java b/app/src/main/java/com/fox2code/mmm/module/ModuleHolder.java index 18261b3..db7c15f 100644 --- a/app/src/main/java/com/fox2code/mmm/module/ModuleHolder.java +++ b/app/src/main/java/com/fox2code/mmm/module/ModuleHolder.java @@ -172,6 +172,10 @@ public final class ModuleHolder implements Comparable { public void getButtons(Context context, List 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); } diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java index 0625e8d..d21ef3f 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java @@ -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"); diff --git a/app/src/main/java/com/fox2code/mmm/utils/AddCookiesInterceptor.java b/app/src/main/java/com/fox2code/mmm/utils/AddCookiesInterceptor.java new file mode 100644 index 0000000..5ee31ac --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/utils/AddCookiesInterceptor.java @@ -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 preferences = (HashSet) 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()); + } +} diff --git a/app/src/main/java/com/fox2code/mmm/utils/Http.java b/app/src/main/java/com/fox2code/mmm/utils/Http.java index bb1f6b0..1e0f226 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -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(); } diff --git a/app/src/main/java/com/fox2code/mmm/utils/ReceivedCookiesInterceptor.java b/app/src/main/java/com/fox2code/mmm/utils/ReceivedCookiesInterceptor.java new file mode 100644 index 0000000..85a9711 --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/utils/ReceivedCookiesInterceptor.java @@ -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 cookies = (HashSet) 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; + } +} diff --git a/app/src/main/res/layout/setup_box.xml b/app/src/main/res/layout/setup_box.xml index 5feb8c2..b77f4bf 100644 --- a/app/src/main/res/layout/setup_box.xml +++ b/app/src/main/res/layout/setup_box.xml @@ -38,19 +38,11 @@ android:text="@string/setup_message" /> - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ce85db8..a6c08c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -276,5 +276,5 @@ Developed in part by Androidacy Huge shoutout to Androidacy for their integration and contributions to the app. 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. - Inspecting module… + Inspecting module…Warning!This module has indicators it may have been installed without your knowledge, or may be attempting to hide itself. Uninstall is strongly recommended.I understandChoose theme