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