refactor(gemini_flash): implement CSP-compliant single-file architecture

- Replace inline script injection with dynamic asset route /gemini.js to satisfy script-src 'self' CSP policies.

- Implement server-side parameter injection via URL params (	oken, q) to eliminate XSS vectors from data-* attributes.

- Secure JS injection using json.dumps for proper serialization.

- Integrate markupsafe.Markup to prevent Jinja2 template auto-escaping of the result injection.

- Reduce log verbosity in the streaming loop while maintaining visibility for critical hooks.

- Remove external dependencies, relying solely on standard lib and host environment packages.
This commit is contained in:
cra88y/pc
2026-01-10 18:13:12 -06:00
parent f849763c4a
commit 531128e828
+50 -35
View File
@@ -1,8 +1,9 @@
import json, secrets, time, http.client, ssl, os, logging import json, secrets, time, http.client, ssl, os, logging, html, urllib.parse
from flask import Response, request, abort from flask import Response, request, abort
from searx.plugins import Plugin, PluginInfo from searx.plugins import Plugin, PluginInfo
from searx.result_types import EngineResults from searx.result_types import EngineResults
from flask_babel import gettext from flask_babel import gettext
from markupsafe import Markup
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -33,14 +34,11 @@ class SXNGPlugin(Plugin):
t = request.args.get('token') t = request.args.get('token')
q = request.args.get('q', '') q = request.args.get('q', '')
logger.info(f"[{self.id}] Stream requested. Token: {t[:5]}...") # Maintenance
# Maintenance: Only runs when a stream is requested, not during search.
current_time = time.time() current_time = time.time()
self.tokens = {k: v for k, v in self.tokens.items() if v > current_time} self.tokens = {k: v for k, v in self.tokens.items() if v > current_time}
if t not in self.tokens or not self.api_key: if t not in self.tokens or not self.api_key:
logger.warning(f"[{self.id}] Unauthorized stream attempt or missing key.")
abort(403) abort(403)
del self.tokens[t] del self.tokens[t]
@@ -48,14 +46,12 @@ class SXNGPlugin(Plugin):
host = "generativelanguage.googleapis.com" host = "generativelanguage.googleapis.com"
path = f"/v1beta/models/{self.model}:streamGenerateContent?key={self.api_key}" path = f"/v1beta/models/{self.model}:streamGenerateContent?key={self.api_key}"
try: try:
logger.info(f"[{self.id}] Connecting to Google API...")
context = ssl.create_default_context() context = ssl.create_default_context()
conn = http.client.HTTPSConnection(host, context=context) conn = http.client.HTTPSConnection(host, context=context)
conn.request("POST", path, body=json.dumps({"contents": [{"parts": [{"text": q}]}]}), conn.request("POST", path, body=json.dumps({"contents": [{"parts": [{"text": q}]}]}),
headers={"Content-Type": "application/json"}) headers={"Content-Type": "application/json"})
res = conn.getresponse() res = conn.getresponse()
logger.info(f"[{self.id}] Google API connected. Streaming...")
buffer = "" buffer = ""
for chunk in res: for chunk in res:
buffer += chunk.decode('utf-8') buffer += chunk.decode('utf-8')
@@ -78,41 +74,36 @@ class SXNGPlugin(Plugin):
except: except:
buffer = buffer[end:] buffer = buffer[end:]
conn.close() conn.close()
logger.info(f"[{self.id}] Stream finished.")
except Exception as e: except Exception as e:
logger.error(f"[{self.id}] Stream error: {e}") logger.error(f"[{self.id}] Stream error: {e}")
yield f" [Stream Error: {str(e)}]" yield f" [Error: {str(e)}]"
return Response(generate(), mimetype='text/plain') return Response(generate(), mimetype='text/plain')
def post_search(self, request, search) -> EngineResults: @app.route('/gemini.js')
logger.info(f"[{self.id}] post_search hook triggered. Page: {search.search_query.pageno}, Active: {self.active}, Key Present: {bool(self.api_key)}") def g_script():
# Get parameters from the SCRIPT SRC url
token = request.args.get('token', '')
query = request.args.get('q', '')
results = EngineResults() # Safe injection of query into the JS string
# Page 1 only + check for API key # We use json.dumps to ensure it is a valid JS string literal (handles quotes/escapes)
if search.search_query.pageno > 1 or not self.active or not self.api_key: js_query = json.dumps(query)
logger.warning(f"[{self.id}] Skipping injection. Criteria failed.") js_token = json.dumps(token)
return results
# ULTRA-LEAN EXECUTION: No loops, no maintenance. Just token creation and return. js_code = f"""
tk = secrets.token_urlsafe(16)
self.tokens[tk] = time.time() + 90
logger.info(f"[{self.id}] Injecting script for query: {search.search_query.query[:20]}...")
# Ensure we escape the query properly for JS
query_json = json.dumps(search.search_query.query)
html = f"""
<div id="ai-shell" style="display:none; margin-bottom: 2rem; padding: 1.2rem; border-bottom: 1px solid var(--color-result-border);">
<div id="ai-out" style="line-height: 1.7; white-space: pre-wrap; color: var(--color-result-description); font-size: 0.95rem;"></div>
</div>
<script>
(async () => {{ (async () => {{
const out = document.getElementById('ai-out');
const shell = document.getElementById('ai-shell'); const shell = document.getElementById('ai-shell');
const out = document.getElementById('ai-out');
if (!shell || !out) return;
const token = {js_token};
const query = {js_query};
try {{ try {{
const res = await fetch(`/gemini-stream?token={tk}&q=` + encodeURIComponent({query_json})); const res = await fetch(`/gemini-stream?token=${{token}}&q=` + encodeURIComponent(query));
if (!res.ok) throw new Error(res.statusText);
const reader = res.body.getReader(); const reader = res.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
while (true) {{ while (true) {{
@@ -124,9 +115,33 @@ class SXNGPlugin(Plugin):
out.innerText += chunk; out.innerText += chunk;
}} }}
}} }}
}} catch (e) {{ console.error("Gemini Failure", e); }} }} catch (e) {{ console.error("Gemini Stream Failed", e); }}
}})(); }})();
</script>
""" """
results.add(results.types.Answer(answer=html)) return Response(js_code, mimetype='application/javascript')
def post_search(self, request, search) -> EngineResults:
results = EngineResults()
if search.search_query.pageno > 1 or not self.active or not self.api_key:
return results
tk = secrets.token_urlsafe(16)
self.tokens[tk] = time.time() + 90
logger.warning(f"[{self.id}] Injecting Answer for query: {search.search_query.query[:20]}...")
# Encode query for the URL parameter in the script tag
safe_query_param = urllib.parse.quote(search.search_query.query)
# HTML Payload:
# 1. The Container (Hidden by default)
# 2. The Script Tag (Pointing to our dynamic route with params)
html_payload = f'''
<div id="ai-shell" style="display:none; margin-bottom: 2rem; padding: 1.2rem; border-bottom: 1px solid var(--color-result-border);">
<div id="ai-out" style="line-height: 1.7; white-space: pre-wrap; color: var(--color-result-description); font-size: 0.95rem;"></div>
</div>
<script src="/gemini.js?token={tk}&q={safe_query_param}"></script>
'''
results.add(results.types.Answer(answer=Markup(html_payload)))
return results return results