WebhookUrlValidator.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. <?php
  2. namespace App\Module\OpenAPI\Validators;
  3. use UCore\Validator;
  4. /**
  5. * Webhook URL验证器
  6. */
  7. class WebhookUrlValidator extends Validator
  8. {
  9. /**
  10. * 验证Webhook URL是否有效
  11. *
  12. * @param mixed $value URL地址
  13. * @param array $data 验证数据
  14. * @return bool 验证是否通过
  15. */
  16. public function validate(mixed $value, array $data): bool
  17. {
  18. if (empty($value)) {
  19. $this->addError('Webhook URL不能为空');
  20. return false;
  21. }
  22. // 验证URL格式
  23. if (!filter_var($value, FILTER_VALIDATE_URL)) {
  24. $this->addError('Webhook URL格式错误');
  25. return false;
  26. }
  27. // 解析URL
  28. $urlParts = parse_url($value);
  29. if (!$urlParts) {
  30. $this->addError('无法解析Webhook URL');
  31. return false;
  32. }
  33. // 验证协议
  34. if (!$this->validateScheme($urlParts)) {
  35. return false;
  36. }
  37. // 验证主机
  38. if (!$this->validateHost($urlParts)) {
  39. return false;
  40. }
  41. // 验证端口
  42. if (!$this->validatePort($urlParts)) {
  43. return false;
  44. }
  45. // 验证路径
  46. if (!$this->validatePath($urlParts)) {
  47. return false;
  48. }
  49. return true;
  50. }
  51. /**
  52. * 验证URL协议
  53. *
  54. * @param array $urlParts
  55. * @return bool
  56. */
  57. protected function validateScheme(array $urlParts): bool
  58. {
  59. $scheme = $urlParts['scheme'] ?? '';
  60. if (!in_array($scheme, ['http', 'https'])) {
  61. $this->addError('Webhook URL必须使用HTTP或HTTPS协议');
  62. return false;
  63. }
  64. // 生产环境建议使用HTTPS
  65. if ($scheme === 'http' && app()->environment('production')) {
  66. $this->addWarning('生产环境建议使用HTTPS协议以确保安全');
  67. }
  68. return true;
  69. }
  70. /**
  71. * 验证主机名
  72. *
  73. * @param array $urlParts
  74. * @return bool
  75. */
  76. protected function validateHost(array $urlParts): bool
  77. {
  78. $host = $urlParts['host'] ?? '';
  79. if (empty($host)) {
  80. $this->addError('Webhook URL缺少主机名');
  81. return false;
  82. }
  83. // 验证主机名格式
  84. if (!$this->isValidHostname($host)) {
  85. $this->addError('Webhook URL主机名格式错误');
  86. return false;
  87. }
  88. // 检查是否是内网地址
  89. if ($this->isPrivateHost($host)) {
  90. $this->addWarning('Webhook URL指向内网地址,请确认是否正确');
  91. }
  92. // 检查是否是本地地址
  93. if ($this->isLocalHost($host)) {
  94. $this->addWarning('Webhook URL指向本地地址,可能无法正常工作');
  95. }
  96. return true;
  97. }
  98. /**
  99. * 验证端口
  100. *
  101. * @param array $urlParts
  102. * @return bool
  103. */
  104. protected function validatePort(array $urlParts): bool
  105. {
  106. $port = $urlParts['port'] ?? null;
  107. if ($port !== null) {
  108. if ($port < 1 || $port > 65535) {
  109. $this->addError('Webhook URL端口号必须在1-65535范围内');
  110. return false;
  111. }
  112. // 检查常见的不安全端口
  113. $unsafePorts = [22, 23, 25, 53, 110, 143, 993, 995];
  114. if (in_array($port, $unsafePorts)) {
  115. $this->addError('Webhook URL不能使用系统保留端口');
  116. return false;
  117. }
  118. }
  119. return true;
  120. }
  121. /**
  122. * 验证路径
  123. *
  124. * @param array $urlParts
  125. * @return bool
  126. */
  127. protected function validatePath(array $urlParts): bool
  128. {
  129. $path = $urlParts['path'] ?? '/';
  130. // 验证路径长度
  131. if (strlen($path) > 2000) {
  132. $this->addError('Webhook URL路径过长');
  133. return false;
  134. }
  135. // 验证路径字符
  136. if (!preg_match('/^[a-zA-Z0-9\/_\-\.~%]+$/', $path)) {
  137. $this->addError('Webhook URL路径包含无效字符');
  138. return false;
  139. }
  140. return true;
  141. }
  142. /**
  143. * 验证主机名格式
  144. *
  145. * @param string $host
  146. * @return bool
  147. */
  148. protected function isValidHostname(string $host): bool
  149. {
  150. // 检查是否是IP地址
  151. if (filter_var($host, FILTER_VALIDATE_IP)) {
  152. return true;
  153. }
  154. // 检查是否是有效的域名
  155. return filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
  156. }
  157. /**
  158. * 检查是否是内网主机
  159. *
  160. * @param string $host
  161. * @return bool
  162. */
  163. protected function isPrivateHost(string $host): bool
  164. {
  165. // 如果是IP地址,检查是否是私有IP
  166. if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
  167. return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) === false;
  168. }
  169. // 检查常见的内网域名
  170. $privateDomains = [
  171. 'localhost',
  172. '*.local',
  173. '*.internal',
  174. '*.corp',
  175. '*.lan'
  176. ];
  177. foreach ($privateDomains as $domain) {
  178. if ($domain === $host || (str_starts_with($domain, '*.') && str_ends_with($host, substr($domain, 1)))) {
  179. return true;
  180. }
  181. }
  182. return false;
  183. }
  184. /**
  185. * 检查是否是本地主机
  186. *
  187. * @param string $host
  188. * @return bool
  189. */
  190. protected function isLocalHost(string $host): bool
  191. {
  192. $localHosts = [
  193. 'localhost',
  194. '127.0.0.1',
  195. '::1',
  196. '0.0.0.0'
  197. ];
  198. return in_array($host, $localHosts);
  199. }
  200. }