core.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import json
  4. import logging
  5. import threading
  6. from datetime import datetime
  7. import requests
  8. from library.wechatbase.exceptions import InvalidSignatureException, APILimitedException, \
  9. WeChatPayException, WechatNetworkException
  10. from . import update_certificates
  11. from .type import RequestType, SignType
  12. from .utils import (aes_decrypt, build_authorization, hmac_sign,
  13. load_certificate, load_private_key, rsa_decrypt,
  14. rsa_encrypt, rsa_sign, rsa_verify)
  15. class Core(object):
  16. def __init__(self, mchid, cert_serial_no, private_key, apiv3_key, certificate_str_list = None,
  17. logger = None, proxy = None, timeout = None, auto_retry = False):
  18. self._proxy = proxy
  19. self._mchid = mchid
  20. self._cert_serial_no = cert_serial_no
  21. self._private_key = load_private_key(private_key)
  22. self._apiv3_key = apiv3_key
  23. self._gate_way = 'https://api.mch.weixin.qq.com'
  24. self._certificates = []
  25. self.auto_retry = auto_retry
  26. self.my_retry_count = threading.local()
  27. self.timeout = timeout
  28. self._logger = logger or logging.getLogger(__name__)
  29. if certificate_str_list:
  30. for certificate_str in certificate_str_list:
  31. certificate = load_certificate(certificate_str)
  32. if not certificate:
  33. continue
  34. now = datetime.utcnow()
  35. if now < certificate.not_valid_before or now > certificate.not_valid_after:
  36. continue
  37. self._certificates.append(load_certificate(certificate_str))
  38. if not self._certificates:
  39. self._update_certificates()
  40. if not self._certificates:
  41. raise Exception('No wechatpay platform certificate, please double check your init params.')
  42. @property
  43. def mchid(self):
  44. return self._mchid
  45. @property
  46. def retry_count(self):
  47. if not hasattr(self.my_retry_count, 'retry_count'):
  48. self.my_retry_count.retry_count = 0
  49. return self.my_retry_count.retry_count
  50. @retry_count.setter
  51. def retry_count(self, retry_count):
  52. self.my_retry_count.retry_count = retry_count
  53. def _update_certificates(self):
  54. path = '/v3/certificates'
  55. self._certificates = []
  56. message = self.request(path, skip_verify = True)
  57. cert_str_list = []
  58. data = message.get('data')
  59. for value in data:
  60. serial_no = value.get('serial_no')
  61. effective_time = value.get('effective_time')
  62. expire_time = value.get('expire_time')
  63. encrypt_certificate = value.get('encrypt_certificate')
  64. algorithm = nonce = associated_data = ciphertext = None
  65. if encrypt_certificate:
  66. algorithm = encrypt_certificate.get('algorithm')
  67. nonce = encrypt_certificate.get('nonce')
  68. associated_data = encrypt_certificate.get('associated_data')
  69. ciphertext = encrypt_certificate.get('ciphertext')
  70. if not (
  71. serial_no and effective_time and expire_time and algorithm and nonce and associated_data and ciphertext):
  72. continue
  73. cert_str = aes_decrypt(
  74. nonce = nonce,
  75. ciphertext = ciphertext,
  76. associated_data = associated_data,
  77. apiv3_key = self._apiv3_key)
  78. certificate = load_certificate(cert_str)
  79. if not certificate:
  80. continue
  81. now = datetime.utcnow()
  82. if now < certificate.not_valid_before or now > certificate.not_valid_after:
  83. continue
  84. self._certificates.append(certificate)
  85. cert_str_list.append(cert_str)
  86. update_certificates.send(self, mchid = self.mchid, cert_str_list = cert_str_list)
  87. def _verify_signature(self, headers, body):
  88. signature = headers.get('Wechatpay-Signature')
  89. timestamp = headers.get('Wechatpay-Timestamp')
  90. nonce = headers.get('Wechatpay-Nonce')
  91. serial_no = headers.get('Wechatpay-Serial')
  92. cert_found = False
  93. for cert in self._certificates:
  94. if int('0x' + serial_no, 16) == cert.serial_number:
  95. cert_found = True
  96. certificate = cert
  97. break
  98. if not cert_found:
  99. self._update_certificates()
  100. for cert in self._certificates:
  101. if int('0x' + serial_no, 16) == cert.serial_number:
  102. cert_found = True
  103. certificate = cert
  104. break
  105. if not cert_found:
  106. return False
  107. if not rsa_verify(timestamp, nonce, body, signature, certificate):
  108. return False
  109. return True
  110. def _decode_result(self, res):
  111. try:
  112. result = json.loads(res.content.decode('utf-8', 'ignore'), strict = False)
  113. except (TypeError, ValueError):
  114. # Return origin response object if we can not decode it as JSON
  115. self._logger.debug('Can not decode response as JSON', exc_info = True)
  116. return res
  117. return result
  118. def _handle_error(self, path, res, errcode, errmsg, **kwargs):
  119. if errcode == 'FREQUENCY_LIMITED':
  120. raise APILimitedException(
  121. errCode = errcode,
  122. errMsg = errmsg,
  123. client = self,
  124. request = res.request,
  125. response = res)
  126. auto_retry = False
  127. if errcode in ('SYSTEM_ERROR',):
  128. auto_retry = True
  129. if not auto_retry:
  130. raise WeChatPayException(
  131. errCode = errcode,
  132. errMsg = errmsg,
  133. client = self,
  134. request = res.request,
  135. response = res)
  136. else:
  137. self.retry_count = self.retry_count + 1
  138. if self.retry_count >= 3:
  139. self._logger.debug('reached the maximum number of retries. url = {}'.format(path))
  140. self.retry_count = 0
  141. raise WeChatPayException(
  142. errCode = errcode,
  143. errMsg = errmsg,
  144. client = self,
  145. request = res.request,
  146. response = res)
  147. else:
  148. self._logger.debug('retry wechat url = {}'.format(path))
  149. return self.request(path, **kwargs)
  150. def _handle_result(self, path, res, **kwargs):
  151. if not isinstance(res, dict):
  152. result = self._decode_result(res)
  153. else:
  154. result = res
  155. self._logger.debug('Response for Wechat API<url={}> result: {}'.format(path, result))
  156. if not isinstance(result, dict):
  157. return result
  158. if 'code' in result:
  159. return self._handle_error(path, res, result)
  160. return result
  161. def request(self, path, method = RequestType.GET, data = None, skip_verify = False, sign_data = None, files = None,
  162. cipher_data = False, headers = {}):
  163. if files:
  164. headers.update({'Content-Type': 'multipart/form-data'})
  165. else:
  166. headers.update({'Content-Type': 'application/json'})
  167. headers.update({'Accept': 'application/json'})
  168. headers.update({'User-Agent': 'wechatpay v3 api python sdk(https://github.com/minibear2021/wechatpayv3)'})
  169. if cipher_data:
  170. headers.update({'Wechatpay-Serial': hex(self._last_certificate().serial_number)[2:].upper().rstrip("L")})
  171. authorization = build_authorization(
  172. path,
  173. method.value,
  174. self._mchid,
  175. self._cert_serial_no,
  176. self._private_key,
  177. data = sign_data if sign_data else data)
  178. headers.update({'Authorization': authorization})
  179. self._logger.debug('Request url: %s' % self._gate_way + path)
  180. self._logger.debug('Request type: %s' % method.value)
  181. self._logger.debug('Request headers: %s' % headers)
  182. self._logger.debug('Request params: %s' % data)
  183. kwargs = {'headers': headers, 'proxies': self._proxy}
  184. if method == RequestType.GET:
  185. pass
  186. elif method == RequestType.POST:
  187. kwargs.update({
  188. 'json': None if files else data,
  189. 'data': data if files else None,
  190. 'files': files
  191. })
  192. elif method in [RequestType.PATCH, RequestType.PUT]:
  193. kwargs.update({
  194. 'json': data
  195. })
  196. elif method == RequestType.DELETE:
  197. pass
  198. else:
  199. raise Exception('wechatpayv3 does no support this request type.')
  200. with requests.sessions.Session() as _session:
  201. res = _session.request(
  202. method = method.value,
  203. url = self._gate_way + path,
  204. timeout = self.timeout,
  205. **kwargs)
  206. self._logger.debug('Response status code: %s' % res.status_code)
  207. self._logger.debug('Response headers: %s' % res.headers)
  208. self._logger.debug('Response content: %s' % res.text)
  209. try:
  210. res.raise_for_status()
  211. except requests.RequestException as reqe:
  212. if not isinstance(res, dict):
  213. result = self._decode_result(res)
  214. else:
  215. result = res
  216. if isinstance(result, dict) and 'code' in result:
  217. self._handle_error(
  218. path, res, result['code'], result['message'], method = method, data = data,
  219. skip_verify = skip_verify,
  220. sign_data = sign_data, files = files, cipher_data = cipher_data, headers = headers)
  221. else:
  222. raise WechatNetworkException(
  223. errCode = 'HTTP{}'.format(res.status_code),
  224. errMsg = result or reqe.message,
  225. client = self,
  226. request = reqe.request,
  227. response = reqe.response)
  228. if not skip_verify:
  229. if not self._verify_signature(res.headers, res.text):
  230. raise InvalidSignatureException(u'无效签名')
  231. if res.status_code == 204:
  232. return {}
  233. else:
  234. if not isinstance(res, dict):
  235. result = self._decode_result(res)
  236. else:
  237. result = res
  238. if not isinstance(result, dict):
  239. return result
  240. if 'code' in result:
  241. return self._handle_error(
  242. path, res, result['code'], result['message'], method = method, data = data,
  243. skip_verify = skip_verify,
  244. sign_data = sign_data, files = files, cipher_data = cipher_data, headers = headers)
  245. else:
  246. return result
  247. def sign(self, data, sign_type = SignType.RSA_SHA256):
  248. if sign_type == SignType.RSA_SHA256:
  249. sign_str = '\n'.join(data) + '\n'
  250. return rsa_sign(self._private_key, sign_str)
  251. elif sign_type == SignType.HMAC_SHA256:
  252. key_list = sorted(data.keys())
  253. sign_str = ''
  254. for k in key_list:
  255. v = data[k]
  256. sign_str += str(k) + '=' + str(v) + '&'
  257. sign_str += 'key=' + self._apiv3_key
  258. return hmac_sign(self._apiv3_key, sign_str)
  259. else:
  260. raise ValueError('unexpected value of sign_type.')
  261. def decrypt_callback(self, headers, body):
  262. if isinstance(body, bytes):
  263. body = body.decode('UTF-8')
  264. if self._logger:
  265. self._logger.debug('Callback Header: %s' % headers)
  266. self._logger.debug('Callback Body: %s' % body)
  267. if not self._verify_signature(headers, body):
  268. return None
  269. data = json.loads(body)
  270. resource_type = data.get('resource_type')
  271. if resource_type != 'encrypt-resource':
  272. return None
  273. resource = data.get('resource')
  274. if not resource:
  275. return None
  276. algorithm = resource.get('algorithm')
  277. if algorithm != 'AEAD_AES_256_GCM':
  278. raise Exception('wechatpayv3 does not support this algorithm')
  279. nonce = resource.get('nonce')
  280. ciphertext = resource.get('ciphertext')
  281. associated_data = resource.get('associated_data')
  282. if not (nonce and ciphertext):
  283. return None
  284. if not associated_data:
  285. associated_data = ''
  286. result = aes_decrypt(
  287. nonce = nonce,
  288. ciphertext = ciphertext,
  289. associated_data = associated_data,
  290. apiv3_key = self._apiv3_key)
  291. if self._logger:
  292. self._logger.debug('Callback resource: %s' % result)
  293. return result
  294. def callback(self, headers, body):
  295. if isinstance(body, bytes):
  296. body = body.decode('UTF-8')
  297. result = self.decrypt_callback(headers = headers, body = body)
  298. if result:
  299. data = json.loads(body)
  300. data.update({'resource': json.loads(result)})
  301. return data
  302. else:
  303. return result
  304. def decrypt(self, ciphtext):
  305. return rsa_decrypt(ciphertext = ciphtext, private_key = self._private_key)
  306. def encrypt(self, text):
  307. return rsa_encrypt(text = text, certificate = self._last_certificate())
  308. def _last_certificate(self):
  309. if not self._certificates:
  310. self._update_certificates()
  311. certificate = self._certificates[0]
  312. for cert in self._certificates:
  313. if certificate.not_valid_after < cert.not_valid_after:
  314. certificate = cert
  315. return certificate