forexsmartbot 3.2.4__tar.gz → 3.3.0__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.
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/PKG-INFO +1 -1
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/__init__.py +1 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/cloud/__init__.py +11 -1
- forexsmartbot-3.3.0/forexsmartbot/cloud/api_client.py +231 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/cloud/api_server.py +76 -28
- forexsmartbot-3.3.0/forexsmartbot/cloud/auth_utils.py +151 -0
- forexsmartbot-3.3.0/forexsmartbot/cloud/cloud_sync.py +389 -0
- forexsmartbot-3.3.0/forexsmartbot/cloud/companion_api.py +324 -0
- forexsmartbot-3.3.0/forexsmartbot/cloud/error_handler.py +194 -0
- forexsmartbot-3.3.0/forexsmartbot/cloud/mobile_api.py +184 -0
- forexsmartbot-3.3.0/forexsmartbot/cloud/remote_monitor.py +582 -0
- forexsmartbot-3.3.0/forexsmartbot/cloud/security.py +249 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/monitoring/strategy_monitor.py +37 -13
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/testing/ci_cd_pipeline.py +39 -15
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/testing/paper_account_tester.py +11 -4
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/testing/strategy_sandbox.py +21 -7
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/PKG-INFO +1 -1
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/SOURCES.txt +4 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/pyproject.toml +1 -1
- forexsmartbot-3.3.0/tests/test_cloud_hardening.py +226 -0
- forexsmartbot-3.3.0/tests/test_companion_q2.py +190 -0
- forexsmartbot-3.2.4/forexsmartbot/cloud/api_client.py +0 -188
- forexsmartbot-3.2.4/forexsmartbot/cloud/cloud_sync.py +0 -279
- forexsmartbot-3.2.4/forexsmartbot/cloud/error_handler.py +0 -150
- forexsmartbot-3.2.4/forexsmartbot/cloud/mobile_api.py +0 -213
- forexsmartbot-3.2.4/forexsmartbot/cloud/remote_monitor.py +0 -310
- forexsmartbot-3.2.4/forexsmartbot/cloud/security.py +0 -208
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/LICENSE +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/README.md +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/brokers/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/brokers/ib_tws_broker.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/brokers/mt4_broker.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/brokers/paper_broker.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/brokers/rest_broker.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/alpha_vantage_provider.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/config.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/csv_provider.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/dummy_provider.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/mt4_provider.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/multi_provider.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/oanda_provider.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/stooq_provider.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/twelve_data_provider.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/yfinance_provider.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/chart_patterns.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/correlation_matrix.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/economic_calendar.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/enhanced_chart.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/live_metrics.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/market_depth.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/performance_attribution.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/portfolio_analytics.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/risk_analytics.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/strategy_comparison.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/analytics/trade_journal.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/brokers/mt4_bridge.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/builder/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/builder/code_generator.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/builder/strategy_builder.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/builder/strategy_template.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/core/backtester.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/core/config.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/core/interfaces.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/core/paper_broker.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/core/portfolio.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/core/risk.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/core/risk_engine.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/core/strategy.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/core/trade_manager.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/gui/main_window.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/gui/theme.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/marketplace/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/marketplace/marketplace.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/marketplace/strategy_rating.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/monitoring/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/monitoring/health_check.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/monitoring/performance_tracker.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/optimization/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/optimization/adaptive_parameters.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/optimization/genetic_optimizer.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/optimization/hyperparameter_optimizer.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/optimization/monte_carlo.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/optimization/multi_objective_optimizer.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/optimization/parameter_sensitivity.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/optimization/walk_forward.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/backtest.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/calculation_engine.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/controller.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/data_streaming.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/enhanced_backtest.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/fast_trading_controller.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/gpu_backtest.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/language_manager.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/notification_service.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/persistence.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/services/startup_manager.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/adaptive_trend_flow.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/breakout_atr.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/ensemble_ml_strategy.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/fear_index.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/lstm_strategy.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/mean_reversion.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/ml_adaptive_supertrend.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/momentum_breakout.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/multi_timeframe_strategy.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/news_trading.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/rl_strategy.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/rsi_reversion.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/scalping_ma.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/sma_crossover.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/svm_strategy.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/strategies/transformer_strategy.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/testing/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/analytics_dialog.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/analytics_widget.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/backtest_dialog.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/charts.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/cloud_dialog.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/enhanced_charts.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/enhanced_main_window.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/export_dialog.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/main_window.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/marketplace_dialog.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/monitoring_dialog.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/monitoring_widget.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/optimization_dialog.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/settings_dialog.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/simple_chart.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/strategy_builder_dialog.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/theme.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/ui/tradingview_chart.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/utils/__init__.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/utils/backtest_utils.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/utils/gpu_numpy.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/utils/gpu_utils.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot/utils/native_indicators.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/dependency_links.txt +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/entry_points.txt +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/requires.txt +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/top_level.txt +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/setup.cfg +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/tests/test_backtest_dialog_ui.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/tests/test_config.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/tests/test_live_metrics.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/tests/test_multi_timeframe_strategy.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/tests/test_native_indicators.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/tests/test_risk.py +0 -0
- {forexsmartbot-3.2.4 → forexsmartbot-3.3.0}/tests/test_strategies.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Cloud Integration Module for ForexSmartBot
|
|
2
|
+
Cloud Integration Module for ForexSmartBot
|
|
3
3
|
Provides cloud sync, remote monitoring, and API access.
|
|
4
4
|
"""
|
|
5
5
|
|
|
@@ -7,8 +7,11 @@ from .cloud_sync import CloudSyncManager
|
|
|
7
7
|
from .remote_monitor import RemoteMonitorServer
|
|
8
8
|
from .api_server import APIServer, WebSocketServer
|
|
9
9
|
from .mobile_api import MobileAppAPI, PushNotificationService
|
|
10
|
+
from .companion_api import CompanionAPI, WebhookNotifier
|
|
10
11
|
from .security import SecurityManager, RateLimiter
|
|
11
12
|
from .api_client import APIClient, WebSocketClient
|
|
13
|
+
from .error_handler import CloudErrorHandler, CloudHealthMonitor, retry_call
|
|
14
|
+
from .auth_utils import generate_api_token, package_version
|
|
12
15
|
|
|
13
16
|
__all__ = [
|
|
14
17
|
'CloudSyncManager',
|
|
@@ -17,8 +20,15 @@ __all__ = [
|
|
|
17
20
|
'WebSocketServer',
|
|
18
21
|
'MobileAppAPI',
|
|
19
22
|
'PushNotificationService',
|
|
23
|
+
'CompanionAPI',
|
|
24
|
+
'WebhookNotifier',
|
|
20
25
|
'SecurityManager',
|
|
21
26
|
'RateLimiter',
|
|
22
27
|
'APIClient',
|
|
23
28
|
'WebSocketClient',
|
|
29
|
+
'CloudErrorHandler',
|
|
30
|
+
'CloudHealthMonitor',
|
|
31
|
+
'retry_call',
|
|
32
|
+
'generate_api_token',
|
|
33
|
+
'package_version',
|
|
24
34
|
]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python SDK for ForexSmartBot API
|
|
3
|
+
Provides easy-to-use client for REST API and WebSocket API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
import websockets
|
|
14
|
+
from dotenv import load_dotenv
|
|
15
|
+
|
|
16
|
+
from .auth_utils import resolve_api_key
|
|
17
|
+
from .error_handler import retry_call
|
|
18
|
+
|
|
19
|
+
load_dotenv(override=False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class APIClient:
|
|
23
|
+
"""Python client for ForexSmartBot REST API."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
api_key: Optional[str] = None,
|
|
28
|
+
base_url: Optional[str] = None,
|
|
29
|
+
*,
|
|
30
|
+
max_retries: int = 3,
|
|
31
|
+
timeout: float = 10.0,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize API client.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
api_key: API key for authentication (defaults to API_KEY env var)
|
|
38
|
+
base_url: Base URL of the API server (defaults to API_BASE_URL env var or http://localhost:5000)
|
|
39
|
+
max_retries: Transient failure retries
|
|
40
|
+
timeout: Request timeout seconds
|
|
41
|
+
"""
|
|
42
|
+
self.api_key = resolve_api_key(api_key, env_names=("API_KEY",))
|
|
43
|
+
default_base = os.getenv("API_BASE_URL", "http://localhost:5000")
|
|
44
|
+
self.base_url = (base_url or default_base).rstrip("/")
|
|
45
|
+
self.max_retries = max_retries
|
|
46
|
+
self.timeout = timeout
|
|
47
|
+
self.session = requests.Session()
|
|
48
|
+
self.session.headers.update({"Content-Type": "application/json"})
|
|
49
|
+
if self.api_key:
|
|
50
|
+
self.session.headers["Authorization"] = f"Bearer {self.api_key}"
|
|
51
|
+
self.session.headers["X-API-Key"] = self.api_key
|
|
52
|
+
|
|
53
|
+
def _request(self, method: str, path: str, **kwargs) -> requests.Response:
|
|
54
|
+
url = f"{self.base_url}{path}"
|
|
55
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
56
|
+
|
|
57
|
+
def _do() -> requests.Response:
|
|
58
|
+
response = self.session.request(method, url, **kwargs)
|
|
59
|
+
if response.status_code >= 500:
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
return response
|
|
63
|
+
|
|
64
|
+
return retry_call(_do, max_retries=self.max_retries, base_delay=1.0)
|
|
65
|
+
|
|
66
|
+
def health(self) -> Dict:
|
|
67
|
+
"""Call public health endpoint."""
|
|
68
|
+
for path in ("/healthz", "/api/v1/health"):
|
|
69
|
+
try:
|
|
70
|
+
response = self.session.get(
|
|
71
|
+
f"{self.base_url}{path}", timeout=min(5.0, self.timeout)
|
|
72
|
+
)
|
|
73
|
+
if response.status_code == 200:
|
|
74
|
+
return response.json()
|
|
75
|
+
except Exception:
|
|
76
|
+
continue
|
|
77
|
+
raise requests.exceptions.ConnectionError(
|
|
78
|
+
f"Health endpoint unreachable at {self.base_url}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def get_balance(self) -> float:
|
|
82
|
+
"""Get account balance."""
|
|
83
|
+
response = self._request("GET", "/api/v1/account/balance")
|
|
84
|
+
return response.json()["balance"]
|
|
85
|
+
|
|
86
|
+
def get_positions(self) -> List[Dict]:
|
|
87
|
+
"""Get open positions."""
|
|
88
|
+
response = self._request("GET", "/api/v1/account/positions")
|
|
89
|
+
return response.json()["positions"]
|
|
90
|
+
|
|
91
|
+
def get_trades(self) -> List[Dict]:
|
|
92
|
+
"""Get trade history."""
|
|
93
|
+
response = self._request("GET", "/api/v1/trades")
|
|
94
|
+
return response.json()["trades"]
|
|
95
|
+
|
|
96
|
+
def create_order(
|
|
97
|
+
self,
|
|
98
|
+
symbol: str,
|
|
99
|
+
side: int,
|
|
100
|
+
quantity: float,
|
|
101
|
+
stop_loss: Optional[float] = None,
|
|
102
|
+
take_profit: Optional[float] = None,
|
|
103
|
+
) -> Dict:
|
|
104
|
+
"""
|
|
105
|
+
Create a new order.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
symbol: Trading symbol
|
|
109
|
+
side: 1 for buy, -1 for sell
|
|
110
|
+
quantity: Order quantity
|
|
111
|
+
stop_loss: Stop loss price (optional)
|
|
112
|
+
take_profit: Take profit price (optional)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Order response dictionary
|
|
116
|
+
"""
|
|
117
|
+
payload = {"symbol": symbol, "side": side, "quantity": quantity}
|
|
118
|
+
if stop_loss:
|
|
119
|
+
payload["stop_loss"] = stop_loss
|
|
120
|
+
if take_profit:
|
|
121
|
+
payload["take_profit"] = take_profit
|
|
122
|
+
|
|
123
|
+
response = self._request("POST", "/api/v1/orders", json=payload)
|
|
124
|
+
return response.json()
|
|
125
|
+
|
|
126
|
+
def close_positions(self, symbol: str) -> Dict:
|
|
127
|
+
"""
|
|
128
|
+
Close all positions for a symbol.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
symbol: Trading symbol
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Response dictionary
|
|
135
|
+
"""
|
|
136
|
+
response = self._request("DELETE", f"/api/v1/orders/{symbol}")
|
|
137
|
+
return response.json()
|
|
138
|
+
|
|
139
|
+
def get_price(self, symbol: str) -> float:
|
|
140
|
+
"""
|
|
141
|
+
Get current price for a symbol.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
symbol: Trading symbol
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Current price
|
|
148
|
+
"""
|
|
149
|
+
response = self._request("GET", f"/api/v1/market/price/{symbol}")
|
|
150
|
+
return response.json()["price"]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class WebSocketClient:
|
|
154
|
+
"""Python client for ForexSmartBot WebSocket API."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, url: Optional[str] = None, api_key: Optional[str] = None):
|
|
157
|
+
"""
|
|
158
|
+
Initialize WebSocket client.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
url: WebSocket server URL (defaults to WS_URL env var or ws://localhost:8765)
|
|
162
|
+
api_key: API key for authentication (defaults to API_KEY env var)
|
|
163
|
+
"""
|
|
164
|
+
default_url = os.getenv("WS_URL", "ws://localhost:8765")
|
|
165
|
+
self.url = url or default_url
|
|
166
|
+
self.api_key = resolve_api_key(api_key, env_names=("API_KEY", "WS_API_KEY"))
|
|
167
|
+
self.websocket = None
|
|
168
|
+
|
|
169
|
+
async def connect(self):
|
|
170
|
+
"""Connect to WebSocket server and authenticate when a key is set."""
|
|
171
|
+
self.websocket = await websockets.connect(self.url)
|
|
172
|
+
if self.api_key:
|
|
173
|
+
await self.websocket.send(
|
|
174
|
+
json.dumps({"command": "auth", "token": self.api_key})
|
|
175
|
+
)
|
|
176
|
+
raw = await self.websocket.recv()
|
|
177
|
+
data = json.loads(raw)
|
|
178
|
+
if not data.get("ok"):
|
|
179
|
+
await self.disconnect()
|
|
180
|
+
raise PermissionError("WebSocket authentication failed")
|
|
181
|
+
|
|
182
|
+
async def disconnect(self):
|
|
183
|
+
"""Disconnect from WebSocket server."""
|
|
184
|
+
if self.websocket:
|
|
185
|
+
await self.websocket.close()
|
|
186
|
+
self.websocket = None
|
|
187
|
+
|
|
188
|
+
async def subscribe(self, symbol: str):
|
|
189
|
+
"""Subscribe to price updates for a symbol."""
|
|
190
|
+
if not self.websocket:
|
|
191
|
+
await self.connect()
|
|
192
|
+
|
|
193
|
+
message = {"command": "subscribe", "symbol": symbol}
|
|
194
|
+
await self.websocket.send(json.dumps(message))
|
|
195
|
+
|
|
196
|
+
async def get_balance(self) -> float:
|
|
197
|
+
"""Get balance via WebSocket."""
|
|
198
|
+
if not self.websocket:
|
|
199
|
+
await self.connect()
|
|
200
|
+
|
|
201
|
+
message = {"command": "get_balance"}
|
|
202
|
+
await self.websocket.send(json.dumps(message))
|
|
203
|
+
|
|
204
|
+
response = await self.websocket.recv()
|
|
205
|
+
data = json.loads(response)
|
|
206
|
+
return data.get("value", 0.0)
|
|
207
|
+
|
|
208
|
+
async def get_positions(self) -> List[Dict]:
|
|
209
|
+
"""Get positions via WebSocket."""
|
|
210
|
+
if not self.websocket:
|
|
211
|
+
await self.connect()
|
|
212
|
+
|
|
213
|
+
message = {"command": "get_positions"}
|
|
214
|
+
await self.websocket.send(json.dumps(message))
|
|
215
|
+
|
|
216
|
+
response = await self.websocket.recv()
|
|
217
|
+
data = json.loads(response)
|
|
218
|
+
return data.get("data", [])
|
|
219
|
+
|
|
220
|
+
async def listen(self, callback):
|
|
221
|
+
"""Listen for real-time updates."""
|
|
222
|
+
if not self.websocket:
|
|
223
|
+
await self.connect()
|
|
224
|
+
|
|
225
|
+
while True:
|
|
226
|
+
try:
|
|
227
|
+
message = await self.websocket.recv()
|
|
228
|
+
data = json.loads(message)
|
|
229
|
+
callback(data)
|
|
230
|
+
except websockets.exceptions.ConnectionClosed:
|
|
231
|
+
break
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
API Server for ForexSmartBot
|
|
2
|
+
API Server for ForexSmartBot
|
|
3
3
|
Provides REST API and WebSocket API for external integrations.
|
|
4
4
|
"""
|
|
5
5
|
|
|
@@ -13,16 +13,26 @@ from flask import Flask, jsonify, request
|
|
|
13
13
|
from flask_cors import CORS
|
|
14
14
|
from flask_limiter import Limiter
|
|
15
15
|
from flask_limiter.util import get_remote_address
|
|
16
|
-
import jwt
|
|
17
16
|
from functools import wraps
|
|
18
17
|
from threading import Thread
|
|
19
18
|
import websockets
|
|
20
19
|
from websockets.server import serve
|
|
21
20
|
from dotenv import load_dotenv
|
|
22
21
|
|
|
22
|
+
from .auth_utils import (
|
|
23
|
+
constant_time_equals,
|
|
24
|
+
extract_bearer_or_api_key,
|
|
25
|
+
is_insecure_secret,
|
|
26
|
+
package_version,
|
|
27
|
+
resolve_api_key,
|
|
28
|
+
validate_server_credentials,
|
|
29
|
+
)
|
|
30
|
+
|
|
23
31
|
# Ensure .env is loaded
|
|
24
32
|
load_dotenv(override=False)
|
|
25
33
|
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
26
36
|
|
|
27
37
|
class APIServer:
|
|
28
38
|
"""REST API server for external integrations."""
|
|
@@ -40,8 +50,8 @@ class APIServer:
|
|
|
40
50
|
"""
|
|
41
51
|
self.host = host or os.getenv('API_HOST', '127.0.0.1')
|
|
42
52
|
self.port = port or int(os.getenv('API_PORT', '5000'))
|
|
43
|
-
self.api_key = api_key
|
|
44
|
-
self.secret_key = secret_key or os.getenv('API_SECRET_KEY', '
|
|
53
|
+
self.api_key = resolve_api_key(api_key, env_names=('API_KEY',))
|
|
54
|
+
self.secret_key = secret_key or os.getenv('API_SECRET_KEY', '')
|
|
45
55
|
|
|
46
56
|
self.app = Flask(__name__)
|
|
47
57
|
CORS(self.app)
|
|
@@ -60,16 +70,28 @@ class APIServer:
|
|
|
60
70
|
self.setup_routes()
|
|
61
71
|
self.server_thread = None
|
|
62
72
|
self.running = False
|
|
73
|
+
self.started_at: Optional[datetime] = None
|
|
63
74
|
|
|
64
75
|
def setup_routes(self):
|
|
65
76
|
"""Setup API routes."""
|
|
66
|
-
|
|
77
|
+
|
|
78
|
+
@self.app.route('/healthz', methods=['GET'])
|
|
67
79
|
@self.app.route('/api/v1/health', methods=['GET'])
|
|
68
80
|
def health():
|
|
69
|
-
"""Health check endpoint."""
|
|
81
|
+
"""Health check endpoint (public)."""
|
|
82
|
+
broker_ok = False
|
|
83
|
+
if self.broker is not None:
|
|
84
|
+
try:
|
|
85
|
+
broker_ok = bool(self.broker.is_connected()) if hasattr(self.broker, 'is_connected') else True
|
|
86
|
+
except Exception:
|
|
87
|
+
broker_ok = False
|
|
70
88
|
return jsonify({
|
|
71
|
-
'status': 'healthy',
|
|
72
|
-
'
|
|
89
|
+
'status': 'healthy' if self.running else 'starting',
|
|
90
|
+
'service': 'api_server',
|
|
91
|
+
'version': package_version(),
|
|
92
|
+
'auth_configured': not is_insecure_secret(self.api_key),
|
|
93
|
+
'broker_connected': broker_ok,
|
|
94
|
+
'started_at': self.started_at.isoformat() if self.started_at else None,
|
|
73
95
|
'timestamp': datetime.now().isoformat()
|
|
74
96
|
})
|
|
75
97
|
|
|
@@ -193,20 +215,19 @@ class APIServer:
|
|
|
193
215
|
return jsonify({'error': str(e)}), 500
|
|
194
216
|
|
|
195
217
|
def require_auth(self, f):
|
|
196
|
-
"""Decorator for API authentication."""
|
|
218
|
+
"""Decorator for API authentication (Bearer or X-API-Key, constant-time)."""
|
|
197
219
|
@wraps(f)
|
|
198
220
|
def decorated_function(*args, **kwargs):
|
|
199
|
-
if
|
|
221
|
+
if is_insecure_secret(self.api_key):
|
|
200
222
|
return jsonify({'error': 'API key not configured'}), 500
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
token
|
|
207
|
-
if token != self.api_key:
|
|
223
|
+
|
|
224
|
+
token = extract_bearer_or_api_key(
|
|
225
|
+
request.headers.get('Authorization', ''),
|
|
226
|
+
request.headers.get('X-API-Key', ''),
|
|
227
|
+
)
|
|
228
|
+
if not constant_time_equals(token, self.api_key):
|
|
208
229
|
return jsonify({'error': 'Invalid API key'}), 401
|
|
209
|
-
|
|
230
|
+
|
|
210
231
|
return f(*args, **kwargs)
|
|
211
232
|
return decorated_function
|
|
212
233
|
|
|
@@ -226,10 +247,19 @@ class APIServer:
|
|
|
226
247
|
"""Start the API server."""
|
|
227
248
|
if self.running:
|
|
228
249
|
return
|
|
229
|
-
|
|
250
|
+
|
|
251
|
+
validate_server_credentials(
|
|
252
|
+
self.api_key,
|
|
253
|
+
host=self.host,
|
|
254
|
+
secret_key=self.secret_key or None,
|
|
255
|
+
service_name='APIServer',
|
|
256
|
+
)
|
|
257
|
+
|
|
230
258
|
self.running = True
|
|
259
|
+
self.started_at = datetime.now()
|
|
231
260
|
self.server_thread = Thread(target=self._run_server, daemon=True)
|
|
232
261
|
self.server_thread.start()
|
|
262
|
+
logger.info("API server started on http://%s:%s", self.host, self.port)
|
|
233
263
|
print(f"API server started on http://{self.host}:{self.port}")
|
|
234
264
|
|
|
235
265
|
def stop(self):
|
|
@@ -257,36 +287,54 @@ class WebSocketServer:
|
|
|
257
287
|
"""
|
|
258
288
|
self.host = host or os.getenv('WS_HOST', '127.0.0.1')
|
|
259
289
|
self.port = port or int(os.getenv('WS_PORT', '8765'))
|
|
260
|
-
self.api_key = api_key
|
|
290
|
+
self.api_key = resolve_api_key(api_key, env_names=('API_KEY', 'WS_API_KEY'))
|
|
261
291
|
self.clients = set()
|
|
292
|
+
self.authenticated_clients = set()
|
|
262
293
|
self.running = False
|
|
263
294
|
self.broker = None
|
|
264
295
|
|
|
265
296
|
async def register_client(self, websocket, path):
|
|
266
|
-
"""Register a new WebSocket client."""
|
|
267
|
-
# Simple authentication check
|
|
268
|
-
if self.api_key:
|
|
269
|
-
# In production, implement proper authentication
|
|
270
|
-
pass
|
|
271
|
-
|
|
297
|
+
"""Register a new WebSocket client (must auth when API key configured)."""
|
|
272
298
|
self.clients.add(websocket)
|
|
273
299
|
print(f"WebSocket client connected: {websocket.remote_address}")
|
|
274
300
|
|
|
275
301
|
try:
|
|
276
302
|
async for message in websocket:
|
|
277
|
-
# Handle incoming messages
|
|
278
303
|
await self.handle_message(websocket, message)
|
|
279
304
|
except websockets.exceptions.ConnectionClosed:
|
|
280
305
|
pass
|
|
281
306
|
finally:
|
|
282
|
-
self.clients.
|
|
307
|
+
self.clients.discard(websocket)
|
|
308
|
+
self.authenticated_clients.discard(websocket)
|
|
283
309
|
print(f"WebSocket client disconnected: {websocket.remote_address}")
|
|
310
|
+
|
|
311
|
+
def _ws_authorized(self, websocket) -> bool:
|
|
312
|
+
if is_insecure_secret(self.api_key):
|
|
313
|
+
return True
|
|
314
|
+
return websocket in self.authenticated_clients
|
|
284
315
|
|
|
285
316
|
async def handle_message(self, websocket, message):
|
|
286
317
|
"""Handle incoming WebSocket message."""
|
|
287
318
|
try:
|
|
288
319
|
data = json.loads(message)
|
|
289
320
|
command = data.get('command')
|
|
321
|
+
|
|
322
|
+
if command == 'auth':
|
|
323
|
+
token = data.get('token') or data.get('api_key') or ''
|
|
324
|
+
if constant_time_equals(token, self.api_key):
|
|
325
|
+
self.authenticated_clients.add(websocket)
|
|
326
|
+
await websocket.send(json.dumps({'type': 'auth', 'ok': True}))
|
|
327
|
+
else:
|
|
328
|
+
self.authenticated_clients.discard(websocket)
|
|
329
|
+
await websocket.send(json.dumps({'type': 'auth', 'ok': False}))
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
if not self._ws_authorized(websocket):
|
|
333
|
+
await websocket.send(json.dumps({
|
|
334
|
+
'type': 'error',
|
|
335
|
+
'message': 'Unauthorized — send {"command":"auth","token":"..."} first'
|
|
336
|
+
}))
|
|
337
|
+
return
|
|
290
338
|
|
|
291
339
|
if command == 'subscribe':
|
|
292
340
|
symbol = data.get('symbol')
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared authentication helpers for remote monitoring and cloud APIs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import hmac
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import secrets
|
|
11
|
+
from typing import Optional, Tuple
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Weak / placeholder secrets that must never be used in production.
|
|
16
|
+
_INSECURE_SECRETS = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"",
|
|
19
|
+
"default-secret-key-change-in-production",
|
|
20
|
+
"changeme",
|
|
21
|
+
"secret",
|
|
22
|
+
"password",
|
|
23
|
+
"api_key",
|
|
24
|
+
"test",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def package_version() -> str:
|
|
30
|
+
"""Return installed package version, falling back to local __version__."""
|
|
31
|
+
try:
|
|
32
|
+
from importlib.metadata import version
|
|
33
|
+
|
|
34
|
+
return version("forexsmartbot")
|
|
35
|
+
except Exception:
|
|
36
|
+
try:
|
|
37
|
+
from forexsmartbot import __version__
|
|
38
|
+
|
|
39
|
+
return __version__
|
|
40
|
+
except Exception:
|
|
41
|
+
return "0.0.0"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_insecure_secret(value: Optional[str]) -> bool:
|
|
45
|
+
"""True if the value is empty or a known placeholder secret."""
|
|
46
|
+
if value is None:
|
|
47
|
+
return True
|
|
48
|
+
normalized = value.strip().lower()
|
|
49
|
+
return normalized in _INSECURE_SECRETS or len(value.strip()) < 16
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def allow_insecure_mode() -> bool:
|
|
53
|
+
"""Explicit opt-in for local/dev use without a strong API key."""
|
|
54
|
+
return os.getenv("FSB_ALLOW_INSECURE_MONITOR", "").strip().lower() in {
|
|
55
|
+
"1",
|
|
56
|
+
"true",
|
|
57
|
+
"yes",
|
|
58
|
+
"on",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def extract_bearer_or_api_key(
|
|
63
|
+
authorization_header: str = "",
|
|
64
|
+
api_key_header: str = "",
|
|
65
|
+
) -> Optional[str]:
|
|
66
|
+
"""
|
|
67
|
+
Extract a token from Authorization: Bearer <token> or X-API-Key.
|
|
68
|
+
|
|
69
|
+
Returns None when neither header provides a token.
|
|
70
|
+
"""
|
|
71
|
+
auth = (authorization_header or "").strip()
|
|
72
|
+
if auth.lower().startswith("bearer "):
|
|
73
|
+
token = auth[7:].strip()
|
|
74
|
+
if token:
|
|
75
|
+
return token
|
|
76
|
+
key = (api_key_header or "").strip()
|
|
77
|
+
return key or None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def constant_time_equals(provided: Optional[str], expected: Optional[str]) -> bool:
|
|
81
|
+
"""Constant-time string compare; False if either side is missing."""
|
|
82
|
+
if provided is None or expected is None:
|
|
83
|
+
return False
|
|
84
|
+
try:
|
|
85
|
+
return hmac.compare_digest(provided.encode("utf-8"), expected.encode("utf-8"))
|
|
86
|
+
except Exception:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def generate_api_token(nbytes: int = 32) -> str:
|
|
91
|
+
"""Generate a cryptographically strong URL-safe API token."""
|
|
92
|
+
return secrets.token_urlsafe(nbytes)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def resolve_api_key(
|
|
96
|
+
explicit: Optional[str] = None,
|
|
97
|
+
env_names: Tuple[str, ...] = ("API_KEY", "CLOUD_API_KEY"),
|
|
98
|
+
) -> str:
|
|
99
|
+
"""Resolve API key from constructor arg or environment (first non-empty)."""
|
|
100
|
+
if explicit is not None and explicit.strip():
|
|
101
|
+
return explicit.strip()
|
|
102
|
+
for name in env_names:
|
|
103
|
+
value = os.getenv(name, "")
|
|
104
|
+
if value and value.strip():
|
|
105
|
+
return value.strip()
|
|
106
|
+
return ""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def validate_server_credentials(
|
|
110
|
+
api_key: str,
|
|
111
|
+
*,
|
|
112
|
+
host: str = "127.0.0.1",
|
|
113
|
+
secret_key: Optional[str] = None,
|
|
114
|
+
service_name: str = "service",
|
|
115
|
+
) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Enforce safe defaults before binding a network service.
|
|
118
|
+
|
|
119
|
+
Raises ValueError when credentials are too weak for the bind address
|
|
120
|
+
unless FSB_ALLOW_INSECURE_MONITOR is set.
|
|
121
|
+
"""
|
|
122
|
+
loopback = host in {"127.0.0.1", "localhost", "::1"}
|
|
123
|
+
insecure_key = is_insecure_secret(api_key)
|
|
124
|
+
insecure_secret = secret_key is not None and is_insecure_secret(secret_key)
|
|
125
|
+
|
|
126
|
+
if not insecure_key and not insecure_secret:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
if allow_insecure_mode():
|
|
130
|
+
logger.warning(
|
|
131
|
+
"%s starting with insecure credentials (FSB_ALLOW_INSECURE_MONITOR=1)",
|
|
132
|
+
service_name,
|
|
133
|
+
)
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
if not loopback:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"{service_name}: strong API_KEY required when binding to {host}. "
|
|
139
|
+
"Set API_KEY (min 16 chars) or FSB_ALLOW_INSECURE_MONITOR=1 for local testing only."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if insecure_key:
|
|
143
|
+
logger.warning(
|
|
144
|
+
"%s has no strong API key; localhost-only. Set API_KEY for authenticated access.",
|
|
145
|
+
service_name,
|
|
146
|
+
)
|
|
147
|
+
if insecure_secret:
|
|
148
|
+
logger.warning(
|
|
149
|
+
"%s using weak API_SECRET_KEY; set a strong secret for JWT signing.",
|
|
150
|
+
service_name,
|
|
151
|
+
)
|