frida-fusion 0.1.17__tar.gz → 0.1.21__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of frida-fusion might be problematic. Click here for more details.
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/PKG-INFO +2 -1
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/README.md +1 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/__meta__.py +2 -2
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/config.py +16 -2
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/fusion.py +45 -29
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/libs/database.py +45 -21
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/libs/helpers.js +49 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/libs/logger.py +40 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/module.py +73 -5
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/crypto/crypto.py +1 -1
- frida_fusion-0.1.21/frida_fusion/modules/hermes_injector/hermes_hook.js +36 -0
- frida_fusion-0.1.21/frida_fusion/modules/hermes_injector/hermes_injector.js +191 -0
- frida_fusion-0.1.21/frida_fusion/modules/hermes_injector/hermes_injector.py +89 -0
- frida_fusion-0.1.21/frida_fusion/modules/log/log.js +83 -0
- frida_fusion-0.1.21/frida_fusion/modules/log/log.py +73 -0
- frida_fusion-0.1.21/frida_fusion/modules/okhttp_logging/okhttp-logging.py +134 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/reflection/reflection-stalker.py +1 -1
- frida_fusion-0.1.21/frida_fusion/modules/shared_preferences/__init__.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/shared_preferences/shared_preferences.py +24 -29
- frida_fusion-0.1.21/frida_fusion/modules/tls_unpinning/__init__.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion.egg-info/PKG-INFO +2 -1
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion.egg-info/SOURCES.txt +9 -2
- frida_fusion-0.1.17/frida_fusion/modules/okhttp-logging/okhttp-logging.py +0 -80
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/LICENSE +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/__init__.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/__main__.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/args.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/exceptions.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/libs/__init__.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/libs/color.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/libs/scriptlocation.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/__init__.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/android_setings/__init__.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/android_setings/settings.js +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/android_setings/settings.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/crypto/__init__.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/crypto/crypto.js +0 -0
- {frida_fusion-0.1.17/frida_fusion/modules/shared_preferences → frida_fusion-0.1.21/frida_fusion/modules/hermes_injector}/__init__.py +0 -0
- {frida_fusion-0.1.17/frida_fusion/modules/tls_unpinning → frida_fusion-0.1.21/frida_fusion/modules/log}/__init__.py +0 -0
- {frida_fusion-0.1.17/frida_fusion/modules/okhttp-logging → frida_fusion-0.1.21/frida_fusion/modules/okhttp_logging}/okhttp-logging.js +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/reflection/reflection-stalker.js +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/shared_preferences/shared_preferences.js +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion/modules/tls_unpinning/frida_multiple_unpinning.py +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion.egg-info/dependency_links.txt +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion.egg-info/entry_points.txt +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion.egg-info/requires.txt +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/frida_fusion.egg-info/top_level.txt +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/pyproject.toml +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/setup.cfg +0 -0
- {frida_fusion-0.1.17 → frida_fusion-0.1.21}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frida-fusion
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.21
|
|
4
4
|
Summary: Hook your mobile tests with Frida
|
|
5
5
|
Author-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
|
|
6
6
|
Maintainer-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
|
|
@@ -34,6 +34,7 @@ Requires-Dist: frida-tools>=10.8.0
|
|
|
34
34
|
Dynamic: license-file
|
|
35
35
|
|
|
36
36
|
# Frida Fusion
|
|
37
|
+
<img src="./fusion_logo.svg" alt="Frida Fusion logo" align="right" width="20%"/>
|
|
37
38
|
|
|
38
39
|
Hook your mobile tests with Frida.
|
|
39
40
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
__version__ = '0.1.
|
|
1
|
+
__version__ = '0.1.21'
|
|
2
2
|
__title__ = "Frida Fusion"
|
|
3
3
|
__description__ = "📱 frida-fusion - runtime mobile exploration"
|
|
4
4
|
__url__ = "https://github.com/helviojunior/frida-fusion"
|
|
5
|
-
__build__ =
|
|
5
|
+
__build__ = 0x3a1fd58
|
|
6
6
|
__author__ = "Helvio Junior (M4v3r1ck)"
|
|
7
7
|
__author_email__ = "helvio_junior@hotmail.com"
|
|
8
8
|
__license__ = "GPL-3.0"
|
|
@@ -7,7 +7,7 @@ import signal
|
|
|
7
7
|
from argparse import Namespace
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
-
from .module import Module, ModuleManager, InternalModule, ExternalModule
|
|
10
|
+
from .module import Module, ModuleManager, InternalModule, ExternalModule, LocalModule
|
|
11
11
|
from .libs.color import Color
|
|
12
12
|
from .libs.logger import Logger
|
|
13
13
|
from .__meta__ import __version__
|
|
@@ -70,6 +70,8 @@ class Configuration(object):
|
|
|
70
70
|
|
|
71
71
|
sys.argv[0] = 'frida-fusion'
|
|
72
72
|
|
|
73
|
+
Configuration.cmd_line = ' '.join([word for word in sys.argv])
|
|
74
|
+
|
|
73
75
|
list_modules = any(['--list-modules' == word for word in sys.argv])
|
|
74
76
|
#show_help = any(['-h' == word for word in sys.argv])
|
|
75
77
|
|
|
@@ -132,6 +134,8 @@ class Configuration(object):
|
|
|
132
134
|
Color.pl('{!} {R}error: you must specify just one parameter {O}--package{R} or {O}--attach-pid{R}{W}\r\n')
|
|
133
135
|
Configuration.mandatory()
|
|
134
136
|
|
|
137
|
+
Logger.pl(' {C}command line:{O} %s{W}' % Configuration.cmd_line)
|
|
138
|
+
|
|
135
139
|
if args.app_id is not None:
|
|
136
140
|
Configuration.package = args.app_id
|
|
137
141
|
Logger.pl(' {C}package:{O} %s{W}' % Configuration.package)
|
|
@@ -184,9 +188,10 @@ class Configuration(object):
|
|
|
184
188
|
|
|
185
189
|
Logger.pl(' {C}min debug level:{O} %s{W}' % str(args.debug_level).upper())
|
|
186
190
|
|
|
191
|
+
mods = ModuleManager.list_modules(local_path=Path(Configuration.frida_scripts))
|
|
187
192
|
if (args.enabled_modules is not None and isinstance(args.enabled_modules, list)) or \
|
|
188
193
|
(args.ignore_messages_modules is not None and isinstance(args.ignore_messages_modules, list)):
|
|
189
|
-
|
|
194
|
+
|
|
190
195
|
for mod in [
|
|
191
196
|
m.strip()
|
|
192
197
|
for md in args.enabled_modules
|
|
@@ -206,6 +211,7 @@ class Configuration(object):
|
|
|
206
211
|
name = fm.safe_name()
|
|
207
212
|
if name not in Configuration.enabled_modules.keys():
|
|
208
213
|
Configuration.enabled_modules[name] = fm
|
|
214
|
+
|
|
209
215
|
if args.ignore_messages_modules is not None and isinstance(args.ignore_messages_modules, list):
|
|
210
216
|
for mod in [
|
|
211
217
|
m.strip()
|
|
@@ -227,6 +233,14 @@ class Configuration(object):
|
|
|
227
233
|
if name not in Configuration.ignore_messages_modules.keys():
|
|
228
234
|
Configuration.ignore_messages_modules[name] = fm
|
|
229
235
|
|
|
236
|
+
# Enable user defined local modules
|
|
237
|
+
for _, fm in mods.items():
|
|
238
|
+
if isinstance(fm, LocalModule):
|
|
239
|
+
name = fm.safe_name()
|
|
240
|
+
if name not in Configuration.ignore_messages_modules.keys():
|
|
241
|
+
Configuration.ignore_messages_modules[name] = fm
|
|
242
|
+
Configuration.enabled_modules[name] = fm
|
|
243
|
+
|
|
230
244
|
if len(Configuration.enabled_modules) > 0:
|
|
231
245
|
Logger.pl(' {C}modules:{O} %s{W}' % ', '.join([
|
|
232
246
|
m.name
|
|
@@ -84,9 +84,9 @@ class Fusion(object):
|
|
|
84
84
|
elif Configuration.remote_host is not None:
|
|
85
85
|
self.device = process.add_remote_device(Configuration.remote_host)
|
|
86
86
|
|
|
87
|
-
except Exception as
|
|
87
|
+
except Exception as err:
|
|
88
88
|
self.device = None
|
|
89
|
-
Logger.
|
|
89
|
+
Logger.print_exception(err)
|
|
90
90
|
|
|
91
91
|
return self.device
|
|
92
92
|
|
|
@@ -144,7 +144,8 @@ class Fusion(object):
|
|
|
144
144
|
src += dyn
|
|
145
145
|
|
|
146
146
|
if os.path.isfile(Configuration.frida_scripts):
|
|
147
|
-
|
|
147
|
+
if Path(Configuration.frida_scripts).suffix.lower() == ".js":
|
|
148
|
+
files_js += [Configuration.frida_scripts]
|
|
148
149
|
else:
|
|
149
150
|
files_js += [
|
|
150
151
|
os.path.join(Configuration.frida_scripts, f)
|
|
@@ -152,7 +153,15 @@ class Fusion(object):
|
|
|
152
153
|
if f.endswith(".js")
|
|
153
154
|
]
|
|
154
155
|
|
|
156
|
+
# Keep unique files
|
|
157
|
+
# Do not use list(set(files_js)) because it will lose the order of modules
|
|
158
|
+
done: set[str] = set()
|
|
155
159
|
for file_path in files_js:
|
|
160
|
+
if file_path in done:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
done.add(file_path)
|
|
164
|
+
|
|
156
165
|
file_name = Path(file_path).name
|
|
157
166
|
file_data = self.sanitize_js(open(file_path, 'r', encoding='utf-8').read())
|
|
158
167
|
if '#NOLOAD' in file_data:
|
|
@@ -172,7 +181,7 @@ class Fusion(object):
|
|
|
172
181
|
|
|
173
182
|
line_cnt = len(file_data.split("\n")) - 1
|
|
174
183
|
|
|
175
|
-
self.script_trace[file_name] = (offset, offset + line_cnt)
|
|
184
|
+
self.script_trace[file_name] = (offset, offset + line_cnt - 1)
|
|
176
185
|
offset += line_cnt
|
|
177
186
|
|
|
178
187
|
src += file_data
|
|
@@ -186,10 +195,17 @@ class Fusion(object):
|
|
|
186
195
|
s = self.session.create_script(src, name="fusion_bundle")
|
|
187
196
|
s.on("message", self.make_handler("fusion_bundle.js")) # register the message handler
|
|
188
197
|
s.load()
|
|
189
|
-
except Exception as
|
|
190
|
-
|
|
198
|
+
except Exception as err:
|
|
191
199
|
try:
|
|
192
|
-
|
|
200
|
+
from traceback import format_exc
|
|
201
|
+
err_txt = 'Error:{O} %s{W}' % str(err)
|
|
202
|
+
err_txt += '\n{O}Full stack trace below\n'
|
|
203
|
+
err_txt += format_exc().strip()
|
|
204
|
+
|
|
205
|
+
err_txt = err_txt.replace('\n', '\n{W} ')
|
|
206
|
+
err_txt = err_txt.replace(' File', '{W}{D}File')
|
|
207
|
+
err_txt = err_txt.replace(' Exception: ', '{R}Exception: {O}')
|
|
208
|
+
|
|
193
209
|
pattern = re.compile(r'script\(line (\d+)\):')
|
|
194
210
|
matches = [
|
|
195
211
|
(
|
|
@@ -199,15 +215,16 @@ class Fusion(object):
|
|
|
199
215
|
line=m.group(1),
|
|
200
216
|
))
|
|
201
217
|
)
|
|
202
|
-
for m in pattern.finditer(
|
|
218
|
+
for m in pattern.finditer(err_txt)
|
|
203
219
|
]
|
|
204
220
|
for m in matches:
|
|
205
|
-
|
|
206
|
-
|
|
221
|
+
err_txt = err_txt.replace(m[0], f"{m[1].file_name}(line {m[1].line})")
|
|
222
|
+
|
|
223
|
+
Logger.pl(err_txt)
|
|
207
224
|
print("")
|
|
208
225
|
sys.exit(1)
|
|
209
|
-
except Exception:
|
|
210
|
-
Logger.
|
|
226
|
+
except Exception as e2:
|
|
227
|
+
Logger.print_exception(e2)
|
|
211
228
|
print("")
|
|
212
229
|
sys.exit(1)
|
|
213
230
|
|
|
@@ -383,7 +400,7 @@ class Fusion(object):
|
|
|
383
400
|
skm = str(sk)
|
|
384
401
|
|
|
385
402
|
self.print_message_inst("D", "Silent kill requested",
|
|
386
|
-
|
|
403
|
+
script_location=Logger.get_caller_info(stack_index=1))
|
|
387
404
|
Fusion.running = False
|
|
388
405
|
time.sleep(0.2)
|
|
389
406
|
if skm != "":
|
|
@@ -436,7 +453,7 @@ class Fusion(object):
|
|
|
436
453
|
}))
|
|
437
454
|
|
|
438
455
|
self.print_message_inst("F", description + stack,
|
|
439
|
-
|
|
456
|
+
script_location=script_location)
|
|
440
457
|
Fusion.running = False
|
|
441
458
|
time.sleep(0.2)
|
|
442
459
|
Logger.pl('\n{+} {O}Exiting...{O}{W}')
|
|
@@ -465,7 +482,6 @@ class Fusion(object):
|
|
|
465
482
|
Logger.pl("")
|
|
466
483
|
self.done.set()
|
|
467
484
|
|
|
468
|
-
|
|
469
485
|
def _replace_location(self, message: str) -> str:
|
|
470
486
|
try:
|
|
471
487
|
matches = [
|
|
@@ -501,11 +517,11 @@ class Fusion(object):
|
|
|
501
517
|
)
|
|
502
518
|
except SilentKillError as ske:
|
|
503
519
|
raise ske
|
|
504
|
-
except Exception as
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
520
|
+
except Exception as err:
|
|
521
|
+
self.print_exception(
|
|
522
|
+
err,
|
|
523
|
+
script_location=Logger.get_error_info_from_format_exc(stack_index=-1)
|
|
524
|
+
)
|
|
509
525
|
|
|
510
526
|
def _raise_data_event(self,
|
|
511
527
|
script_location: ScriptLocation = None,
|
|
@@ -520,14 +536,14 @@ class Fusion(object):
|
|
|
520
536
|
)
|
|
521
537
|
except SilentKillError as ske:
|
|
522
538
|
raise ske
|
|
523
|
-
except Exception as
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
539
|
+
except Exception as err:
|
|
540
|
+
self.print_exception(
|
|
541
|
+
err,
|
|
542
|
+
script_location=Logger.get_error_info_from_format_exc(stack_index=-1)
|
|
543
|
+
)
|
|
528
544
|
|
|
529
545
|
def print_message_inst(self, level: str = "*", message: str = "",
|
|
530
|
-
|
|
546
|
+
script_location: ScriptLocation = None):
|
|
531
547
|
|
|
532
548
|
return type(self)._print_message(
|
|
533
549
|
level=level,
|
|
@@ -546,7 +562,7 @@ class Fusion(object):
|
|
|
546
562
|
|
|
547
563
|
@classmethod
|
|
548
564
|
def _print_message(cls, level: str = "*", message: str = "",
|
|
549
|
-
|
|
565
|
+
script_location: ScriptLocation = None):
|
|
550
566
|
|
|
551
567
|
if Fusion.running is False and Logger.debug_level >= 2:
|
|
552
568
|
return
|
|
@@ -580,7 +596,7 @@ class Fusion(object):
|
|
|
580
596
|
db.insert_history(**kwargs)
|
|
581
597
|
|
|
582
598
|
@classmethod
|
|
583
|
-
def print_exception(cls, err):
|
|
599
|
+
def print_exception(cls, err, script_location: ScriptLocation = None):
|
|
584
600
|
from traceback import format_exc
|
|
585
601
|
err_txt = 'Error:{O} %s{W}' % str(err)
|
|
586
602
|
err_txt += '\n{O}Full stack trace below\n'
|
|
@@ -594,7 +610,7 @@ class Fusion(object):
|
|
|
594
610
|
level="E",
|
|
595
611
|
message=Color.s(err_txt),
|
|
596
612
|
filename_col_len=Fusion.max_filename,
|
|
597
|
-
script_location=Logger.get_caller_info(stack_index=2)
|
|
613
|
+
script_location=script_location if script_location is not None else Logger.get_caller_info(stack_index=2)
|
|
598
614
|
)
|
|
599
615
|
|
|
600
616
|
@classmethod
|
|
@@ -4,6 +4,7 @@ import math
|
|
|
4
4
|
import shutil
|
|
5
5
|
import os.path
|
|
6
6
|
import sqlite3
|
|
7
|
+
import time
|
|
7
8
|
from functools import reduce
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from sqlite3 import Connection, ProgrammingError
|
|
@@ -78,7 +79,7 @@ class Database(object):
|
|
|
78
79
|
(columns, values) = self.parse_args(kwargs)
|
|
79
80
|
sql = "INSERT INTO {} ({}) VALUES ({})" \
|
|
80
81
|
.format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
|
|
81
|
-
|
|
82
|
+
self.resilient_execute(conn, sql, values)
|
|
82
83
|
conn.commit()
|
|
83
84
|
|
|
84
85
|
@connect
|
|
@@ -89,7 +90,7 @@ class Database(object):
|
|
|
89
90
|
(columns, values) = self.parse_args(kwargs)
|
|
90
91
|
sql = "INSERT INTO {} ({}) VALUES ({})" \
|
|
91
92
|
.format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
|
|
92
|
-
|
|
93
|
+
self.resilient_execute(conn, sql, values)
|
|
93
94
|
conn.commit()
|
|
94
95
|
|
|
95
96
|
@connect
|
|
@@ -98,7 +99,7 @@ class Database(object):
|
|
|
98
99
|
(columns, values) = self.parse_args(kwargs)
|
|
99
100
|
sql = "INSERT OR IGNORE INTO {} ({}) VALUES ({})" \
|
|
100
101
|
.format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
|
|
101
|
-
|
|
102
|
+
self.resilient_execute(conn, sql, values)
|
|
102
103
|
conn.commit()
|
|
103
104
|
|
|
104
105
|
@connect
|
|
@@ -107,7 +108,7 @@ class Database(object):
|
|
|
107
108
|
(columns, values) = self.parse_args(kwargs)
|
|
108
109
|
sql = "INSERT OR REPLACE INTO {} ({}) VALUES ({})" \
|
|
109
110
|
.format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
|
|
110
|
-
|
|
111
|
+
self.resilient_execute(conn, sql, values)
|
|
111
112
|
conn.commit()
|
|
112
113
|
|
|
113
114
|
def insert_update_one(self, table_name: str, **kwargs):
|
|
@@ -119,7 +120,7 @@ class Database(object):
|
|
|
119
120
|
(columns, values) = self.parse_args(kwargs)
|
|
120
121
|
sql = "INSERT OR IGNORE INTO {} ({}) VALUES ({})" \
|
|
121
122
|
.format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
|
|
122
|
-
c =
|
|
123
|
+
c = self.resilient_execute(conn, sql, values)
|
|
123
124
|
|
|
124
125
|
status = {'inserted': c.rowcount, 'updated': 0}
|
|
125
126
|
|
|
@@ -135,7 +136,7 @@ class Database(object):
|
|
|
135
136
|
sql += "{}".format(', '.join([f'{col} = ?' for col in u_columns]))
|
|
136
137
|
if len(f_columns) > 0:
|
|
137
138
|
sql += " WHERE {}".format(f' and '.join([f'{col} = ?' for col in f_columns]))
|
|
138
|
-
c =
|
|
139
|
+
c = self.resilient_execute(conn, sql, tuple(u_values + f_values, ))
|
|
139
140
|
conn.commit()
|
|
140
141
|
|
|
141
142
|
status['updated'] = c.rowcount
|
|
@@ -155,12 +156,16 @@ class Database(object):
|
|
|
155
156
|
if len(columns) > 0:
|
|
156
157
|
sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in columns]))
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
data = []
|
|
160
|
+
with conn: # Transaction
|
|
161
|
+
cursor = self.resilient_execute(conn, sql, values)
|
|
162
|
+
if cursor.rowcount == 0:
|
|
163
|
+
return data
|
|
161
164
|
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
columns = cursor.description
|
|
166
|
+
data = [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
|
|
167
|
+
|
|
168
|
+
return data
|
|
164
169
|
|
|
165
170
|
def select_first(self, table_name, **kwargs):
|
|
166
171
|
data = self.select(table_name, **kwargs)
|
|
@@ -170,11 +175,15 @@ class Database(object):
|
|
|
170
175
|
|
|
171
176
|
@connect
|
|
172
177
|
def select_raw(self, conn: Connection, sql: str, args: any):
|
|
173
|
-
cursor =
|
|
178
|
+
cursor = self.resilient_execute(conn, sql, tuple(args,))
|
|
174
179
|
if cursor.rowcount == 0:
|
|
175
180
|
return []
|
|
176
181
|
columns = cursor.description
|
|
177
|
-
|
|
182
|
+
data = []
|
|
183
|
+
with conn: # Transaction
|
|
184
|
+
data = [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
|
|
185
|
+
|
|
186
|
+
return data
|
|
178
187
|
|
|
179
188
|
@connect
|
|
180
189
|
def select_count(self, conn: Connection, table_name, **kwargs) -> int:
|
|
@@ -187,10 +196,13 @@ class Database(object):
|
|
|
187
196
|
sql = f"SELECT count(*) FROM {table_name}"
|
|
188
197
|
if len(columns) > 0:
|
|
189
198
|
sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in columns]))
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
199
|
+
|
|
200
|
+
data = []
|
|
201
|
+
with conn: # Transaction
|
|
202
|
+
cursor = self.resilient_execute(conn, sql, values)
|
|
203
|
+
if cursor.rowcount == 0:
|
|
204
|
+
return 0
|
|
205
|
+
data = cursor.fetchone()
|
|
194
206
|
|
|
195
207
|
return int(data[0])
|
|
196
208
|
|
|
@@ -205,7 +217,7 @@ class Database(object):
|
|
|
205
217
|
sql = f"DELETE FROM {table_name}"
|
|
206
218
|
if len(columns) > 0:
|
|
207
219
|
sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in columns]))
|
|
208
|
-
|
|
220
|
+
self.resilient_execute(conn, sql, values)
|
|
209
221
|
conn.commit()
|
|
210
222
|
|
|
211
223
|
@connect
|
|
@@ -221,7 +233,7 @@ class Database(object):
|
|
|
221
233
|
sql += "{}".format(', '.join([f'{col} = ?' for col in u_columns]))
|
|
222
234
|
if len(f_columns) > 0:
|
|
223
235
|
sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in f_columns]))
|
|
224
|
-
|
|
236
|
+
self.resilient_execute(conn, sql, tuple(u_values + f_values, ))
|
|
225
237
|
conn.commit()
|
|
226
238
|
|
|
227
239
|
def get_constraints(self, conn: Connection) -> dict:
|
|
@@ -238,7 +250,7 @@ class Database(object):
|
|
|
238
250
|
' il.origin = "u" '
|
|
239
251
|
'ORDER BY table_name, key_name, ii.seqno')
|
|
240
252
|
|
|
241
|
-
cursor =
|
|
253
|
+
cursor = self.resilient_execute(conn, sql)
|
|
242
254
|
columns = cursor.description
|
|
243
255
|
db_scheme = [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
|
|
244
256
|
|
|
@@ -333,7 +345,7 @@ class Database(object):
|
|
|
333
345
|
|
|
334
346
|
conn.commit()
|
|
335
347
|
|
|
336
|
-
#Must get the constraints
|
|
348
|
+
# Must get the constraints
|
|
337
349
|
self.get_constraints(conn)
|
|
338
350
|
|
|
339
351
|
def insert_history(self, source: str, data: str, stack_trace: str = ''):
|
|
@@ -363,3 +375,15 @@ class Database(object):
|
|
|
363
375
|
str
|
|
364
376
|
"""
|
|
365
377
|
return ''.join(k for k in input_string if k.isalnum() or k in '_-')
|
|
378
|
+
|
|
379
|
+
@classmethod
|
|
380
|
+
def resilient_execute(cls, conn: Connection, *args, **kwargs):
|
|
381
|
+
for _ in range(5):
|
|
382
|
+
try:
|
|
383
|
+
return conn.execute(*args, **kwargs)
|
|
384
|
+
except sqlite3.OperationalError as e:
|
|
385
|
+
if 'database is locked' in str(e).lower():
|
|
386
|
+
time.sleep(0.3)
|
|
387
|
+
else:
|
|
388
|
+
raise e
|
|
389
|
+
|
|
@@ -181,6 +181,55 @@ function fusion_bytesToBase64(byteArray){
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
function fusion_base64ToString(b64) {
|
|
185
|
+
try {
|
|
186
|
+
const StringClass = Java.use('java.lang.String');
|
|
187
|
+
const Base64Class = Java.use('android.util.Base64');
|
|
188
|
+
|
|
189
|
+
// Flags úteis (só para referência/legibilidade)
|
|
190
|
+
const BASE64_DEFAULT = 0x00000000; // decode padrão
|
|
191
|
+
const BASE64_URL_SAFE = 0x00000008; // para strings base64 url-safe
|
|
192
|
+
|
|
193
|
+
// Normaliza entrada
|
|
194
|
+
let s = ('' + b64).trim();
|
|
195
|
+
// Remove prefixo data URI, se existir
|
|
196
|
+
s = s.replace(/^data:.*;base64,/, '');
|
|
197
|
+
// Remove espaços/linhas quebradas
|
|
198
|
+
s = s.replace(/\s+/g, '');
|
|
199
|
+
|
|
200
|
+
// Função para padding quando faltam '='
|
|
201
|
+
function padBase64(x) {
|
|
202
|
+
const m = x.length % 4;
|
|
203
|
+
return m === 0 ? x : x + '===='.slice(m);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let decoded = null;
|
|
207
|
+
|
|
208
|
+
// 1) Tenta DEFAULT
|
|
209
|
+
try {
|
|
210
|
+
decoded = Base64Class.decode(s, BASE64_DEFAULT);
|
|
211
|
+
} catch (e1) {
|
|
212
|
+
// 2) Tenta URL_SAFE
|
|
213
|
+
try {
|
|
214
|
+
decoded = Base64Class.decode(s, BASE64_URL_SAFE);
|
|
215
|
+
} catch (e2) {
|
|
216
|
+
// 3) Tenta com padding
|
|
217
|
+
const sp = padBase64(s);
|
|
218
|
+
decoded = Base64Class.decode(sp, BASE64_DEFAULT);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Converte bytes -> String UTF-8
|
|
223
|
+
const result = StringClass.$new(decoded, 'utf-8').toString();
|
|
224
|
+
return result;
|
|
225
|
+
|
|
226
|
+
} catch (err) {
|
|
227
|
+
// mesmo logger que você usa na encode
|
|
228
|
+
fusion_sendMessage("W", err);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
184
233
|
function fusion_normalizePtr(addr) {
|
|
185
234
|
let p = ptr(addr);
|
|
186
235
|
if (Process.arch === 'arm64') p = p.and('0x00FFFFFFFFFFFFFF'); // limpa TBI
|
|
@@ -4,7 +4,9 @@ import base64
|
|
|
4
4
|
import inspect
|
|
5
5
|
import json
|
|
6
6
|
import datetime
|
|
7
|
+
import re
|
|
7
8
|
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
8
10
|
|
|
9
11
|
from .scriptlocation import ScriptLocation
|
|
10
12
|
from ..libs.color import Color
|
|
@@ -83,6 +85,31 @@ class Logger(object):
|
|
|
83
85
|
line=str(line_number)
|
|
84
86
|
)
|
|
85
87
|
|
|
88
|
+
@classmethod
|
|
89
|
+
def get_error_info_from_format_exc(cls, stack_index: int = -1) -> Optional[ScriptLocation]:
|
|
90
|
+
"""
|
|
91
|
+
Faz o *parse* do texto gerado por traceback.format_exc() e extrai arquivo/linha/função.
|
|
92
|
+
- frame_index: -1 pega o último frame (onde a exceção estourou).
|
|
93
|
+
Formato esperado das linhas:
|
|
94
|
+
File "/caminho/mod.py", line 123, in func
|
|
95
|
+
"""
|
|
96
|
+
from traceback import format_exc
|
|
97
|
+
|
|
98
|
+
# Captura tuplas (arquivo, linha, função)
|
|
99
|
+
# Obs.: tolera espaços e caminhos com aspas; não captura a linha de código em si.
|
|
100
|
+
pattern = r'(?i:File) "(.+?)", (?i:line) (\d+), (?i:in) ([^\n\r]+)'
|
|
101
|
+
matches = re.findall(pattern, format_exc())
|
|
102
|
+
|
|
103
|
+
if not matches:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
file_path, lineno, func = matches[stack_index]
|
|
107
|
+
return ScriptLocation(
|
|
108
|
+
file_name=Path(file_path).name,
|
|
109
|
+
function_name=func.strip(),
|
|
110
|
+
line=str(lineno),
|
|
111
|
+
)
|
|
112
|
+
|
|
86
113
|
@staticmethod
|
|
87
114
|
def json_serial(obj):
|
|
88
115
|
"""JSON serializer for objects not serializable by default json code"""
|
|
@@ -167,3 +194,16 @@ class Logger(object):
|
|
|
167
194
|
f"{Color.color_reset} {fg_color}{line}{Color.color_reset}")
|
|
168
195
|
|
|
169
196
|
Logger.pl(f_message)
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def print_exception(cls, err):
|
|
200
|
+
from traceback import format_exc
|
|
201
|
+
err_txt = 'Error:{O} %s{W}' % str(err)
|
|
202
|
+
err_txt += '\n{O}Full stack trace below\n'
|
|
203
|
+
err_txt += format_exc().strip()
|
|
204
|
+
|
|
205
|
+
err_txt = err_txt.replace('\n', '\n{W} ')
|
|
206
|
+
err_txt = err_txt.replace(' File', '{W}{D}File')
|
|
207
|
+
err_txt = err_txt.replace(' Exception: ', '{R}Exception: {O}')
|
|
208
|
+
|
|
209
|
+
Logger.pl(f"{err_txt}\n")
|