Browse Source

fix: 实时预览flvUrl/hlsUrl按请求来源IP生成,支持内外网双IP访问

master
wangguangyuan 2 days ago
parent
commit
091e235c11
1 changed files with 222 additions and 0 deletions
  1. +222
    -0
      src/main/java/com/inspect/nvr/hik/service/HikNvrLivePublicBaseUrlResolver.java

+ 222
- 0
src/main/java/com/inspect/nvr/hik/service/HikNvrLivePublicBaseUrlResolver.java View File

@ -0,0 +1,222 @@
package com.inspect.nvr.hik.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Locale;
/**
* 按浏览器请求的目标地址生成实时预览对外播放地址
*
* <p>现场同一套服务会同时暴露内网与外网两个 IP例如 192.168.20.210 192.168.134.10
* flvUrl/hlsUrl 只能返回固定地址时从另一侧访问的浏览器会拿到不可达的地址
* 这里用请求中的 X-Forwarded-Host/Host 主机名替换 ZLM_HTTP_BASE_URL 的主机部分
* <b>协议和端口仍沿用 .env 配置</b>ZLMediaKit HTTP 端口与网关端口不一致时地址依然正确
* 请求不携带可用主机名时回退到 ZLM_HTTP_BASE_URL</p>
*/
@Slf4j
@Component
public class HikNvrLivePublicBaseUrlResolver {
/** 回退使用的固定基础地址,与实时服务读取的配置项保持一致。 */
@Value("${hik.playback.hls.http-base-url:http://127.0.0.1:18081}")
private String configuredBaseUrl = "http://127.0.0.1:18081";
/** 规范化后的回退基础地址。 */
private String fallbackBaseUrl;
/** 校验并规范化回退基础地址。 */
@PostConstruct
public void initialize() {
// 回退地址缺省时保留空串交由实时服务的配置校验统一报错
this.fallbackBaseUrl = trimTrailingSlash(
configuredBaseUrl == null ? "" : configuredBaseUrl.trim());
}
/**
* 解析本次请求应返回给浏览器的 ZLMediaKit 基础地址
*
* @param request 当前 HTTP 请求后台线程或内部调用传入 null 时使用固定配置
* @return 带协议和端口的基础地址例如 http://192.168.134.10:18081
*/
public String resolve(HttpServletRequest request) {
// 空闲回收状态补偿等后台调用没有请求上下文直接沿用固定配置
if (request == null) {
return fallbackBaseUrl;
}
String requestHost = firstUsableHost(request);
// 代理链可能把原始地址放在 X-Forwarded-Host其次才是 Host
String resolved = replaceAuthorityHost(fallbackBaseUrl, requestHost);
if (resolved == null) {
// 取不到可信主机名时保持原有固定地址行为避免生成畸形播放地址
if (!requestHost.isEmpty()) {
log.debug("[live] 请求地址无法用于生成播放地址 requestHost={} fallback={}",
requestHost, fallbackBaseUrl);
}
return fallbackBaseUrl;
}
// 回环地址说明代理没有透传原始 Host远程浏览器无法访问该地址此时优先保留可配置的固定地址
if (isLoopback(requestHost) && !isLoopback(hostOf(fallbackBaseUrl))) {
log.warn("[live] 请求Host为回环地址,已回退到配置的播放地址 requestHost={} fallback={}",
requestHost, fallbackBaseUrl);
return fallbackBaseUrl;
}
return resolved;
}
/** 依次读取 X-Forwarded-Host 和 Host,返回第一个可用的主机名。 */
private String firstUsableHost(HttpServletRequest request) {
String forwardedHost = normalizeHost(request.getHeader("X-Forwarded-Host"));
if (!forwardedHost.isEmpty()) {
return forwardedHost;
}
return normalizeHost(request.getHeader("Host"));
}
/**
* 从请求头值中提取纯主机名忽略代理链后续节点去掉端口部分
*
* @param rawHost 请求头原始值可能形如 "192.168.20.210:18891, proxy1"
* @return 合法主机名无法识别时返回空串
*/
private String normalizeHost(String rawHost) {
if (rawHost == null) {
return "";
}
String host = rawHost.trim();
// X-Forwarded-Host 可能是逗号分隔的代理链只取最前面的原始客户端地址
int comma = host.indexOf(',');
if (comma >= 0) {
host = host.substring(0, comma).trim();
}
if (host.isEmpty()) {
return "";
}
// IPv6 字面量形如 [fe80::1]:18891保留方括号部分即可
if (host.startsWith("[")) {
int end = host.indexOf(']');
if (end < 0 || !host.substring(0, end + 1).matches("\\[[0-9A-Fa-f:.]+\\]")) {
return "";
}
return host.substring(0, end + 1);
}
// 只保留端口前的主机名ZLM 端口固定取自配置不接受请求里的端口
int colon = host.indexOf(':');
if (colon >= 0) {
host = host.substring(0, colon).trim();
}
// 仅接受常规主机名字符防止把路径空白或头注入内容拼进播放地址
if (!host.matches("[A-Za-z0-9._-]+")) {
return "";
}
return host;
}
/**
* 用请求主机名替换基础地址中的主机部分保留协议端口和路径前缀
*
* @param baseUrl 配置的基础地址例如 http://192.168.20.210:18891
* @param host 请求主机名例如 192.168.134.10
* @return 替换后的地址基础地址不可解析时返回 null
*/
private String replaceAuthorityHost(String baseUrl, String host) {
if (baseUrl == null || baseUrl.isEmpty() || host.isEmpty()) {
return null;
}
int schemeEnd = baseUrl.indexOf("://");
// 缺少协议或无主机部分的配置无法安全替换保持原值由调用方回退
if (schemeEnd <= 0) {
return null;
}
String scheme = baseUrl.substring(0, schemeEnd + 3);
String remainder = baseUrl.substring(schemeEnd + 3);
int pathStart = remainder.indexOf('/');
String authority = pathStart < 0 ? remainder : remainder.substring(0, pathStart);
String path = pathStart < 0 ? "" : remainder.substring(pathStart);
// 基础地址不应包含用户信息出现时放弃替换避免拼出畸形地址
if (authority.isEmpty() || authority.indexOf('@') >= 0) {
return null;
}
String port = configuredPort(authority);
// 端口非法时不做替换避免把畸形地址返回给浏览器
if (port == null) {
return null;
}
// ZLMediaKit 端口固定沿用配置只把主机名换成浏览器实际访问的那一个
return scheme + host + port + path;
}
/**
* 提取基础地址中的端口部分
*
* @param authority 基础地址的主机部分例如 192.168.20.210:18891 [fe80::1]:18891
* @return 形如 ":18891" 的端口未配置端口时返回空串端口非法时返回 null
*/
private String configuredPort(String authority) {
// IPv6 字面量的端口写在方括号之后
if (authority.startsWith("[")) {
int end = authority.indexOf(']');
if (end < 0) {
return null;
}
if (end + 1 >= authority.length()) {
return "";
}
if (authority.charAt(end + 1) != ':') {
return null;
}
String port = authority.substring(end + 1);
return port.matches(":\\d{1,5}") ? port : null;
}
int colon = authority.indexOf(':');
if (colon < 0) {
return "";
}
String port = authority.substring(colon);
return port.matches(":\\d{1,5}") ? port : null;
}
/** 读取地址中的主机名部分,用于回环判断。 */
private String hostOf(String baseUrl) {
if (baseUrl == null || baseUrl.isEmpty()) {
return "";
}
int schemeEnd = baseUrl.indexOf("://");
if (schemeEnd <= 0) {
return "";
}
String remainder = baseUrl.substring(schemeEnd + 3);
int pathStart = remainder.indexOf('/');
String authority = pathStart < 0 ? remainder : remainder.substring(0, pathStart);
int at = authority.lastIndexOf('@');
if (at >= 0) {
authority = authority.substring(at + 1);
}
if (authority.startsWith("[")) {
int end = authority.indexOf(']');
return end < 0 ? "" : authority.substring(0, end + 1).toLowerCase(Locale.ROOT);
}
int colon = authority.indexOf(':');
return (colon < 0 ? authority : authority.substring(0, colon)).toLowerCase(Locale.ROOT);
}
/** 判断主机名是否为回环地址。 */
private boolean isLoopback(String host) {
return "127.0.0.1".equals(host)
|| "localhost".equals(host)
|| "::1".equals(host)
|| "[::1]".equals(host);
}
/** 去除基础地址末尾多余斜杠。 */
private String trimTrailingSlash(String value) {
int end = value.length();
while (end > 0 && value.charAt(end - 1) == '/') {
end--;
}
return value.substring(0, end);
}
}

Loading…
Cancel
Save