mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-14 23:28:05 +00:00
fix: bound the logo download, and stop malformed input reading as a fault
Review findings on the coverage branch.
The download size cap I added checked len(response.content), which has
already buffered the whole body -- it stopped the bytes reaching disk but
not memory, which was the point. A server that omits Content-Length and
never stops sending would still exhaust the process. Stream it instead,
counting as it arrives, into a sibling .part file that is replaced over
the target only once it decodes. A transfer that dies midway now leaves
nothing behind rather than a truncated logo for load_logo() to cache.
The follower's control-message handler caught three exception types, but
two reachable UDP payloads raise others: a bare JSON scalar makes
msg.get() raise AttributeError, and an "sx" carrying a non-numeric x
raises ValueError or TypeError from float(). Those escaped to the outer
handler, skipping the legacy-PNG fallback and -- since this branch added
a backoff there -- charging one malformed packet a 0.1s stall on the
receive path. The legacy-PNG path also decoded without the dimension cap
its TCP counterpart applies, so a crafted 65KB frame could force a large
allocation on the render thread; both paths now share one constant.
Three repo_url handlers called .strip() on client input without checking
it was a string, so {"repo_url": 12345} answered 500. The credentials
upload parsed the same file twice, the second time inside a bare except
that a preceding parse had already made unreachable. And both raw-config
handlers kept a json.JSONDecodeError arm that get_json(silent=True) had
turned into dead code, collapsing "sent something unparseable" into "sent
nothing" -- they now say which.
Two of the new tests were not testing what they claimed. The pruning
round-trip wrote ten backups inside one second, so all ten landed on the
same int(time.time()) filename and overwrote each other; it never reached
the limit it asserted. And the sync clock helper patched attributes on the
stdlib time module, freezing time process-wide for every daemon thread
earlier tests had left running.
Full suite: 3352 passed, coverage 54%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
This commit is contained in:
@@ -1345,18 +1345,20 @@ def save_raw_main_config():
|
||||
if not api_v3.config_manager:
|
||||
return jsonify({'status': 'error', 'message': 'Config manager not initialized'}), 500
|
||||
|
||||
# silent=True so a malformed body returns None instead of raising
|
||||
# Werkzeug's own BadRequest, which would answer in a different
|
||||
# shape than this API's. Distinguish the two causes: a body that
|
||||
# was sent but does not parse is a different mistake from no body.
|
||||
data = request.get_json(silent=True)
|
||||
if data is None and request.get_data():
|
||||
return jsonify({'status': 'error', 'message': 'Invalid JSON in request body'}), 400
|
||||
if not data:
|
||||
return jsonify({'status': 'error', 'message': 'No data provided'}), 400
|
||||
|
||||
# Validate that it's valid JSON (already parsed by request.get_json())
|
||||
# Save the raw config file
|
||||
api_v3.config_manager.save_raw_file_content('main', data)
|
||||
|
||||
return jsonify({'status': 'success', 'message': 'Main configuration saved successfully'})
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error('Invalid JSON', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'Invalid JSON in request body'}), 400
|
||||
except Exception as e:
|
||||
from src.exceptions import ConfigError
|
||||
logger.error("Error saving raw main config", exc_info=True)
|
||||
@@ -1391,7 +1393,11 @@ def save_raw_secrets_config():
|
||||
if not api_v3.config_manager:
|
||||
return jsonify({'status': 'error', 'message': 'Config manager not initialized'}), 500
|
||||
|
||||
# See save_raw_main_config: silent parsing, with a sent-but-broken
|
||||
# body reported separately from a missing one.
|
||||
data = request.get_json(silent=True)
|
||||
if data is None and request.get_data():
|
||||
return jsonify({'status': 'error', 'message': 'Invalid JSON in request body'}), 400
|
||||
if not data:
|
||||
return jsonify({'status': 'error', 'message': 'No data provided'}), 400
|
||||
|
||||
@@ -1403,9 +1409,6 @@ def save_raw_secrets_config():
|
||||
api_v3.plugin_store_manager.github_token = api_v3.plugin_store_manager._load_github_token()
|
||||
|
||||
return jsonify({'status': 'success', 'message': 'Secrets configuration saved successfully'})
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error('Invalid JSON', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'Invalid JSON in request body'}), 400
|
||||
except Exception as e:
|
||||
from src.exceptions import ConfigError
|
||||
logger.error("Error saving raw secrets config", exc_info=True)
|
||||
@@ -3975,6 +3978,11 @@ def install_plugin_from_url():
|
||||
if not data or 'repo_url' not in data:
|
||||
return jsonify({'status': 'error', 'message': 'repo_url required'}), 400
|
||||
|
||||
# A non-string repo_url is a client mistake, not a server fault:
|
||||
# .strip() would raise and the catch-all would report it as a 500.
|
||||
if not isinstance(data['repo_url'], str) or not data['repo_url'].strip():
|
||||
return jsonify({'status': 'error', 'message': 'repo_url must be a non-empty string'}), 400
|
||||
|
||||
repo_url = data['repo_url'].strip()
|
||||
plugin_id = data.get('plugin_id') # Optional, for monorepo installations
|
||||
plugin_path = data.get('plugin_path') # Optional, for monorepo subdirectory
|
||||
@@ -4030,6 +4038,11 @@ def get_registry_from_url():
|
||||
if not data or 'repo_url' not in data:
|
||||
return jsonify({'status': 'error', 'message': 'repo_url required'}), 400
|
||||
|
||||
# A non-string repo_url is a client mistake, not a server fault:
|
||||
# .strip() would raise and the catch-all would report it as a 500.
|
||||
if not isinstance(data['repo_url'], str) or not data['repo_url'].strip():
|
||||
return jsonify({'status': 'error', 'message': 'repo_url must be a non-empty string'}), 400
|
||||
|
||||
repo_url = data['repo_url'].strip()
|
||||
|
||||
# Get registry from the URL
|
||||
@@ -4075,6 +4088,11 @@ def add_saved_repository():
|
||||
if not data or 'repo_url' not in data:
|
||||
return jsonify({'status': 'error', 'message': 'repo_url required'}), 400
|
||||
|
||||
# A non-string repo_url is a client mistake, not a server fault:
|
||||
# .strip() would raise and the catch-all would report it as a 500.
|
||||
if not isinstance(data['repo_url'], str) or not data['repo_url'].strip():
|
||||
return jsonify({'status': 'error', 'message': 'repo_url must be a non-empty string'}), 400
|
||||
|
||||
repo_url = data['repo_url'].strip()
|
||||
name = data.get('name')
|
||||
|
||||
@@ -7289,25 +7307,16 @@ def upload_calendar_credentials():
|
||||
try:
|
||||
file_content = file.read()
|
||||
file.seek(0)
|
||||
json.loads(file_content)
|
||||
creds_data = json.loads(file_content)
|
||||
except json.JSONDecodeError:
|
||||
return jsonify({'status': 'error', 'message': 'File is not valid JSON'}), 400
|
||||
|
||||
# Validate it looks like Google OAuth credentials. The content
|
||||
# already parsed as JSON above, so anything raising here means it is
|
||||
# not credentials-shaped — a bare scalar, for instance, where the
|
||||
# membership test raises TypeError. Reject rather than swallow: a
|
||||
# file saved as credentials.json but not usable as credentials only
|
||||
# fails later, somewhere less obvious.
|
||||
try:
|
||||
file.seek(0)
|
||||
creds_data = json.loads(file.read())
|
||||
file.seek(0)
|
||||
is_oauth_shaped = 'installed' in creds_data or 'web' in creds_data
|
||||
except Exception:
|
||||
is_oauth_shaped = False
|
||||
|
||||
if not is_oauth_shaped:
|
||||
# Validate it looks like Google OAuth credentials. A bare scalar, a
|
||||
# list, true/null — all valid JSON, none of them credentials. Reject
|
||||
# rather than save: a file written as credentials.json but unusable
|
||||
# as credentials only fails later, somewhere less obvious.
|
||||
if not isinstance(creds_data, dict) or not (
|
||||
'installed' in creds_data or 'web' in creds_data):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'File does not appear to be a valid Google OAuth credentials file'
|
||||
|
||||
Reference in New Issue
Block a user