diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f829c41..79b5dfb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,10 +39,15 @@
+
+
.
+ *
+ * Copyright 2023 - Emanuele Faranda
+ */
+
+package com.pcapdroid.mitm;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.UriPermission;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+import android.provider.DocumentsContract;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class AddonsActivity extends Activity implements AddonsAdapter.AddonListener {
+ private static final String TAG = "UserAddons";
+ private static final String USER_DIR_PREF = "user-dir";
+ private static final String ENABLED_ADDONS_PREF = "enabled-addons";
+ private static final int OPEN_DIR_TREE_CODE = 1;
+
+ private TextView mEmptyText;
+ private SharedPreferences mPrefs;
+ private AddonsAdapter mAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.addons);
+ setContentView(R.layout.simple_list);
+
+ mEmptyText = findViewById(R.id.list_empty);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ mAdapter = new AddonsAdapter(this);
+ mAdapter.setListener(this);
+ ((ListView)findViewById(R.id.listview))
+ .setAdapter(mAdapter);
+ refreshAddons();
+ }
+
+ private void refreshAddons() {
+ Set enabledAddons = getEnabledAddons(this);
+ List addons = new ArrayList<>();
+
+ // Internal addons
+ addons.add(new Addon("Js Injector",
+ "Inject javascript into web pages",
+ enabledAddons.contains("Js Injector"),
+ Addon.AddonType.JsInjector));
+
+ // User addons
+ Uri publicUri = Uri.parse(mPrefs.getString(USER_DIR_PREF, ""));
+ String descr = getString(R.string.user_addon);
+
+ if((publicUri.getHost() != null) && hasUriPersistablePermission(this, publicUri)) {
+ for (Uri uri : listUserAddons(this, publicUri)) {
+ String path = uri.getPath();
+ int slash = path.lastIndexOf("/");
+ if (slash > 0) {
+ String fname = path.substring(slash + 1);
+ if (fname.endsWith(".py")) {
+ String script = fname.substring(0, fname.length() - 3);
+ addons.add(new Addon(script, descr, enabledAddons.contains(script)));
+ }
+ }
+ }
+ }
+
+ mAdapter.reload(addons);
+ recheckListSize();
+
+ if(MitmService.isRunning())
+ (Toast.makeText(this, R.string.restart_to_apply, Toast.LENGTH_SHORT)).show();
+ }
+
+ // get the file name of the enabled addons
+ public static Set getEnabledAddons(Context ctx) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+ return prefs.getStringSet(ENABLED_ADDONS_PREF, new HashSet<>());
+ }
+
+ private void recheckListSize() {
+ mEmptyText.setVisibility((mAdapter.getCount() == 0) ? View.VISIBLE : View.GONE);
+ }
+
+ public static List listUserAddons(Context ctx, Uri publicUri) {
+ List rv = new ArrayList<>();
+
+ try {
+ Uri srcFolder = DocumentsContract.buildChildDocumentsUriUsingTree(publicUri, DocumentsContract.getTreeDocumentId(publicUri));
+
+ try (Cursor cursor = ctx.getContentResolver().query(srcFolder,
+ new String[]{ DocumentsContract.Document.COLUMN_DOCUMENT_ID },
+ null, null, null)) {
+ if ((cursor != null) && cursor.moveToFirst()) {
+ do {
+ Uri uri = DocumentsContract.buildDocumentUriUsingTree(publicUri, cursor.getString(0));
+ rv.add(uri);
+ } while (cursor.moveToNext());
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(TAG, e.toString());
+ }
+
+ return rv;
+ }
+
+ /* Since we can only access the addons dir via the ContentResolver, we copy all the addons
+ * to the app private dir to make python import work. */
+ public static boolean copyAddonsToPrivDir(Context ctx, String privDir) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+ Uri publicUri = Uri.parse(prefs.getString(USER_DIR_PREF, ""));
+
+ if((publicUri.getHost() == null) || !hasUriPersistablePermission(ctx, publicUri))
+ return false;
+
+ File privAddons = new File(privDir);
+ privAddons.delete();
+ privAddons.mkdirs();
+
+ Uri srcFolder = DocumentsContract.buildChildDocumentsUriUsingTree(publicUri, DocumentsContract.getTreeDocumentId(publicUri));
+ Log.d(TAG, "Addons source dir: " + srcFolder);
+ List addonsUris = listUserAddons(ctx, publicUri);
+
+ for(Uri srcUri: addonsUris) {
+ String path = srcUri.getPath();
+ int slash = path.lastIndexOf("/");
+ if (slash > 0) {
+ String srcFname = path.substring(slash + 1);
+ File outFile = new File(privAddons.getAbsolutePath() + "/" + srcFname);
+ Log.d(TAG, "Found addon: " + srcFname);
+
+ // Copy from srcUri to outFile
+ try {
+ try (InputStream in = new BufferedInputStream(ctx.getContentResolver().openInputStream(srcUri))) {
+ try (OutputStream out = new BufferedOutputStream(new FileOutputStream(outFile))) {
+ byte[] bytesIn = new byte[4096];
+ int read;
+ while ((read = in.read(bytesIn)) != -1)
+ out.write(bytesIn, 0, read);
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ Log.e(TAG, e.toString());
+
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean hasUriPersistablePermission(Context ctx, Uri uri) {
+ List persistableUris = ctx.getContentResolver().getPersistedUriPermissions();
+
+ for(UriPermission perm: persistableUris) {
+ if(perm.getUri().equals(uri) && perm.isReadPermission())
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.addons, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if(id == R.id.show_hint) {
+ Utils.showHintDialog(this, R.string.addons_hint);
+ return true;
+ } else if(id == R.id.update) {
+ refreshAddons();
+ return true;
+ } else if(id == R.id.select_user_dir) {
+ selectUserDir();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void selectUserDir() {
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // Initial path
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(Environment.getExternalStorageDirectory().getAbsolutePath()));
+ }
+
+ (Toast.makeText(this, R.string.specify_user_dir, Toast.LENGTH_LONG)).show();
+ startActivityForResult(intent, OPEN_DIR_TREE_CODE);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent resultData) {
+ if((requestCode == OPEN_DIR_TREE_CODE) && (resultCode == RESULT_OK) && (resultData != null)) {
+ Uri user_dir = resultData.getData();
+
+ // Persist access across restarts
+ getContentResolver().takePersistableUriPermission(user_dir, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ mPrefs.edit()
+ .putString(USER_DIR_PREF, user_dir.toString())
+ .apply();
+
+ refreshAddons();
+ }
+ }
+
+ @Override
+ public void onAddonToggled(Addon addon, boolean enabled) {
+ Set enabledAddons = getEnabledAddons(AddonsActivity.this);
+ addon.enabled = enabled;
+
+ if(enabled)
+ enabledAddons.add(addon.fname);
+ else
+ enabledAddons.remove(addon.fname);
+
+ mPrefs.edit()
+ .putStringSet(ENABLED_ADDONS_PREF, enabledAddons)
+ .apply();
+
+ refreshAddons();
+ }
+
+ @Override
+ public void onAddonSettingsClicked(Addon addon) {
+ if(addon.type == Addon.AddonType.JsInjector) {
+ Intent intent = new Intent(this, JsInjectorActivity.class);
+ startActivity(intent);
+ }
+ }
+}
diff --git a/app/src/main/java/com/pcapdroid/mitm/AddonsAdapter.java b/app/src/main/java/com/pcapdroid/mitm/AddonsAdapter.java
new file mode 100644
index 0000000..40952cd
--- /dev/null
+++ b/app/src/main/java/com/pcapdroid/mitm/AddonsAdapter.java
@@ -0,0 +1,92 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2023 - Emanuele Faranda
+ */
+
+package com.pcapdroid.mitm;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+public class AddonsAdapter extends ArrayAdapter {
+ public interface AddonListener {
+ void onAddonToggled(Addon addon, boolean enabled);
+ void onAddonSettingsClicked(Addon addon);
+ }
+
+ private final LayoutInflater mLayoutInflater;
+ private WeakReference mListener;
+
+ public AddonsAdapter(Context context) {
+ super(context, R.layout.addon_item);
+
+ mLayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if(convertView == null) {
+ convertView = mLayoutInflater.inflate(R.layout.addon_item, parent, false);
+
+ convertView.findViewById(R.id.toggle_btn)
+ .setOnClickListener((v) -> {
+ AddonListener listener = mListener.get();
+ if(listener != null)
+ listener.onAddonToggled(getItem(position), ((Switch)v).isChecked());
+ });
+
+ convertView.findViewById(R.id.settings)
+ .setOnClickListener((v) -> {
+ AddonListener listener = mListener.get();
+ if(listener != null)
+ listener.onAddonSettingsClicked(getItem(position));
+ });
+ }
+
+ TextView fname = convertView.findViewById(R.id.fname);
+ TextView info = convertView.findViewById(R.id.info);
+ Switch toggle = convertView.findViewById(R.id.toggle_btn);
+ ImageButton settings = convertView.findViewById(R.id.settings);
+
+ Addon addon = getItem(position);
+ fname.setText(addon.fname);
+ info.setText(addon.description);
+ toggle.setChecked(addon.enabled);
+ settings.setVisibility((addon.type == Addon.AddonType.UserAddon) ?
+ View.GONE : View.VISIBLE);
+
+ return convertView;
+ }
+
+ public void setListener(AddonListener listener) {
+ mListener = new WeakReference<>(listener);
+ }
+
+ public void reload(List addons) {
+ clear();
+ addAll(addons);
+ }
+}
diff --git a/app/src/main/java/com/pcapdroid/mitm/JsInjectorActivity.java b/app/src/main/java/com/pcapdroid/mitm/JsInjectorActivity.java
index 77cf59f..b3b28a3 100644
--- a/app/src/main/java/com/pcapdroid/mitm/JsInjectorActivity.java
+++ b/app/src/main/java/com/pcapdroid/mitm/JsInjectorActivity.java
@@ -1,8 +1,26 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2023 - Emanuele Faranda
+ */
+
package com.pcapdroid.mitm;
import android.app.Activity;
import android.app.AlertDialog;
-import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
@@ -10,8 +28,6 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
-import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.ActionMode;
import android.view.LayoutInflater;
@@ -248,7 +264,7 @@ public class JsInjectorActivity extends Activity {
int id = item.getItemId();
if(id == R.id.show_hint) {
- showHintDialog(R.string.js_injector_hint);
+ Utils.showHintDialog(this, R.string.js_injector_hint);
return true;
} else if(id == R.id.add) {
showAddScriptDialog();
@@ -260,29 +276,12 @@ public class JsInjectorActivity extends Activity {
}
refreshScripts();
+ return true;
}
return super.onOptionsItemSelected(item);
}
- private void showHintDialog(int id) {
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setTitle(R.string.hint);
- builder.setMessage(getString(id));
- builder.setCancelable(true);
- builder.setNeutralButton(android.R.string.ok,
- (dialog, id1) -> dialog.cancel());
-
- AlertDialog alert = builder.create();
- alert.show();
-
- TextView message = (TextView)alert.findViewById(android.R.id.message);
- if(message != null) {
- message.setMovementMethod(LinkMovementMethod.getInstance());
- message.setText(id);
- }
- }
-
private void showAddScriptDialog() {
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.add_script_dialog, null);
diff --git a/app/src/main/java/com/pcapdroid/mitm/MainActivity.java b/app/src/main/java/com/pcapdroid/mitm/MainActivity.java
index 071965f..340c59f 100644
--- a/app/src/main/java/com/pcapdroid/mitm/MainActivity.java
+++ b/app/src/main/java/com/pcapdroid/mitm/MainActivity.java
@@ -62,9 +62,9 @@ public class MainActivity extends Activity {
(Toast.makeText(this, R.string.app_not_found, Toast.LENGTH_SHORT)).show();
});
- findViewById(R.id.open_js_injector).setOnClickListener(v -> {
- Intent intent = new Intent(MainActivity.this, JsInjectorActivity.class);
+ findViewById(R.id.addons).setOnClickListener(v -> {
+ Intent intent = new Intent(MainActivity.this, AddonsActivity.class);
startActivity(intent);
});
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pcapdroid/mitm/MitmService.java b/app/src/main/java/com/pcapdroid/mitm/MitmService.java
index d6e9da6..514252f 100644
--- a/app/src/main/java/com/pcapdroid/mitm/MitmService.java
+++ b/app/src/main/java/com/pcapdroid/mitm/MitmService.java
@@ -36,6 +36,7 @@ import com.chaquo.python.Python;
import java.io.IOException;
import java.lang.ref.WeakReference;
+import java.util.Set;
import com.pcapdroid.mitm.MitmAPI.MitmConfig;
@@ -47,12 +48,18 @@ public class MitmService extends Service implements Runnable {
Thread mThread;
PyObject mitm;
MitmConfig mConf;
+ String m_home;
@Override
public void onCreate() {
Python py = Python.getInstance();
mitm = py.getModule("mitm");
+ PyObject os = py.getModule("os");
+ PyObject env = os.get("environ");
+ m_home = env.callAttr("get", "HOME").toString();
+ Log.d(TAG, "Chaquopy home at " + m_home);
+
INSTANCE = this;
super.onCreate();
}
@@ -157,8 +164,12 @@ public class MitmService extends Service implements Runnable {
// Transparent mode is used with root mode where we capture the internet interface, so we must dump the server connection
boolean dump_client = !mConf.transparentMode;
+ AddonsActivity.copyAddonsToPrivDir(this, m_home);
+ String[] enabled_addons = AddonsActivity.getEnabledAddons(this).toArray(new String[]{});
+
try {
- mitm.callAttr("run", mFd.getFd(), dump_client, mConf.dumpMasterSecrets, mConf.shortPayload, args);
+ mitm.callAttr("run", mFd.getFd(), enabled_addons, dump_client,
+ mConf.dumpMasterSecrets, mConf.shortPayload, args);
} finally {
try {
if(mFd != null)
@@ -244,4 +255,8 @@ public class MitmService extends Service implements Runnable {
if(instance != null)
instance.mitm.callAttr("reloadJsUserscripts");
}
+
+ public static boolean isRunning() {
+ return (INSTANCE != null);
+ }
}
diff --git a/app/src/main/java/com/pcapdroid/mitm/ScriptsAdapter.java b/app/src/main/java/com/pcapdroid/mitm/ScriptsAdapter.java
index 6ef75e2..d4f6990 100644
--- a/app/src/main/java/com/pcapdroid/mitm/ScriptsAdapter.java
+++ b/app/src/main/java/com/pcapdroid/mitm/ScriptsAdapter.java
@@ -1,3 +1,22 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2023 - Emanuele Faranda
+ */
+
package com.pcapdroid.mitm;
import android.content.Context;
diff --git a/app/src/main/java/com/pcapdroid/mitm/Utils.java b/app/src/main/java/com/pcapdroid/mitm/Utils.java
new file mode 100644
index 0000000..5d56616
--- /dev/null
+++ b/app/src/main/java/com/pcapdroid/mitm/Utils.java
@@ -0,0 +1,45 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2023 - Emanuele Faranda
+ */
+
+package com.pcapdroid.mitm;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.text.method.LinkMovementMethod;
+import android.widget.TextView;
+
+public class Utils {
+ public static void showHintDialog(Context ctx, int id) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
+ builder.setTitle(R.string.hint);
+ builder.setMessage(ctx.getString(id));
+ builder.setCancelable(true);
+ builder.setNeutralButton(android.R.string.ok,
+ (dialog, id1) -> dialog.cancel());
+
+ AlertDialog alert = builder.create();
+ alert.show();
+
+ TextView message = (TextView)alert.findViewById(android.R.id.message);
+ if(message != null) {
+ message.setMovementMethod(LinkMovementMethod.getInstance());
+ message.setText(id);
+ }
+ }
+}
diff --git a/app/src/main/python/mitm.py b/app/src/main/python/mitm.py
index b2916e2..7b4b14b 100644
--- a/app/src/main/python/mitm.py
+++ b/app/src/main/python/mitm.py
@@ -21,6 +21,7 @@
import os
MITMPROXY_CONF_DIR = os.environ["HOME"] + "/.mitmproxy"
+USER_ADDONS_DIR = os.environ["HOME"] + "/mitmproxy-addons"
CA_CERT_PATH = MITMPROXY_CONF_DIR + "/mitmproxy-ca-cert.cer"
from mitmproxy import options
@@ -37,6 +38,7 @@ import traceback
import socket
import asyncio
import sys
+import importlib
master = None
pcapdroid = None
@@ -83,9 +85,26 @@ def server_event_proxy(handler, event):
pcapdroid.server_error(hook_data)
return orig_server_event(handler, event)
+def load_addon(modname, addons):
+ try:
+ m = importlib.import_module(modname)
+ if m and hasattr(m, "addons") and isinstance(m.addons, list):
+ for addon in m.addons:
+ addons.add(addon)
+ except Exception:
+ sys.stderr.write("Failed to load addon " + modname)
+ sys.stderr.write(traceback.format_exc())
+
+def jarray_to_set(arr):
+ rv = set()
+ for elem in arr:
+ rv.add(elem)
+ return rv
+
# Entrypoint: runs mitmproxy
# From mitmproxy.tools.main.run, without the signal handlers
-def run(fd: int, dump_client: bool, dump_keylog: bool, short_payload: bool, mitm_args: str):
+def run(fd: int, jenabled_addons, dump_client: bool, dump_keylog: bool,
+ short_payload: bool, mitm_args: str):
global master
global running
global pcapdroid, js_injector
@@ -99,14 +118,28 @@ def run(fd: int, dump_client: bool, dump_keylog: bool, short_payload: bool, mitm
opts = options.Options()
master = dump.DumpMaster(opts)
- # JsInjector addon (before PCAPdroid)
- js_injector = JsInjector()
- master.addons.add(js_injector)
-
# instantiate PCAPdroid early to send error log via the API
pcapdroid = PCAPdroid(sock, AddonOpts(dump_client, dump_keylog, short_payload))
+
+ enabled_addons = jarray_to_set(jenabled_addons)
+
+ # Load addons (order is important)
master.addons.add(pcapdroid)
+ # JsInjector addon
+ if "Js Injector" in enabled_addons:
+ js_injector = JsInjector()
+ master.addons.add(js_injector)
+
+ sys.path.append(USER_ADDONS_DIR)
+ for f in os.listdir(USER_ADDONS_DIR):
+ if f.endswith(".py"):
+ fname = f[:-3]
+
+ if fname in enabled_addons:
+ print("Loading addon: " + f)
+ load_addon(fname, master.addons)
+
print("mitmdump " + mitm_args)
parser = cmdline.mitmdump(opts)
args = parser.parse_args(mitm_args.split())
diff --git a/app/src/main/res/drawable/ic_baseline_settings.xml b/app/src/main/res/drawable/ic_baseline_settings.xml
new file mode 100644
index 0000000..6db7490
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_settings.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/addon_item.xml b/app/src/main/res/layout/addon_item.xml
new file mode 100644
index 0000000..c136927
--- /dev/null
+++ b/app/src/main/res/layout/addon_item.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml
index ec3a2ef..76bfff1 100644
--- a/app/src/main/res/layout/main_activity.xml
+++ b/app/src/main/res/layout/main_activity.xml
@@ -140,10 +140,10 @@
android:layout_gravity="center" />
\ No newline at end of file
diff --git a/app/src/main/res/menu/addons.xml b/app/src/main/res/menu/addons.xml
new file mode 100644
index 0000000..1d85046
--- /dev/null
+++ b/app/src/main/res/menu/addons.xml
@@ -0,0 +1,24 @@
+
+
\ 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 406b4d6..8d627a5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -30,4 +30,10 @@
Update
Script download is already in progress
App not found
+ Addons
+ Select the directory where your custom scripts are located
+ User addon
+ Set user dir
+ You can create mitmproxy addons to implement your custom logic. Select a user directory and place your scripts there
+ Restart the capture to apply the changes