Compare commits

...

5 Commits

Author SHA1 Message Date
阿斌
c6507a0d50
Pre Merge pull request !46 from 阿斌/N/A 2026-05-19 15:36:07 +00:00
lin
ce81a0724f 国标录像回放支持切换播放器 2026-05-19 10:59:40 +08:00
lin
459a8cd77a 部标-修复0003注销消息处理 2026-05-18 10:39:41 +08:00
lin
75575f939c 部标-完善位置日志打印 2026-05-18 09:28:55 +08:00
阿斌
c011389c3f
SDP 注入攻击 / 非法 SDP 协议数据
java.text.ParseException: [C@2f1fec26
ID expected
        at gov.nist.core.LexerCore.match(LexerCore.java:229)
        at gov.nist.javax.sdp.parser.OriginFieldParser.originField(OriginFieldParser.java:90)
        at gov.nist.javax.sdp.parser.OriginFieldParser.parse(OriginFieldParser.java:108)
        at gov.nist.javax.sdp.parser.SDPAnnounceParser.parse(SDPAnnounceParser.java:113)
        at javax.sdp.SdpFactory.createSessionDescription(SdpFactory.java:129)
        at com.genersoft.iot.vmp.gb28181.utils.SipUtils.parseSDP(SipUtils.java:229)
        at com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.InviteRequestProcessor.decode(InviteRequestProcessor.java:275)
        at com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.InviteRequestProcessor.process(InviteRequestProcessor.java:125)
        at com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver.processRequest(SIPProcessorObserver.java:71)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
        at java.base/java.lang.reflect.Method.invoke(Method.java:580)
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
        at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:114)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
        at java.base/java.lang.Thread.run(Thread.java:1583)
java.text.ParseException: o=- ' OR 'a'='a'; -- 1 IN IP4 179.43.150.26
        at gov.nist.javax.sdp.parser.OriginFieldParser.originField(OriginFieldParser.java:103)
        at gov.nist.javax.sdp.parser.OriginFieldParser.parse(OriginFieldParser.java:108)
        at gov.nist.javax.sdp.parser.SDPAnnounceParser.parse(SDPAnnounceParser.java:113)
        at javax.sdp.SdpFactory.createSessionDescription(SdpFactory.java:129)
        at com.genersoft.iot.vmp.gb28181.utils.SipUtils.parseSDP(SipUtils.java:229)
        at com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.InviteRequestProcessor.decode(InviteRequestProcessor.java:275)
        at com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.InviteRequestProcessor.process(InviteRequestProcessor.java:125)
        at com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver.processRequest(SIPProcessorObserver.java:71)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
        at java.base/java.lang.reflect.Method.invoke(Method.java:580)
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
        at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:114)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
        at java.base/java.lang.Thread.run(Thread.java:1583)


Signed-off-by: 阿斌 <38912748@qq.com>
2026-03-21 16:15:47 +00:00
10 changed files with 131 additions and 40 deletions

View File

