addError('Webhook URL不能为空'); return false; } // 验证URL格式 if (!filter_var($value, FILTER_VALIDATE_URL)) { $this->addError('Webhook URL格式错误'); return false; } // 解析URL $urlParts = parse_url($value); if (!$urlParts) { $this->addError('无法解析Webhook URL'); return false; } // 验证协议 if (!$this->validateScheme($urlParts)) { return false; } // 验证主机 if (!$this->validateHost($urlParts)) { return false; } // 验证端口 if (!$this->validatePort($urlParts)) { return false; } // 验证路径 if (!$this->validatePath($urlParts)) { return false; } return true; } /** * 验证URL协议 * * @param array $urlParts * @return bool */ protected function validateScheme(array $urlParts): bool { $scheme = $urlParts['scheme'] ?? ''; if (!in_array($scheme, ['http', 'https'])) { $this->addError('Webhook URL必须使用HTTP或HTTPS协议'); return false; } // 生产环境建议使用HTTPS if ($scheme === 'http' && app()->environment('production')) { $this->addWarning('生产环境建议使用HTTPS协议以确保安全'); } return true; } /** * 验证主机名 * * @param array $urlParts * @return bool */ protected function validateHost(array $urlParts): bool { $host = $urlParts['host'] ?? ''; if (empty($host)) { $this->addError('Webhook URL缺少主机名'); return false; } // 验证主机名格式 if (!$this->isValidHostname($host)) { $this->addError('Webhook URL主机名格式错误'); return false; } // 检查是否是内网地址 if ($this->isPrivateHost($host)) { $this->addWarning('Webhook URL指向内网地址,请确认是否正确'); } // 检查是否是本地地址 if ($this->isLocalHost($host)) { $this->addWarning('Webhook URL指向本地地址,可能无法正常工作'); } return true; } /** * 验证端口 * * @param array $urlParts * @return bool */ protected function validatePort(array $urlParts): bool { $port = $urlParts['port'] ?? null; if ($port !== null) { if ($port < 1 || $port > 65535) { $this->addError('Webhook URL端口号必须在1-65535范围内'); return false; } // 检查常见的不安全端口 $unsafePorts = [22, 23, 25, 53, 110, 143, 993, 995]; if (in_array($port, $unsafePorts)) { $this->addError('Webhook URL不能使用系统保留端口'); return false; } } return true; } /** * 验证路径 * * @param array $urlParts * @return bool */ protected function validatePath(array $urlParts): bool { $path = $urlParts['path'] ?? '/'; // 验证路径长度 if (strlen($path) > 2000) { $this->addError('Webhook URL路径过长'); return false; } // 验证路径字符 if (!preg_match('/^[a-zA-Z0-9\/_\-\.~%]+$/', $path)) { $this->addError('Webhook URL路径包含无效字符'); return false; } return true; } /** * 验证主机名格式 * * @param string $host * @return bool */ protected function isValidHostname(string $host): bool { // 检查是否是IP地址 if (filter_var($host, FILTER_VALIDATE_IP)) { return true; } // 检查是否是有效的域名 return filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; } /** * 检查是否是内网主机 * * @param string $host * @return bool */ protected function isPrivateHost(string $host): bool { // 如果是IP地址,检查是否是私有IP if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) === false; } // 检查常见的内网域名 $privateDomains = [ 'localhost', '*.local', '*.internal', '*.corp', '*.lan' ]; foreach ($privateDomains as $domain) { if ($domain === $host || (str_starts_with($domain, '*.') && str_ends_with($host, substr($domain, 1)))) { return true; } } return false; } /** * 检查是否是本地主机 * * @param string $host * @return bool */ protected function isLocalHost(string $host): bool { $localHosts = [ 'localhost', '127.0.0.1', '::1', '0.0.0.0' ]; return in_array($host, $localHosts); } }