File size: 4,952 Bytes
671970a
 
2411edd
 
 
671970a
 
 
2411edd
671970a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2411edd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
"""开放资金接口的实时插件授权与钱包写锁;事务由调用方提交/回滚。"""
import hmac
import hashlib
import json
import re
from decimal import Decimal

from fastapi import HTTPException
from models_sql import RegisteredPlugin, OpenApiOperation, Transaction
from plugin_permissions import parse_permissions
from tip_transaction import lock_wallet_accounts


def validate_live_plugin(row, key, permission):
    if row is None or not row.enabled:
        raise HTTPException(status_code=403, detail="插件已停用或不存在")
    if not isinstance(row.plugin_key, str) or not isinstance(key, str) or not hmac.compare_digest(row.plugin_key.encode('utf-8'), key.encode('utf-8')):
        raise HTTPException(status_code=403, detail="Plugin Key 已失效")
    try:
        permissions = parse_permissions(row.permissions)
    except Exception:
        raise HTTPException(status_code=503, detail="插件权限配置暂不可用")
    if permission not in permissions:
        raise HTTPException(status_code=403, detail="插件无权限调用该接口")
    return row


def lock_open_wallet(session, account, plugin_id, key, permission):
    # 首项资金SQL取得SQLite写事务;其它方言先锁钱包再锁插件,所有开放资金路径同序。
    wallet = lock_wallet_accounts(session, (account,))[account]
    row = session.query(RegisteredPlugin).filter(RegisteredPlugin.plugin_id == plugin_id).populate_existing().with_for_update().first()
    validate_live_plugin(row, key, permission)
    return wallet, row


def positive_integer(value):
    if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
        raise HTTPException(status_code=503, detail="插件额度配置暂不可用")
    return value


def quota_total(value):
    """SQL聚合可返回整数或精确Decimal;拒绝浮点/字符串/非整数与坏统计。"""
    if isinstance(value, Decimal) and value.is_finite() and value >= 0 and value == value.to_integral_value():
        return int(value)
    if isinstance(value, bool) or not isinstance(value, int) or value < 0:
        raise ValueError("invalid quota history")
    return value


def open_operation_identity(request_id, account, plugin_id, kind, payload):
    if request_id is None:
        return None
    if not isinstance(request_id, str) or re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{7,127}", request_id) is None:
        raise HTTPException(status_code=422, detail="OPEN_REQUEST_ID_INVALID")
    encode = lambda value: json.dumps(value, ensure_ascii=False, separators=(',', ':'), allow_nan=False).encode('utf-8')
    key = hashlib.sha256(encode([plugin_id, account, kind, request_id])).hexdigest()
    fingerprint = hashlib.sha256(encode(payload)).hexdigest()
    return key, fingerprint


def read_open_operation(db, identity, account, plugin_id, kind, amount, reference_id):
    if identity is None:
        return None
    key, fingerprint = identity
    row = db.get(OpenApiOperation, key)
    if row is None:
        if db.get(Transaction, f'{kind}_{key}') is not None:
            raise HTTPException(status_code=503, detail="OPEN_OPERATION_UNAVAILABLE")
        return None
    if row.request_hash != fingerprint:
        raise HTTPException(status_code=409, detail="OPEN_REQUEST_CONFLICT")
    try:
        response = json.loads(row.response_payload)
        transaction = db.get(Transaction, row.transaction_id)
        if (not isinstance(response, dict) or set(response) != {'success', 'remaining_balance', 'transaction_id'}
                or response['success'] is not True or type(response['remaining_balance']) is not int
                or response['transaction_id'] != row.transaction_id or row.transaction_id != f'{kind}_{key}'
                or transaction is None or transaction.account != account or transaction.tx_type != kind
                or transaction.source_plugin != plugin_id or transaction.related_account != plugin_id
                or type(transaction.amount) is not int or transaction.amount != amount
                or transaction.item_id != reference_id
                or not isinstance(transaction.prev_hash, str)
                or transaction.tx_hash != hashlib.sha256(
                    f"{transaction.tx_id}{account}{kind}{amount}{transaction.prev_hash}".encode()).hexdigest()):
            raise ValueError('invalid open operation')
    except Exception:
        raise HTTPException(status_code=503, detail="OPEN_OPERATION_UNAVAILABLE")
    return response


def save_open_operation(db, identity, response):
    if identity is not None:
        key, fingerprint = identity
        db.add(OpenApiOperation(request_key=key, request_hash=fingerprint,
            transaction_id=response['transaction_id'],
            response_payload=json.dumps(response, ensure_ascii=False, separators=(',', ':'), allow_nan=False)))