123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- # -*- coding: utf-8 -*-
- # !/usr/bin/env python
- import json
- import logging
- import threading
- from datetime import datetime
- import requests
- from library.wechatbase.exceptions import InvalidSignatureException, APILimitedException, \
- WechatNetworkException, WeChatException
- from . import update_certificates
- from .type import RequestType, SignType
- from .utils import (aes_decrypt, build_authorization, hmac_sign,
- load_certificate, load_private_key, rsa_decrypt,
- rsa_encrypt, rsa_sign, rsa_verify)
- class Core(object):
- def __init__(self, mchid, cert_serial_no, private_key, apiv3_key, certificate_str_list = None,
- logger = None, proxy = None, timeout = None, auto_retry = False):
- self._proxy = proxy
- self._mchid = mchid
- self._cert_serial_no = cert_serial_no
- self._private_key = load_private_key(private_key)
- self._apiv3_key = apiv3_key
- self._gate_way = 'https://api.mch.weixin.qq.com'
- self._certificates = []
- self.auto_retry = auto_retry
- self.my_retry_count = threading.local()
- self.timeout = timeout
- self._logger = logger or logging.getLogger(__name__)
- if certificate_str_list:
- for certificate_str in certificate_str_list:
- certificate = load_certificate(certificate_str)
- if not certificate:
- continue
- now = datetime.utcnow()
- if now < certificate.not_valid_before or now > certificate.not_valid_after:
- continue
- self._certificates.append(load_certificate(certificate_str))
- if not self._certificates:
- self._update_certificates()
- if not self._certificates:
- raise Exception('No wechatpay platform certificate, please double check your init params.')
- @property
- def mchid(self):
- return self._mchid
- @property
- def retry_count(self):
- if not hasattr(self.my_retry_count, 'retry_count'):
- self.my_retry_count.retry_count = 0
- return self.my_retry_count.retry_count
- @retry_count.setter
- def retry_count(self, retry_count):
- self.my_retry_count.retry_count = retry_count
- def _update_certificates(self):
- path = '/v3/certificates'
- self._certificates = []
- message = self.request(path, skip_verify = True)
- cert_str_list = []
- data = message.get('data')
- for value in data:
- serial_no = value.get('serial_no')
- effective_time = value.get('effective_time')
- expire_time = value.get('expire_time')
- encrypt_certificate = value.get('encrypt_certificate')
- algorithm = nonce = associated_data = ciphertext = None
- if encrypt_certificate:
- algorithm = encrypt_certificate.get('algorithm')
- nonce = encrypt_certificate.get('nonce')
- associated_data = encrypt_certificate.get('associated_data')
- ciphertext = encrypt_certificate.get('ciphertext')
- if not (
- serial_no and effective_time and expire_time and algorithm and nonce and associated_data and ciphertext):
- continue
- cert_str = aes_decrypt(
- nonce = nonce,
- ciphertext = ciphertext,
- associated_data = associated_data,
- apiv3_key = self._apiv3_key)
- certificate = load_certificate(cert_str)
- if not certificate:
- continue
- now = datetime.utcnow()
- if now < certificate.not_valid_before or now > certificate.not_valid_after:
- continue
- self._certificates.append(certificate)
- cert_str_list.append(cert_str)
- update_certificates.send(self, mchid = self.mchid, cert_str_list = cert_str_list)
- def _verify_signature(self, headers, body):
- signature = headers.get('Wechatpay-Signature')
- timestamp = headers.get('Wechatpay-Timestamp')
- nonce = headers.get('Wechatpay-Nonce')
- serial_no = headers.get('Wechatpay-Serial')
- cert_found = False
- for cert in self._certificates:
- if int('0x' + serial_no, 16) == cert.serial_number:
- cert_found = True
- certificate = cert
- break
- if not cert_found:
- self._update_certificates()
- for cert in self._certificates:
- if int('0x' + serial_no, 16) == cert.serial_number:
- cert_found = True
- certificate = cert
- break
- if not cert_found:
- return False
- if not rsa_verify(timestamp, nonce, body, signature, certificate):
- return False
- return True
- def _decode_result(self, res):
- try:
- result = json.loads(res.content.decode('utf-8', 'ignore'), strict = False)
- except (TypeError, ValueError):
- # Return origin response object if we can not decode it as JSON
- self._logger.debug('Can not decode response as JSON', exc_info = True)
- return res
- return result
- def _handle_error(self, path, res, errcode, errmsg, **kwargs):
- if errcode == 'FREQUENCY_LIMITED':
- raise APILimitedException(
- errCode = errcode,
- errMsg = errmsg,
- client = self,
- request = res.request,
- response = res)
- auto_retry = False
- if errcode in ('SYSTEM_ERROR',):
- auto_retry = True
- if not auto_retry:
- raise WeChatException(
- errCode = errcode,
- errMsg = errmsg,
- client = self,
- request = res.request,
- response = res)
- else:
- self.retry_count = self.retry_count + 1
- if self.retry_count >= 3:
- self._logger.debug('reached the maximum number of retries. url = {}'.format(path))
- self.retry_count = 0
- raise WeChatException(
- errCode = errcode,
- errMsg = errmsg,
- client = self,
- request = res.request,
- response = res)
- else:
- self._logger.debug('retry wechat url = {}'.format(path))
- return self.request(path, **kwargs)
- def _handle_result(self, path, res, **kwargs):
- if not isinstance(res, dict):
- result = self._decode_result(res)
- else:
- result = res
- self._logger.debug('Response for Wechat API<url={}> result: {}'.format(path, result))
- if not isinstance(result, dict):
- return result
- if 'code' in result:
- return self._handle_error(path, res, result)
- return result
- def request(self, path, method = RequestType.GET, data = None, skip_verify = False, sign_data = None, files = None,
- cipher_data = False, headers = {}):
- if files:
- headers.update({'Content-Type': 'multipart/form-data'})
- else:
- headers.update({'Content-Type': 'application/json'})
- headers.update({'Accept': 'application/json'})
- headers.update({'User-Agent': 'weifule/v1.0.0 (https://www.washpayer.com)'})
- if cipher_data:
- headers.update({'Wechatpay-Serial': hex(self._last_certificate().serial_number)[2:].upper().rstrip("L")})
- headers.update({'Authorization': build_authorization(
- path, method.value, self._mchid, self._cert_serial_no,
- self._private_key, data = sign_data if sign_data else data)})
- self._logger.debug('Request url: %s' % self._gate_way + path)
- self._logger.debug('Request type: %s' % method.value)
- self._logger.debug('Request headers: %s' % headers)
- self._logger.debug('Request data: %s' % data)
- kwargs = {'headers': headers, 'proxies': self._proxy}
- if method == RequestType.GET:
- pass
- elif method == RequestType.POST:
- kwargs.update({
- 'json': None if files else data,
- 'data': data if files else None,
- 'files': files
- })
- elif method in [RequestType.PATCH, RequestType.PUT]:
- kwargs.update({
- 'json': data
- })
- elif method == RequestType.DELETE:
- pass
- else:
- raise Exception('wechatpayv3 does no support this request type.')
- with requests.sessions.Session() as _session:
- res = _session.request(
- method = method.value,
- url = self._gate_way + path,
- timeout = self.timeout,
- **kwargs)
- self._logger.debug('Response status code: %s' % res.status_code)
- self._logger.debug('Response headers: %s' % res.headers)
- self._logger.debug('Response content: %s' % res.text)
- try:
- res.raise_for_status()
- except requests.RequestException as reqe:
- if not isinstance(res, dict):
- result = self._decode_result(res)
- else:
- result = res
- if isinstance(result, dict) and 'code' in result:
- self._handle_error(
- path, res, result['code'], result['message'], method = method, data = data,
- skip_verify = skip_verify,
- sign_data = sign_data, files = files, cipher_data = cipher_data, headers = headers)
- else:
- raise WechatNetworkException(
- errCode = 'HTTP{}'.format(res.status_code),
- errMsg = result or reqe.message,
- client = self,
- request = reqe.request,
- response = reqe.response)
- if not skip_verify:
- if not self._verify_signature(res.headers, res.text):
- raise InvalidSignatureException()
- if res.status_code == 204:
- return {}
- else:
- if not isinstance(res, dict):
- result = self._decode_result(res)
- else:
- result = res
- if not isinstance(result, dict):
- return result
- if 'code' in result:
- return self._handle_error(
- path, res, result['code'], result['message'], method = method, data = data,
- skip_verify = skip_verify,
- sign_data = sign_data, files = files, cipher_data = cipher_data, headers = headers)
- else:
- return result
- def sign(self, data, sign_type = SignType.RSA_SHA256):
- if sign_type == SignType.RSA_SHA256:
- sign_str = '\n'.join(data) + '\n'
- return rsa_sign(self._private_key, sign_str)
- elif sign_type == SignType.HMAC_SHA256:
- key_list = sorted(data.keys())
- sign_str = ''
- for k in key_list:
- v = data[k]
- sign_str += str(k) + '=' + str(v) + '&'
- sign_str += 'key=' + self._apiv3_key
- return hmac_sign(self._apiv3_key, sign_str)
- else:
- raise ValueError('unexpected value of sign_type.')
- def decrypt_callback(self, headers, body):
- if isinstance(body, bytes):
- body = body.decode('UTF-8')
- if self._logger:
- self._logger.debug('Callback Header: %s' % headers)
- self._logger.debug('Callback Body: %s' % body)
- if not self._verify_signature(headers, body):
- return None
- data = json.loads(body)
- resource_type = data.get('resource_type')
- if resource_type != 'encrypt-resource':
- return None
- resource = data.get('resource')
- if not resource:
- return None
- algorithm = resource.get('algorithm')
- if algorithm != 'AEAD_AES_256_GCM':
- raise Exception('wechatpayv3 does not support this algorithm')
- nonce = resource.get('nonce')
- ciphertext = resource.get('ciphertext')
- associated_data = resource.get('associated_data')
- if not (nonce and ciphertext):
- return None
- if not associated_data:
- associated_data = ''
- result = aes_decrypt(
- nonce = nonce,
- ciphertext = ciphertext,
- associated_data = associated_data,
- apiv3_key = self._apiv3_key)
- if self._logger:
- self._logger.debug('Callback resource: %s' % result)
- return result
- def callback(self, headers, body):
- if isinstance(body, bytes):
- body = body.decode('UTF-8')
- result = self.decrypt_callback(headers = headers, body = body)
- if result:
- data = json.loads(body)
- data.update({'resource': json.loads(result)})
- return data
- else:
- return result
- def decrypt(self, ciphtext):
- return rsa_decrypt(ciphertext = ciphtext, private_key = self._private_key)
- def encrypt(self, text):
- return rsa_encrypt(text = text, certificate = self._last_certificate())
- def _last_certificate(self):
- if not self._certificates:
- self._update_certificates()
- certificate = self._certificates[0]
- for cert in self._certificates:
- if certificate.not_valid_after < cert.not_valid_after:
- certificate = cert
- return certificate
|