酒店预订平台
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

376 lines
10 KiB

  1. <?php
  2. /*
  3. * This file is part of the overtrue/wechat.
  4. *
  5. * (c) overtrue <i@overtrue.me>
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. namespace EasyWeChat\Kernel;
  11. use EasyWeChat\Kernel\Contracts\MessageInterface;
  12. use EasyWeChat\Kernel\Exceptions\BadRequestException;
  13. use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
  14. use EasyWeChat\Kernel\Messages\Message;
  15. use EasyWeChat\Kernel\Messages\News;
  16. use EasyWeChat\Kernel\Messages\NewsItem;
  17. use EasyWeChat\Kernel\Messages\Raw as RawMessage;
  18. use EasyWeChat\Kernel\Messages\Text;
  19. use EasyWeChat\Kernel\Support\XML;
  20. use EasyWeChat\Kernel\Traits\Observable;
  21. use EasyWeChat\Kernel\Traits\ResponseCastable;
  22. use Symfony\Component\HttpFoundation\Response;
  23. /**
  24. * Class ServerGuard.
  25. *
  26. * 1. url 里的 signature 只是将 token+nonce+timestamp 得到的签名,只是用于验证当前请求的,在公众号环境下一直有
  27. * 2. 企业号消息发送时是没有的,因为固定为完全模式,所以 url 里不会存在 signature, 只有 msg_signature 用于解密消息的
  28. *
  29. * @author overtrue <i@overtrue.me>
  30. */
  31. class ServerGuard
  32. {
  33. use Observable;
  34. use ResponseCastable;
  35. /**
  36. * @var bool
  37. */
  38. protected $alwaysValidate = false;
  39. /**
  40. * Empty string.
  41. */
  42. const SUCCESS_EMPTY_RESPONSE = 'success';
  43. /**
  44. * @var array
  45. */
  46. const MESSAGE_TYPE_MAPPING = [
  47. 'text' => Message::TEXT,
  48. 'image' => Message::IMAGE,
  49. 'voice' => Message::VOICE,
  50. 'video' => Message::VIDEO,
  51. 'shortvideo' => Message::SHORT_VIDEO,
  52. 'location' => Message::LOCATION,
  53. 'link' => Message::LINK,
  54. 'device_event' => Message::DEVICE_EVENT,
  55. 'device_text' => Message::DEVICE_TEXT,
  56. 'event' => Message::EVENT,
  57. 'file' => Message::FILE,
  58. 'miniprogrampage' => Message::MINIPROGRAM_PAGE,
  59. ];
  60. /**
  61. * @var \EasyWeChat\Kernel\ServiceContainer
  62. */
  63. protected $app;
  64. /**
  65. * Constructor.
  66. *
  67. * @codeCoverageIgnore
  68. *
  69. * @param \EasyWeChat\Kernel\ServiceContainer $app
  70. */
  71. public function __construct(ServiceContainer $app)
  72. {
  73. $this->app = $app;
  74. foreach ($this->app->extension->observers() as $observer) {
  75. call_user_func_array([$this, 'push'], $observer);
  76. }
  77. }
  78. /**
  79. * Handle and return response.
  80. *
  81. * @return Response
  82. *
  83. * @throws BadRequestException
  84. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  85. * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
  86. */
  87. public function serve(): Response
  88. {
  89. $this->app['logger']->debug('Request received:', [
  90. 'method' => $this->app['request']->getMethod(),
  91. 'uri' => $this->app['request']->getUri(),
  92. 'content-type' => $this->app['request']->getContentType(),
  93. 'content' => $this->app['request']->getContent(),
  94. ]);
  95. $response = $this->validate()->resolve();
  96. $this->app['logger']->debug('Server response created:', ['content' => $response->getContent()]);
  97. return $response;
  98. }
  99. /**
  100. * @return $this
  101. *
  102. * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
  103. */
  104. public function validate()
  105. {
  106. if (!$this->alwaysValidate && !$this->isSafeMode()) {
  107. return $this;
  108. }
  109. if ($this->app['request']->get('signature') !== $this->signature([
  110. $this->getToken(),
  111. $this->app['request']->get('timestamp'),
  112. $this->app['request']->get('nonce'),
  113. ])) {
  114. throw new BadRequestException('Invalid request signature.', 400);
  115. }
  116. return $this;
  117. }
  118. /**
  119. * Force validate request.
  120. *
  121. * @return $this
  122. */
  123. public function forceValidate()
  124. {
  125. $this->alwaysValidate = true;
  126. return $this;
  127. }
  128. /**
  129. * Get request message.
  130. *
  131. * @return array|\EasyWeChat\Kernel\Support\Collection|object|string
  132. *
  133. * @throws BadRequestException
  134. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  135. * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
  136. */
  137. public function getMessage()
  138. {
  139. $message = $this->parseMessage($this->app['request']->getContent(false));
  140. if (!is_array($message) || empty($message)) {
  141. throw new BadRequestException('No message received.');
  142. }
  143. if ($this->isSafeMode() && !empty($message['Encrypt'])) {
  144. $message = $this->decryptMessage($message);
  145. // Handle JSON format.
  146. $dataSet = json_decode($message, true);
  147. if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
  148. return $dataSet;
  149. }
  150. $message = XML::parse($message);
  151. }
  152. return $this->detectAndCastResponseToType($message, $this->app->config->get('response_type'));
  153. }
  154. /**
  155. * Resolve server request and return the response.
  156. *
  157. * @return \Symfony\Component\HttpFoundation\Response
  158. *
  159. * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
  160. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  161. * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
  162. */
  163. protected function resolve(): Response
  164. {
  165. $result = $this->handleRequest();
  166. if ($this->shouldReturnRawResponse()) {
  167. $response = new Response($result['response']);
  168. } else {
  169. $response = new Response(
  170. $this->buildResponse($result['to'], $result['from'], $result['response']),
  171. 200,
  172. ['Content-Type' => 'application/xml']
  173. );
  174. }
  175. $this->app->events->dispatch(new Events\ServerGuardResponseCreated($response));
  176. return $response;
  177. }
  178. /**
  179. * @return string|null
  180. */
  181. protected function getToken()
  182. {
  183. return $this->app['config']['token'];
  184. }
  185. /**
  186. * @param string $to
  187. * @param string $from
  188. * @param \EasyWeChat\Kernel\Contracts\MessageInterface|string|int $message
  189. *
  190. * @return string
  191. *
  192. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  193. */
  194. public function buildResponse(string $to, string $from, $message)
  195. {
  196. if (empty($message) || self::SUCCESS_EMPTY_RESPONSE === $message) {
  197. return self::SUCCESS_EMPTY_RESPONSE;
  198. }
  199. if ($message instanceof RawMessage) {
  200. return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
  201. }
  202. if (is_string($message) || is_numeric($message)) {
  203. $message = new Text((string) $message);
  204. }
  205. if (is_array($message) && reset($message) instanceof NewsItem) {
  206. $message = new News($message);
  207. }
  208. if (!($message instanceof Message)) {
  209. throw new InvalidArgumentException(sprintf('Invalid Messages type "%s".', gettype($message)));
  210. }
  211. return $this->buildReply($to, $from, $message);
  212. }
  213. /**
  214. * Handle request.
  215. *
  216. * @return array
  217. *
  218. * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
  219. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  220. * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
  221. */
  222. protected function handleRequest(): array
  223. {
  224. $castedMessage = $this->getMessage();
  225. $messageArray = $this->detectAndCastResponseToType($castedMessage, 'array');
  226. $response = $this->dispatch(self::MESSAGE_TYPE_MAPPING[$messageArray['MsgType'] ?? $messageArray['msg_type'] ?? 'text'], $castedMessage);
  227. return [
  228. 'to' => $messageArray['FromUserName'] ?? '',
  229. 'from' => $messageArray['ToUserName'] ?? '',
  230. 'response' => $response,
  231. ];
  232. }
  233. /**
  234. * Build reply XML.
  235. *
  236. * @param string $to
  237. * @param string $from
  238. * @param \EasyWeChat\Kernel\Contracts\MessageInterface $message
  239. *
  240. * @return string
  241. */
  242. protected function buildReply(string $to, string $from, MessageInterface $message): string
  243. {
  244. $prepends = [
  245. 'ToUserName' => $to,
  246. 'FromUserName' => $from,
  247. 'CreateTime' => time(),
  248. 'MsgType' => $message->getType(),
  249. ];
  250. $response = $message->transformToXml($prepends);
  251. if ($this->isSafeMode()) {
  252. $this->app['logger']->debug('Messages safe mode is enabled.');
  253. $response = $this->app['encryptor']->encrypt($response);
  254. }
  255. return $response;
  256. }
  257. /**
  258. * @param array $params
  259. *
  260. * @return string
  261. */
  262. protected function signature(array $params)
  263. {
  264. sort($params, SORT_STRING);
  265. return sha1(implode($params));
  266. }
  267. /**
  268. * Parse message array from raw php input.
  269. *
  270. * @param string $content
  271. *
  272. * @return array
  273. *
  274. * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
  275. */
  276. protected function parseMessage($content)
  277. {
  278. try {
  279. if (0 === stripos($content, '<')) {
  280. $content = XML::parse($content);
  281. } else {
  282. // Handle JSON format.
  283. $dataSet = json_decode($content, true);
  284. if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
  285. $content = $dataSet;
  286. }
  287. }
  288. return (array) $content;
  289. } catch (\Exception $e) {
  290. throw new BadRequestException(sprintf('Invalid message content:(%s) %s', $e->getCode(), $e->getMessage()), $e->getCode());
  291. }
  292. }
  293. /**
  294. * Check the request message safe mode.
  295. *
  296. * @return bool
  297. */
  298. protected function isSafeMode(): bool
  299. {
  300. return $this->app['request']->get('signature') && 'aes' === $this->app['request']->get('encrypt_type');
  301. }
  302. /**
  303. * @return bool
  304. */
  305. protected function shouldReturnRawResponse(): bool
  306. {
  307. return false;
  308. }
  309. /**
  310. * @param array $message
  311. *
  312. * @return mixed
  313. */
  314. protected function decryptMessage(array $message)
  315. {
  316. return $message = $this->app['encryptor']->decrypt(
  317. $message['Encrypt'],
  318. $this->app['request']->get('msg_signature'),
  319. $this->app['request']->get('nonce'),
  320. $this->app['request']->get('timestamp')
  321. );
  322. }
  323. }