@ -206,6 +206,21 @@ public class SipUtils {
public static Gb28181Sdp parseSDP(String sdpStr) throws SdpParseException { public static Gb28181Sdp parseSDP(String sdpStr) throws SdpParseException {
// 校验拦截空内容与注入攻击特征
if (sdpStr == null || sdpStr.trim().isEmpty()) {
throw new SdpParseException(0, 0, "SDP内容为空");
}
// 标准SDP每行格式固定为 "x=value"不存在SQL关键字出现则视为注入攻击
String sdpUpper = sdpStr.toUpperCase();
if (sdpUpper.contains("' OR '") || sdpUpper.contains("' OR 1") || sdpUpper.contains(" OR 1=1")
|| sdpUpper.contains("--") || sdpUpper.contains("/*") || sdpUpper.contains("*/")
|| sdpUpper.contains("DROP ") || sdpUpper.contains("INSERT ") || sdpUpper.contains("UPDATE ")
|| sdpUpper.contains("DELETE ") || sdpUpper.contains("UNION ") || sdpUpper.contains("SELECT ")) {
log.error("[SDP注入攻击] 检测到非法SDP内容已拒绝解析内容长度: {}", sdpStr.length());
throw new SdpParseException(0, 0, "非法SDP内容");
}
//校验结束
// jainSip不支持y= f=字段 移除以解析 // jainSip不支持y= f=字段 移除以解析
int ssrcIndex = sdpStr.indexOf("y="); int ssrcIndex = sdpStr.indexOf("y=");
int mediaDescriptionIndex = sdpStr.indexOf("f="); int mediaDescriptionIndex = sdpStr.indexOf("f=");

View File

@ -12,17 +12,12 @@ import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.ByteToMessageDecoder;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* @author QingtaiJiang * @author QingtaiJiang
@ -93,6 +88,8 @@ public class Jt808Decoder extends ByteToMessageDecoder {
} }
/** /**
* 转义与验证校验码 * 转义与验证校验码
* *

View File

@ -1,13 +1,13 @@
package com.genersoft.iot.vmp.jt1078.proc.request; package com.genersoft.iot.vmp.jt1078.proc.request;
import com.genersoft.iot.vmp.jt1078.annotation.MsgId; import com.genersoft.iot.vmp.jt1078.annotation.MsgId;
import com.genersoft.iot.vmp.jt1078.bean.JTDevice;
import com.genersoft.iot.vmp.jt1078.event.DeviceUpdateEvent;
import com.genersoft.iot.vmp.jt1078.proc.Header; import com.genersoft.iot.vmp.jt1078.proc.Header;
import com.genersoft.iot.vmp.jt1078.proc.response.Rs; import com.genersoft.iot.vmp.jt1078.proc.response.Rs;
import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service;
import com.genersoft.iot.vmp.jt1078.session.Session; import com.genersoft.iot.vmp.jt1078.session.Session;
import com.genersoft.iot.vmp.jt1078.session.SessionManager;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
@ -20,27 +20,30 @@ import org.springframework.context.ApplicationEvent;
@MsgId(id = "0003") @MsgId(id = "0003")
public class J0003 extends Re { public class J0003 extends Re {
int respNo; private JTDevice deviceForUpdate;
String respId;
int result;
@Override @Override
protected Rs decode0(ByteBuf buf, Header header, Session session) { protected Rs decode0(ByteBuf buf, Header header, Session session) {
respNo = buf.readUnsignedShort();
respId = ByteBufUtil.hexDump(buf.readSlice(2));
result = buf.readUnsignedByte();
log.info("[JT-注销] 设备: {}", header.getPhoneNumber()); log.info("[JT-注销] 设备: {}", header.getPhoneNumber());
return null; return null;
} }
@Override @Override
protected Rs handler(Header header, Session session, Ijt1078Service service) { protected Rs handler(Header header, Session session, Ijt1078Service service) {
SessionManager.INSTANCE.response(header.getPhoneNumber(), "0001", (long) respNo, result); // SessionManager.INSTANCE.response(header.getPhoneNumber(), "0001", (long) respNo, result);
JTDevice device = service.getDevice(header.getPhoneNumber());
if (device != null && device.isStatus()) {
deviceForUpdate = device;
deviceForUpdate.setStatus(false);
service.updateDevice(device);
}
return null; return null;
} }
@Override @Override
public ApplicationEvent getEvent() { public ApplicationEvent getEvent() {
return null; DeviceUpdateEvent registerEvent = new DeviceUpdateEvent(this);
registerEvent.setDevice(deviceForUpdate);
return registerEvent;
} }
} }

View File

@ -25,7 +25,9 @@ public class J0200 extends Re {
@Override @Override
protected Rs decode0(ByteBuf buf, Header header, Session session) { protected Rs decode0(ByteBuf buf, Header header, Session session) {
positionInfo = JTPositionBaseInfo.decode(buf); positionInfo = JTPositionBaseInfo.decode(buf);
log.debug("[JT-位置汇报]: phoneNumber={} {}", header.getPhoneNumber(), positionInfo.toSimpleString()); if (log.isDebugEnabled()) {
log.debug("[JT-位置汇报]: phoneNumber={} {}", header.getPhoneNumber(), positionInfo.toSimpleString());
}
// 读取附加信息 // 读取附加信息
// JTPositionAdditionalInfo positionAdditionalInfo = new JTPositionAdditionalInfo(); // JTPositionAdditionalInfo positionAdditionalInfo = new JTPositionAdditionalInfo();
// Map<Integer, byte[]> additionalMsg = new HashMap<>(); // Map<Integer, byte[]> additionalMsg = new HashMap<>();

View File

@ -5,10 +5,14 @@
spring: spring:
spring:
threads:
virtual:
enabled: true
cache: cache:
type: redis type: redis
thymeleaf: thymeleaf:
cache: false cache: false
# 设置接口超时时间 # 设置接口超时时间
mvc: mvc:
async: async:
@ -181,9 +185,6 @@ media:
# 录像辅助服务, 部署此服务可以实现zlm录像的管理与下载 0 表示不使用 # 录像辅助服务, 部署此服务可以实现zlm录像的管理与下载 0 表示不使用
record-assist-port: 0 record-assist-port: 0
# [可选] 日志配置, 如果不需要在jar外修改日志内容那么可以不配置此项
logging:
config: classpath:logback-spring.xml
# [根据业务需求配置] # [根据业务需求配置]
user-settings: user-settings:
@ -281,3 +282,12 @@ springdoc:
enabled: false enabled: false
swagger-ui: swagger-ui:
enabled: false enabled: false
logging:
level:
# 日志级别 debug/info/warn/error
root: info
logback:
rollingpolicy:
# 日志文件最大历史保留天数默认30天
max-history: 30

View File

@ -5,7 +5,7 @@
@dblclick="fullscreenSwich" @dblclick="fullscreenSwich"
> >
<div style="width:100%; padding-top: 56.25%; position: relative;" /> <div style="width:100%; padding-top: 56.25%; position: relative;" />
<div id="buttonsBox" class="buttons-box" > <div id="buttonsBox" class="buttons-box" v-if="showButton">
<div class="buttons-box-left"> <div class="buttons-box-left">
<i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick" /> <i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick" />
<i v-if="playing" class="iconfont icon-pause jessibuca-btn" @click="pause" /> <i v-if="playing" class="iconfont icon-pause jessibuca-btn" @click="pause" />

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="rtcPlayer"> <div id="rtcPlayer">
<video id="webRtcPlayerBox" controls autoplay style="text-align:left;"> <video id="webRtcPlayerBox" :controls="showControls" autoplay style="text-align:left;">
Your browser is too old which doesn't support HTML5 video. Your browser is too old which doesn't support HTML5 video.
</video> </video>
</div> </div>
@ -10,7 +10,12 @@
let webrtcPlayer = null let webrtcPlayer = null
export default { export default {
name: 'RtcPlayer', name: 'RtcPlayer',
props: ['videoUrl', 'error', 'hasaudio'], props: {
videoUrl: { type: String, default: '' },
error: { type: String, default: '' },
hasaudio: { type: Boolean, default: false },
showControls: { type: Boolean, default: true }
},
data() { data() {
return { return {
timer: null timer: null
@ -97,10 +102,12 @@ export default {
} }
#rtcPlayer{ #rtcPlayer{
width: 100%; width: 100%;
height: 100%;
} }
#webRtcPlayerBox{ #webRtcPlayerBox{
width: 100%; width: 100%;
max-height: 56vh; height: 100%;
max-height: 100%;
background-color: #000; background-color: #000;
} }
/* 隐藏logo */ /* 隐藏logo */

View File

@ -49,8 +49,8 @@
</div> </div>
</div> </div>
</div> </div>
<div id="playerBox" style="width: 100%"> <div id="playerBox" style="width: 100%;">
<div class="playBox" style="height: calc(100% - 90px); width: 100%; background-color: #000000"> <div class="playBox" style="height: calc(100vh - 220px); width: 100%; background-color: #000000">
<div <div
v-if="playLoading" v-if="playLoading"
style="position: relative; left: calc(50% - 32px); top: 43%; z-index: 100;color: #fff;float: left; text-align: center;" style="position: relative; left: calc(50% - 32px); top: 43%; z-index: 100;color: #fff;float: left; text-align: center;"
@ -58,10 +58,32 @@
<div class="el-icon-loading" /> <div class="el-icon-loading" />
<div style="width: 100%; line-height: 2rem">正在加载</div> <div style="width: 100%; line-height: 2rem">正在加载</div>
</div> </div>
<h265web <jessibucaPlayer
v-if="activePlayer === 'jessibuca'"
ref="recordVideoPlayer"
:has-audio="true"
:height="'calc(100vh - 90px)'"
:show-button="false"
autoplay
@playStatusChange="playingChange"
@playTimeChange="showPlayTimeChange"
/>
<rtcPlayer
v-if="activePlayer === 'webRTC'"
ref="recordVideoPlayer" ref="recordVideoPlayer"
:video-url="videoUrl" :video-url="videoUrl"
:height="'calc(100vh - 250px)'" :has-audio="true"
:show-controls="false"
style="height: calc(100vh - 220px)"
autoplay
@playStatusChange="playingChange"
@playTimeChange="showPlayTimeChange"
/>
<h265web
v-if="activePlayer === 'h265web'"
ref="recordVideoPlayer"
:video-url="videoUrl"
:height="'calc(100vh - 220px)'"
:show-button="false" :show-button="false"
:has-audio="true" :has-audio="true"
@playStatusChange="playingChange" @playStatusChange="playingChange"
@ -181,6 +203,18 @@
</div> </div>
<div style="text-align: right;"> <div style="text-align: right;">
<div class="record-play-control" style="background-color: transparent; box-shadow: 0 0 10px transparent"> <div class="record-play-control" style="background-color: transparent; box-shadow: 0 0 10px transparent">
<el-dropdown @command="changePlayer">
<a
target="_blank"
class="record-play-control-item record-play-control-speed"
title="切换播放器"
>{{ playerLabel }}</a>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="jessibuca">Jessibuca</el-dropdown-item>
<el-dropdown-item command="webRTC">WebRTC</el-dropdown-item>
<el-dropdown-item command="h265web">H265Web</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<a <a
v-if="!isFullScreen" v-if="!isFullScreen"
target="_blank" target="_blank"
@ -208,6 +242,8 @@
<script> <script>
import h265web from '../../common/h265web.vue' import h265web from '../../common/h265web.vue'
import jessibucaPlayer from '../../common/jessibuca.vue'
import rtcPlayer from '../../common/rtcPlayer.vue'
import VideoTimeline from '../../common/VideoTimeLine/index.vue' import VideoTimeline from '../../common/VideoTimeLine/index.vue'
import recordDownload from '../../dialog/recordDownload.vue' import recordDownload from '../../dialog/recordDownload.vue'
import ChooseTimeRange from '../../dialog/chooseTimeRange.vue' import ChooseTimeRange from '../../dialog/chooseTimeRange.vue'
@ -217,7 +253,7 @@ import screenfull from 'screenfull'
export default { export default {
name: 'DeviceRecord', name: 'DeviceRecord',
components: { components: {
h265web, VideoTimeline, recordDownload, ChooseTimeRange h265web, jessibucaPlayer, rtcPlayer, VideoTimeline, recordDownload, ChooseTimeRange
}, },
data() { data() {
return { return {
@ -251,6 +287,12 @@ export default {
timelineControl: false, timelineControl: false,
showOtherSpeed: true, showOtherSpeed: true,
timeSegments: [], timeSegments: [],
activePlayer: 'jessibuca',
playerUrls: {
jessibuca: ['ws_flv', 'wss_flv'],
webRTC: ['rtc', 'rtcs'],
h265web: ['ws_flv', 'wss_flv']
},
pickerOptions: { pickerOptions: {
cellClassName: (date) => { cellClassName: (date) => {
// //
@ -266,6 +308,10 @@ export default {
} }
}, },
computed: { computed: {
playerLabel() {
const labels = { jessibuca: 'Jessibuca', webRTC: 'WebRTC', h265web: 'H265Web' }
return labels[this.activePlayer] || 'Jessibuca'
},
boxStyle() { boxStyle() {
if (this.showSidebar) { if (this.showSidebar) {
return { return {
@ -300,6 +346,18 @@ export default {
window.removeEventListener('beforeunload', this.stopPlayRecord) window.removeEventListener('beforeunload', this.stopPlayRecord)
}, },
methods: { methods: {
changePlayer(player) {
if (this.activePlayer === player) return
this.activePlayer = player
if (this.streamInfo) {
this.videoUrl = this.getUrlByStreamInfo()
this.$nextTick(() => {
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.play(this.videoUrl)
}
})
}
},
sidebarControl() { sidebarControl() {
this.showSidebar = !this.showSidebar this.showSidebar = !this.showSidebar
}, },
@ -468,14 +526,20 @@ export default {
this.streamInfo = data this.streamInfo = data
this.videoUrl = this.getUrlByStreamInfo() this.videoUrl = this.getUrlByStreamInfo()
this.hasAudio = this.streamInfo.tracks && this.streamInfo.tracks.length > 1 this.hasAudio = this.streamInfo.tracks && this.streamInfo.tracks.length > 1
this.$nextTick(() => {
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.play(this.videoUrl)
}
})
}) })
} }
}, },
getUrlByStreamInfo() { getUrlByStreamInfo() {
const keys = this.playerUrls[this.activePlayer]
if (location.protocol === 'https:') { if (location.protocol === 'https:') {
this.videoUrl = this.streamInfo['wss_flv'] this.videoUrl = this.streamInfo[keys[1]]
} else { } else {
this.videoUrl = this.streamInfo['ws_flv'] this.videoUrl = this.streamInfo[keys[0]]
} }
return this.videoUrl return this.videoUrl
}, },

View File

@ -1,4 +1,3 @@
drop table IF EXISTS wvp_jt_terminal;
create table IF NOT EXISTS wvp_jt_terminal ( create table IF NOT EXISTS wvp_jt_terminal (
id serial primary key, id serial primary key,
phone_number character varying(50), phone_number character varying(50),
@ -23,7 +22,6 @@ create table IF NOT EXISTS wvp_jt_terminal (
constraint uk_jt_device_id_device_id unique (id, phone_number) constraint uk_jt_device_id_device_id unique (id, phone_number)
); );
drop table IF EXISTS wvp_jt_channel;
create table IF NOT EXISTS wvp_jt_channel ( create table IF NOT EXISTS wvp_jt_channel (
id serial primary key, id serial primary key,
terminal_db_id integer, terminal_db_id integer,
@ -141,8 +139,6 @@ call wvp_202601025();
DROP PROCEDURE wvp_202601025; DROP PROCEDURE wvp_202601025;
DELIMITER ; DELIMITER ;
drop table IF EXISTS wvp_alarm;
create table IF NOT EXISTS wvp_alarm ( create table IF NOT EXISTS wvp_alarm (
id serial primary key COMMENT '主键ID', id serial primary key COMMENT '主键ID',
channel_id integer COMMENT '关联通道的数据库id', channel_id integer COMMENT '关联通道的数据库id',

View File

@ -1,4 +1,3 @@
drop table IF EXISTS wvp_jt_terminal;
create table IF NOT EXISTS wvp_jt_terminal ( create table IF NOT EXISTS wvp_jt_terminal (
id serial primary key, id serial primary key,
phone_number character varying(50), phone_number character varying(50),
@ -22,7 +21,6 @@ create table IF NOT EXISTS wvp_jt_terminal (
sdp_ip character varying(50), sdp_ip character varying(50),
constraint uk_jt_device_id_device_id unique (id, phone_number) constraint uk_jt_device_id_device_id unique (id, phone_number)
); );
drop table IF EXISTS wvp_jt_channel;
create table IF NOT EXISTS wvp_jt_channel ( create table IF NOT EXISTS wvp_jt_channel (
id serial primary key, id serial primary key,
terminal_db_id integer, terminal_db_id integer,
@ -48,7 +46,6 @@ drop index uk_media_server_unique_ip_http_port on wvp_media_server;
ALTER table wvp_device DROP COLUMN IF EXISTS register_time; ALTER table wvp_device DROP COLUMN IF EXISTS register_time;
ALTER table wvp_device DROP COLUMN IF EXISTS keepalive_time; ALTER table wvp_device DROP COLUMN IF EXISTS keepalive_time;
drop table IF EXISTS wvp_alarm;
create table IF NOT EXISTS wvp_alarm ( create table IF NOT EXISTS wvp_alarm (
id serial primary key, id serial primary key,
channel_id integer, channel_id integer,