Add ability to load addons from storage

- Select a folder and load addons from it
- View list of available addons, including Js Injector
- Ability to turn addons on/off

See #8
This commit is contained in:
emanuele-f 2023-06-25 22:40:03 +02:00
parent 1ff8416fd6
commit 746e22c90b
15 changed files with 630 additions and 34 deletions

View File

@ -39,10 +39,15 @@
</activity>
<activity
android:name=".JsInjectorActivity"
android:name=".AddonsActivity"
android:launchMode="singleTop"
android:parentActivityName=".MainActivity" />
<activity
android:name=".JsInjectorActivity"
android:launchMode="singleTop"
android:parentActivityName=".AddonsActivity" />
<service
android:name=".MitmService"
android:exported="true"

View File

@ -0,0 +1,24 @@
package com.pcapdroid.mitm;
public class Addon {
public String fname;
String description;
boolean enabled;
AddonType type;
public enum AddonType {
UserAddon,
JsInjector
}
public Addon(String fname, String description, boolean enabled, AddonType type) {
this.fname = fname;
this.description = description;
this.enabled = enabled;
this.type = type;
}
public Addon(String fname, String description, boolean enabled) {
this(fname, description, enabled, AddonType.UserAddon);
}
}

View File

@ -0,0 +1,284 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* 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<String> enabledAddons = getEnabledAddons(this);
List<Addon> 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<String> 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<Uri> listUserAddons(Context ctx, Uri publicUri) {
List<Uri> 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<Uri> 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<UriPermission> 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<String> 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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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<Addon> {
public interface AddonListener {
void onAddonToggled(Addon addon, boolean enabled);
void onAddonSettingsClicked(Addon addon);
}
private final LayoutInflater mLayoutInflater;
private WeakReference<AddonListener> 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<Addon> addons) {
clear();
addAll(addons);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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);

View File

@ -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);
});
}
}
}

View File

@ -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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Copyright 2023 - Emanuele Faranda
*/
package com.pcapdroid.mitm;
import android.content.Context;

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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);
}
}
}

View File

@ -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())

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#6C6C6C"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:paddingHorizontal="4dp"
android:paddingVertical="8dp"
android:gravity="center_vertical"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="10dp">
<TextView
android:id="@+id/fname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="14sp"
tools:text="Addon"/>
<TextView
android:id="@+id/info"
tools:text="An addon to ..."
android:textSize="12sp"
android:layout_marginTop="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<ImageButton
android:id="@+id/settings"
android:src="@drawable/ic_baseline_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/toggle_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical|end"/>
</LinearLayout>

View File

@ -140,10 +140,10 @@
android:layout_gravity="center" />
<Button
android:id="@+id/open_js_injector"
android:id="@+id/addons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/js_injector"
android:text="@string/addons"
android:layout_marginTop="10dp"
android:layout_gravity="center" />
</LinearLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/show_hint"
android:title="@string/hint"
android:orderInCategory="10"
android:icon="@drawable/ic_help"
android:showAsAction="ifRoom" />
<item
android:id="@+id/update"
android:title="@string/update"
android:orderInCategory="15"
android:icon="@drawable/ic_refresh"
android:showAsAction="ifRoom" />
<item
android:id="@+id/select_user_dir"
android:title="@string/set_user_dir"
android:orderInCategory="20"
android:showAsAction="never" />
</menu>

View File

@ -30,4 +30,10 @@
<string name="update">Update</string>
<string name="download_in_progress">Script download is already in progress</string>
<string name="app_not_found">App not found</string>
<string name="addons">Addons</string>
<string name="specify_user_dir">Select the directory where your custom scripts are located</string>
<string name="user_addon">User addon</string>
<string name="set_user_dir">Set user dir</string>
<string name="addons_hint">You can create mitmproxy addons to implement your custom logic. Select a user directory and place your scripts there</string>
<string name="restart_to_apply">Restart the capture to apply the changes</string>
</resources>