fpbinject 1.6.8__py3-none-any.whl

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.
Files changed (113) hide show
  1. fpbinject/__init__.py +10 -0
  2. fpbinject/__main__.py +9 -0
  3. fpbinject/app/__init__.py +41 -0
  4. fpbinject/app/auto_ban.py +365 -0
  5. fpbinject/app/middleware.py +155 -0
  6. fpbinject/app/routes/__init__.py +42 -0
  7. fpbinject/app/routes/connection.py +402 -0
  8. fpbinject/app/routes/files.py +280 -0
  9. fpbinject/app/routes/fpb.py +758 -0
  10. fpbinject/app/routes/logs.py +308 -0
  11. fpbinject/app/routes/patch.py +233 -0
  12. fpbinject/app/routes/symbols.py +1521 -0
  13. fpbinject/app/routes/transfer.py +830 -0
  14. fpbinject/app/routes/watch.py +174 -0
  15. fpbinject/app/routes/watch_expr.py +237 -0
  16. fpbinject/app/utils/__init__.py +0 -0
  17. fpbinject/app/utils/sse.py +80 -0
  18. fpbinject/cli/__init__.py +12 -0
  19. fpbinject/cli/connection_plan.py +67 -0
  20. fpbinject/cli/discover.py +295 -0
  21. fpbinject/cli/fpb_cli.py +2242 -0
  22. fpbinject/cli/handle_cache.py +128 -0
  23. fpbinject/cli/server_proxy.py +553 -0
  24. fpbinject/core/__init__.py +2 -0
  25. fpbinject/core/compile_commands.py +426 -0
  26. fpbinject/core/compiler.py +738 -0
  27. fpbinject/core/config_schema.py +500 -0
  28. fpbinject/core/elf_utils.py +779 -0
  29. fpbinject/core/file_transfer.py +699 -0
  30. fpbinject/core/gdb_bridge.py +469 -0
  31. fpbinject/core/gdb_json_print.py +115 -0
  32. fpbinject/core/gdb_manager.py +359 -0
  33. fpbinject/core/gdb_session.py +1281 -0
  34. fpbinject/core/patch_generator.py +493 -0
  35. fpbinject/core/serial_protocol.py +1182 -0
  36. fpbinject/core/state.py +390 -0
  37. fpbinject/core/watch_evaluator.py +290 -0
  38. fpbinject/fpb_cli.py +42 -0
  39. fpbinject/fpb_inject.py +837 -0
  40. fpbinject/main.py +573 -0
  41. fpbinject/routes.py +59 -0
  42. fpbinject/services/__init__.py +16 -0
  43. fpbinject/services/config_file_watcher.py +0 -0
  44. fpbinject/services/device_worker.py +306 -0
  45. fpbinject/services/file_watcher.py +314 -0
  46. fpbinject/services/file_watcher_manager.py +504 -0
  47. fpbinject/services/log_recorder.py +124 -0
  48. fpbinject/services/mdns_advertiser.py +224 -0
  49. fpbinject/services/timer.py +129 -0
  50. fpbinject/services/virtual_serial.py +266 -0
  51. fpbinject/static/css/style.css +1192 -0
  52. fpbinject/static/css/tutorial.css +397 -0
  53. fpbinject/static/css/workbench.css +3168 -0
  54. fpbinject/static/js/app.js +65 -0
  55. fpbinject/static/js/core/config-schema.js +727 -0
  56. fpbinject/static/js/core/connection.js +423 -0
  57. fpbinject/static/js/core/i18n.js +175 -0
  58. fpbinject/static/js/core/logs.js +200 -0
  59. fpbinject/static/js/core/slots.js +277 -0
  60. fpbinject/static/js/core/sse.js +81 -0
  61. fpbinject/static/js/core/state.js +225 -0
  62. fpbinject/static/js/core/terminal.js +342 -0
  63. fpbinject/static/js/core/theme.js +90 -0
  64. fpbinject/static/js/core/version.js +13 -0
  65. fpbinject/static/js/features/autoinject.js +549 -0
  66. fpbinject/static/js/features/config.js +602 -0
  67. fpbinject/static/js/features/editor.js +499 -0
  68. fpbinject/static/js/features/elfwatcher.js +163 -0
  69. fpbinject/static/js/features/filebrowser.js +187 -0
  70. fpbinject/static/js/features/fpb.js +554 -0
  71. fpbinject/static/js/features/inline-edit.js +228 -0
  72. fpbinject/static/js/features/patch.js +639 -0
  73. fpbinject/static/js/features/quick-commands.js +1779 -0
  74. fpbinject/static/js/features/symbols.js +1265 -0
  75. fpbinject/static/js/features/transfer.js +2478 -0
  76. fpbinject/static/js/features/tutorial.js +1163 -0
  77. fpbinject/static/js/features/watch.js +815 -0
  78. fpbinject/static/js/lib/beautify.min.js +4915 -0
  79. fpbinject/static/js/lib/i18next.min.js +2632 -0
  80. fpbinject/static/js/locales/en.js +708 -0
  81. fpbinject/static/js/locales/zh-CN.js +665 -0
  82. fpbinject/static/js/locales/zh-TW.js +667 -0
  83. fpbinject/static/js/ui/sash.js +185 -0
  84. fpbinject/static/js/ui/sidebar.js +268 -0
  85. fpbinject/templates/base.html +52 -0
  86. fpbinject/templates/index.html +6 -0
  87. fpbinject/templates/partials/activitybar.html +67 -0
  88. fpbinject/templates/partials/editor.html +70 -0
  89. fpbinject/templates/partials/modals.html +28 -0
  90. fpbinject/templates/partials/scripts.html +110 -0
  91. fpbinject/templates/partials/sidebar.html +248 -0
  92. fpbinject/templates/partials/sidebar_config.html +15 -0
  93. fpbinject/templates/partials/sidebar_device.html +110 -0
  94. fpbinject/templates/partials/sidebar_quick_commands.html +269 -0
  95. fpbinject/templates/partials/sidebar_transfer.html +145 -0
  96. fpbinject/templates/partials/statusbar.html +30 -0
  97. fpbinject/templates/partials/terminal.html +38 -0
  98. fpbinject/templates/partials/titlebar.html +30 -0
  99. fpbinject/templates/partials/tutorial.html +34 -0
  100. fpbinject/utils/__init__.py +13 -0
  101. fpbinject/utils/crc.py +299 -0
  102. fpbinject/utils/helpers.py +108 -0
  103. fpbinject/utils/net.py +146 -0
  104. fpbinject/utils/port_lock.py +169 -0
  105. fpbinject/utils/serial.py +344 -0
  106. fpbinject/utils/toolchain.py +48 -0
  107. fpbinject/version.py +13 -0
  108. fpbinject-1.6.8.dist-info/METADATA +255 -0
  109. fpbinject-1.6.8.dist-info/RECORD +113 -0
  110. fpbinject-1.6.8.dist-info/WHEEL +5 -0
  111. fpbinject-1.6.8.dist-info/entry_points.txt +3 -0
  112. fpbinject-1.6.8.dist-info/licenses/LICENSE +21 -0
  113. fpbinject-1.6.8.dist-info/top_level.txt +1 -0
