django-cfg 1.2.29__py3-none-any.whl → 1.3.1__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -9
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +600 -108
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +470 -64
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
- django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +16 -20
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +207 -67
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -284
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +344 -468
- django_cfg/apps/payments/services/core/subscription_service.py +425 -484
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +232 -71
- django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
- django_cfg/apps/payments/services/providers/registry.py +429 -80
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +211 -130
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +129 -98
- django_cfg/apps/payments/signals/subscription_signals.py +195 -143
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +46 -47
- django_cfg/apps/payments/urls_admin.py +49 -0
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +75 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -498
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +65 -12
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -178
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/managers/__init__.py +0 -22
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -83
- django_cfg/apps/payments/managers/payment_manager.py +0 -44
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -56
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -55
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -297
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -637
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/urls_templates.py +0 -52
- django_cfg/apps/payments/utils/__init__.py +0 -45
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -245
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -62
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -111
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -312
- django_cfg/apps/payments/views/templates/base.py +0 -204
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -164
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -240
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -65
- django_cfg/core/integration.py +0 -160
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,475 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Enhanced Webhook Signature Validation Service.
|
3
|
-
Critical Foundation Security Component.
|
4
|
-
"""
|
5
|
-
|
6
|
-
import json
|
7
|
-
import hmac
|
8
|
-
import hashlib
|
9
|
-
import logging
|
10
|
-
import time
|
11
|
-
from typing import Dict, Any, Optional, Tuple
|
12
|
-
from datetime import datetime, timedelta
|
13
|
-
from django.core.cache import cache
|
14
|
-
from django.utils import timezone
|
15
|
-
from django.conf import settings
|
16
|
-
|
17
|
-
from django_cfg.apps.payments.config import get_payments_config
|
18
|
-
from django_cfg.apps.payments.models.events import PaymentEvent
|
19
|
-
|
20
|
-
logger = logging.getLogger(__name__)
|
21
|
-
|
22
|
-
|
23
|
-
class WebhookValidator:
|
24
|
-
"""
|
25
|
-
Secure webhook signature validation with replay attack protection.
|
26
|
-
|
27
|
-
Foundation Security Component - CRITICAL for system security.
|
28
|
-
"""
|
29
|
-
|
30
|
-
def __init__(self):
|
31
|
-
self.config = get_payments_config()
|
32
|
-
self.nonce_cache_timeout = 3600 # 1 hour nonce validity
|
33
|
-
self.max_timestamp_drift = 300 # 5 minutes max timestamp drift
|
34
|
-
|
35
|
-
def validate_webhook(
|
36
|
-
self,
|
37
|
-
provider: str,
|
38
|
-
webhook_data: Dict[str, Any],
|
39
|
-
request_headers: Dict[str, str],
|
40
|
-
raw_body: bytes = None
|
41
|
-
) -> Tuple[bool, Optional[str]]:
|
42
|
-
"""
|
43
|
-
Comprehensive webhook validation with security checks.
|
44
|
-
|
45
|
-
Returns:
|
46
|
-
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
47
|
-
"""
|
48
|
-
|
49
|
-
try:
|
50
|
-
# Step 1: Provider-specific signature validation
|
51
|
-
signature_valid, signature_error = self._validate_provider_signature(
|
52
|
-
provider, webhook_data, request_headers, raw_body
|
53
|
-
)
|
54
|
-
|
55
|
-
if not signature_valid:
|
56
|
-
self._log_security_event('signature_validation_failed', provider, signature_error)
|
57
|
-
return False, signature_error
|
58
|
-
|
59
|
-
# Step 2: Replay attack protection
|
60
|
-
replay_valid, replay_error = self._validate_against_replay(
|
61
|
-
provider, webhook_data, request_headers
|
62
|
-
)
|
63
|
-
|
64
|
-
if not replay_valid:
|
65
|
-
self._log_security_event('replay_attack_detected', provider, replay_error)
|
66
|
-
return False, replay_error
|
67
|
-
|
68
|
-
# Step 3: Timestamp validation
|
69
|
-
timestamp_valid, timestamp_error = self._validate_timestamp(
|
70
|
-
webhook_data, request_headers
|
71
|
-
)
|
72
|
-
|
73
|
-
if not timestamp_valid:
|
74
|
-
self._log_security_event('timestamp_validation_failed', provider, timestamp_error)
|
75
|
-
return False, timestamp_error
|
76
|
-
|
77
|
-
# Step 4: Rate limiting check
|
78
|
-
rate_limit_valid, rate_limit_error = self._check_rate_limits(
|
79
|
-
provider, request_headers
|
80
|
-
)
|
81
|
-
|
82
|
-
if not rate_limit_valid:
|
83
|
-
self._log_security_event('rate_limit_exceeded', provider, rate_limit_error)
|
84
|
-
return False, rate_limit_error
|
85
|
-
|
86
|
-
# All validations passed
|
87
|
-
self._log_security_event('webhook_validated', provider, 'Validation successful')
|
88
|
-
return True, None
|
89
|
-
|
90
|
-
except Exception as e:
|
91
|
-
error_msg = f"Webhook validation error: {str(e)}"
|
92
|
-
logger.error(f"Critical validation error for {provider}: {e}", exc_info=True)
|
93
|
-
self._log_security_event('validation_exception', provider, error_msg)
|
94
|
-
return False, error_msg
|
95
|
-
|
96
|
-
def _validate_provider_signature(
|
97
|
-
self,
|
98
|
-
provider: str,
|
99
|
-
webhook_data: Dict[str, Any],
|
100
|
-
request_headers: Dict[str, str],
|
101
|
-
raw_body: bytes = None
|
102
|
-
) -> Tuple[bool, Optional[str]]:
|
103
|
-
"""Validate signature based on provider-specific method."""
|
104
|
-
|
105
|
-
if provider == 'cryptapi':
|
106
|
-
return self._validate_cryptapi_signature(webhook_data, request_headers)
|
107
|
-
elif provider == 'cryptomus':
|
108
|
-
return self._validate_cryptomus_signature(webhook_data, request_headers, raw_body)
|
109
|
-
elif provider == 'nowpayments':
|
110
|
-
return self._validate_nowpayments_signature(webhook_data, request_headers, raw_body)
|
111
|
-
elif provider == 'test':
|
112
|
-
return True, None # Allow test webhooks in development
|
113
|
-
else:
|
114
|
-
return False, f"Unknown provider: {provider}"
|
115
|
-
|
116
|
-
def _validate_cryptapi_signature(
|
117
|
-
self,
|
118
|
-
webhook_data: Dict[str, Any],
|
119
|
-
request_headers: Dict[str, str]
|
120
|
-
) -> Tuple[bool, Optional[str]]:
|
121
|
-
"""
|
122
|
-
CryptAPI signature validation with nonce verification.
|
123
|
-
|
124
|
-
CRITICAL FIX: Proper nonce validation to prevent replay attacks.
|
125
|
-
"""
|
126
|
-
|
127
|
-
# Get security nonce from webhook data
|
128
|
-
security_nonce = webhook_data.get('nonce')
|
129
|
-
if not security_nonce:
|
130
|
-
return False, "Missing security nonce in CryptAPI webhook"
|
131
|
-
|
132
|
-
# Validate nonce format and uniqueness
|
133
|
-
nonce_valid, nonce_error = self._validate_nonce(security_nonce, 'cryptapi')
|
134
|
-
if not nonce_valid:
|
135
|
-
return False, f"Invalid security nonce: {nonce_error}"
|
136
|
-
|
137
|
-
# Check required fields
|
138
|
-
required_fields = ['order_id', 'value_coin', 'confirmations']
|
139
|
-
missing_fields = [field for field in required_fields if field not in webhook_data]
|
140
|
-
if missing_fields:
|
141
|
-
return False, f"Missing required fields: {', '.join(missing_fields)}"
|
142
|
-
|
143
|
-
# Validate order_id format
|
144
|
-
order_id = webhook_data.get('order_id')
|
145
|
-
if not self._validate_order_id_format(order_id):
|
146
|
-
return False, f"Invalid order_id format: {order_id}"
|
147
|
-
|
148
|
-
# CryptAPI specific validation: check if address exists in our system
|
149
|
-
address_in = webhook_data.get('address')
|
150
|
-
if address_in and not self._validate_payment_address(address_in, 'cryptapi'):
|
151
|
-
return False, f"Unknown payment address: {address_in}"
|
152
|
-
|
153
|
-
return True, None
|
154
|
-
|
155
|
-
def _validate_cryptomus_signature(
|
156
|
-
self,
|
157
|
-
webhook_data: Dict[str, Any],
|
158
|
-
request_headers: Dict[str, str],
|
159
|
-
raw_body: bytes = None
|
160
|
-
) -> Tuple[bool, Optional[str]]:
|
161
|
-
"""
|
162
|
-
Cryptomus webhook signature validation.
|
163
|
-
|
164
|
-
Uses HMAC-SHA256 signature verification.
|
165
|
-
"""
|
166
|
-
|
167
|
-
# Get webhook secret from configuration
|
168
|
-
if not self.config or not hasattr(self.config, 'providers'):
|
169
|
-
return False, "Cryptomus configuration not found"
|
170
|
-
|
171
|
-
cryptomus_config = self.config.providers.get('cryptomus')
|
172
|
-
if not cryptomus_config:
|
173
|
-
return False, "Cryptomus provider not configured"
|
174
|
-
|
175
|
-
webhook_secret = getattr(cryptomus_config, 'webhook_secret', None)
|
176
|
-
if not webhook_secret:
|
177
|
-
logger.warning("Cryptomus webhook secret not configured, skipping validation")
|
178
|
-
return True, None # Allow if not configured (development mode)
|
179
|
-
|
180
|
-
# Get signature from headers
|
181
|
-
signature = request_headers.get('HTTP_X_CRYPTOMUS_SIGNATURE')
|
182
|
-
if not signature:
|
183
|
-
return False, "Missing Cryptomus signature header"
|
184
|
-
|
185
|
-
# Calculate expected signature
|
186
|
-
if raw_body:
|
187
|
-
payload = raw_body
|
188
|
-
else:
|
189
|
-
payload = json.dumps(webhook_data, separators=(',', ':'), sort_keys=True).encode()
|
190
|
-
|
191
|
-
expected_signature = hmac.new(
|
192
|
-
webhook_secret.encode(),
|
193
|
-
payload,
|
194
|
-
hashlib.sha256
|
195
|
-
).hexdigest()
|
196
|
-
|
197
|
-
# Secure comparison
|
198
|
-
if not hmac.compare_digest(signature, expected_signature):
|
199
|
-
return False, "Invalid Cryptomus signature"
|
200
|
-
|
201
|
-
# Validate required fields
|
202
|
-
required_fields = ['order_id', 'status']
|
203
|
-
missing_fields = [field for field in required_fields if field not in webhook_data]
|
204
|
-
if missing_fields:
|
205
|
-
return False, f"Missing required fields: {', '.join(missing_fields)}"
|
206
|
-
|
207
|
-
return True, None
|
208
|
-
|
209
|
-
def _validate_nowpayments_signature(
|
210
|
-
self,
|
211
|
-
webhook_data: Dict[str, Any],
|
212
|
-
request_headers: Dict[str, str],
|
213
|
-
raw_body: bytes = None
|
214
|
-
) -> Tuple[bool, Optional[str]]:
|
215
|
-
"""
|
216
|
-
NowPayments IPN signature validation.
|
217
|
-
|
218
|
-
Uses HMAC-SHA512 signature verification.
|
219
|
-
"""
|
220
|
-
|
221
|
-
# Get IPN secret from configuration
|
222
|
-
if not self.config or not hasattr(self.config, 'providers'):
|
223
|
-
return False, "NowPayments configuration not found"
|
224
|
-
|
225
|
-
nowpayments_config = self.config.providers.get('nowpayments')
|
226
|
-
if not nowpayments_config:
|
227
|
-
return False, "NowPayments provider not configured"
|
228
|
-
|
229
|
-
ipn_secret = getattr(nowpayments_config, 'ipn_secret', None)
|
230
|
-
if not ipn_secret:
|
231
|
-
logger.warning("NowPayments IPN secret not configured, skipping validation")
|
232
|
-
return True, None # Allow if not configured (development mode)
|
233
|
-
|
234
|
-
# Get signature from headers
|
235
|
-
signature = request_headers.get('HTTP_X_NOWPAYMENTS_SIG')
|
236
|
-
if not signature:
|
237
|
-
return False, "Missing NowPayments signature header"
|
238
|
-
|
239
|
-
# Calculate expected signature
|
240
|
-
if raw_body:
|
241
|
-
payload = raw_body.decode('utf-8')
|
242
|
-
else:
|
243
|
-
payload = json.dumps(webhook_data, separators=(',', ':'), sort_keys=True)
|
244
|
-
|
245
|
-
expected_signature = hmac.new(
|
246
|
-
ipn_secret.encode(),
|
247
|
-
payload.encode(),
|
248
|
-
hashlib.sha512
|
249
|
-
).hexdigest()
|
250
|
-
|
251
|
-
# Secure comparison
|
252
|
-
if not hmac.compare_digest(signature, expected_signature):
|
253
|
-
return False, "Invalid NowPayments signature"
|
254
|
-
|
255
|
-
return True, None
|
256
|
-
|
257
|
-
def _validate_against_replay(
|
258
|
-
self,
|
259
|
-
provider: str,
|
260
|
-
webhook_data: Dict[str, Any],
|
261
|
-
request_headers: Dict[str, str]
|
262
|
-
) -> Tuple[bool, Optional[str]]:
|
263
|
-
"""
|
264
|
-
Protect against replay attacks using idempotency keys.
|
265
|
-
"""
|
266
|
-
|
267
|
-
# Generate idempotency key
|
268
|
-
idempotency_key = self._generate_idempotency_key(provider, webhook_data, request_headers)
|
269
|
-
|
270
|
-
# Check if we've seen this webhook before
|
271
|
-
cache_key = f"webhook_idempotency:{idempotency_key}"
|
272
|
-
if cache.get(cache_key):
|
273
|
-
return False, f"Replay attack detected: duplicate webhook {idempotency_key}"
|
274
|
-
|
275
|
-
# Store idempotency key to prevent replays
|
276
|
-
cache.set(cache_key, True, timeout=self.nonce_cache_timeout)
|
277
|
-
|
278
|
-
return True, None
|
279
|
-
|
280
|
-
def _validate_timestamp(
|
281
|
-
self,
|
282
|
-
webhook_data: Dict[str, Any],
|
283
|
-
request_headers: Dict[str, str]
|
284
|
-
) -> Tuple[bool, Optional[str]]:
|
285
|
-
"""
|
286
|
-
Validate webhook timestamp to prevent old webhook replay.
|
287
|
-
"""
|
288
|
-
|
289
|
-
# Try different timestamp fields
|
290
|
-
timestamp_fields = ['timestamp', 'created_at', 'updated_at', 'time']
|
291
|
-
webhook_timestamp = None
|
292
|
-
|
293
|
-
for field in timestamp_fields:
|
294
|
-
if field in webhook_data:
|
295
|
-
webhook_timestamp = webhook_data[field]
|
296
|
-
break
|
297
|
-
|
298
|
-
# Also check headers
|
299
|
-
if not webhook_timestamp:
|
300
|
-
webhook_timestamp = request_headers.get('HTTP_X_TIMESTAMP')
|
301
|
-
|
302
|
-
if not webhook_timestamp:
|
303
|
-
# If no timestamp provided, skip validation (some providers don't include it)
|
304
|
-
return True, None
|
305
|
-
|
306
|
-
try:
|
307
|
-
# Parse timestamp (support multiple formats)
|
308
|
-
if isinstance(webhook_timestamp, (int, float)):
|
309
|
-
webhook_time = datetime.fromtimestamp(webhook_timestamp, tz=timezone.utc)
|
310
|
-
else:
|
311
|
-
# Try to parse ISO format
|
312
|
-
webhook_time = datetime.fromisoformat(webhook_timestamp.replace('Z', '+00:00'))
|
313
|
-
|
314
|
-
current_time = timezone.now()
|
315
|
-
time_diff = abs((current_time - webhook_time).total_seconds())
|
316
|
-
|
317
|
-
if time_diff > self.max_timestamp_drift:
|
318
|
-
return False, f"Webhook timestamp too old or too new: {time_diff}s drift"
|
319
|
-
|
320
|
-
return True, None
|
321
|
-
|
322
|
-
except Exception as e:
|
323
|
-
logger.warning(f"Could not validate timestamp: {e}")
|
324
|
-
return True, None # Skip validation if timestamp format is unknown
|
325
|
-
|
326
|
-
def _check_rate_limits(
|
327
|
-
self,
|
328
|
-
provider: str,
|
329
|
-
request_headers: Dict[str, str]
|
330
|
-
) -> Tuple[bool, Optional[str]]:
|
331
|
-
"""
|
332
|
-
Check webhook rate limits to prevent abuse.
|
333
|
-
"""
|
334
|
-
|
335
|
-
# Extract IP address
|
336
|
-
ip_address = (
|
337
|
-
request_headers.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() or
|
338
|
-
request_headers.get('HTTP_X_REAL_IP', '') or
|
339
|
-
request_headers.get('REMOTE_ADDR', 'unknown')
|
340
|
-
)
|
341
|
-
|
342
|
-
# Rate limit key
|
343
|
-
rate_limit_key = f"webhook_rate_limit:{provider}:{ip_address}"
|
344
|
-
|
345
|
-
# Check current rate
|
346
|
-
current_count = cache.get(rate_limit_key, 0)
|
347
|
-
max_webhooks_per_minute = 60 # Configurable limit
|
348
|
-
|
349
|
-
if current_count >= max_webhooks_per_minute:
|
350
|
-
return False, f"Rate limit exceeded: {current_count} webhooks/minute from {ip_address}"
|
351
|
-
|
352
|
-
# Increment counter
|
353
|
-
cache.set(rate_limit_key, current_count + 1, timeout=60)
|
354
|
-
|
355
|
-
return True, None
|
356
|
-
|
357
|
-
def _validate_nonce(self, nonce: str, provider: str) -> Tuple[bool, Optional[str]]:
|
358
|
-
"""
|
359
|
-
Validate nonce format and uniqueness.
|
360
|
-
|
361
|
-
CRITICAL: Prevents replay attacks for CryptAPI.
|
362
|
-
"""
|
363
|
-
|
364
|
-
# Validate nonce format
|
365
|
-
if not nonce or len(nonce) < 8:
|
366
|
-
return False, "Nonce too short"
|
367
|
-
|
368
|
-
if len(nonce) > 64:
|
369
|
-
return False, "Nonce too long"
|
370
|
-
|
371
|
-
# Check nonce uniqueness
|
372
|
-
nonce_key = f"webhook_nonce:{provider}:{nonce}"
|
373
|
-
if cache.get(nonce_key):
|
374
|
-
return False, "Nonce already used (replay attack)"
|
375
|
-
|
376
|
-
# Store nonce to prevent reuse
|
377
|
-
cache.set(nonce_key, True, timeout=self.nonce_cache_timeout)
|
378
|
-
|
379
|
-
return True, None
|
380
|
-
|
381
|
-
def _validate_order_id_format(self, order_id: str) -> bool:
|
382
|
-
"""Validate order ID format."""
|
383
|
-
if not order_id:
|
384
|
-
return False
|
385
|
-
|
386
|
-
# Basic validation (customize per your order ID format)
|
387
|
-
if len(order_id) < 3 or len(order_id) > 50:
|
388
|
-
return False
|
389
|
-
|
390
|
-
# Allow alphanumeric, dash, underscore
|
391
|
-
import re
|
392
|
-
if not re.match(r'^[a-zA-Z0-9_-]+$', order_id):
|
393
|
-
return False
|
394
|
-
|
395
|
-
return True
|
396
|
-
|
397
|
-
def _validate_payment_address(self, address: str, provider: str) -> bool:
|
398
|
-
"""
|
399
|
-
Validate that payment address exists in our system.
|
400
|
-
|
401
|
-
CRITICAL: Prevents webhooks for unknown addresses.
|
402
|
-
"""
|
403
|
-
from ..models.payments import UniversalPayment
|
404
|
-
|
405
|
-
try:
|
406
|
-
# Check if address exists in our payments
|
407
|
-
return UniversalPayment.objects.filter(
|
408
|
-
provider=provider,
|
409
|
-
pay_address=address
|
410
|
-
).exists()
|
411
|
-
except Exception as e:
|
412
|
-
logger.error(f"Error validating payment address: {e}")
|
413
|
-
return True # Allow if validation fails (avoid false negatives)
|
414
|
-
|
415
|
-
def _generate_idempotency_key(
|
416
|
-
self,
|
417
|
-
provider: str,
|
418
|
-
webhook_data: Dict[str, Any],
|
419
|
-
request_headers: Dict[str, str]
|
420
|
-
) -> str:
|
421
|
-
"""Generate secure idempotency key for webhook deduplication."""
|
422
|
-
|
423
|
-
# Use multiple fields for uniqueness
|
424
|
-
payment_id = (
|
425
|
-
webhook_data.get('payment_id') or
|
426
|
-
webhook_data.get('order_id') or
|
427
|
-
webhook_data.get('id') or
|
428
|
-
webhook_data.get('uuid') or
|
429
|
-
'unknown'
|
430
|
-
)
|
431
|
-
|
432
|
-
# Include status to allow multiple status updates for same payment
|
433
|
-
status = webhook_data.get('status', 'unknown')
|
434
|
-
|
435
|
-
# Include timestamp for additional uniqueness
|
436
|
-
timestamp = (
|
437
|
-
webhook_data.get('timestamp') or
|
438
|
-
webhook_data.get('created_at') or
|
439
|
-
webhook_data.get('updated_at') or
|
440
|
-
str(int(time.time()))
|
441
|
-
)
|
442
|
-
|
443
|
-
# Create secure hash
|
444
|
-
key_data = f"{provider}:{payment_id}:{status}:{timestamp}"
|
445
|
-
return hashlib.sha256(key_data.encode()).hexdigest()[:32]
|
446
|
-
|
447
|
-
def _log_security_event(self, event_type: str, provider: str, details: str):
|
448
|
-
"""Log security events for monitoring and alerting."""
|
449
|
-
|
450
|
-
try:
|
451
|
-
# Create security event log
|
452
|
-
PaymentEvent.objects.create(
|
453
|
-
event_type=f'security_{event_type}',
|
454
|
-
provider=provider,
|
455
|
-
metadata={
|
456
|
-
'event_type': event_type,
|
457
|
-
'provider': provider,
|
458
|
-
'details': details,
|
459
|
-
'timestamp': timezone.now().isoformat(),
|
460
|
-
'severity': 'HIGH' if 'attack' in event_type else 'MEDIUM'
|
461
|
-
}
|
462
|
-
)
|
463
|
-
|
464
|
-
# Log to application logger
|
465
|
-
if 'attack' in event_type or 'failed' in event_type:
|
466
|
-
logger.warning(f"🚨 Security Event [{event_type}] {provider}: {details}")
|
467
|
-
else:
|
468
|
-
logger.info(f"🔒 Security Event [{event_type}] {provider}: {details}")
|
469
|
-
|
470
|
-
except Exception as e:
|
471
|
-
logger.error(f"Failed to log security event: {e}")
|
472
|
-
|
473
|
-
|
474
|
-
# Singleton instance for import
|
475
|
-
webhook_validator = WebhookValidator()
|