ServerGuard.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. public const SUCCESS_EMPTY_RESPONSE = 'success';
  43. /**
  44. * @var array
  45. */
  46. public 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. * @throws BadRequestException
  82. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  83. * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
  84. */
  85. public function serve(): Response
  86. {
  87. $this->app['logger']->debug('Request received:', [
  88. 'method' => $this->app['request']->getMethod(),
  89. 'uri' => $this->app['request']->getUri(),
  90. 'content-type' => $this->app['request']->getContentType(),
  91. 'content' => $this->app['request']->getContent(),
  92. ]);
  93. $response = $this->validate()->resolve();
  94. $this->app['logger']->debug('Server response created:', ['content' => $response->getContent()]);
  95. return $response;
  96. }
  97. /**
  98. * @return $this
  99. *
  100. * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
  101. */
  102. public function validate()
  103. {
  104. if (!$this->alwaysValidate && !$this->isSafeMode()) {
  105. return $this;
  106. }
  107. if ($this->app['request']->get('signature') !== $this->signature([
  108. $this->getToken(),
  109. $this->app['request']->get('timestamp'),
  110. $this->app['request']->get('nonce'),
  111. ])) {
  112. throw new BadRequestException('Invalid request signature.', 400);
  113. }
  114. return $this;
  115. }
  116. /**
  117. * Force validate request.
  118. *
  119. * @return $this
  120. */
  121. public function forceValidate()
  122. {
  123. $this->alwaysValidate = true;
  124. return $this;
  125. }
  126. /**
  127. * Get request message.
  128. *
  129. * @return array|\EasyWeChat\Kernel\Support\Collection|object|string
  130. *
  131. * @throws BadRequestException
  132. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  133. * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
  134. */
  135. public function getMessage()
  136. {
  137. $message = $this->parseMessage($this->app['request']->getContent(false));
  138. if (!is_array($message) || empty($message)) {
  139. throw new BadRequestException('No message received.');
  140. }
  141. if ($this->isSafeMode() && !empty($message['Encrypt'])) {
  142. $message = $this->decryptMessage($message);
  143. // Handle JSON format.
  144. $dataSet = json_decode($message, true);
  145. if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
  146. return $dataSet;
  147. }
  148. $message = XML::parse($message);
  149. }
  150. return $this->detectAndCastResponseToType($message, $this->app->config->get('response_type'));
  151. }
  152. /**
  153. * Resolve server request and return the response.
  154. *
  155. * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
  156. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  157. * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
  158. */
  159. protected function resolve(): Response
  160. {
  161. $result = $this->handleRequest();
  162. if ($this->shouldReturnRawResponse()) {
  163. $response = new Response($result['response']);
  164. } else {
  165. $response = new Response(
  166. $this->buildResponse($result['to'], $result['from'], $result['response']),
  167. 200,
  168. ['Content-Type' => 'application/xml']
  169. );
  170. }
  171. $this->app->events->dispatch(new Events\ServerGuardResponseCreated($response));
  172. return $response;
  173. }
  174. /**
  175. * @return string|null
  176. */
  177. protected function getToken()
  178. {
  179. return $this->app['config']['token'];
  180. }
  181. /**
  182. * @param \EasyWeChat\Kernel\Contracts\MessageInterface|string|int $message
  183. *
  184. * @return string
  185. *
  186. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  187. */
  188. public function buildResponse(string $to, string $from, $message)
  189. {
  190. if (empty($message) || self::SUCCESS_EMPTY_RESPONSE === $message) {
  191. return self::SUCCESS_EMPTY_RESPONSE;
  192. }
  193. if ($message instanceof RawMessage) {
  194. return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
  195. }
  196. if (is_string($message) || is_numeric($message)) {
  197. $message = new Text((string) $message);
  198. }
  199. if (is_array($message) && reset($message) instanceof NewsItem) {
  200. $message = new News($message);
  201. }
  202. if (!($message instanceof Message)) {
  203. throw new InvalidArgumentException(sprintf('Invalid Messages type "%s".', gettype($message)));
  204. }
  205. return $this->buildReply($to, $from, $message);
  206. }
  207. /**
  208. * Handle request.
  209. *
  210. * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
  211. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  212. * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
  213. */
  214. protected function handleRequest(): array
  215. {
  216. $castedMessage = $this->getMessage();
  217. $messageArray = $this->detectAndCastResponseToType($castedMessage, 'array');
  218. $response = $this->dispatch(self::MESSAGE_TYPE_MAPPING[$messageArray['MsgType'] ?? $messageArray['msg_type'] ?? 'text'], $castedMessage);
  219. return [
  220. 'to' => $messageArray['FromUserName'] ?? '',
  221. 'from' => $messageArray['ToUserName'] ?? '',
  222. 'response' => $response,
  223. ];
  224. }
  225. /**
  226. * Build reply XML.
  227. */
  228. protected function buildReply(string $to, string $from, MessageInterface $message): string
  229. {
  230. $prepends = [
  231. 'ToUserName' => $to,
  232. 'FromUserName' => $from,
  233. 'CreateTime' => time(),
  234. 'MsgType' => $message->getType(),
  235. ];
  236. $response = $message->transformToXml($prepends);
  237. if ($this->isSafeMode()) {
  238. $this->app['logger']->debug('Messages safe mode is enabled.');
  239. $response = $this->app['encryptor']->encrypt($response);
  240. }
  241. return $response;
  242. }
  243. /**
  244. * @return string
  245. */
  246. protected function signature(array $params)
  247. {
  248. sort($params, SORT_STRING);
  249. return sha1(implode($params));
  250. }
  251. /**
  252. * Parse message array from raw php input.
  253. *
  254. * @param string $content
  255. *
  256. * @return array
  257. *
  258. * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
  259. */
  260. protected function parseMessage($content)
  261. {
  262. try {
  263. if (0 === stripos($content, '<')) {
  264. $content = XML::parse($content);
  265. } else {
  266. // Handle JSON format.
  267. $dataSet = json_decode($content, true);
  268. if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
  269. $content = $dataSet;
  270. }
  271. }
  272. return (array) $content;
  273. } catch (\Exception $e) {
  274. throw new BadRequestException(sprintf('Invalid message content:(%s) %s', $e->getCode(), $e->getMessage()), $e->getCode());
  275. }
  276. }
  277. /**
  278. * Check the request message safe mode.
  279. */
  280. protected function isSafeMode(): bool
  281. {
  282. return $this->app['request']->get('signature') && 'aes' === $this->app['request']->get('encrypt_type');
  283. }
  284. protected function shouldReturnRawResponse(): bool
  285. {
  286. return false;
  287. }
  288. /**
  289. * @return mixed
  290. */
  291. protected function decryptMessage(array $message)
  292. {
  293. return $message = $this->app['encryptor']->decrypt(
  294. $message['Encrypt'],
  295. $this->app['request']->get('msg_signature'),
  296. $this->app['request']->get('nonce'),
  297. $this->app['request']->get('timestamp')
  298. );
  299. }
  300. }