fpbinject/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """FPBInject — runtime code injection for ARM Cortex-M via the FPB unit.
2
+
3
+ This package bundles the WebServer, CLI, and supporting modules. The physical
4
+ layout lives under ``Tools/WebServer/`` and is mapped to the import name
5
+ ``fpbinject`` via setuptools ``package-dir`` (see pyproject.toml).
6
+ """
7
+
8
+ from fpbinject.version import __version__
9
+
10
+ __all__ = ["__version__"]
fpbinject/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Enable ``python -m fpbinject`` to launch the WebServer.
2
+
3
+ Equivalent to the ``fpbinject-server`` console script.
4
+ """
5
+
6
+ from fpbinject.main import main
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # MIT License
4
+ # Copyright (c) 2025 - 2026 _VIFEXTech
5
+
6
+ """
7
+ FPBInject WebServer Flask Application Package.
8
+
9
+ This package contains the Flask application factory and route blueprints.
10
+ """
11
+
12
+ import os
13
+
14
+ from flask import Flask
15
+ from flask_cors import CORS
16
+
17
+ # Locate the package root (holds templates/ and static/) via importlib.resources
18
+ # so it resolves correctly whether run from source or an installed wheel.
19
+ try:
20
+ from importlib.resources import files as _res_files
21
+
22
+ WEBSERVER_DIR = str(_res_files("fpbinject"))
23
+ except Exception: # pragma: no cover - fallback for odd layouts
24
+ WEBSERVER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
25
+
26
+
27
+ def create_app():
28
+ """Create and configure the Flask application."""
29
+ app = Flask(
30
+ "fpbinject",
31
+ template_folder=os.path.join(WEBSERVER_DIR, "templates"),
32
+ static_folder=os.path.join(WEBSERVER_DIR, "static"),
33
+ )
34
+ CORS(app)
35
+
36
+ # Import and register routes
37
+ from fpbinject.routes import register_routes
38
+
39
+ register_routes(app)
40
+
41
+ return app
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # MIT License
4
+ # Copyright (c) 2025 - 2026 _VIFEXTech
5
+
6
+ """
7
+ Auto-ban engine for FPBInject Web Server.
8
+
9
+ Detects and bans malicious IPs based on:
10
+ 1. Known vulnerability scan path fingerprints
11
+ 2. Request rate limiting for auth-rejected requests
12
+
13
+ Banned IPs receive tarpit (slow) responses to waste scanner resources.
14
+ """
15
+
16
+ import ipaddress
17
+ import logging
18
+ import time
19
+ from collections import defaultdict
20
+ from dataclasses import dataclass, field
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Known vulnerability scan path fingerprints
25
+ MALICIOUS_PATH_PATTERNS = [
26
+ # Java middleware
27
+ "/xxl-job-admin",
28
+ "/jmx-console",
29
+ "/invoker/JMXInvokerServlet",
30
+ "/invoker/readonly",
31
+ "/wls-wsat/",
32
+ "/ws_utc/",
33
+ "/console/j_security_check",
34
+ "/j_acegi_security_check",
35
+ "/axis2/services",
36
+ # Spring Boot
37
+ "/actuator/env",
38
+ "/actuator/health",
39
+ "/actuator/gateway",
40
+ "/application-dev.properties",
41
+ "/application-prod.properties",
42
+ "/application-stage.properties",
43
+ "/application-pre.properties",
44
+ "/application-prd.properties",
45
+ "/application-production.properties",
46
+ "/application-staging.properties",
47
+ "/application-stg.properties",
48
+ "/application-preview.properties",
49
+ # Spring Cloud Config
50
+ "/config/application",
51
+ # PHP frameworks
52
+ "/index.php/index/index/testsql",
53
+ "/thinkphp/index.php",
54
+ "/general/login/index.php",
55
+ # Monitoring systems
56
+ "/zabbix/",
57
+ "/zabbix/setup.php",
58
+ "/setup.php",
59
+ "/jsrpc.php",
60
+ "/solr/admin/cores",
61
+ "/remote_agent.php",
62
+ # CMS / admin panels
63
+ "/wp/v2/posts",
64
+ "/xmlrpc.php",
65
+ "/resin-admin/",
66
+ "/manager/html",
67
+ # Gateways / APIs
68
+ "/apisix/batch-requests",
69
+ "/apisix/admin/",
70
+ "/kong/status",
71
+ # Path traversal
72
+ "/../",
73
+ "/..;/",
74
+ "../../../../",
75
+ "/etc/passwd",
76
+ "/win.ini",
77
+ # JNDI injection (Log4Shell CVE-2021-44228)
78
+ "${jndi:",
79
+ # OGNL / SpEL expression injection (Struts2, Spring)
80
+ "${(#",
81
+ "${T(",
82
+ "@java.lang.Runtime@",
83
+ "getRuntime().exec(",
84
+ # Netflix Hystrix SpEL injection
85
+ "/hystrix/",
86
+ # Laravel Ignition RCE (CVE-2021-3129)
87
+ "/_ignition/execute-solution",
88
+ # Apache Druid RCE (CVE-2021-25646)
89
+ "/druid/indexer/v1/sampler",
90
+ # MinIO bootstrap
91
+ "/minio/bootstrap/",
92
+ # CGI-BIN traversal
93
+ "/cgi-bin/",
94
+ # Other known vulnerabilities
95
+ "/CFIDE/administrator",
96
+ "/webtools/control/xmlrpc",
97
+ "/webtools/control/SOAPService",
98
+ "/fileserver/",
99
+ "/uddiexplorer/",
100
+ "/ueditor/",
101
+ "/javax.faces.resource/dynamiccontent",
102
+ "/vpn/../vpns/cfg/smb.conf",
103
+ "/openam/",
104
+ "/debug/exec",
105
+ "/tmui/login.jsp",
106
+ "/zentao/",
107
+ "/CTCWebService/",
108
+ "/manage/log/view",
109
+ "/log/view",
110
+ # JBoss MQ HTTP IL deserialization (CVE-2017-7504)
111
+ "/jbossmq-httpil/HTTPServerILServlet",
112
+ # Confluence OGNL injection (CVE-2022-26134)
113
+ "/pages/createpage-entervariables.action",
114
+ # VMware vCenter / ESXi
115
+ "/ui/vropspluginui/",
116
+ "/eam/vib",
117
+ "/analytics/telemetry/ph/",
118
+ # F5 BIG-IP iControl REST RCE (CVE-2022-1388)
119
+ "/mgmt/tm/util/bash",
120
+ # phpMyAdmin
121
+ "/phpMyAdmin/",
122
+ "/phpmyadmin/",
123
+ "/pma/",
124
+ # Apache Ambari
125
+ "/ambari/api/v1/users/",
126
+ # Fortinet FortiGate VPN (CVE-2018-13379)
127
+ "/remote/logincheck",
128
+ # SaltStack API (CVE-2020-11651)
129
+ "/v1/tools/run",
130
+ # Seeyon OA file upload
131
+ "/develop/systparam/softlogo/",
132
+ # DataEase BI
133
+ "/de2api/datasource/",
134
+ # Exchange Autodiscover SSRF (CVE-2021-34473)
135
+ "/autodiscover/autodiscover.json",
136
+ # Apache Airflow
137
+ "/admin/airflow/",
138
+ # MicroStrategy BI
139
+ "/MicroStrategy/servlet/",
140
+ "/servlet/taskProc",
141
+ # Grafana user creation (CVE-2021-43798)
142
+ "/create_user/",
143
+ # Jira information disclosure
144
+ "/secure/ContactAdministrators",
145
+ # JumpServer session leak
146
+ "/api/v1/terminal/sessions/",
147
+ # Nacos console
148
+ "/api/console/api_server",
149
+ # Roundcube Webmail
150
+ "/composer/send_email",
151
+ # GraphQL introspection
152
+ "/graphql",
153
+ # Azkaban scheduler
154
+ "/azkaban",
155
+ # CASA / NetIQ
156
+ "/casa/nodes/thumbprints",
157
+ # iLO / BMC IPMI
158
+ "/rest/v1/AccountService/",
159
+ # VMware vSAN SpEL injection (CVE-2021-21985)
160
+ "/ui/h5-vsan/rest/proxy/",
161
+ # Struts2 JSON plugin
162
+ "/json",
163
+ # GitLab
164
+ "/users/sign_in",
165
+ "/uploads/user",
166
+ # osinstall
167
+ "/osinstall/v1/device/",
168
+ ]
169
+
170
+
171
+ @dataclass
172
+ class IPRecord:
173
+ """Behavior record for a single IP."""
174
+
175
+ first_seen: float = 0.0
176
+ hit_count: int = 0
177
+ reject_count: int = 0
178
+ malicious_score: int = 0
179
+ banned_until: float = 0.0
180
+ ban_count: int = 0
181
+ last_seen: float = 0.0
182
+ recent_timestamps: list = field(default_factory=list)
183
+
184
+
185
+ class AutoBanEngine:
186
+ """Automatic IP ban engine based on behavior analysis."""
187
+
188
+ def __init__(
189
+ self,
190
+ rate_window=10,
191
+ rate_limit=20,
192
+ malicious_threshold=3,
193
+ ban_duration=3600,
194
+ ban_escalation=2.0,
195
+ max_ban_duration=86400,
196
+ whitelist=None,
197
+ tarpit_delay=10.0,
198
+ ):
199
+ """Initialize the auto-ban engine.
200
+
201
+ Args:
202
+ rate_window: Rate detection window in seconds.
203
+ rate_limit: Max rejected requests within window before ban.
204
+ malicious_threshold: Malicious path hits before ban.
205
+ ban_duration: Base ban duration in seconds.
206
+ ban_escalation: Ban duration multiplier per repeat offense.
207
+ max_ban_duration: Maximum ban duration in seconds.
208
+ whitelist: List of trusted IPs or CIDR ranges.
209
+ tarpit_delay: Seconds to delay response for banned IPs.
210
+ """
211
+ self.rate_window = rate_window
212
+ self.rate_limit = rate_limit
213
+ self.malicious_threshold = malicious_threshold
214
+ self.ban_duration = ban_duration
215
+ self.ban_escalation = ban_escalation
216
+ self.max_ban_duration = max_ban_duration
217
+ self.whitelist = set(whitelist or [])
218
+ self.tarpit_delay = tarpit_delay
219
+ self.records = defaultdict(IPRecord)
220
+
221
+ def is_whitelisted(self, ip):
222
+ """Check if IP is in the whitelist (exact or CIDR match)."""
223
+ if ip in self.whitelist:
224
+ return True
225
+ try:
226
+ addr = ipaddress.ip_address(ip)
227
+ for w in self.whitelist:
228
+ if "/" in w:
229
+ if addr in ipaddress.ip_network(w, strict=False):
230
+ return True
231
+ except ValueError:
232
+ pass
233
+ return False
234
+
235
+ def is_malicious_path(self, path):
236
+ """Check if request path matches known vulnerability scan fingerprints."""
237
+ path_lower = path.lower()
238
+ return any(p.lower() in path_lower for p in MALICIOUS_PATH_PATTERNS)
239
+
240
+ def check_and_record(self, ip, path):
241
+ """Pre-auth check: only ban status and malicious path detection.
242
+
243
+ This is called BEFORE token verification. It only checks:
244
+ 1. Whether the IP is already banned (tarpit)
245
+ 2. Whether the path matches known malicious fingerprints
246
+
247
+ Rate limiting is NOT done here — it is handled by record_reject()
248
+ which is called only after token verification fails. This prevents
249
+ legitimate authenticated users from being rate-limited by normal
250
+ frontend polling.
251
+
252
+ Args:
253
+ ip: Client IP address.
254
+ path: Request path.
255
+
256
+ Returns:
257
+ dict with keys:
258
+ action: "allow" or "tarpit"
259
+ reason: Human-readable reason string
260
+ ban_remaining: Seconds remaining in ban (0 if not banned)
261
+ """
262
+ if self.is_whitelisted(ip):
263
+ return {"action": "allow", "reason": "whitelisted", "ban_remaining": 0}
264
+
265
+ now = time.time()
266
+ rec = self.records[ip]
267
+
268
+ if not rec.first_seen:
269
+ rec.first_seen = now
270
+
271
+ rec.last_seen = now
272
+ rec.hit_count += 1
273
+
274
+ # Already banned -> tarpit
275
+ if rec.banned_until > now:
276
+ remaining = rec.banned_until - now
277
+ return {"action": "tarpit", "reason": "banned", "ban_remaining": remaining}
278
+
279
+ # Malicious path detection (ban immediately on threshold)
280
+ if self.is_malicious_path(path):
281
+ rec.malicious_score += 1
282
+ rec.reject_count += 1
283
+ if rec.malicious_score >= self.malicious_threshold:
284
+ self._ban_ip(
285
+ ip,
286
+ rec,
287
+ f"malicious path threshold ({rec.malicious_score})",
288
+ )
289
+ return {
290
+ "action": "tarpit",
291
+ "reason": f"banned: malicious_score={rec.malicious_score}",
292
+ "ban_remaining": rec.banned_until - now,
293
+ }
294
+
295
+ return {"action": "allow", "reason": "passed", "ban_remaining": 0}
296
+
297
+ def record_reject(self, ip, path):
298
+ """Record an auth rejection (called after token check fails).
299
+
300
+ This feeds the rate limiter without re-running malicious path check.
301
+ """
302
+ if self.is_whitelisted(ip):
303
+ return
304
+
305
+ now = time.time()
306
+ rec = self.records[ip]
307
+ rec.reject_count += 1
308
+ rec.recent_timestamps.append(now)
309
+ cutoff = now - self.rate_window
310
+ rec.recent_timestamps = [t for t in rec.recent_timestamps if t > cutoff]
311
+
312
+ if len(rec.recent_timestamps) > self.rate_limit:
313
+ self._ban_ip(
314
+ ip,
315
+ rec,
316
+ f"rate limit after reject ({len(rec.recent_timestamps)}/{self.rate_window}s)",
317
+ )
318
+
319
+ def _ban_ip(self, ip, rec, reason):
320
+ """Ban an IP with escalating duration."""
321
+ rec.ban_count += 1
322
+ duration = min(
323
+ self.ban_duration * (self.ban_escalation ** (rec.ban_count - 1)),
324
+ self.max_ban_duration,
325
+ )
326
+ rec.banned_until = time.time() + duration
327
+ logger.warning(
328
+ f"AUTO-BAN: {ip} banned for {duration:.0f}s "
329
+ f"(count={rec.ban_count}, reason={reason}, "
330
+ f"total_hits={rec.hit_count}, rejects={rec.reject_count})"
331
+ )
332
+
333
+ def get_banned_ips(self):
334
+ """Get list of currently banned IPs."""
335
+ now = time.time()
336
+ result = []
337
+ for ip, rec in self.records.items():
338
+ if rec.banned_until > now:
339
+ result.append(
340
+ {
341
+ "ip": ip,
342
+ "banned_until": rec.banned_until,
343
+ "remaining": rec.banned_until - now,
344
+ "ban_count": rec.ban_count,
345
+ "total_hits": rec.hit_count,
346
+ "malicious_score": rec.malicious_score,
347
+ }
348
+ )
349
+ return result
350
+
351
+ def get_stats(self):
352
+ """Get engine statistics."""
353
+ now = time.time()
354
+ active_bans = sum(1 for r in self.records.values() if r.banned_until > now)
355
+ return {
356
+ "tracked_ips": len(self.records),
357
+ "active_bans": active_bans,
358
+ "total_bans_issued": sum(r.ban_count for r in self.records.values()),
359
+ }
360
+
361
+ def unban_ip(self, ip):
362
+ """Manually unban an IP."""
363
+ if ip in self.records:
364
+ self.records[ip].banned_until = 0
365
+ logger.info(f"MANUAL-UNBAN: {ip}")
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # MIT License
4
+ # Copyright (c) 2025 - 2026 _VIFEXTech
5
+
6
+ """
7
+ Authentication middleware for FPBInject Web Server.
8
+
9
+ Provides token-based authentication for non-localhost access.
10
+ Localhost requests are always allowed without authentication.
11
+
12
+ Includes auto-ban engine that detects and throttles malicious
13
+ vulnerability scanners via path fingerprinting and rate limiting.
14
+
15
+ Security hardening:
16
+ - Constant-time token comparison (prevents timing attacks)
17
+ - Non-blocking tarpit via streaming response (prevents thread exhaustion)
18
+ - CSP and Referrer-Policy headers
19
+ """
20
+
21
+ import hmac
22
+ import logging
23
+
24
+ from flask import request, after_this_request, jsonify, Response
25
+
26
+ from fpbinject.app.auto_ban import AutoBanEngine
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Addresses considered localhost (exempt from auth)
31
+ LOCALHOST_ADDRS = {"127.0.0.1", "::1"}
32
+
33
+
34
+ def _constant_time_compare(a, b):
35
+ """Compare two strings in constant time to prevent timing attacks.
36
+
37
+ Uses hmac.compare_digest which is designed to prevent timing
38
+ side-channel attacks on token/password comparison.
39
+ """
40
+ if a is None or b is None:
41
+ return False
42
+ return hmac.compare_digest(a.encode("utf-8"), b.encode("utf-8"))
43
+
44
+
45
+ def _make_tarpit_response(delay):
46
+ """Create a streaming response that delays without blocking the worker thread.
47
+
48
+ Instead of time.sleep() which blocks the entire Werkzeug worker thread,
49
+ this uses a generator that yields empty chunks with a pause, allowing
50
+ the WSGI server to handle other requests on remaining threads.
51
+ """
52
+ import time
53
+
54
+ def slow_generator():
55
+ time.sleep(delay)
56
+ yield b""
57
+
58
+ return Response(
59
+ slow_generator(),
60
+ status=403,
61
+ content_type="text/plain",
62
+ headers={"Cache-Control": "no-store"},
63
+ )
64
+
65
+
66
+ def init_auth(app, token):
67
+ """Register authentication middleware with auto-ban protection.
68
+
69
+ Args:
70
+ app: Flask application instance
71
+ token: The authentication token string
72
+ """
73
+ # Create auto-ban engine instance
74
+ ban_engine = AutoBanEngine(
75
+ rate_window=10,
76
+ rate_limit=20,
77
+ malicious_threshold=3,
78
+ ban_duration=3600,
79
+ tarpit_delay=10.0,
80
+ whitelist=["127.0.0.1", "::1"],
81
+ )
82
+
83
+ # Store engine on app for test access
84
+ app.ban_engine = ban_engine
85
+
86
+ @app.before_request
87
+ def check_token():
88
+ """Check authentication token for non-localhost requests."""
89
+ # Localhost is always allowed
90
+ if request.remote_addr in LOCALHOST_ADDRS:
91
+ return None
92
+
93
+ # Static resources are public (they contain no sensitive data,
94
+ # and the page itself is protected by token auth)
95
+ if request.path.startswith("/static/"):
96
+ return None
97
+
98
+ # Auto-ban check (before token verification)
99
+ decision = ban_engine.check_and_record(request.remote_addr, request.path)
100
+ if decision["action"] == "tarpit":
101
+ logger.warning(
102
+ f"Tarpit: {request.remote_addr} -> {request.path} "
103
+ f"(remaining: {decision['ban_remaining']:.0f}s)"
104
+ )
105
+ return _make_tarpit_response(ban_engine.tarpit_delay)
106
+
107
+ # Check token from query, header, or cookie
108
+ req_token = (
109
+ request.args.get("token")
110
+ or request.headers.get("X-Auth-Token")
111
+ or request.cookies.get("fpbinject_token")
112
+ )
113
+
114
+ if not _constant_time_compare(req_token, token):
115
+ logger.warning(f"Auth rejected: {request.remote_addr} -> {request.path}")
116
+ # Record rejection for rate limiting
117
+ ban_engine.record_reject(request.remote_addr, request.path)
118
+ # Return JSON for API routes so frontend can parse the error
119
+ if request.path.startswith("/api/"):
120
+ response = jsonify({"success": False, "error": "Forbidden"})
121
+ response.status_code = 403
122
+ else:
123
+ response = app.make_response(("Forbidden", 403))
124
+ response.headers["Cache-Control"] = "no-store"
125
+ return response
126
+
127
+ # Set cookie on first successful token auth via query/header
128
+ if not request.cookies.get("fpbinject_token"):
129
+
130
+ @after_this_request
131
+ def set_cookie(response):
132
+ response.set_cookie(
133
+ "fpbinject_token",
134
+ token,
135
+ httponly=True,
136
+ samesite="Lax",
137
+ )
138
+ return response
139
+
140
+ @app.after_request
141
+ def add_security_headers(response):
142
+ """Add security headers to all responses."""
143
+ response.headers["X-Content-Type-Options"] = "nosniff"
144
+ response.headers["X-Frame-Options"] = "SAMEORIGIN"
145
+ response.headers["Referrer-Policy"] = "same-origin"
146
+ response.headers["Content-Security-Policy"] = (
147
+ "default-src 'self'; "
148
+ "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
149
+ "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
150
+ "img-src 'self' data: blob:; "
151
+ "connect-src 'self'; "
152
+ "font-src 'self' https://cdn.jsdelivr.net; "
153
+ "worker-src 'self' blob: https://cdn.jsdelivr.net"
154
+ )
155
+ return response
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # MIT License
4
+ # Copyright (c) 2025 - 2026 _VIFEXTech
5
+
6
+ """
7
+ Flask API Routes Package.
8
+
9
+ This package contains all API route blueprints organized by functionality.
10
+ During migration, routes are gradually moved from the legacy routes.py module.
11
+ """
12
+
13
+ from flask import Flask
14
+
15
+
16
+ def register_blueprints(app: Flask):
17
+ """Register all route blueprints with the Flask app."""
18
+ from . import (
19
+ connection,
20
+ files,
21
+ fpb,
22
+ logs,
23
+ patch,
24
+ symbols,
25
+ transfer,
26
+ watch,
27
+ watch_expr,
28
+ )
29
+
30
+ # Register blueprints with /api prefix
31
+ app.register_blueprint(connection.bp, url_prefix="/api")
32
+ app.register_blueprint(fpb.bp, url_prefix="/api")
33
+ app.register_blueprint(logs.bp, url_prefix="/api")
34
+ app.register_blueprint(files.bp, url_prefix="/api")
35
+ app.register_blueprint(watch.bp, url_prefix="/api")
36
+ app.register_blueprint(patch.bp, url_prefix="/api")
37
+ app.register_blueprint(symbols.bp, url_prefix="/api")
38
+ app.register_blueprint(transfer.bp, url_prefix="/api")
39
+ app.register_blueprint(watch_expr.bp, url_prefix="/api")
40
+
41
+
42
+ __all__ = ["register_blueprints"]